The pool that isn’t one
Every Postgres connection is an operating-system process. max_connections=100 isn’t a queue in front of the database — it’s permission for up to 100 backend processes to compete for your cores, your locks, and your buffer pool at once. When latency climbs, the reflex fix is to raise the cap; now 500 backends thrash where a few dozen could work, and throughput falls as p99 rises. The saturation curve looks like a database problem, but it’s arithmetic: a machine does roughly cores-times-a-small-factor of useful database work at once, and every connection past that point pays context-switch and contention tax without adding throughput. The pool your system needs is a real one — in front of Postgres, in transaction mode — not a bigger cap.
max_connections = 100◂ processes, not a queue # the classic “fix” that makes it worse max_connections = 500◂ 500 backends thrash, throughput falls
Put a transaction-mode pooler (PgBouncer or your platform’s equivalent) in front; size its server pool near cores×2–4 for OLTP, and keep max_connections modest — the pooler queues the burst instead of letting it thrash.
Tradeoff — Transaction pooling changes session semantics: session-level state (SET, advisory locks, LISTEN) needs care, and prepared statements need a pooler version that tracks them at the protocol level.