Deze winst brengt specifieke kosten met zich mee: de transactiemodus verbreekt stilletjes alles wat een stabiele Postgres-verbinding veronderstelt, van het ene verzoek naar het volgende. Sessie SET, LISTEN/NOTIFY, adviesvergrendelingen, cursors die de transactie overleven, benoemde voorbereide verklaringen. Dit artikel beschrijft het mechanisme, somt deze limieten op met hun exacte symptomen, en laat vervolgens zien hoe een backend in productie (de onze, rechtstreeks geverifieerd in de repository) deze configureert zonder erdoor gevangen te worden. Voor de meetmethode achter de hier genoemde prestatiecijfers, zie onze benchmarkmethodologie.
De essentie
- Transactiemodus: de serververbinding wordt aan het einde van elke transactie vrijgegeven, niet wanneer de client de verbinding verbreekt. Dit is de meest efficiënte modus voor het delen van korte verbindingen van het REST API-type.
- Incompatibel door constructie met: sessie SET/RESET, LISTEN/NOTIFY, sessie-adviesvergrendelingen, WITH HOLD-cursors, tijdelijke tabellen die van het ene verzoek naar het andere worden hergebruikt.
- De meest voorkomende valkuil in de praktijk: benoemde voorbereide instructies, die standaard door verschillende stuurprogramma's (sqlx, asyncpg, het JDBC pgjdbc-stuurprogramma) worden geactiveerd, kunnen op een andere serververbinding worden afgespeeld en onder belasting een fout als
prepared statement does not existactiveren. - Sinds versie 1.21 kan PgBouncer protocol-opgestelde instructies volgen in transactiemodus (LRU-cache via serververbinding). Dit verhindert niet dat u de cache aan de clientzijde uitschakelt als uw toepassing bij elk verzoek
search_pathwijzigt. - Geverifieerd in de Aurabase-code: de tenantpools draaien met
statement_cache_capacity(0)en PgBouncer inpool_mode=transaction, terwijl PostgREST vrijwillig in directe verbinding blijft voor het herladen van het schema via LISTEN/NOTIFY.
De 3 poolmodi van PgBouncer
PgBouncer biedt drie modi, die alleen verschillen wanneer de Postgres-serververbinding terugkeert naar de gemeenschappelijke pool. De officiële documentatie noemt ze session, transaction en statement (pgbouncer.org/features.html, sectie "Poolingmodi", geraadpleegd op 24 augustus 2026).
| Mode | Serververbinding los | Compatibiliteit van sessies |
|---|---|---|
| sessie (standaard) | Bij het verbreken van de clientverbinding | Totaal: SET, LISTEN, cursors, alles werkt als live |
| transactie | Aan het einde van elke transactie (COMMIT/ROLLBACK) | Gedeeltelijk: alleen wat lokaal blijft voor de transactie |
| verklaring | Na elk individueel verzoek | Minimaal: expliciete transacties met meerdere zoekopdrachten zijn verboden |
De sessiemodus is het meest tolerant, maar het minst effectief wat betreft schaalbaarheid: een Postgres-verbinding blijft gereserveerd voor een client zolang deze verbonden blijft, zelfs als deze tussen twee verzoeken niets doet. De verklaringsmodus is gereserveerd voor zeer specifieke gevallen (alleen-lezen proxy, gezondheidscontroles) en verbreekt zelfs klassieke expliciete transacties. De transactiemodus is het compromis dat in de praktijk domineert voor een REST API: elk HTTP-verzoek komt doorgaans overeen met een enkele korte Postgres-transactie.
Hoe de transactiemodus werkt, verbinding voor verbinding
In de transactiemodus maakt PgBouncer alleen een serververbinding met een client wanneer deze een transactie opent, en stuurt deze terug naar de pool bij COMMIT of ROLLBACK. Tussen twee transacties kan het voorkomen dat dezelfde client opnieuw wordt toegewezen aan een geheel andere serververbinding.
Concreet kan PgBouncer met een default_pool_size van 20 honderden gelijktijdige klanten absorberen die op een gegeven moment slechts een paar lopende transacties hebben. Het is deze verhouding die de transactiemodus rechtvaardigt voor een REST API met veel verkeer maar korte transacties: de zeldzame bron (een Postgres-verbinding, duur in geheugen aan de serverzijde) wordt alleen bezet voor de tijd die strikt noodzakelijk is.
Deze laatste opmerking vat de essentie samen: de transactiemodus werkt omdat het opzettelijk de link tussen "mijn applicatiesessie" en "mijn Postgres-verbinding" verbreekt. Alles gebaseerd op deze link gaat kapot. In het volgende gedeelte wordt precies vermeld wat.
Wat breekt er in de modus voor transactiepooling
De officiële PgBouncer-documentatie vermeldt expliciet PostgreSQL-functies die hun betekenis verliezen zodra een serververbinding kan worden gerecycled tussen twee verzoeken van dezelfde client.
| Beïnvloede functionaliteit | Waarom breekt het | Typisch symptoom |
|---|---|---|
| INSTELLEN / SESSIE INSTELLEN | De instelling geldt voor een verbinding die direct daarna kan worden gerecycled | Het lijkt erop dat een parameter tussen twee verzoeken willekeurig wordt vergeten |
| LUISTEREN / MELDEN | Er wordt uitgegaan van een permanente verbinding om meldingen te ontvangen | De klant wordt nooit of slechts sporadisch op de hoogte gebracht |
| Sessie-adviessloten | De vergrendeling wordt vastgehouden door de serververbinding, niet door de logische client | Een vergrendeling wordt vrijgegeven vóór de verwachte voltooiing, of wordt nooit vrijgegeven |
| MET HOLD-schuifregelaars | Moet overleven na de transactie waarmee het werd geopend | Fout 'cursor bestaat niet' bij de volgende iteratie |
| Tijdelijke tafels | Gerelateerd aan de Postgres-sessie, niet aan de transactie | De tabel "verdwijnt" bij de volgende query |
| Opgestelde verklaringen genoemd | Voorbereid op een specifieke serververbinding, afgespeeld op een andere | "voorbereide verklaring... bestaat niet" onder belasting |
De meeste van deze beperkingen komen niet tot uiting in de lokale ontwikkeling, waar doorgaans één enkele verbinding al het verkeer bedient. Ze verschijnen onder echte belasting, wanneer verschillende clients de pool daadwerkelijk delen en een serververbinding daadwerkelijk van eigenaar wisselt tussen twee verzoeken van dezelfde logische client. Een rooktest brengt ze vrijwel nooit aan het licht.
Opgestelde verklaringen: de meest onbegrepen limiet
De meeste moderne Postgres-stuurprogramma's bereiden standaard benoemde verzoeken aan de protocolkant voor, zonder dat de applicatiecode hier expliciet om vraagt. Dit is precies wat het moeilijk maakt om op deze val te anticiperen.
Een op protocol voorbereide instructie wordt benoemd en in de cache opgeslagen op een specifieke serververbinding, op het moment van Parse. In de transactiemodus kan deze verbinding opnieuw worden toegewezen aan een andere client tussen twee verzoeken van dezelfde logische client. Als het stuurprogramma vervolgens dezelfde instructienaam opnieuw afspeelt op een verbinding waar deze nooit is voorbereid, reageert Postgres met een expliciete fout, meestal prepared statement "sqlx_s_N" does not exist voor een sqlx-client. Het gedrag is intermitterend: het hangt af van hoe de verbindingen presteren onder belasting, en niet van een deterministische bug die bij elke oproep reproduceerbaar is.
De correctie aan de clientzijde is hetzelfde, ongeacht de taal: schakel de cache van benoemde voorbereide instructies uit, of forceer niet-benoemde zoekopdrachten, voor elke pool die een pooler kruist in de transactiemodus. In Rust met sqlx gaat het via statement_cache_capacity(0) over de verbindingsopties.
Sinds versie 1.21 verhelpt PgBouncer een deel van het probleem aan de serverzijde: het kan in het protocol voorbereide instructies volgen in de transactiemodus en deze direct voorbereiden op de toegewezen verbinding, met een LRU-cache per verbinding waarvan de grootte wordt aangepast via max_prepared_statements. Dit vermindert het aantal missers, maar ontslaat u niet van het uitschakelen van de clientcache in een pool met meerdere tenants waar de search_path van het ene verzoek naar het andere verandert: een in de cache opgeslagen plan bevriest de interne identificatie (OID) van de opgeloste tabel op het moment van Parse, en het opnieuw afspelen ervan onder een ander schema kan gegevens van de verkeerde tenant retourneren in plaats van een eenvoudige fout.
Hoe Aurabase PgBouncer configureert in transactiemodus
De Aurabase-repository implementeert PgBouncer als pool_mode=transaction vóór het gedeelde datavlak (deploy/helm/aurabase/templates/infra/pgbouncer.yaml), en een identiek geconfigureerde Pooler CNPG vóór elke speciale Postgres-instantie van een tenant (deploy/cnpg/tenant-pooler.yaml). Bij beide trajecten wordt dezelfde discipline toegepast als hierboven beschreven.
De broncode documenteert een specifieke veiligheidsreden voor deze keuze, niet alleen een stabiliteitsreden. Postgres-pools die tussen tenants worden gedeeld, positioneren een andere search_path per project op hergebruikte verbindingen. Een in de cache opgeslagen instructie bevriest de OID van de opgeloste tabel op het moment van Parse; Als u de query opnieuw afspeelt voor een andere tenant op dezelfde verbinding, wordt de query uitgevoerd op basis van het schema van de eerste tenant, een isolatie-bypass en niet alleen een toepassingsfout. statement_cache_capacity(0) wordt daarom zonder uitzondering toegepast, ook op speciale instanties die ook in transactiemodus via een CNPG Pooler gaan.
Hygiënemaatregel voor de tweede sessie: wanneer elke verbinding terugkeert naar de pool, voert een hook DISCARD ALL uit (instellingen resetten, de toewijzing van voorbereide instructies aan de serverzijde ongedaan maken, adviesvergrendelingen vrijgeven, cursors en tijdelijke tabellen opschonen). Zonder deze hook zou een sessieresidu van een eerder verzoek kunnen lekken bij het volgende verzoek van een andere tenant die dezelfde gerecyclede verbinding hergebruikt.
Uitzondering geaccepteerd: speciale PostgREST-instanties blijven in directe verbinding met de primaire instantie, zonder via de pooler te gaan. Het herladen van het PostgREST-schema is afhankelijk van LISTEN/NOTIFY, dat uitgaat van een persistente verbinding, precies de functionaliteit die de transactiemodus verbreekt (details zijn al gedocumenteerd in ons artikel over PostgREST-compatibiliteit bij Aurabase). RLS-instellingen per verzoek worden binnen een expliciete transactie doorgegeven aan SET LOCAL, de enige manier om compatibel te blijven met een pool die serververbindingen bij elke COMMIT kan wijzigen (zie ons artikel overmulti-tenant RLS-isolatie).
Schakel de transactiemodus in zonder uw applicatie te verbreken
Een korte checklist, toepasbaar op elke backend die overstapt van een directe Postgres-verbinding naar een PgBouncer in transactiemodus.
- Controleer de applicatiecode. Zoek naar niet-transactie
SET,LISTEN/NOTIFY, sessie-adviesblokkeringen,WITH HOLDcursors en tijdelijke tabellen die tussen query's worden hergebruikt. - Vervang sessie-SETs door LOCAL SETs binnen een expliciete transactie. Dit is de enige instelling die het recyclen van verbindingen goed overleeft, omdat deze wordt opgeschoond bij COMMIT/ROLLBACK in plaats van te lekken bij de volgende verbinding.
- Schakel de cache voor voorbereide instructies aan de bestuurderszijde uit als uw pool de pooler doorloopt en het schema of de rol van het ene verzoek naar het andere verandert. De prestatiekosten zijn reëel maar meetbaar, en veel lager dan het risico op lekkage tussen huurders.
- Isoleer verbindingen die echt de sessiemodus nodig hebben (migraties, beheerdersscripts, alles wat afhankelijk is van LISTEN/NOTIFY) naar een directe niet-poolerverbinding, in plaats van af te zien van de transactiemodus voor al het andere verkeer.
- Grootte
default_pool_sizeenmax_client_connrelatief ten opzichte van Postgres' werkelijkemax_connections, niet door een willekeurig getal gekopieerd uit een ander project. - Test onder echte belasting, niet alleen een rooktest. Opgestelde instructiefouten en lekken van sessie-instellingen verschijnen bijna nooit op een enkele lokale verbinding.
- Monitor
SHOW POOLSenSHOW STATSvanaf de PgBouncer-beheerconsole zodra deze in productie is, om poolverzadiging te detecteren voordat deze zichtbaar wordt aan de clientzijde.
Moet u altijd de transactiemodus kiezen in plaats van de sessie?
Nee, maar het is de juiste standaardkeuze voor de overgrote meerderheid van REST API's. De sessiemodus blijft de voorkeur genieten voor een oudere applicatie die sterk afhankelijk is van sessiefunctionaliteiten die u niet snel kunt herstructureren, of voor weinig verkeer waarbij de poolingwinst de migratie-inspanning niet compenseert.
PgBouncer is ook niet de enige implementatie van dit poolingmodel: Supavisor (Supabase) en PgCat zijn twee recente alternatieven, met verschillende afwegingen op het gebied van belastingverdeling en clustering. Bekijk onze gedetailleerde vergelijking, PgBouncer vs Supavisor vs PgCat, om tussen de drie te kiezen, afhankelijk van uw topologie.
Veelgestelde vragen
De vragen die het vaakst naar voren komen zodra de transactiemodus in productie is geactiveerd.