Notes
← All posts

Connection pooling is not a cache

A pattern I've now watched play out three times: an application gets slow under load, someone raises max_connections, and it gets slower. Then they raise it again.

Why more connections make it worse

Every Postgres connection is a backend process. Each one costs memory for its own work area and, more importantly, adds contention on shared structures. Past the point where active queries exceed available cores, additional connections don't add throughput — they add context switching and lock waiting.

The throughput curve has a peak and then falls off. Most tuning advice puts that peak somewhere near cores × 2 plus a small allowance for disk concurrency. That number is usually far smaller than people expect.

What pgbouncer actually does

It does not make queries faster. It decouples the number of clients from the number of backends, so a hundred application workers can share a dozen server connections. In transaction pooling mode a connection is handed back to the pool at commit, which is why the ratio can be so lopsided.

The cost is that session state stops being reliable. Anything that assumes a connection persists across transactions breaks:

That last one causes the most confusion, because it usually works fine in staging and fails intermittently under real concurrency.

The order I'd do it in

  1. Measure how many queries are actually active, not how many connections are open. pg_stat_activity filtered on state = 'active' is the number that matters.
  2. Lower max_connections until it's close to that peak.
  3. Put a pooler in front so clients stop getting refused.
  4. Only then look at the queries themselves.

Step four is usually where the real problem was.