Reported symptoms
Every new connection returns FATAL: sorry, too many clients already.
The on-call engineer cannot connect either — not over the network, and
not with psql over the local socket after SSH-ing to the database host.
The cluster is not down. Existing connections work; queries on them complete normally. CPU is at 12 percent, the disks are idle, and there is 40 GB of free memory.
max_connections was raised from 200 to 500 in February and from 500 to
900 in June, each time in response to this same symptom.
The incident began 40 seconds after a rolling restart of the application
tier. Earlier in the same incident, some clients saw a different
message mentioning pg_use_reserved_connections.
Evidence provided
$ grep -oE 'FATAL:.*' /var/log/postgresql/postgresql-18-main.log | sort -uFATAL: remaining connection slots are reserved for roles with privileges of the "pg_use_reserved_connections" role
FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute
FATAL: sorry, too many clients alreadyFrom an existing session: 897 client backends, 812 of them idle,
and 640 of those idle for over twenty minutes.
The application runs 40 pods. Each pool is configured with a minimum
and maximum of 25 — a demand of 1000 against a max_connections of
900. The rolling restart brought new pods up before old pods drained.
Average query duration across the cluster is 1.8 ms. The busiest table is
200 MB. shared_buffers is 8 GB on a 64 GB host.
Work the evidence before reading on
- Three different messages appeared. What does each one tell you?
- 812 sessions are
idle, notidle in transaction. Does that distinction matter here? max_connectionshas been raised twice. What did each raise achieve?- You have no connection and you need one. What are your options, and what does each cost?
Root cause
The demand is arithmetic, and it does not fit
Forty pods times a pool max of 25 is 1000 against 900. That is a shortfall in steady state. During a rolling restart it is worse: new pods open their full 25 before old pods release theirs, so demand briefly approaches double.
The pool minimum is also 25, so every pod claims its entire allocation the moment it starts. Nothing grows into its allocation; every pod arrives at full size.
The connections are not doing anything
812 of 897 sessions are idle — not idle in transaction, simply
idle, holding a slot and a backend process while the pool waits for the
application to need them. 640 have been that way for over twenty minutes.
The cluster is answering 1.8 ms queries against a 200 MB working set on a machine at 12 percent CPU. It needs a small number of busy connections, not nine hundred quiet ones.
Raising max_connections made the next occurrence worse
The lockout is why nobody could diagnose it
Resolution
You need a connection and you cannot get one. Do not restart the cluster to obtain it — that is a full outage in exchange for a diagnosis you can get without one.
Terminate a single backend from the operating system. SIGTERM to a
backend is exactly what pg_terminate_backend() sends, and it needs no
connection:
$ ps -o pid,args -C postgres | head && kill -TERM 305 110 postgres: app postgres 172.17.0.12(60026) SELECT
112 postgres: app postgres 172.17.0.12(60030) SELECT
118 postgres: app postgres 172.17.0.12(60042) SELECT
...
client_backends | max_connections
-----------------+-----------------
15 | 15
(1 row)With one slot free, measure before acting:
SELECT state, count(*),
count(*) FILTER (WHERE state_change < now() - interval '10 minutes') AS idle_over_10m
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY state ORDER BY 2 DESC;
SELECT usename, application_name, client_addr, count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY 1,2,3 ORDER BY 4 DESC LIMIT 20;
The second query names the pods holding the slots, which is what you need to fix this at the right layer.
Reclaim capacity from long-idle sessions — not busy ones, and not arbitrarily:
SELECT pg_terminate_backend(pid), usename, client_addr,
now() - state_change AS idle_for
FROM pg_stat_activity
WHERE backend_type = 'client backend'
AND state = 'idle'
AND state_change < now() - interval '15 minutes';
The pools will reopen them, so this buys minutes rather than a fix. Use
those minutes to reduce demand: lower the pool maximum so pods x pool_max sits comfortably under max_connections, and set the pool
minimum low so pods do not claim their full allocation at start-up.
Do not raise max_connections again.
Verification
New connections succeed, from the application and from an operator.
Backend count sits well below max_connections, with headroom that
survives a rolling restart — and the way to know that is to perform one
and watch:
SELECT count(*) FILTER (WHERE backend_type = 'client backend') AS backends,
current_setting('max_connections')::int AS max_connections
FROM pg_stat_activity;
The long-idle population is small, measured directly rather than as part of the total:
SELECT count(*) FROM pg_stat_activity
WHERE state = 'idle' AND state_change < now() - interval '10 minutes';
No further refusal messages of any of the three kinds appear in the log.
An operator connection succeeds during peak load, using a role holding
pg_use_reserved_connections. Test that once, deliberately, rather than
discovering during the next incident that the reserve was never
configured.
Prevention
Size the pools, not the server. What a cluster can serve well is a
function of CPU and disk, not of what the pools ask for. pods x pool_max must be a number somebody chose, checked in, and reviews when
pod counts change.
Set pool minimum low and maximum modest. A minimum equal to the maximum makes a rolling restart the worst moment of the day.
Put a pooler in front at high pod counts. In transaction mode, many client connections share few server connections — in a measured example, 100 clients were served by 11 backend connections. A pooler shapes capacity; it does not make queries faster, and expecting it to will disappoint you.
Configure the reserve tiers deliberately, and grant the reserve to the role your team actually uses:
GRANT pg_use_reserved_connections TO ops;
Alert on connection utilisation as a ratio, at 75 percent rather than 99. Alert separately on sessions idle over ten minutes — that is the leading indicator.
Set idle_session_timeout per role, so a pool that never releases
cannot accumulate indefinitely, without disturbing operator sessions:
ALTER ROLE app SET idle_session_timeout = '10min';
Know the three refusal messages, and never kill -9 a backend.