PostgreSQL "sorry, too many clients already": find the culprit
Tested with PostgreSQL 16 and Prometheus 3.5.
Every connection slot is taken. Connect as a superuser, group pg_stat_activity by user, application and address to see who holds them, and end the sessions stuck idle in transaction. Raising max_connections only buys time if something is leaking connections.
Your app servers start throwing timeouts, the load balancer returns 502s, and the alarms go off on the web tier. The real problem is one server further back: the database has run out of connections.
What the error means
PostgreSQL has a fixed number of connection slots, set by max_connections (100 by default). When every slot is taken, each new connection is refused with:
FATAL: sorry, too many clients already
A few slots are held back for superusers (superuser_reserved_connections, 3 by default), so ordinary users often hit a slightly different message first:
FATAL: remaining connection slots are reserved for non-replication superuser connections
On PostgreSQL 16 and later that second message ends "…reserved for roles with the SUPERUSER attribute". Either way, the database itself is usually healthy. Something is holding connections and not giving them back.
Find who is holding the connections
Connect as a superuser (the reserved slots are for exactly this) and group the open connections:
SELECT usename, application_name, client_addr, state, count(*) FROM pg_stat_activity WHERE backend_type = 'client backend' GROUP BY 1, 2, 3, 4 ORDER BY 5 DESC;
One line usually stands out: a single user, application or client address with far more connections than the rest. Then look at what those connections are doing and for how long:
SELECT pid, usename, client_addr, state,
now() - state_change AS in_state_for,
left(query, 80) AS last_query
FROM pg_stat_activity
WHERE backend_type = 'client backend'
ORDER BY state_change
LIMIT 20;
What you are looking for:
idle in transactionfor minutes or hours. A transaction was opened and never committed: a bug, a crashed worker, or someone's interactive session left open. These also hold locks, which can make everything else queue behind them.- Hundreds of
idleconnections from one address. A connection pool sized too large, or many app instances each with their own pool. - Many
activeconnections all running the same slow query. The database is busy, not leaking: fix the query or the index.
Get service back
End the stuck sessions, starting with the oldest idle in transaction ones. pg_terminate_backend closes the connection and rolls back its open transaction:
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction' AND now() - state_change > interval '10 minutes';
Check the list before you run it. Raising max_connections needs a restart and only buys time if something is leaking: each connection is a separate process using memory, so a much higher limit can trade this outage for a memory one.
Stop it happening again
- Do the pool arithmetic. Pool size × number of app instances, for every service using the database, must stay below
max_connectionsminus the reserved slots, with room to spare for deploys, when old and new instances run at once. - Time out abandoned transactions with
idle_in_transaction_session_timeout, for example'5min'. - Cap the noisy client so one application can't take every slot:
ALTER ROLE reporting CONNECTION LIMIT 20; - Put a pooler in front, such as PgBouncer in transaction mode, when you have many app instances.
Alert before the slots run out
With postgres_exporter, warn at 80% of the limit (metric names vary a little between exporter versions, so check them on its /metrics page):
- alert: PostgresConnectionsHigh
expr: |
sum by (instance) (pg_stat_activity_count)
/ max by (instance) (pg_settings_max_connections) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} is using over 80% of its connection slots"
If you collect PostgreSQL's logs in Loki, you can also alert on the error itself. Adjust the label selector to match how your logs are labelled:
sum by (host) (count_over_time({app="postgres"} |= "too many clients already" [5m])) > 0
More rules built the same way are in Alert rules that won't burn you out.
Why the alarm went off somewhere else
The awkward part of this incident is that the database rarely alarms first. The connection count climbs quietly, and the first thing anyone notices is the app servers timing out, because they are the ones being health-checked. On-call starts on the web tier, restarts it, and the restarted app opens a fresh pool of connections into a database that is already full.
That pattern, a cause on one server and the alarms on another, is what Whyfire is built for. It compares every server's signals with the hour before and names the first one that changed. Our live demo is exactly this outage: the database's connections went from 2 to 302 six minutes before the app servers alarmed.