PgBouncer Pool Sizing: A Runbook Before Your Next Deploy Storm
A database that can handle a normal day's traffic can still fall over during a deploy, when every application instance restarts and opens a fresh batch of connections at once. Connection pooling doesn't just save overhead per query, it's what stands between a rolling restart and a connection storm that pins your database's max_connections limit.
This covers PgBouncer's three pooling modes, how to size the pool, the deploy-time failure pattern most teams hit before they've thought about any of it, and the prepared-statement bug that usually shows up a week after someone switches modes without checking the driver.
Session, Transaction, and Statement Pooling Are Not Interchangeable
Session pooling assigns one database connection per client connection for its whole session, which is safe but doesn't save you much. Transaction pooling returns the connection to the pool between transactions, which is where most of the efficiency gain lives, but it breaks anything that depends on session state persisting across transactions: prepared statements, session-level advisory locks, `SET` commands meant to persist. Statement pooling is the most aggressive and breaks multi-statement transactions outright. Most production setups want transaction pooling, but only after an audit of what session-level features the application actually relies on, since the failures it introduces are often intermittent rather than immediate.
Sizing the Pool to Your Database, Not Your Traffic
The pool size should be set relative to your database's `max_connections` and how many other things share it, other services, background workers, a read replica's own connections, not to your peak concurrent request count. A common mistake is sizing the pool to match expected concurrency and discovering the database itself can't sustain that many active connections under load. Start from `max_connections` minus headroom for admin and replication connections, divide across the services that share the database, and size each pool from there, revisiting the split whenever a new service starts sharing the same cluster.
The Deploy-Time Connection Storm
A rolling deploy that restarts every application instance within a short window means every instance tries to reconnect to the pooler at roughly the same moment. If your pooler's own connection limit isn't sized for that burst, or your deploy doesn't stagger restarts, you get a thundering herd against the pooler itself, which then queues or rejects connections right when the new version needs them most. Staggering instance restarts and giving the pooler headroom above steady-state usage specifically for this burst is the fix most teams learn about during an incident instead of before one, usually the first time a deploy coincides with an already-busy traffic period.
Run through this before your next deploy:
- Size the pooler's own connection limit for a burst where every application instance reconnects at the same moment.
- Stagger restarts during a rolling deploy instead of restarting every instance within a short window.
- Keep thirty to fifty percent headroom above steady-state connections, adjusted for how many instances restart together.
- Disable server-side prepared statement caching in your driver or ORM before switching to transaction pooling.
- Alert on pool wait time, since queuing behind an exhausted pool shows up as latency before errors.
Prepared Statements Are the Most Common Transaction-Pooling Casualty
ORMs and drivers that use server-side prepared statements assume the same connection persists across queries, which transaction pooling doesn't guarantee. The failure shows up as intermittent, hard-to-reproduce errors about a prepared statement that "doesn't exist," because the client prepared it on one backend connection and PgBouncer handed the next query to a different one. The fix is usually disabling server-side prepared statement caching in the driver configuration when running behind a transaction-mode pooler, not abandoning pooling entirely; most modern drivers expose this as a single connection option once you know to look for it.
Monitoring Pool Saturation, Not Just Connection Count
Total connections in use tells you less than wait time for a connection: if requests are queuing behind pool exhaustion, that shows up as latency before it shows up as errors. Alert on pool wait time crossing a threshold, and separately on the ratio of active to idle connections trending toward saturation, so you catch the problem building during a traffic ramp instead of after requests start timing out waiting on the pool. A dashboard that only shows total connections in use will look healthy right up until it doesn't.
What to Check Before You Add a Second Pooler Layer
Some teams run a pooler per application instance in addition to a shared one in front of the database, to reduce connection count at each hop; this adds real value at scale but also adds a second layer where a misconfiguration can hide. Before adding a layer, confirm the problem is actually connection count rather than query latency or lock contention upstream of pooling entirely, since a second pooler won't fix a slow query, it will just queue more requests behind the same underlying bottleneck.
What Good Looks Like
The pool should be sized to your database's real connection ceiling, not your peak concurrent request count, and monitored on wait time, not just connection totals.
Building The Capability (5-Stage Skill Ladder)
How to Get Started
Frequently Asked Questions
Can we run transaction pooling if our ORM uses prepared statements?
Yes, but you need to disable server-side prepared statement caching in the driver or ORM configuration first, since the statement lifecycle assumes a persistent connection that transaction pooling doesn't provide. Most modern drivers have a flag for this; check your ORM's PgBouncer compatibility notes before switching modes.
How much headroom should the pooler have above steady-state connections?
Enough to absorb a full rolling deploy's worth of simultaneous reconnects without queuing, which for most teams means thirty to fifty percent above steady state, though the right number depends on how many instances restart within your deploy window.
Does connection pooling reduce load on the database itself, or just on connection setup overhead?
Both, but the bigger win is usually connection setup overhead. A database spending CPU on establishing and tearing down connections under load has less headroom for actual query execution, so pooling that keeps a smaller number of connections active longer frees up real capacity.
About the numbers
This guide doesn't quote a sourced benchmark. Figures in it are estimates or general guidance, so check them against your own numbers.
Related Guides
PgBouncer in Production: A Connection Pooling Checklist
Why Postgres runs out of connections before it runs out of CPU, and a rollout checklist for putting PgBouncer in front of it safely.
The Connection Pool Setting That Takes Down Production at 2am
Why connection pools exhaust under load, how PgBouncer's pool modes actually differ, and the four settings worth checking before your next incident.
The Connection Pooling Setup That Keeps Postgres From Falling Over Under Load
How connection exhaustion actually happens in Postgres, and the PgBouncer configuration that prevents a traffic spike from taking your database down.
What Actually Happens When Your Database Runs Out of Connections
A step-by-step walkthrough of how connection exhaustion happens, why adding more app servers makes it worse, and how a pooler like PgBouncer fixes it.
The Connection Pool Checklist Most Teams Skip Until an Outage
A pre-flight checklist for database connection pooling that catches the pool-exhaustion mistakes most teams only discover during a production outage.
PGBouncer and the Real Limits of Postgres Connection Pooling
Why Postgres connection limits break under load, how PGBouncer's pooling modes actually differ, and the failure modes worth checking for first.