# Which PgBouncer pooling mode should I pick — Session, Transaction, or Statement?

> Pick Transaction mode for a web fleet; the price is protocol-level prepared statements, so disable them in the app or turn on PgBouncer 1.21+ support.

- Asked: 2026-05-20
- Answered: 2026-05-23
- Asked by: Pelin
- Tags: performans, postgresql, veritabani
- Source: https://www.muhammetsafak.com.tr/en/just-ask/pgbouncer-pooling-modes-and-prepared-statements/
- Language: en-US
- Author: Muhammet Şafak

---
**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?


Short answer: for a web/microservice fleet, pick **Transaction mode** — for multi-statement traffic, it's the most practical option for maximizing 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`.

1. **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.
2. **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.
3. **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.
4. **The real catch: Transaction mode breaks prepared statements and session state.** Protocol-level prepared statements and connection-bound state like `SET`, advisory locks, and `LISTEN` are lost once you land on a different server connection. Fix: disable client-side prepared statements (e.g. PDO `ATTR_EMULATE_PREPARES`) **or** turn on PgBouncer 1.21+ prepared-statement support; and never rely on session state across statements.
5. **Keep `default_pool_size` sane.** 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

- [Is PostgreSQL Enough for Everything?](https://sade.dev/en/journal/is-postgresql-enough-for-everything) — sade.dev
- [Should I use a partial index on a queue table where I only ever scan 'pending' rows?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-use-a-partial-index-on-a-queue-table-where/) — Just Ask
- [How do I choose the right composite index column order in PostgreSQL?](https://www.muhammetsafak.com.tr/en/just-ask/choosing-the-right-composite-index-order-in-postgresql/) — Just Ask
- [Why does a query use an Index Scan in staging but a Seq Scan in production, and how do I diagnose it with EXPLAIN?](https://www.muhammetsafak.com.tr/en/just-ask/why-does-a-query-use-an-index-scan-in-staging-but/) — Just Ask
