Which PgBouncer pooling mode should I pick — Session, Transaction, or Statement?
Question
Our microservices (~50 pods total) connect directly to PostgreSQL, and each pod opens at least 10 connections. Under heavy load we hit the `max_connections` limit (say 500) and start getting errors; raising the limit multiplies RAM vertically. We're planning to put PgBouncer in between. Which of Session, Transaction, or Statement mode should we pick? And how do our app's prepared statements get affected by that choice?
Answer
Short answer: for a web/microservice fleet, pick Transaction mode — it’s the one option that maximizes connection reuse.
What you’re hitting is clear: 50 pods × min connections exhausts max_connections. PgBouncer solves exactly this — it multiplexes many client connections onto a few server connections, so you rescue RAM without inflating max_connections.
- Transaction mode gives the highest reuse. It returns the server connection to the pool at the end of each transaction; two consecutive transactions may land on different server connections. For hundreds of short-lived microservice requests this is the right mode, because connections don’t sit idle.
- Session mode throws the benefit away. It binds one server connection to each client for its whole session — so in practice your 50 pods still hold 50+ server connections and the multiplexing gain disappears. Only worth it if you genuinely need session state.
- Statement mode is too restrictive. It returns the connection at the end of each statement, which breaks multi-statement transactions. In practice it’s not used for web apps; it only works in single-statement, autocommit worlds.
- The real catch: Transaction mode breaks prepared statements and session state. Protocol-level prepared statements and connection-bound state like
SET, advisory locks, andLISTENare lost once you land on a different server connection. Fix: disable client-side prepared statements (e.g. PDOATTR_EMULATE_PREPARES) or turn on PgBouncer 1.21+ prepared-statement support; and never rely on session state across statements. - Keep
default_pool_sizesane. Don’t make the server-connection pool huge — the whole point of multiplexing is serving many clients with few server connections. Measure your real concurrency first, then size the pool to it.
Bottom line: I’d pick Transaction mode, disable server-side/protocol prepared statements in the app (or upgrade PgBouncer to 1.21+), and keep default_pool_size tight against measured concurrency. Session mode doesn’t solve your problem, and Statement mode breaks your app. And by the way — solving connection pooling instead of multiplying max_connections and RAM vertically is often exactly the move that keeps PostgreSQL “enough”; I dug into that debate in the sade.dev piece.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.