DEV Community

Cover image for PostgreSQL "FATAL: sorry, too many clients already" — What It Means and How to Fix It
Runsite Team
Runsite Team

Posted on Originally published at runsite.app

PostgreSQL "FATAL: sorry, too many clients already" — What It Means and How to Fix It

Cross-posted from the Runsite blog.

A deploy goes out, traffic picks up, and your app starts throwing 500s. You open the logs and there it is, over and over: FATAL: sorry, too many clients already. Nothing changed in your queries, the database is barely breaking a sweat on CPU, and yet new requests can't get in.

The error reads like the database is overloaded. It almost never is. What's actually full is the connection pool, and once you see why, the fix is straightforward and permanent.

What the error actually means

PostgreSQL accepts a fixed number of simultaneous client connections, set by the max_connections parameter (100 by default). When connection number 101 tries to open while the first 100 are still held, the server doesn't queue it or slow it down. It refuses outright.

You'll sometimes see its sibling, remaining connection slots are reserved for non-replication superuser connections, which is the same wall hit a few slots earlier — Postgres keeps a handful in reserve so an admin can still log in to fix the mess.

The key thing: this is a count of open connections, not a measure of load. An app running ten trivial SELECT 1 queries can exhaust the limit while one running a single heavy report stays well under it. The error is about how many doors are open, not how much work is going through them.

Why Postgres has a connection ceiling at all

It helps to know why the limit exists, because it explains why "just raise it" is a trap.

PostgreSQL uses a process-per-connection model: every connection forks a dedicated backend process on the server, and each one reserves memory for its work area, query plans, and buffers — idle or not. A hundred connections doing nothing still cost real RAM.

Push max_connections to 500 on a small instance and you can starve the database of the memory it needs to actually run queries, trading a clean refusal for thrashing and OOM kills. The ceiling isn't arbitrary; it's the database protecting itself.

Find out what's eating your connections

Before changing anything, look at who's actually connected. Postgres exposes this in the pg_stat_activity view. Group it by state to get the shape of the problem in one query:

-- How many connections, and what are they doing?
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count(*) DESC;
Enter fullscreen mode Exit fullscreen mode

The state column is the tell, and each value points at a different fix:

  • active — real concurrent work in flight. The answer is pooling, so fewer connections do the same amount of work.
  • idle — your app opened the connection and walked away without closing it. The answer is fixing how it manages connections.
  • idle in transaction — the worst of the three: a connection that ran a query, never committed or rolled back, and is now holding both a slot and its locks hostage. A few of these can wedge a whole app.

The usual culprit: connections multiply with instances

If each app instance opens its own pool of, say, 20 connections, then four instances plus a couple of background workers and a cron job quietly add up to well past 100 — even though no single piece looks unreasonable.

Connection math is per-fleet, not per-process. Count every instance, worker, and scheduled job that talks to the database.

The real fix: pool your connections

Opening a fresh connection per request is the root cause behind most of these incidents. Connections are expensive to create and strictly limited, so the fix is to open a small set once and reuse them. There are two layers to do it at, and busy apps want both.

1. Pool inside your app

Every serious database driver ships a pool. Use it, and cap it deliberately instead of leaving it on a generous default. The cap is per process, so multiply it by how many instances you run and keep the total comfortably under max_connections:

import { Pool } from 'pg';

// One pool per process, reused across requests.
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10, // ceiling PER instance: 10 x instances must stay under max_connections
});

// Borrow and return; never open a connection per request.
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
Enter fullscreen mode Exit fullscreen mode

This alone resolves a surprising share of "too many clients" fires. The trap to avoid is creating the pool inside a request handler, which spins up a brand-new pool on every call and reproduces the exact problem you're trying to kill. Create it once, at startup.

2. Put a pooler in front of the database

App-side pools have a hard limit: they can't coordinate across instances. Each one only knows its own connections, so a fleet that scales horizontally will still blow the ceiling no matter how careful each process is.

The fix is a single pooler that sits between your apps and Postgres, multiplexing thousands of client connections down onto a small set of real database ones. PgBouncer is the standard tool, and in transaction pooling mode it hands a real connection to a client only for the duration of a transaction, then returns it to the shared set — so a handful of backend connections can serve a large fleet.

A common way to expose this is two ports on the same database: one for a direct session, one that routes through the pooler.

# Direct connection (port 5432)
DATABASE_URL=postgresql://user:pass@db.example.com:5432/mydb

# Pooled through PgBouncer (port 6432)
DATABASE_URL=postgresql://user:pass@db.example.com:6432/mydb?pgbouncer=true
Enter fullscreen mode Exit fullscreen mode

One caveat worth knowing up front: transaction pooling doesn't play nicely with session-level features like prepared statements or SET that expect to live for a whole session. Most ORMs have a setting for this — check yours before you switch the port in production rather than improvising mid-incident.

Rule of thumb: pooled endpoint for app traffic, direct endpoint for migrations and admin tasks.

Hunt down leaked and idle connections

If pg_stat_activity showed a wall of idle or idle in transaction rows, pooling alone won't save you, because something is opening connections and never letting go. The common causes are short: a code path that opens a connection and returns before closing it, a transaction that errors out without a rollback, or a worker that holds a connection open while it waits on something slow.

To buy breathing room in the moment, you can terminate the connections that have been sitting idle the longest:

-- Free connections idle for more than 10 minutes
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
  AND state_change < now() - interval '10 minutes'
  AND pid <> pg_backend_pid();
Enter fullscreen mode Exit fullscreen mode

That's a fire extinguisher, not a fix; the leak will refill the slots if you don't find it. The durable version is to let the database reap them automatically by setting idle_in_transaction_session_timeout, so a forgotten transaction can't hold a slot indefinitely.

Where this error usually comes from

The error text is identical everywhere, but the thing filling the slots depends on what you're running. Four setups account for most cases, and each has a different tell.

Docker and Docker Compose

A Postgres container ships with the stock max_connections of 100, and Compose makes it easy to outgrow that without noticing. docker compose up --scale api=4 quadruples your pools while the database config stays where it was.

Restarting an app container makes it worse before it makes it better: the old backends aren't closed cleanly, so Postgres keeps them until TCP keepalive expires, and the fresh container opens a full pool on top of them. If the count drops on its own a few minutes after a restart, that's what you were looking at.

Set the pool size per container from the fleet total, not per service, and raise the container's max_connections deliberately rather than letting the default decide.

Django

Django's CONN_MAX_AGE is the usual cause. At the default of 0 every request closes its connection — wasteful but safe. Set it to a non-zero value (or None) without a pooler in front and every Gunicorn worker holds a connection open for its lifetime.

The arithmetic is workers × instances: 16 workers across three instances is 48 permanent connections before Celery or a management command touches the database.

Either keep CONN_MAX_AGE low and let a pooler do the reuse, or keep persistent connections and size the worker count against max_connections on purpose. On Django 4.2+, turn on conn_health_checks so a recycled connection that died server-side doesn't surface as an error in the request.

DBeaver, pgAdmin, and other GUI clients

A single connection in a database IDE is rarely a single connection to Postgres. DBeaver opens a separate session for metadata, and typically another per SQL editor tab, and holds all of them for as long as the app is open.

Three developers with a handful of tabs each can quietly occupy a fifth of a 100-connection limit while doing nothing at all. If pg_stat_activity shows idle rows whose application_name is a client tool, that's your answer.

A local Postgres on Ubuntu or Debian

The package default is the same 100, set in /etc/postgresql/<version>/main/postgresql.conf. Changing it needs a full sudo systemctl restart postgresql, not a reload, because max_connections is allocated at startup.

On a dev machine the culprit is usually simpler than a config value: psql sessions left in old terminal tabs, plus every project's dev server holding its own pool against the same instance.

When raising max_connections is (and isn't) the answer

Sometimes the limit really is too low for legitimate, well-pooled traffic, and the right move is genuinely a bigger instance. The order matters, though:

  1. Pool first.
  2. Find any leaks second.
  3. Only then raise the ceiling — and raise the instance's memory along with it, not just the number.

A higher limit on the same small instance buys instability, not headroom.

The short version

"Too many clients already" is a connection-count problem wearing the costume of an overload.

  • Check pg_stat_activity to see whether you're looking at real concurrency, idle leftovers, or stuck transactions.
  • Pool connections inside each app, and put PgBouncer in front of the fleet so thousands of clients ride on a handful of real connections.
  • Reach for a bigger max_connections last, and when you do, give it the memory to match.

Do that and the error stops being a recurring incident and becomes a line you never see again.


Related, if you want to go deeper: connection pooling with PgBouncer and self-hosted vs managed PostgreSQL.

Disclosure: I work on Runsite, a deployment platform whose managed Postgres ships with PgBouncer on port 6432 — which is why that setup is the one I reach for by reflex. Everything above works the same on any Postgres you run yourself.

Top comments (0)