Skip to content

Build1 publisher3 min readPublished

"Too Many Clients Already" Is Arithmetic, Not Capacity

Postgres refusing connections is a counting problem. Install a pooler before you find the multiplier and you buy the same outage next month, plus one more hop in the path.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying "Too Many Clients Already" Is Arithmetic, Not Capacity
Generated illustration

What happened

  • The Postgres error "FATAL: sorry, too many clients already" almost never means the database is out of capacity; it means something in the stack opened more connections than max_connections allows.
  • The usual causes are a per-process pool multiplied by more processes than you remembered running, or connections parked in idle in transaction.
  • Adding PgBouncer fixes the symptom, but installing it without finding the multiplier first produces the same failure a month later with an extra moving part in the path.
  • The author's working order is: count the connections and their states, find the multiplier, shrink the app-side pool, and only then decide whether a pooler is warranted.
  • Postgres allocates a backend process per connection, max_connections is a hard ceiling set at server start, and when clients exceed it the connection attempt is rejected outright.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

`FATAL: sorry, too many clients already` almost never means the database ran out of capacity; it means something in the stack opened more connections than `max_connections` allows [1]. That distinction decides what you do next, because the reflex fix, installing PgBouncer, addresses the symptom and leaves the cause in place: put a pooler in without finding the multiplier first and, according to a dev.to write-up on diagnosing the error, you get the same failure a month later with an extra moving part in the path [3].

The mechanics are unglamorous. Postgres allocates one backend process per connection, `max_connections` is a hard ceiling fixed at server start, and attempts past it are rejected outright [5]. There is a second message worth recognising separately: `FATAL: remaining connection slots are reserved for non-replication superuser connections` means you have hit `max_connections` minus `superuser_reserved_connections`, so ordinary users are locked out while the reserved slots keep a superuser login available for exactly this moment [6]. Both are refusals at the door rather than evidence of load, and CPU and IO can sit near idle while it happens [7].

So count before you build. The author's first query groups `pg_stat_activity` by user, `application_name` and state, with a count and `max(now() - state_change)`, filtered to `backend_type = 'client backend'` [8]. Three shapes recur. A wall of `idle` connections under a single `application_name` is a pool sized correctly per process and then replicated across more processes than you remembered running [9]. `idle in transaction` with a longest-in-state measured in minutes is a code path that opened a transaction, did something slow or fallible outside the database, usually an HTTP call, and never committed; those connections hold locks and are useless to everyone else [10]. A scatter of `active` connections above your intended pool size means something bypasses the pool entirely: a migration runner, a cron job, an admin tool, a metrics exporter [11].

If `idle in transaction` appears at all, fix it before touching pool sizes, because a pooler will leak those just as happily [13]. Setting `idle_in_transaction_session_timeout` to 60 seconds and reloading the config kills sessions that hold a transaction open past a minute, converting a silent connection leak into a loud, attributable application error [12].

The number itself comes from multiplication: pool size times processes per instance times instances, doubled again if you run a separate replica or read pool [14]. Eight Gunicorn or Puma workers with a pool of 10 across three pods is 240 connections before anything goes wrong [15], which is 2.4 times a default `max_connections` of 100 [1] and, once you add a background worker deployment on the same defaults, several times over it [16]. Multiply the ceiling, not the steady state, because most application pools have a burst setting above nominal size, such as SQLAlchemy's `max_overflow` or HikariCP's `maximumPoolSize` against minimum idle [18]. The sizing rule the author trusts is HikariCP's: connections should be a small multiple of core count, not a function of request concurrency, since the extras just queue inside Postgres instead of inside your app [19].

Shrinking pools is sufficient when the set of long-lived processes is fixed; a pooler earns its place when the client count is genuinely unbounded, as with serverless or autoscaled workers [20]. Serverless is the extreme case, where every warm function instance holds its own connection and concurrency spikes create instances faster than any pool config can constrain [17].

Worth watching in your own estate: whether your pool ceiling times process count times replica count already exceeds `max_connections` on a normal day, and whether anything outside the pool is dialling the database directly [14][11]. Run the count first, then the arithmetic, then decide about the extra hop [4].

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories