Questo guadagno ha un costo specifico: la modalità di transazione interrompe silenziosamente tutto ciò che presuppone una connessione Postgres stabile da una richiesta a quella successiva. Sessione SET, ASCOLTA/NOTIFICA, blocchi consultivi, cursori che sopravvivono alla transazione, istruzioni preparate denominate. Questo articolo dettaglia il meccanismo, elenca questi limiti con i loro esatti sintomi, poi mostra come un backend in produzione (il nostro, verificato direttamente nel suo repository) lo configura senza esserne intrappolato. Per il metodo di misurazione alla base dei dati sulle prestazioni qui citati, consultare la nostra metodologia di benchmark .
L'essenziale
- Modalità transazione: la connessione al server viene rilasciata al termine di ogni transazione, non quando il client si disconnette. Questa è la modalità più efficiente per condividere connessioni brevi di tipo API REST.
- Incompatibile per costruzione con: sessione SET/RESET, LISTEN/NOTIFY, blocchi consultivi di sessione, cursori WITH HOLD, tabelle temporanee riutilizzate da una richiesta all'altra.
- La trappola più comune nella pratica: le istruzioni preparate con nome, che diversi driver (sqlx, asyncpg, il driver JDBC pgjdbc) attivano per impostazione predefinita, possono essere riprodotte su un'altra connessione al server e innescare un errore come
prepared statement does not existsotto carico. - Dalla versione 1.21, PgBouncer può seguire le istruzioni preparate dal protocollo in modalità transazione (cache LRU tramite connessione al server). Ciò non impedisce di disabilitare la cache lato client se l'applicazione cambia
search_patha ogni richiesta. - Verificato nel codice Aurabase: i pool tenant vengono eseguiti con
statement_cache_capacity(0)e PgBouncer inpool_mode=transaction, mentre PostgREST rimane volontariamente in connessione diretta per il ricaricamento dello schema tramite LISTEN/NOTIFY.
Le 3 modalità di pooling di PgBouncer
PgBouncer offre tre modalità, che differiscono solo nel momento in cui la connessione al server Postgres ritorna al pool comune. La documentazione ufficiale li nomina session, transaction e statement (pgbouncer.org/features.html, sezione "Modalità pooling", accesso il 24 agosto 2026).
| Moda | Connessione al server allentata | Compatibilità della sessione |
|---|---|---|
| sessione (impostazione predefinita) | Alla disconnessione del client | Totale: IMPOSTA, ASCOLTA, cursori, tutto funziona come dal vivo |
| transazione | Al termine di ogni transazione (COMMIT/ROLLBACK) | Parziale: solo ciò che rimane locale alla transazione |
| dichiarazione | Dopo ogni singola richiesta | Minimo: transazioni multi-query esplicite vietate |
La modalità Session è la più permissiva ma la meno efficace in termini di scalabilità: una connessione Postgres rimane riservata per un client finché rimane connesso, anche se non fa nulla tra due richieste. La modalità Statement è riservata a casi molto specifici (proxy di sola lettura, controlli di integrità) e interrompe anche le classiche transazioni esplicite. La modalità di transazione è il compromesso che domina nella pratica per un'API REST: ogni richiesta HTTP corrisponde generalmente a una singola breve transazione Postgres.
Come funziona la modalità transazione, connessione per connessione
In modalità transazione, PgBouncer collega una connessione server a un client solo quando quest'ultimo apre una transazione e la restituisce al pool dopo COMMIT o ROLLBACK. Tra due transazioni, lo stesso client potrebbe trovarsi riassegnato a una connessione server completamente diversa.
Concretamente, con un default_pool_size di 20, PgBouncer può assorbire diverse centinaia di clienti simultanei che, in ogni momento, hanno solo poche transazioni effettivamente in corso. È questo rapporto che giustifica la modalità di transazione per un'API REST con traffico elevato ma transazioni brevi: la risorsa rara (una connessione Postgres, costosa in memoria lato server) viene occupata solo per il tempo strettamente necessario.
Quest'ultimo commento riassume l'essenza: la modalità transazione funziona perché interrompe deliberatamente il collegamento tra "la mia sessione dell'applicazione" e "la mia connessione Postgres". Tutto basato su questo collegamento si interrompe. La sezione successiva elenca esattamente cosa.
Cosa si interrompe nella modalità di pooling delle transazioni
La documentazione ufficiale di PgBouncer elenca esplicitamente le funzionalità di PostgreSQL che perdono il loro significato non appena una connessione al server può essere riciclata tra due richieste dello stesso client.
| Funzionalità interessata | Perché si rompe | Sintomo tipico |
|---|---|---|
| IMPOSTA/IMPOSTA SESSIONE | L'impostazione si applica a una connessione che può essere riciclata immediatamente dopo | Un parametro sembra essere dimenticato casualmente tra due richieste |
| ASCOLTA/INFORMA | Presuppone una connessione permanente per ricevere notifiche | Il client non viene mai informato o solo in modo intermittente |
| Blocchi consultivi sulla sessione | Il blocco è mantenuto dalla connessione al server, non dal client logico | Un blocco viene rilasciato prima del completamento previsto oppure non viene mai rilasciato |
| CON cursori ATTESA | Deve sopravvivere oltre la transazione che lo ha aperto | Errore "il cursore non esiste" alla successiva iterazione |
| Tabelle temporanee | Relativo alla sessione Postgres, non alla transazione | La tabella "scompare" nella query successiva |
| Dichiarazioni preparate denominate | Preparato su una connessione server specifica, riprodotto su un'altra | "istruzione preparata... non esiste" sotto carico |
La maggior parte di queste limitazioni non si manifestano nello sviluppo locale, dove in genere una singola connessione serve tutto il traffico. Appaiono in condizioni di carico reale, quando diversi client condividono effettivamente il pool e una connessione al server passa effettivamente di mano tra due richieste dallo stesso client logico. Un test del fumo non li rivela quasi mai.
Dichiarazioni preparate: il limite più frainteso
La maggior parte dei moderni driver Postgres preparano richieste denominate sul lato protocollo per impostazione predefinita, senza che il codice dell'applicazione lo richieda esplicitamente. Questo è proprio ciò che rende difficile anticipare questa trappola.
Un'istruzione preparata dal protocollo viene denominata e memorizzata nella cache su una connessione server specifica, al momento di Parse. In modalità transazione, questa connessione può essere riassegnata a un altro client tra due richieste provenienti dallo stesso client logico. Se il driver riproduce quindi lo stesso nome dell'istruzione su una connessione in cui non è mai stato preparato, Postgres risponde con un errore esplicito, in genere prepared statement "sqlx_s_N" does not exist per un client sqlx. Il comportamento è intermittente: dipende da come si comportano le connessioni sotto carico, non da un bug deterministico riproducibile ad ogni chiamata.
La correzione lato client è la stessa indipendentemente dalla lingua: disabilitare la cache delle istruzioni preparate denominate o forzare query senza nome per qualsiasi pool che attraversa un pooler in modalità transazione. In Rust con sqlx, passa attraverso statement_cache_capacity(0) nelle opzioni di connessione.
Dalla versione 1.21, PgBouncer allevia parte del problema lato server: può seguire le istruzioni preparate dal protocollo in modalità transazione e prepararle al volo sulla connessione assegnata, con una cache LRU per connessione la cui dimensione viene regolata tramite max_prepared_statements. Ciò riduce il numero di errori, ma non esonera dal disabilitare la cache del client in un pool multi-tenant in cui search_path cambia da una richiesta all'altra: un piano memorizzato nella cache blocca l'identificatore interno (OID) della tabella risolta al momento di Parsee la sua riproduzione con un altro schema potrebbe restituire dati dal tenant sbagliato anziché un semplice errore.
Come Aurabase configura PgBouncer in modalità transazione
Il repository Aurabase distribuisce PgBouncer come pool_mode=transaction davanti al piano dati condiviso (deploy/helm/aurabase/templates/infra/pgbouncer.yaml) e un CNPG Pooler configurato in modo identico davanti a ciascuna istanza Postgres dedicata di un tenant (deploy/cnpg/tenant-pooler.yaml). Entrambi i percorsi applicano la stessa disciplina sopra descritta.
Il codice sorgente documenta una specifica ragione di sicurezza per questa scelta, non solo una ragione di stabilità. I pool Postgres condivisi tra tenant posizionano un search_path diverso per progetto sulle connessioni riutilizzate. Un'istruzione preparata memorizzata nella cache blocca l'OID della tabella risolta al momento di Parse; riprodurlo per un altro tenant sulla stessa connessione eseguirebbe la query sullo schema del primo tenant, un bypass di isolamento, non solo un errore dell'applicazione. statement_cache_capacity(0) viene quindi applicato senza eccezioni, anche su istanze dedicate che passano anche attraverso un CNPG Pooler in modalità transazione.
Seconda misura di igiene della sessione: quando ogni connessione ritorna al pool, un hook esegue DISCARD ALL (reimpostazione delle impostazioni, deallocazione delle istruzioni preparate sul lato server, rilascio dei blocchi consultivi, eliminazione dei cursori e delle tabelle temporanee). Senza questo hook, un residuo di sessione posto da una richiesta precedente potrebbe fuoriuscire nella richiesta successiva da un tenant diverso che riutilizza la stessa connessione riciclata.
Eccezione accettata: le istanze PostgREST dedicate rimangono in connessione diretta al primario, senza passare attraverso il pooler. Il ricaricamento dello schema PostgREST si basa su LISTEN/NOTIFY, che presuppone una connessione persistente, esattamente la funzionalità interrotta dalla modalità di transazione (dettagli già documentati nel nostro articolo sulla compatibilità PostgREST su Aurabase). Le impostazioni RLS per richiesta vengono passate a SET LOCAL all'interno di una transazione esplicita, l'unico modo per rimanere compatibile con un pool che può modificare le connessioni del server in qualsiasi COMMIT (vedere il nostro articolo sull'isolamento RLSmulti-tenant).
Abilita la modalità transazione senza interrompere l'applicazione
Una breve checklist, applicabile a qualsiasi backend che passa da una connessione Postgres diretta a un PgBouncer in modalità transazione.
- Controlla il codice dell'applicazione. Cerca
SETdi non transazione,LISTEN/NOTIFY, blocchi consultivi di sessione, cursoriWITH HOLDe tabelle temporanee riutilizzate tra le query. - Sostituisci i SET di sessione con i SET LOCALI all'interno di una transazione esplicita. Questa è l'unica impostazione che sopravvive correttamente al riciclo della connessione, perché viene ripulita al COMMIT/ROLLBACK invece che perdere alla connessione successiva.
- Disabilita la cache delle istruzioni preparate lato driver se il pool attraversa il pooler e lo schema o il ruolo cambia da una richiesta all'altra. Il costo delle prestazioni è reale ma misurabile e molto inferiore al rischio di perdite tra inquilini.
- Isola le connessioni che necessitano realmente della modalità sessione (migrazioni, script di amministrazione, tutto ciò che dipende da LISTEN/NOTIFY) su una connessione diretta non pooler, anziché rinunciare alla modalità transazione per tutto il resto del traffico.
- Dimensioni
default_pool_sizeemax_client_connrelative all'effettivomax_connectionsdi Postgres, non da una cifra arbitraria copiata da un altro progetto. - Test sotto carico reale, non solo test del fumo. Gli errori delle istruzioni preparate e le perdite di impostazioni della sessione non vengono quasi mai visualizzati su una singola connessione locale.
- Monitora
SHOW POOLSeSHOW STATSdalla console di amministrazione PgBouncer una volta in produzione, per individuare la saturazione del pool prima che diventi visibile sul lato client.
Dovresti sempre scegliere la modalità di transazione anziché la sessione?
No, ma è la scelta predefinita corretta per la stragrande maggioranza delle API REST. La modalità sessione rimane preferibile per un'applicazione legacy fortemente dipendente dalle funzionalità di sessione di cui non è possibile eseguire il refactoring rapidamente o per un traffico ridotto in cui il guadagno di pooling non compensa lo sforzo di migrazione.
PgBouncer non è nemmeno l'unica implementazione di questo modello di pooling: Supavisor (Supabase) e PgCat sono due alternative recenti, con diversi compromessi sulla distribuzione del carico e sul clustering. Guarda il nostro confronto dettagliato, PgBouncer vs Supavisor vs PgCat, per scegliere tra i tre a seconda della tua topologia.
Domande frequenti
Le domande che sorgono più spesso una volta attivata la modalità transazione in produzione.