On this page
Connection Pool Exhausted: What It Is and How to Fix It
Pool exhaustion means connections are held rather than missing. Learn why raising the limit makes it worse, how to find the leak, and the four real causes.
Quick answer
- “Pool exhausted” means all your allowed connections are currently in use — it is about circulation, not quantity.
- Raising the pool size is usually the wrong fix. It moves the queue into the database, where it hits
max_connectionsand gets worse.- The four real causes: a connection never returned (leak), a slow query holding it, transactions left open, or a pool too small for genuine concurrency.
- Diagnose by watching the checked-out count over time. Monotonic growth that never returns to baseline is a leak.
What does connection pool exhausted mean?
A connection pool is a fixed set of database connections your application reuses, rather than opening a new connection per request. The pool has a maximum size, and a connection is checked out for the duration of a request and returned afterwards.
“Pool exhausted” means every connection in that set is currently checked out, so a request that needs the database cannot get one and waits. The important insight is that this is usually not a shortage of connections — it is a failure to circulate them. The same number of connections is available either way; the problem is that too many are being held at once.
That distinction is why the instinctive fix is often wrong. If you double the pool size because requests are queuing, you have not solved the underlying hold, you have simply allowed twice as many operations to be in flight against the database simultaneously. The database now does more concurrent work with the same constraint, and you will likely meet its own max_connections limit — at which point new connections are rejected outright and the situation is strictly worse.
Where this bites vibecoders
The specific way this bites vibecoders is a connection opened per request with nothing releasing it. An assistant writes
db.connect()at the top of a handler, uses the connection, and relies on the language’s garbage collector to close it — which it does not do deterministically, and in several common stacks does not do at all. Under low local traffic the pool never fills, so it looks fine; in production, under real concurrency, every request leaks one connection and the app falls over within minutes. The failure is load-dependent, which is exactly why it survives testing.
What causes pool exhaustion?
1. Connections are never returned. The classic leak: a connection is acquired and the function returns early, raises an exception before the release, or the object holding it goes out of scope without closing. A pool plus a leak is worse than no pool, because the connections are still consumed. Generating code hits this constantly, because the assistant has no way to know your framework’s release semantics and will write code that acquires and forgets.
2. Queries are slow. Every slow query holds its connection for its duration. If a request issues five sequential queries and one takes two seconds, that connection is occupied for the whole time. This is not a bug so much as a design issue, and it interacts badly with the N+1 problem — more queries per request means longer holds.
3. Transactions are left open. An open transaction holds its connection and its locks, and blocks other writers. An uncommitted transaction in a finally-less error path is a common and severe variant, because it also blocks vacuum and can hold locks indefinitely.
4. The pool is genuinely too small. Real concurrency exceeds the configured maximum. This is a legitimate reason to raise the size — but only after confirming the first three are not happening, because otherwise you are treating a symptom.
5. Nested acquisition. Code that takes a connection, then calls a function that takes another from the same pool, consumes two slots for one request. Under load this deadlocks when the pool cannot satisfy the nested demand.
How do I diagnose it?
1. Check whether connections are returning. The single most informative measurement:
-- Postgres: current and idle backends
SELECT state, count(*), max(now() - state_change) AS oldest
FROM pg_stat_activity
GROUP BY state;If idle connections accumulate and their age grows, connections are being opened and not returned. That is a leak, and the pool size is irrelevant.
2. Watch the pool’s checked-out count over time. Most pool implementations expose this, and the shape of the graph tells you which of the four causes you have:
- Monotonic climb that never returns to baseline — a leak.
- Rises with load and falls when it subsides, but the peak exceeds the pool — pool too small, or slow queries.
- Flat and healthy, but errors still occur — look at nested acquisition, or a pool configured smaller than a single request needs.
3. Turn on leak detection. Your driver almost certainly has a threshold option that logs a stack trace for any connection held longer than N seconds. This identifies the exact line responsible, and it is usually faster than reasoning about the code:
# SQLAlchemy — log any connection held longer than 2 seconds
engine = create_engine(
DATABASE_URL,
pool_size=5,
max_overflow=10,
pool_timeout=30,
pool_recycle=1800,
connect_args={"options": "-c statement_timeout=5000"},
)A statement_timeout is worth setting independently — it converts an indefinitely-held connection into a fast error, which is how one hung query stops taking the whole pool down.
4. Check the database’s own view. Compare what your app thinks it has against what the server sees:
SHOW max_connections;
SELECT count(*) FROM pg_stat_activity;If the server is at its limit, raising your pool size cannot help — you are already at the ceiling the whole account shares.
5. Look for the slow query. If connections are held rather than leaked, find the query:
-- Postgres: longest-running active queries
SELECT pid, now() - query_start AS duration, query
FROM pg_stat_activity
WHERE state = 'active' AND query_start IS NOT NULL
ORDER BY duration DESC
LIMIT 10;How do I fix it?
| Cause | Fix |
|---|---|
| Leak | Release in a finally/context manager; never acquire without a guaranteed release path |
| Slow query | Add an index, reduce the query count, or paginate — see How to Add an Index to a Slow SQL Query |
| N+1 | Eager-load or batch the queries — see What Is the N+1 Query Problem? |
| Open transaction | Close in finally; set an idle-in-transaction timeout on the server |
| Pool too small | Raise pool_size — but only after the above are ruled out |
| Nested acquisition | Pass the existing connection down instead of taking a new one |
| Downstream slowness | Add a circuit breaker so a slow dependency does not hold connections |
| Server at limit | Reduce total instances × pool size, or use a pooler such as PgBouncer in transaction mode |
The sizing rule that keeps this sane: total connections across all instances must stay under the database’s max_connections, with headroom for administration. With three app instances at a pool of 10 each, you have already committed to 30. On a small VPS with a default of 100, that leaves little room, and the fix for a genuinely exhausted pool is often a pooler in front of the database rather than a larger pool behind it.
Where AI coding assistants get this wrong
- Opening a connection per request instead of using a shared pool, with no guaranteed close on the error path.
- Opening a connection in a helper that is called inside a request that already holds one, consuming two slots for one request.
- Not closing on the exception path, because the release sits after the code that can throw.
- Increasing the pool size as the first suggestion, which moves the queue into the database.
- No
statement_timeout, so one slow query occupies a connection indefinitely. - Leaving a transaction open in an error path, holding locks as well as the connection.
- Assuming garbage collection closes connections, which is not guaranteed and is frequently false.
- Never having tested under concurrency, where every one of these is invisible.
Checklist
- Use a shared pool, not a connection per request.
- Guarantee release with a context manager or
finallyon every path, including exceptions. - Watch
pg_stat_activitygrouped by state; growing idle age means a leak. - Enable your driver’s leak detection with a threshold and read the stack trace.
- Set a
statement_timeoutso a hung query cannot hold a connection indefinitely. - Eliminate N+1 queries, which extend every hold.
- Close transactions in
finally, and setidle_in_transaction_session_timeouton the server. - Pass an existing connection into helpers rather than acquiring a nested one.
- Size the pool from real concurrency, not from a tutorial default.
- Verify
instances × pool_sizestays undermax_connectionswith admin headroom. - Consider a pooler in front of the database rather than a larger pool behind it.
- Load-test with realistic concurrency before trusting any of this.
FAQ
What does connection pool exhausted mean?
It means every connection your application is allowed to open is currently checked out and in use, so a request needing the database waits instead of getting one. The pool size is a maximum, not a target — the fix is almost never to raise it, because raising it moves the queue into the database, which then becomes the bottleneck and fails under its own connection limit.
Should I just increase the pool size?
Usually no. If connections are being held rather than returned, a larger pool means more work is queued inside the database, which is slower and can push you past the server’s own max_connections limit. Increasing the pool is correct only when you have confirmed connections are returned promptly and the limit is simply too low for your concurrency.
How do I find a connection leak?
Check whether the pool’s checked-out count grows over time while traffic is steady. A leak shows as a monotonic climb that never returns to baseline, and it correlates with a specific code path. Enabling leak detection in your driver, which logs a stack trace for any connection held beyond a threshold, usually identifies the exact line within minutes.
Why does this only happen in production?
Because it is load-dependent. Under low local traffic the pool never fills, so a leak accumulates slowly enough that you never observe it. In production, real concurrency consumes the pool faster than it is returned, and the same code that was fine at two concurrent requests fails at fifty.
How many database connections should my app use?
Keep total connections across all instances below the database’s max_connections, with headroom left for administration and maintenance. Beyond that, size each pool to your actual concurrent request count rather than to a number copied from a tutorial, and prefer a connection pooler when many instances share one database.
Is pool exhaustion the same as too many connections?
Related but not identical. “Too many connections” is the server rejecting new connections because its own limit is reached, which can be caused by many processes including administrative tools. Pool exhaustion is your application unable to obtain a connection from its own limited set. Raising a server limit does not fix a leak, and raising a pool size does not fix a server that is already at its ceiling.
Related topics
- What Is a Connection Pool?
- How to Fix ‘Too Many Connections’ in Postgres
- What Is the N+1 Query Problem?
- What Is a Circuit Breaker Pattern?
- What Is a Memory Leak (and Why Does Your App Get Slower the Longer It Runs)?