BlueByte
53300Fixed

PostgreSQL: FATAL — sorry, too many clients already

By Haneul SeoUpdated September 16, 20266 min

Hi, it's BlueByte. The app was fine an hour ago, and now every new connection dies with FATAL: sorry, too many clients already. Nothing is corrupted — the sessions already open keep working, PostgreSQL just has no free slot to hand out. We'll walk through that line and its variants, how to count what is holding your slots, the fix for each cause, and how to keep the pool from filling again.

What the FATAL line means when Postgres turns a client away

The rejection happens before any query runs:

psql: error: connection to server at "db.internal" (10.0.3.12), port 5432 failed:
FATAL:  sorry, too many clients already

You may see a different wording:

FATAL:  remaining connection slots are reserved for non-replication superuser connections
FATAL:  remaining connection slots are reserved for roles with the SUPERUSER attribute
FATAL:  remaining connection slots are reserved for roles with privileges of the "pg_use_reserved_connections" role

All carry SQLSTATE 53300 (too_many_connections) and mean the same thing to your app: no slot for you. The wording follows your major version — PostgreSQL 16 added the reserved_connections setting and the pg_use_reserved_connections role, splitting the older single message into the two newer ones. If your driver surfaces only the SQLSTATE, match on 53300.

Where the connection slots actually go

max_connections is a hard ceiling, typically 100 and lower if initdb found a tighter kernel limit — and not what your app gets, because the reserve comes off the top. superuser_reserved_connections defaults to three, and on 16 and later reserved_connections defaults to zero, leaving 97 slots for ordinary roles.

Four things consume them:

  • Pools multiplied by replicas. Every app process keeps its own pool: ten pods with a pool of ten is a hundred connections while the site sits idle.
  • Sessions parked in an open transaction. A request that opens a transaction and waits on an external API holds its slot the whole time.
  • Leaked connections from migration jobs, a forgotten psql, BI dashboards, and crashed workers whose sockets haven't timed out.
  • Short-lived clients reconnecting faster than they disconnect — cron jobs, serverless functions, health checks that open a connection per invocation.

Count what is connected before you change a setting

Don't raise max_connections until you know what is on the other end. Connect as a superuser — the reserve exists so you still can — and ask:

SHOW max_connections;
SHOW superuser_reserved_connections;
 
SELECT state, count(*) FROM pg_stat_activity
 WHERE backend_type = 'client backend'
 GROUP BY state ORDER BY count DESC;
        state        | count
---------------------+-------
 idle                |    71
 idle in transaction |    19
 active              |     7

That shape is the diagnosis. Mostly idle means oversized pools. A large idle in transaction count means the app opens transactions and waits on something slow. Mostly active means you are genuinely at capacity and need a pooler or a bigger server. Then find the owners:

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 10;

Free the slots that idle transactions are holding

If idle in transaction dominates, those sessions burn a slot and hold back vacuum at once. List the worst offenders first:

SELECT pid, usename, now() - state_change AS idle_for, left(query, 50) AS last_query
FROM pg_stat_activity
WHERE state = 'idle in transaction' AND now() - state_change > interval '5 minutes'
ORDER BY idle_for DESC;

Terminating one rolls its open transaction back and closes the socket; a healthy client reconnects. It is safe on an idle session — nothing is executing — but read last_query first, since a paused migration looks identical to a leak:

SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE state = 'idle in transaction' AND now() - state_change > interval '15 minutes';

Then let the server enforce it — this takes effect on reload, no restart:

ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
SELECT pg_reload_conf();

The docs are blunt: an open transaction "prevents vacuuming away recently-dead tuples," so a long idle costs table bloat on top of the slot. The sibling setting idle_session_timeout covers sessions idle outside a transaction, but the docs warn to "be wary of enforcing this timeout on connections made through connection-pooling software" — leave it off if a pooler sits in front.

Cap the client side instead of raising the ceiling

The durable fix is usually fewer connections, not more slots. Budget them: pool size across every replica, plus migrations, plus humans, must stay under max_connections minus the reserve. If that arithmetic doesn't work, put a pooler in front so many clients share a few server connections:

[databases]
app = host=10.0.3.12 port=5432 dbname=app
 
[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20

Point the app at port 6432 and a thousand client connections ride on twenty server slots. Transaction pooling buys that ratio and changes behaviour: anything tied to a session rather than a transaction — LISTEN, advisory locks, prepared statements held across transactions — needs checking first.

When raising max_connections is the right answer

Sometimes the traffic is real and 100 is simply small. Raise it, but know the cost: the docs note PostgreSQL "sizes certain resources based directly on the value of max_connections," shared memory included, so a big jump raises memory use before any client arrives.

ALTER SYSTEM SET max_connections = 200;
sudo systemctl restart postgresql
psql -Atc 'SHOW max_connections;'
200

The restart is not optional — the docs state this parameter "can only be set at server start," so a plain reload leaves the running value untouched while ALTER SYSTEM quietly records the new one. On managed services the value comes from a parameter group derived from instance size: change it there, not in postgresql.conf, and check your provider's default instead of assuming 100.

A real case: a deploy that tripled the worker count

An API scaled from 4 pods to 12 on a Friday afternoon. Each pod's driver kept a pool of 10, so the fleet wanted 120 connections against max_connections = 100. New logins failed with sorry, too many clients already while already-connected pods kept serving — which is why it looked intermittent. pg_stat_activity showed 96 client backends, 88 of them idle, all with the same application_name: nothing leaked, the pools were just too big for the new replica count. Dropping the per-pod pool to 5 brought steady state to about 60 and connections recovered in seconds.

Confirm it held, and keep the pool from filling again

Watch the headroom:

SELECT count(*) AS used,
       current_setting('max_connections')::int AS ceiling,
       round(100.0 * count(*) / current_setting('max_connections')::int, 1) AS pct
FROM pg_stat_activity;

Under 80 percent at peak is comfortable. Alert on it, keep idle_in_transaction_session_timeout set, and give batch work its own role with ALTER ROLE etl CONNECTION LIMIT 5 so one runaway script can't take the server down.

How this differs from the per-role and per-database limits

Two neighbours share SQLSTATE 53300 but have nothing to do with max_connections. FATAL: too many connections for role "app" comes from a per-role cap set with ALTER ROLE ... CONNECTION LIMIT; check SELECT rolconnlimit FROM pg_roles WHERE rolname = 'app'. FATAL: too many connections for database "app" is the same per database, stored as datconnlimit in pg_database. In both the server can be nearly empty, so raising max_connections changes nothing. If the message mentions reserved slots, you are at the ceiling minus the reserve — an ordinary role is blocked while a superuser still gets in, which is what the reserve is for.

Related questions

Restarting the app clears the error for a while. Why?

A restart drops every connection the old pools held, so slots free up immediately and refill as the new pools warm. Treat that as confirmation the pools are oversized rather than as a fix — it will come back at the same traffic level.

Can I raise max_connections without restarting?

No. The docs say the parameter can only be set at server start. ALTER SYSTEM records the new value but the running server keeps the old one until a restart, so check SHOW max_connections afterwards to confirm it took.

Is pg_terminate_backend safe on idle in transaction sessions?

Yes for a genuinely idle session — nothing is executing, the open transaction rolls back, and the client sees a closed connection. Read the last_query column first, because a migration paused mid-transaction looks identical to a leak.

My framework already pools connections. Do I still need PgBouncer?

Framework pools are per process, so the real number is pool size times replicas times workers. If that total exceeds max_connections minus the reserve, a pooler in transaction mode is what collapses it back to something the server can hold.

Superusers can connect but my app role can't. What's blocking it?

superuser_reserved_connections, which defaults to three, keeps slots aside so an admin can still log in and investigate. You are at max_connections minus the reserve, and the app will keep failing until slots free up.

References

Haneul Seo

Infrastructure engineer · 10+ years running Linux fleets

More in this category

detected dubious ownershipFixed

Git: fatal: detected dubious ownership in repository

Since the CVE-2022-24765 fix in Git 2.35.2, Git refuses to read a repository whose working tree or .git directory is owned by a different user than the one running the command. It shows up in containers, CI jobs, sudo sessions and shared drives. Fix the ownership if the repo should be yours, or add the exact path to safe.directory in your global config — never in the repo's own config, which Git ignores for this.

Git
failed calling webhookFixed

Kubernetes: Internal error occurred: failed calling webhook

An admission webhook sits in front of your write, the API server could not get an answer out of it, and failurePolicy: Fail turned that silence into a rejection. The tail of the message is the whole diagnosis: context deadline exceeded means the call went nowhere, no endpoints available means nothing is running, and an x509 line means the API server does not trust the webhook's certificate. Each has a different fix, and none of them is your manifest.

Kubernetes
1205Fixed

MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

A statement waited the full innodb_lock_wait_timeout for a row lock another transaction is still holding, and gave up. sys.innodb_lock_waits names the blocking session and hands you the KILL statement, and a blocking_query of NULL means the blocker is idle on an open transaction. The detail most retry loops get wrong: by default only the timed-out statement is rolled back, so your transaction is still open and still holds every lock it took earlier.

MySQL
MISCONFFixed

Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk

Reads keep working and every write is rejected, because the last background save failed and stop-writes-on-bgsave-error defaults to yes. The log names the real cause — no space, a dir the redis user can't write, a read-only mount at rename time, or fork failing with Cannot allocate memory. Fix the cause, run one BGSAVE, and writes come back on their own with no restart: rdb_last_bgsave_status flips from err to ok. Setting stop-writes-on-bgsave-error no restores writes instantly but leaves the snapshot broken, so treat it as a deliberate trade, not the fix.

Redis
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryFixed

Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (exit 134)

The V8 heap has its own ceiling, derived from system memory and the Node release, and it is often far below the RAM you have; when a build or server reaches it, V8 aborts with FATAL ERROR: Reached heap limit and exit code 134. Read the real limit with v8.getHeapStatistics().heap_size_limit, then raise it with --max-old-space-size (in MiB) or NODE_OPTIONS for a large workload, size it below the cgroup limit inside containers, and use --heapsnapshot-near-heap-limit to catch a leak in a long-running process. Exit 137 with no FATAL ERROR line is a container kill, not this.

Node.js
exec /docker-entrypoint.sh: exec format errorFixed

Docker: "exec format error" when the container starts — wrong-platform image, no emulator, or a script with no shebang

The container exits on its first instruction with exec format error — the kernel's ENOEXEC, meaning the file exists but cannot be executed here. In practice that is an image built on one CPU architecture (an Apple-silicon Mac produces linux/arm64) and run on another (an x86_64 server) with no QEMU handler registered in binfmt_misc, or an entrypoint script whose first line is not a shebang. uname -m, docker image inspect and ls /proc/sys/fs/binfmt_misc tell the causes apart; the fix is an explicit docker buildx build --platform (or a manifest list for both), QEMU registration or --platform when you mean to emulate, and a #!/bin/sh line for the script.

Docker