Ch. 11 · SQL & PostgreSQL

PostgreSQL 'sorry, too many clients already': Cause and Fix

Fix PostgreSQL 'FATAL: sorry, too many clients already': count connections per app instance, find leaks and idle transactions, and pool safely.

~7 min readintermediateupdated Oct 4, 2026

A deploy scales your API from 4 to 12 pods, or a worker starts retrying in a loop, and new connections start failing. On PostgreSQL 16 and 17 the client sees:

FATAL:  sorry, too many clients already
Text

or, when only the reserved slots are left:

FATAL:  remaining connection slots are reserved for roles with the SUPERUSER attribute
Text

The SQLSTATE is 53300 (too_many_connections). PostgreSQL runs one backend process per connection and caps them at max_connections. Every slot is taken, so the server refuses the handshake before any query runs. The exact wording of the reserved-slot message changed in PostgreSQL 16 (it can also name the pg_use_reserved_connections role), so search for the stable part, “remaining connection slots are reserved for”.

Quick fix checklist

  • Check the limit and current use: SHOW max_connections; and SELECT count(*) FROM pg_stat_activity;.
  • Group sessions by application_name, usename and state to see who holds the slots.
  • Look for idle in transaction sessions older than a few seconds; they are almost always bugs.
  • Multiply pool max by processes per pod by pod count and compare with max_connections.
  • Set idle_in_transaction_session_timeout so a forgotten transaction can’t hold a slot forever.
  • If many instances must share one database, put PgBouncer (transaction pooling) in front.
  • Only terminate sessions you’ve identified, with pg_terminate_backend(pid).

Before you start

You need a way in when the server is full. That is what superuser_reserved_connections (default 3) is for: a superuser can still connect with psql. PostgreSQL 16 added reserved_connections (default 0) for roles granted pg_use_reserved_connections, which is useful on managed services where you are not a true superuser. Seeing all rows in pg_stat_activity, including other users’ query text, needs superuser or pg_read_all_stats. Know how your application pools: node-postgres Pool defaults to max: 10, HikariCP to maximumPoolSize 10, and each Node cluster worker or Gunicorn worker gets its own pool.

Why it happens

max_connections (default 100) is a hard ceiling set at server start; changing it needs a restart because shared memory is sized from it. It stays modest on purpose: each backend is an OS process with its own memory, and settings like work_mem apply per sort or hash, per backend. A thousand mostly idle connections still cost memory and add contention on snapshots and locks.

Applications rarely open “too many” connections in one place. The total creeps up through multiplication:

  • Pools per instance. 12 pods × 2 worker processes × pool max 10 = 240 possible connections against a limit of 100. Autoscaling makes this worse at exactly the busiest moment.
  • Leaks. Code that calls pool.connect() and forgets client.release() on an error path keeps the connection checked out. The pool stays at its maximum, opens nothing new, and the app hangs or times out waiting for a client.
  • Idle in transaction. A session ran BEGIN, did some work, and is now waiting for the application (an HTTP call, a slow loop, a crashed handler that never rolled back). It holds a slot and its locks, and it blocks vacuum from cleaning rows newer than its snapshot.
  • Everything else. Migrations, cron jobs, BI tools, a developer’s IDE, monitoring agents and replication connections all count against the same limit.

Step-by-step walkthrough

Step 1: Get in and measure

Connect as a superuser (or a role with reserved slots) and compare use to the limit:

SELECT current_setting('max_connections')::int AS max,
       current_setting('superuser_reserved_connections')::int AS su_reserved,
       count(*) FILTER (WHERE backend_type = 'client backend') AS clients
FROM pg_stat_activity;
sql

Filtering on backend_type matters: pg_stat_activity also lists background workers, the checkpointer and WAL senders, which do not all count the same way.

Step 2: Find who holds the slots

SELECT application_name, usename, client_addr, state, count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY 1, 2, 3, 4
ORDER BY count(*) DESC;
sql

Read the result by shape. Many idle rows from one service spread across many IP addresses means pool sizing: each pod keeps its pool warm. Many active rows means real load or slow queries. Any meaningful number of idle in transaction rows points at a code bug. Set application_name in every connection string (?application_name=orders-api) so this query names the culprit instead of showing blanks.

Step 3: Inspect stuck transactions

SELECT pid, application_name, client_addr,
       now() - xact_start   AS xact_age,
       now() - state_change AS in_state_for,
       left(query, 80)      AS last_query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start;
sql

In this state query shows the last statement that ran, not one that is running. That last statement tells you which code path opened the transaction and then went quiet.

Step 4: Relieve pressure, then fix the cause

To get the service back, terminate the specific stuck sessions you found:

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

Their open transactions roll back. Then put guardrails in place so a forgotten transaction ends by itself:

ALTER ROLE app_user SET idle_in_transaction_session_timeout = '30s';
-- PostgreSQL 17 also has transaction_timeout for the whole transaction
ALTER ROLE app_user SET statement_timeout = '15s';
sql

The role settings apply to new sessions only.

Step 5: Size pools against the server

Budget connections from the database side. Leave room for admin and migrations, then divide the rest across every process that can connect:

budget         = max_connections - reserved - admin/migrations   (100 - 3 - 7 = 90)
per-process max = budget / (pods at max scale × processes per pod) (90 / (12 × 2) ≈ 3)
Text

A pool of 3 sounds small, but a connection is only busy while a query runs. If that is too few, the answer is a pooler, not a bigger max_connections.

Worked scenario

An Express service uses node-postgres. Under load it starts logging timeout exceeded when trying to connect, and other services get sorry, too many clients already. Step 3 shows dozens of idle in transaction sessions from orders-api whose last query is INSERT INTO orders .... The handler:

app.post('/orders', async (req, res) => {
  const client = await pool.connect();
  await client.query('BEGIN');
  await client.query('INSERT INTO orders (user_id, total) VALUES ($1, $2)', [req.user.id, req.body.total]);
  await chargeCard(req.body); // throws on a declined card
  await client.query('COMMIT');
  client.release();
  res.status(201).end();
});
JavaScript

When chargeCard throws, neither ROLLBACK nor release() runs. The connection stays checked out and inside an open transaction. Each declined card leaks one connection, and with 12 pods the server fills up. The fix releases on every path and moves the external call out of the transaction:

app.post('/orders', async (req, res, next) => {
  try {
    await chargeCard(req.body);
  } catch (err) {
    return next(err);
  }
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query('INSERT INTO orders (user_id, total) VALUES ($1, $2)', [req.user.id, req.body.total]);
    await client.query('COMMIT');
    res.status(201).end();
  } catch (err) {
    await client.query('ROLLBACK');
    next(err);
  } finally {
    client.release();
  }
});
JavaScript

The team also set idle_in_transaction_session_timeout = '30s' on the role and lowered the pool max from 20 to 4 per pod.

Common mistake

Raising max_connections to 1000. It makes the error go away for a week. Each backend costs memory, work_mem can be used several times per backend, and throughput usually falls once active connections exceed a small multiple of CPU cores. A leak fills 1000 slots just as surely as 100; it just takes longer.

Using PgBouncer transaction pooling without checking session state. In pool_mode = transaction, consecutive transactions from one client can run on different server connections. Anything tied to the session breaks or leaks between clients: SET without LOCAL, session advisory locks, LISTEN, temporary tables and WITH HOLD cursors. Named prepared statements only work on PgBouncer 1.21+ with max_prepared_statements set above 0. Run migrations and LISTEN consumers through a session-mode pool or a direct connection.

Killing all idle connections on a schedule. The pool simply reconnects, paying handshake cost, and the cause stays.

Verify the behavior

Reproduce the leak locally with a small pool and a forced error, then watch the database:

SELECT state, count(*) FROM pg_stat_activity
WHERE application_name = 'orders-api' GROUP BY state;
sql

Before the fix, repeated failing requests grow the idle in transaction count until it equals the pool max, then requests hang. After the fix, the count stays at zero and idle never exceeds the pool size per instance. In the app, pool.totalCount, pool.idleCount and pool.waitingCount (node-postgres) should return to idle between bursts. Under a load test at maximum replica count, total client backends should stay below your budget.

Interview exercise

Your service runs 20 pods, each with a connection pool of 10, against PostgreSQL with max_connections = 200. It works in staging but fails at peak with “too many clients”. What do you change?

Answer and reasoning

First, the arithmetic: 20 × 10 = 200, which already equals the limit before reserved slots, migrations or other services, so peak load guarantees failure. I would check pg_stat_activity to confirm the split between idle, active and idle in transaction, since a leak would need a code fix regardless. Then I’d cut per-pod pools to fit a budget (for example 4 each, leaving headroom) and measure whether latency suffers. If 20 pods truly need more concurrency than the server can handle, I’d add PgBouncer in transaction mode so hundreds of client connections share a few dozen server connections, after auditing the code for session state. I would not simply raise max_connections, because per-backend memory and contention grow with it and the problem returns when the deployment scales again.

Continue learning

More in SQL & PostgreSQL

read ✓SQL & PostgreSQL · mid

PostgreSQL 'relation does not exist': Cause and Fix

Fix PostgreSQL 'relation does not exist': check the search_path, qualify the schema, confirm migrations ran here, and fix quoted, case-sensitive identifiers.

~6 min readread →
esc