PgBouncer in Production: A Connection Pooling Checklist
Postgres starts refusing new connections while CPU sits comfortably under load, because every open connection costs memory whether it's running a query or sitting idle, and each autoscaled instance or serverless function opens its own.
A connection pooler like PgBouncer fixes the ceiling, but only if you understand which pooling mode you're running, because the wrong one silently breaks features your code already depends on.
Why Direct Connections Run Out Before CPU Does
Postgres reserves a fixed chunk of memory per connection for things like sort buffers and cached query plans, and the default max_connections setting is deliberately conservative. Add an autoscaling app tier, or a burst of short-lived serverless invocations each opening their own connection, and you can exhaust that ceiling long before the database is doing meaningful work.
A connection storm often follows a deploy or a traffic spike: new instances all boot at once and each opens its full pool immediately, rather than ramping up gradually.
Transaction Mode vs Session Mode
Session pooling hands a client one dedicated backend connection for the life of its session, which is safe but doesn't multiply capacity much. Transaction pooling shares a small set of backend connections across many clients, checking a connection back into the pool between transactions instead of between sessions, which is where the real capacity gain comes from.
That gain has a cost: anything scoped to a session rather than a transaction, prepared statement caching, advisory locks, SET commands, LISTEN and NOTIFY, can behave incorrectly under transaction pooling because the next statement might land on a different backend connection entirely.
Sizing the Pool Without Guessing
Set the pooler's default_pool_size based on what the database can actually sustain, not a round number that felt safe. If you run several logical pools against one Postgres instance, their sizes need to add up to something the instance's own max_connections can hold, with headroom for administrative connections.
Watch pool wait time as the signal that matters, not database CPU. A pool that's too small shows rising wait time for a free connection well before the database itself looks stressed, which is exactly the early warning you want.
A Rollout Checklist
- Audit application code for anything that depends on session-level state: advisory locks, prepared statement caching, LISTEN and NOTIFY.
- Put the pooler in front of a read replica first, where the blast radius of a mistake is smaller.
- Record pool wait time before and after the change so you have a real before-and-after comparison, not a guess.
- Set a client-side statement timeout so one stuck query can't hold a pooled connection hostage indefinitely.
Common Mistakes
- Pointing every service at one shared pooler instance with no per-service limit, so a noisy service can starve a quiet one.
- Forgetting that schema migrations generally need a direct connection, because DDL statements don't behave well under transaction pooling.
- Watching database CPU as the health signal instead of watching pool saturation, which hides the actual bottleneck until it's already an outage.
What Changes When You Add a Second Pooler Layer
Some teams run PgBouncer per application instance in addition to a shared pooler in front of the database, usually to smooth out very bursty local connection patterns before they ever reach the shared layer. That extra hop adds a small amount of latency to every query, so it's worth the added complexity only when the shared pooler is genuinely the bottleneck, not by default.
Whichever layout you land on, keep the pooling topology documented somewhere every engineer can find, including which mode each layer runs and why. A pooler that silently switched from session to transaction mode during an unrelated configuration change is a common way teams rediscover the session-state gotchas months after everyone forgot they existed.
Review the pool configuration whenever the underlying database instance size changes too. A pool sized correctly for a smaller instance can quietly become the bottleneck after an upgrade if nobody revisits default_pool_size to match the new headroom, leaving capacity on the table that the database could actually serve.
What Good Looks Like
Good connection pooling means your application never runs out of database connections under load, and you can see rising pool wait time before customers notice slow requests.
Building The Capability (5-Stage Skill Ladder)
How to Get Started
Frequently Asked Questions
Does adding a pooler mean I don't need to raise max_connections on Postgres?
Usually yes, that's the point: a pooler lets hundreds of application connections share a much smaller number of real backend connections to Postgres. You may still raise the database's own limit a little for headroom, but the pooler should be doing most of the work of absorbing client-side concurrency.
What breaks first when you switch to transaction pooling?
Session-scoped features usually break first, including prepared statement caching, advisory locks, and LISTEN and NOTIFY subscriptions. Prepared statement caching assumes the same backend connection every time, advisory locks can be released unexpectedly, and LISTEN and NOTIFY subscriptions stop receiving events once the client's next statement lands on a different backend connection. Audit for these before you switch modes.
How do I know my connection pool is too small?
Watch pool wait time, the time a client spends waiting for a free connection before it can run a query. Rising wait time while your database's own CPU and query latency look normal is the clearest sign the pool itself, not the database, is the bottleneck.
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 Pool Sizing: A Runbook Before Your Next Deploy Storm
How to size a PgBouncer pool, pick a pooling mode, and stop connection storms during deploys from taking down your database.
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.
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.
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.
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.