KoderKoder.ai
PrijzenEnterpriseOnderwijsVoor investeerders
InloggenAan de slag

Product

PrijzenEnterpriseVoor investeerders

Bronnen

Neem contact opOndersteuningOnderwijsBlog

Juridisch

PrivacybeleidGebruiksvoorwaardenBeveiligingBeleid voor acceptabel gebruikMisbruik melden

Sociaal

LinkedInTwitter
Koder.ai
Taal

© 2026 Koder.ai. Alle rechten voorbehouden.

Home›Blog›PostgreSQL connection pooling: app-pooling vs PgBouncer
17 dec 2025·7 min

PostgreSQL connection pooling: app-pooling vs PgBouncer

PostgreSQL connection pooling: vergelijk app-pools en PgBouncer voor Go-backends, metrieken om te monitoren en misconfiguraties die latentiepieken veroorzaken.

PostgreSQL connection pooling: app-pooling vs PgBouncer

Waarom latentiepieken vaak bij verbindingen beginnen

Een databaseverbinding is als een telefoonlijn tussen je app en Postgres. Het openen van een verbinding kost tijd en werk aan beide kanten: TCP/TLS-opzet, authenticatie, geheugen en een backend-proces aan de Postgres-kant. Een connection pool houdt een klein aantal van deze “telefoonlijnen” open zodat je app ze kan hergebruiken in plaats van voor elk verzoek opnieuw te bellen.

Als pooling uitgeschakeld is of verkeerd geschaald, zie je zelden eerst een nette foutmelding. Je krijgt willekeurige traagheid. Requests die normaal 20–50 ms duren, duren plots 500 ms of 5 seconden en de p95 schiet omhoog. Daarna verschijnen timeouts, gevolgd door “too many connections”, of er ontstaat een wachtrij in je app terwijl die wacht op een vrije verbinding.

Limieten voor verbindingen doen er zelfs voor kleine apps toe omdat verkeer bursty is. Een marketingmail, een cron job of een paar trage endpoints kan tientallen requests tegelijk naar de database sturen. Als elk request een nieuwe verbinding opent, kan Postgres veel van zijn capaciteit besteden aan het accepteren en beheren van verbindingen in plaats van queries uit te voeren. Als je al een pool hebt maar die te groot is, kun je Postgres overbelasten met te veel actieve backends en context switching en geheugendruk veroorzaken.

Let op vroege symptomen zoals:

  • p95/p99 latentiepieken terwijl de gemiddelde latency normaal lijkt
  • timeouts die clusteren tijdens verkeerspieken
  • stijgende "waiting for connection"-tijd in de app
  • frequent connect/disconnect of connection saturation op Postgres

Pooling vermindert verbinding-churn en helpt Postgres om pieken af te handelen. Het lost geen trage SQL op. Als een query een volledige tabelscan doet of op locks wacht, verandert pooling vooral hoe het systeem faalt (eerder queueing, later timeouts), niet of het snel is.

App-pooling vs PgBouncer: welk probleem lost elk op

Connection pooling gaat over het beheersen van hoeveel databaseverbindingen er tegelijk bestaan en hoe ze worden hergebruikt. Je kunt dit in je app doen (app-level pooling) of met een aparte service voor Postgres (PgBouncer). Ze lossen verwante maar verschillende problemen op.

App-level pooling (in Go meestal de ingebouwde database/sql-pool) beheert verbindingen per proces. Het beslist wanneer een nieuwe verbinding open moet, wanneer er een hergebruikt kan worden en wanneer idle verbindingen gesloten worden. Dit voorkomt dat je bij elk request de opstartkosten betaalt. Wat het niet kan doen, is coördineren tussen meerdere app-instances. Als je 10 replicas draait, heb je in feite 10 gescheiden pools.

PgBouncer staat tussen je app en Postgres en pooled namens veel clients. Het is het meest nuttig wanneer je veel korte requests hebt, veel app-instances of piekerig verkeer. Het begrenst het aantal server-side verbindingen naar Postgres, zelfs als er honderden client-verbindingen tegelijk binnenkomen.

Een eenvoudige scheiding van verantwoordelijkheden:

  • App-pooling vormt de concurrency binnen één app-instance en voorkomt reconnects per request.
  • PgBouncer begrenst het totale aantal Postgres-verbindingen over alle instances en dempt pieken.
  • Postgres heeft nog steeds harde limieten op CPU, IO en geheugen. Pooling creëert geen extra capaciteit.

Ze kunnen samenwerken zonder problemen met “dubbele pooling” zolang elke laag een duidelijke rol heeft: een verstandige database/sql-pool per Go-process, plus PgBouncer om een globaal connectie-budget af te dwingen.

Een veelvoorkomende verwarring is denken dat “meer pools meer capaciteit betekent.” Meestal betekent het het tegenovergestelde. Als elke service, worker en replica zijn eigen grote pool heeft, kan het totale aantal verbindingen exploderen en queueing, context switching en plotselinge latentiepieken veroorzaken.

Hoe Go database/sql-pooling zich echt gedraagt

In Go is sql.DB een connection pool manager, niet één enkele verbinding. Wanneer je db.Query of db.Exec aanroept, probeert database/sql een idle verbinding te hergebruiken. Als dat niet lukt, kan het een nieuwe openen (tot je limiet) of het verzoek laten wachten.

Die wachttijd is waar de “mystery latency” vaak vandaan komt. Wanneer de pool verzadigd is, queueen requests binnen je app. Van buitenaf lijkt het alsof Postgres traag is, maar de tijd wordt eigenlijk gespendeerd aan wachten op een vrije verbinding.

De knoppen die ertoe doen

De meeste tuning draait om vier instellingen:

  • MaxOpenConns: harde limiet op open verbindingen (idle + in gebruik). Als je dit bereikt, blokkeren aanroepen.
  • MaxIdleConns: hoeveel verbindingen klaar kunnen zitten voor hergebruik. Te laag zorgt voor frequente reconnects.
  • ConnMaxLifetime: dwingt periodieke recycling van verbindingen af. Nuttig bij load balancers en NAT-timeouts, maar te laag veroorzaakt churn.
  • ConnMaxIdleTime: sluit verbindingen die te lang ongebruikt zijn.

Herbruik van verbindingen verlaagt meestal latency en database-CPU omdat je herhaalde opzet (TCP/TLS, auth, sessie-init) vermijdt. Maar een te grote pool kan het tegenovergestelde doen: het staat meer gelijktijdige queries toe dan Postgres goed kan verwerken, wat meer contention en overhead veroorzaakt.

Denk in totalen, niet per proces. Als elke Go-instance 50 open verbindingen toestaat en je schaalt naar 20 instances, heb je effectief 1.000 verbindingen toegestaan. Vergelijk dat met wat je Postgres-server daadwerkelijk soepel kan draaien.

Een praktisch startpunt is MaxOpenConns te koppelen aan verwachte concurrency per instance en dat vervolgens te valideren met pool-metrieken (in-use, idle en wait time) voordat je het verhoogt.

PgBouncer-basis en pooling-modi

PgBouncer is een kleine proxy tussen je app en PostgreSQL. Je service maakt verbinding met PgBouncer, en PgBouncer houdt een beperkt aantal echte serververbindingen naar Postgres. Tijdens pieken queueet PgBouncer client-werk in plaats van meteen meer Postgres-backends te maken. Die wachtrij kan het verschil zijn tussen een gecontroleerde vertraging en een database die omvalt.

De drie pooling-modi

PgBouncer heeft drie pooling-modi:

  • Session pooling: een client houdt dezelfde serververbinding zolang hij verbonden blijft.
  • Transaction pooling: een client leent een serververbinding voor de duur van een transactie en geeft die daarna terug.
  • Statement pooling: een client leent een serververbinding voor één statement.

Session pooling gedraagt zich het meest als directe verbindingen naar Postgres. Het is het minst verrassend, maar bespaart minder serververbindingen bij bursty load.

Wat meestal bij Go HTTP-API’s past

Voor typische Go HTTP-API’s is transaction pooling vaak een sterke default. De meeste requests doen een kleine query of een korte transactie en zijn dan klaar. Transaction pooling laat veel client-verbindingen een kleiner Postgres-verbindingbudget delen.

Het compromis is sessiestate. In transaction-mode kan alles wat ervan uitgaat dat een enkele serververbinding blijft hangen, breken of vreemd gedrag laten zien, zoals:

  • prepared statements die eenmaal gemaakt en later hergebruikt worden
  • sessie-instellingen die je verwacht te behouden (SET, SET ROLE, search_path)
  • tijdelijke tabellen en advisory locks die over statements heen worden gebruikt

Als je app afhankelijk is van dat soort state, is session pooling veiliger. Statement pooling is het meest beperkend en past zelden bij webapps.

Een nuttige regel: als elk request binnen één transactie kan opzetten wat het nodig heeft, houdt transaction pooling de latency meestal constanter onder load. Als je langdurige sessiegedrag nodig hebt, gebruik dan session pooling en focus op striktere limieten in de app.

Hoe je de juiste strategie kiest voor een Go-backend

Bouw je backend sneller
Bouw een Go + PostgreSQL-app vanuit een chatprompt en itereren totdat de latency stabiel blijft.
Probeer gratis

Als je een Go-service met database/sql draait, heb je al app-side pooling. Voor veel teams is dat genoeg: een paar instances, steady traffic en queries die niet extreem piekerig zijn. In dat geval is de eenvoudigste en veiligste keuze de Go-pool tunen, de databaseverbindinglimiet realistisch houden en het daar bij laten.

PgBouncer helpt het meest wanneer de database door te veel clientverbindingen tegelijk wordt geraakt. Dit zie je bij veel app-instances (of serverless-achtige scaling), bursty traffic en veel korte queries.

PgBouncer kan ook schade doen als het in de verkeerde modus wordt gebruikt. Als je code afhankelijk is van sessiestate (temp tables, prepared statements hergebruikt, advisory locks over calls heen of sessieinstellingen), kan transaction pooling verwarrende fouten veroorzaken. Als je echt sessiegedrag nodig hebt, gebruik dan session pooling of sla PgBouncer over en schaal app-pools zorgvuldig.

Een eenvoudige beslisregel

Gebruik deze vuistregel:

  • Als je 1–3 app-instances hebt en het totaal aantal open verbindingen comfortabel onder de database-limiet blijft, gebruik alleen app-pooling.
  • Als je veel instances of autoscaling hebt en de som van max open connections het maximum dat Postgres aankan kan overschrijden, voeg PgBouncer toe.
  • Als de meeste requests kort zijn (snelle reads, kleine writes), betaalt PgBouncer zich meestal eerder terug.
  • Als requests lang verbindingen vasthouden (trage rapporten, lange transacties), repareer dan eerst de queries en wees conservatief met poolgroottes.

Stapsgewijs: sizing en veilig uitrollen van pooling

Connectielimieten zijn een budget. Als je het in één keer uitgeeft, wacht elk nieuw verzoek en springt de tail-latency omhoog. Het doel is concurrency op een gecontroleerde manier te begrenzen en tegelijkertijd de throughput zo stabiel mogelijk te houden.

Een praktische rollout-volgorde

  1. Meet de pieken en tail-latency van vandaag. Noteer piek actieve verbindingen (geen gemiddelden), plus p50/p95/p99 voor requests en belangrijke queries. Noteer eventuele connection errors of timeouts.

  2. Stel een veilig Postgres-verbindingbudget voor de app in. Begin bij max_connections en trek headroom af voor admin-toegang, migraties, achtergrondjobs en pieken. Als meerdere services de database delen, verdeel het budget doelbewust.

  3. Vertaal het budget naar Go-limieten per instance. Deel het app-budget door het aantal instances en stel MaxOpenConns daarop in (of iets lager). Zet MaxIdleConns hoog genoeg om constante reconnects te vermijden en kies lifetimes zodat verbindingen af en toe gerecycled worden zonder churn te veroorzaken.

  4. Voeg PgBouncer alleen toe als het nodig is en kies een modus. Gebruik session pooling als je sessiestate nodig hebt. Gebruik transaction pooling wanneer je de grootste vermindering van serververbindingen wilt en je app compatibel is.

  5. Rol geleidelijk uit en vergelijk voor en na. Verander één ding tegelijk, canary het en vergelijk tail-latency, pool wait time en database-CPU.

Voorbeeld: als Postgres veilig 200 verbindingen aan je service kan geven en je draait 10 Go-instances, begin met MaxOpenConns=15-18 per instance. Dat laat ruimte voor pieken en verkleint de kans dat elke instance tegelijk het plafond raakt.

Metrieken om te monitoren zodat je problemen vroeg vangt

Pooling-problemen verschijnen zelden eerst als “teveel verbindingen.” Meestal zie je een langzaam oplopende wachttijd en dan een plotselinge jump in p95 en p99.

Begin met wat je Go-app rapporteert. Met database/sql monitor open connections, in-use, idle, wait count en wait time. Als wait count stijgt terwijl het verkeer gelijk blijft, is je pool te klein of worden verbindingen te lang vastgehouden.

Aan de database-kant: volg actieve verbindingen versus max, CPU en lock-activiteit. Als CPU laag is maar latency hoog, is het vaak queueing of locks, niet ruwe rekencapaciteit.

Als je PgBouncer draait, voeg dan een derde zicht toe: client connections, server connections naar Postgres en queue depth. Een groeiende wachtrij met stabiele server connections is een duidelijk signaal dat het budget verzadigd is.

Goede alert-signalen:

  • p95/p99 stijgen terwijl p50 normaal blijft
  • connection wait time neemt toe (app-side), vooral vóór timeouts
  • PgBouncer-queue groeit sneller dan hij leegt
  • error rate en timeouts stijgen samen
  • locks nemen toe samen met langlopende queries

Veelvoorkomende misconfiguraties die pieken veroorzaken

Plan je pooling-wijziging
Gebruik Planning Mode om pool-limieten, timeouts en wat je gaat meten vooraf in kaart te brengen.
Begin met bouwen

Pooling-problemen verschijnen vaak tijdens bursts: requests hopen zich op en wachten op een verbinding, en daarna ziet alles er weer normaal uit. De oorzaak is vaak een instelling die op één instance redelijk lijkt maar gevaarlijk wordt zodra je veel kopieën van de service draait.

Veelvoorkomende oorzaken:

  • MaxOpenConns per instance ingesteld zonder globaal budget. 100 verbindingen per instance over 20 instances is 2.000 potentiële verbindingen.
  • Te veel idle verbindingen. Idle backends gebruiken nog steeds geheugen en kunnen ander werk verdringen.
  • ConnMaxLifetime / ConnMaxIdleTime te laag ingesteld. Dit kan reconnect-stormen veroorzaken wanneer veel verbindingen tegelijk gerecycled worden.
  • PgBouncer transaction pooling met code die afhankelijk is van sessiestate. Temp tables, advisory locks en sessie-instellingen kunnen op subtiele manieren breken.
  • Achtergrondjobs en health checks die bursts creëren. Short-interval pings of “open en close per request”-patronen kunnen golven van nieuwe verbindingen veroorzaken.

Een eenvoudige manier om pieken te verminderen is pooling als een gedeeld limiet te behandelen, niet als een app-lokaal default: cap totale verbindingen over alle instances, houd een bescheiden idle pool en gebruik lifetimes lang genoeg om gesynchroniseerde reconnects te vermijden.

Wat te doen als de vraag je connectiebudget overschrijdt

Bij traffic-surges zie je meestal één van drie uitkomsten: requests queueen terwijl ze wachten op een vrije verbinding, requests timen out, of alles wordt zo traag dat retries zich opstapelen.

Queueing is de heimelijke. Je handler draait nog steeds, maar staat geparkeerd terwijl hij wacht op een verbinding. Die wachttijd wordt deel van de responsetijd, dus een kleine pool kan een 50 ms query veranderen in een multi-seconde endpoint onder load.

Een nuttig mentaal model: als je pool 30 bruikbare verbindingen heeft en er ineens 300 gelijktijdige requests komen die allemaal de DB nodig hebben, moeten 270 daarvan wachten. Als elk request een verbinding 100 ms vasthoudt, loopt de tail-latency snel op naar seconden.

Stel een duidelijk timeout-budget en houd je eraan. De app-timeout moet iets korter zijn dan de database-timeout zodat je snel faalt en druk reduceert in plaats van werk te laten hangen.

  • App: een request-deadline, plus een kortere deadline rond de DB-call
  • DB: statement_timeout zodat één slechte query geen verbindingen kan blokkeren
  • Pooler (als aanwezig): een pool-wacht-timeout zodat je een weigering krijgt in plaats van oneindige queueing

Voeg vervolgens backpressure toe zodat je de pool niet overspoelt. Kies één of twee voorspelbare mechanismen, zoals het limiteren van concurrency per endpoint, load shedding met duidelijke fouten (zoals 429), of het scheiden van achtergrondjobs van gebruikerverkeer.

Tot slot: repareer trage queries eerst. Onder pooling-druk houden trage queries verbindingen langer vast, wat wachtijden verhoogt, wat timeouts verhoogt, wat retries triggert. Die feedback-loop is hoe “een beetje traag” verandert in “alles is traag.”

Load testing en capaciteitsplanning zonder giswerk

Breng je team erbij
Werk samen aan tuning, metrieken en fixes over services en omgevingen heen.
Nodig team uit

Behandel load testing als een manier om je connectie-budget te valideren, niet alleen throughput. Het doel is bevestigen dat pooling zich onder druk hetzelfde gedraagt als in staging.

Test met realistisch verkeer: dezelfde request-mix, burst-patronen en hetzelfde aantal app-instances als in productie. "One endpoint"-benchmarks verbergen vaak pool-problemen tot de lanceringsdag.

Neem een warm-up op zodat je koude caches en ramp-up-effecten niet meet. Laat pools hun normale grootte bereiken en begin dan pas met meten.

Als je strategieën vergelijkt, houd de workload identiek en voer uit:

  • alleen app-pooling (getunede database/sql, geen PgBouncer)
  • PgBouncer ervoor (apps houden kleine pools, PgBouncer capteert serverconnections)
  • beide samen (kleine app-pools plus PgBouncer)

Noteer na elke run een klein scorekaartje dat je bij elke release opnieuw kunt gebruiken:

  • p95 en p99 request-latency tijdens steady state en tijdens een burst
  • max totale verbindingen (client-side en server-side)
  • queue time-signalen (wachten op een vrije verbinding)
  • error rate en aantal timeouts
  • throughput op het punt waar latency snel begint te stijgen

Na verloop van tijd maakt dit capacity planning herhaalbaar in plaats van giswerk.

Snelle checklist en vervolgstappen

Voordat je pool-groottes aanpast, schrijf één getal op: je connection budget. Dat is het maximale veilige aantal actieve Postgres-verbindingen voor deze omgeving (dev, staging, prod), inclusief achtergrondjobs en admin-toegang. Als je het niet kunt benoemen, gok je.

Een korte checklist:

  • Stel een expliciete max in Go en zorg dat (instances x MaxOpenConns) binnen het budget (of de PgBouncer-cap) past.
  • Stel timeouts zodat “eeuwig wachten” problemen niet verbergt tot er een spike is.
  • Als je PgBouncer gebruikt, kies een pooling-modus die past bij je gebruik van sessiestate.
  • Vermijd zeer korte connection lifetimes die constante reconnects veroorzaken.
  • Controleer dat max_connections en gereserveerde verbindingen overeenkomen met je plan.

Rollout-plan dat rollback gemakkelijk houdt:

  1. Pas veranderingen toe in staging onder een load test die productie-concurrency en read/write-mix nabootst.
  2. Rol uit naar productie in kleine stappen (een subset van instances of één service tegelijk).
  3. Bekijk p95-latency, pool-wacht-tijd, errors en Postgres-connection counts door minstens één piekvenster heen.
  4. Als p95 omhoogschiet of pool-wacht sterk stijgt, rol terug en verlaag concurrency of pool-limieten.

Als je een Go + PostgreSQL-app bouwt en host op Koder.ai (koder.ai), kan Planning Mode je helpen de wijziging en wat je gaat meten in kaart te brengen, en snapshots plus rollback maken het makkelijker om terug te draaien als tail-latency slechter wordt.

Volgende stap: voeg één meting toe vóór de volgende verkeerspiek. "Time spent waiting for a connection" in de app is vaak het meest bruikbare, omdat het pooling-druk laat zien voordat gebruikers het voelen.

Veelgestelde vragen

What is connection pooling in Postgres, in plain terms?

Een pool houdt een klein aantal PostgreSQL-verbindingen open en hergebruikt die over meerdere requests. Dit voorkomt dat je steeds de opstartkosten (TCP/TLS, authenticatie, backend-proces opzetten) betaalt en helpt de tail-latency stabiel te houden tijdens pieken.

Why do latency spikes show up before I see “too many connections” errors?

Als de pool vol zit, wachten requests in je applicatie op een vrije verbinding en die wachttijd verschijnt als trage responses. Daardoor zie je vaak “willekeurige traagheid” omdat gemiddelden prima kunnen blijven terwijl p95/p99 opdraaien tijdens verkeerspieken.

Will pooling fix slow SQL queries?

Nee. Pooling verandert vooral hoe het systeem zich gedraagt bij load door reconnect-churn te verminderen en concurrency te beperken. Als een query langzaam is door scans, locks of slechte indexering, dan maakt pooling die query niet sneller; het beperkt alleen hoeveel trage queries tegelijk kunnen draaien.

What’s the difference between app-level pooling and PgBouncer?

App-pooling beheert verbindingen per proces, dus elke app-instance heeft zijn eigen pool en grenzen. PgBouncer staat vóór Postgres en handhaaft een globaal connectie-budget over veel clients, wat handig is bij veel replicas of piekerig verkeer.

When should I use only Go `database/sql` pooling, and when do I add PgBouncer?

Als je een klein aantal instances draait en het totaal aantal open verbindingen ruim onder de database-limiet blijft, is het afstemmen van Go’s database/sql-pool meestal voldoende. Voeg PgBouncer toe wanneer veel instances, autoscaling of bursty traffic het totaal aan verbindingen boven wat Postgres goed kan afhandelen zou duwen.

How do I choose a sane `MaxOpenConns` for a Go service?

Een handige aanpak is een connectie-budget voor de service vastleggen en dat door het aantal instances delen. Stel MaxOpenConns iets lager in dan dat resultaat per instance. Begin klein, bekijk wachttijd en p95/p99, en verhoog alleen als de database echt headroom heeft.

Which PgBouncer pooling mode should I pick for a Go HTTP API?

Transaction pooling is vaak een goede standaard voor typische HTTP-API’s omdat het veel client-verbindingen toestaat om een kleiner aantal server-verbindingen te delen en stabieler blijft bij pieken. Gebruik session pooling als je code afhankelijk is van sessiestate die persist over statements moet blijven.

What can break when PgBouncer is in transaction pooling mode?

Prepared statements, tijdelijke tabellen, advisory locks en sessie-instellingen kunnen anders werken omdat een client mogelijk niet steeds dezelfde server-verbinding krijgt. Als je die features nodig hebt, houd dan alles binnen één transactie per request of gebruik session pooling om verwarrende fouten te voorkomen.

What metrics best reveal pooling trouble early?

Houd p95/p99 in de gaten samen met app-side pool-wachttijd, want wachttijd stijgt vaak voordat gebruikers het merken. Op Postgres: actieve verbindingen, CPU en locks monitoren; op PgBouncer: client connections, server connections en queue depth om te zien of je het budget verzadigt.

What should I do if traffic exceeds my connection budget?

Stop eerst onbeperkt wachten door request-deadlines en statement_timeout in te stellen zodat één trage query geen verbindingen voor altijd blokkeert. Voeg daarna backpressure toe door concurrency te limiteren op DB-intensieve endpoints, load shedding (bijv. 429) of achtergrondjobs te scheiden, en vermijd te korte connection lifetimes die reconnect-storms veroorzaken.

Inhoud
Waarom latentiepieken vaak bij verbindingen beginnenApp-pooling vs PgBouncer: welk probleem lost elk opHoe Go `database/sql`-pooling zich echt gedraagtPgBouncer-basis en pooling-modiHoe je de juiste strategie kiest voor een Go-backendStapsgewijs: sizing en veilig uitrollen van poolingMetrieken om te monitoren zodat je problemen vroeg vangtVeelvoorkomende misconfiguraties die pieken veroorzakenWat te doen als de vraag je connectiebudget overschrijdtLoad testing en capaciteitsplanning zonder giswerkSnelle checklist en vervolgstappenVeelgestelde vragen
Delen
Koder.ai
Build your own app with Koder today!

The best way to understand the power of Koder is to see it for yourself.

Start FreeBook a Demo