Developer Productivity & Platform EngineeringPlaybook3 min readUpdated September 2026

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.

Executive Capability Standard

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)

1. Learn:Graph your current connection count against max_connections over your last few traffic spikes to see how close you've already come to the ceiling.
2. Do Manually:Manually audit the two or three services most likely to depend on session-scoped features before you turn on transaction pooling for them.
3. Delegate:Give one engineer ownership of pool sizing and the pool wait time dashboard so sizing decisions aren't made ad hoc during an incident.
4. Automate:Alert automatically on rising pool wait time so you catch undersizing before it turns into rejected connections.
5. Buy:Bring in a database specialist to review pool sizing and session-dependent code paths before a large migration to transaction pooling.

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