Fixing 'Too Many Connections' Without Just Raising the Limit
The error is always the same: the database refuses new connections, the app starts throwing timeouts, and someone's first instinct is to raise max_connections and move on. That buys a few hours, not a fix. Postgres holds real memory per connection, so raising the ceiling without finding out why you needed more connections in the first place usually just delays the same outage until traffic is a little higher.
This walks through a common incident pattern: connections climbing steadily until the database refuses new ones, the diagnosis that finds the real cause, and the pool configuration that holds up during a traffic spike.
The Symptom: Connections Maxed Out Overnight
A common pattern: connection count climbs slowly through the day, nothing looks wrong at normal traffic, and then a batch job or a burst of background workers pushes it over the edge overnight when nobody's watching a dashboard. By the time anyone notices, the database has been rejecting new connections for a while and every service touching it is throwing errors.
The mistake is treating this as a capacity problem. A slow, steady climb in connection count almost never means you need more capacity. It means something is opening connections and not giving them back, and more capacity just gives the leak more room to run before it matters.
Where Connections Actually Go
Three layers can each be holding connections open: your application's own connection pool in the ORM or driver, a pooler like PgBouncer sitting between the app and the database, and Postgres itself. Confusing these layers is the most common reason a fix doesn't work. Raising your app's pool size does nothing if PgBouncer is the layer that's exhausted. Raising PgBouncer's pool does nothing if Postgres itself is capped lower.
Check all three independently: your app framework's pool metrics, PgBouncer's SHOW POOLS output if you're running it, and Postgres's own pg_stat_activity. Whichever one is actually maxed out first tells you where the real bottleneck is, and it's often not the layer closest to the alert that fired.
Finding the Leak Before Touching Any Config
Query pg_stat_activity for connections that have been idle in a transaction for more than a few minutes. That state, idle in transaction, is the single most common cause of a slow connection leak: a request started a transaction, did some work, and then never committed or rolled back, usually because of an exception path that skips cleanup or a connection returned to the pool without the driver closing the transaction first.
Group the idle connections by application name or query text if your driver sets one. That usually points straight at the endpoint or background job responsible, rather than leaving you guessing across the whole codebase. Fix the code path first. A pool sized correctly around a leak just runs out slower.
Sizing the Pool Correctly
Once the leak is fixed, size the pool by what your database can actually serve, not by how many concurrent requests your app might see. A single Postgres instance on typical hardware serves a surprisingly small number of connections well; each one carries real memory overhead, and pushing past what the CPU and disk can actually process just means requests wait longer inside the database instead of failing fast at the pool.
The rule of thumb that holds up in practice: size connections around available CPU cores plus a margin for background jobs, not the peak number of app instances you might scale to. If you need to support more concurrent app connections than that, put a pooler in front of Postgres rather than raising Postgres's own connection ceiling.
Transaction Mode vs. Session Mode in PgBouncer
If you're adding a pooler, the mode setting matters more than the pool size:
- Session mode assigns a database connection to a client for the whole session, which supports every Postgres feature but gives almost no multiplexing benefit.
- Transaction mode returns the connection to the pool as soon as a transaction commits, letting far more clients share the same small set of database connections.
- Transaction mode breaks features that depend on session state persisting across queries: prepared statements, session-level advisory locks, and some ORMs' connection-level settings.
- Test your actual query patterns against transaction mode before rolling it out, since the failure mode is often a confusing error in production rather than a clean startup failure.
Most teams that switch from session to transaction mode see their effective pool capacity increase substantially, but only after finding and fixing the handful of queries that assumed session state would persist.
What Good Looks Like
Good connection pool health means your team can say, at any time, which layer, app pool, PgBouncer, or Postgres itself, is closest to its limit, instead of finding out during an outage.
Building The Capability (5-Stage Skill Ladder)
How to Get Started
Frequently Asked Questions
Should we just raise max_connections when we see this error?
Only as a short-term stopgap. Each connection holds real memory, so raising the ceiling without finding the actual leak usually delays the same outage rather than fixing it. Find what's holding connections open first, then size the pool around your real, leak-free usage.
What does idle in transaction actually mean?
It means a connection started a transaction and never committed or rolled it back. That's the most common cause of a slow connection leak, usually from an exception path that skips cleanup. Query pg_stat_activity for connections stuck in this state to find the responsible code path.
Do we need PgBouncer if our app already has a connection pool?
Your app's pool limits connections from one instance. PgBouncer limits total connections to the database across every instance and service that talks to it. If you run more than a couple of app instances, you likely need both layers, not just one.
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
Build vs. Buy for Verifying Every Device That Connects In
What zero-trust device and identity verification actually requires, what a platform gives you over a homegrown check, and how to decide between them.
Rolling Out Agentic Workflows Without Breaking Production
A practical rollout checklist for shipping an AI agent to production, from a shadow-mode test run through the guardrails that catch it if it misbehaves.
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.
Fixing Connection Pool Exhaustion Before PgBouncer Runs Dry
Why Postgres connection pools run out under normal load, the difference session and transaction pooling make, and how to size PgBouncer correctly.
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.
How to Pick a Shard Key You Won't Regret Later
The criteria that actually predict whether a shard key will hold up, including the resharding cost most teams underestimate until they're stuck with it.