awesome-everything RU
↑ Back to the climb

Databases

The Postgres process model and why raising max_connections degrades throughput

Crux Each Postgres connection forks a backend via the postmaster; throughput peaks at (cores × 2) active processes and drops non-linearly past that — raising max_connections without a pooler is almost always the wrong fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 12 min

A team hits pool exhaustion. They raise max_connections from 100 to 1000. P99 gets worse. They raise it to 2000. The database starts OOM-crashing. Raising max_connections is the instinct; understanding why it fails is the skill.

The Postgres process model in detail

When a client connects, the postmaster (the supervisor process) forks a new backend process. Each backend has:

  • Private memory: ~5–10 MB resident (per-query work memory, plan cache, libpq buffers). On a 16-core machine with 1,000 backends that is ~5–10 GB of private memory just for backends.
  • A shared-memory slot: proc array entry (snapshot acquisition scans this), lock table entry, predicate-lock metadata. These structures are pre-allocated at startup, sized to max_connections. Doubling max_connections doubles the fixed shared-memory footprint.
  • A kernel process with its own stack, file descriptor table, and scheduler entry.

Connection startup costs ~5 ms on the Postgres side (fork + initialization) plus TLS and auth on the network side.

Why processes and not threads? Postgres predates reliable pthreads on most Unix variants. The process model gives crash isolation (one backend OOM does not corrupt shared memory), simple private heap management, and easy parallel-query worker forking. The cost is irreducible per-connection overhead — which pooling absorbs.

Why raising max_connections degrades throughput

Naive teams raise max_connections from 100 to 1,000 when they see pool exhaustion. Four things go wrong:

  1. Shared-memory scaling: max_connections = 1000 pre-allocates ~500 MB+ of fixed shared memory just for proc array, lock hash, and predicate-lock structures — measurable on small instances.

  2. Context-switch overhead: at 1,000 active backends with load, kernel scheduling time becomes a measurable fraction of CPU. Each OS process competes for the scheduler.

  3. Lock-manager contention: proc array iteration during snapshot acquisition is O(active_backends). At 1,000 active backends, every BEGIN scans 1,000 entries. Lock table scans and MultiXact buffer contention scale similarly.

  4. Postmaster fork queue: connection establishment requires the postmaster to fork a new process; at high connection rates this serialises. PgBouncer’s ~2 KB per client connection vs Postgres’s ~10 MB per backend is the core asymmetry.

The throughput curve is non-linear: a 16-core Postgres peaks around 32–48 active backends and degrades past that. Adding backends past the peak means each backend gets a smaller share of CPU while paying more contention overhead.

Active backendsBehaviour (16-core Postgres)
1–32Linear throughput increase; CPU utilisation growing
32–50Throughput plateau; context-switch and lock overhead starts showing
50–200Throughput flat or declining; shared-memory contention visible
200–1000Throughput drops; per-backend throughput may halve
1000+Likely OOM or kernel proc-table exhaustion

The practical max_connections cap

The Postgres community guidance (unchanged for 25+ years): cap max_connections at a few hundred. The practical range for production OLTP is 200–500 — enough for admin connections, replicas, and some headroom above the pooler’s pool_size.

max_connections budget allocation:

  • PgBouncer pool_size (primary) + reserve_pool_size — the real OLTP backends
  • Replica connections (streaming replication)
  • superuser_reserved_connections (default 3) — kept for emergency access
  • Admin and monitoring connections
  • PgBouncer pools for read replicas if any

Example: 8-core primary, pool_size = 24, reserve = 6, 2 replicas each with pool_size = 20, superuser_reserved = 3, admin/monitoring = 10. Total: 24 + 6 + 40 + 3 + 10 = 83. max_connections = 100 is fine; max_connections = 1000 is wasteful and harmful.

Why this works

What happens if max_connections is set too low? Postgres returns “FATAL: remaining connection slots are reserved for non-replication superuser connections” when the last superuser_reserved_connections slots fill up — application workers start failing before the hard cap. Always leave headroom above pool_size + reserve_pool_size to absorb admin access during incidents.

Postgres process model production numbers
Backend private memory
~5–10 MB
max_connections default
100
Practical max_connections cap
200–500
Optimal active backends (16-core, NVMe)
32–48
PgBouncer memory per client conn
~2 KB
Backend startup cost
~5 ms (fork + init)
Shared memory: proc array at 1000 backends
~500 MB+
Quiz

A Postgres instance with max_connections=200 starts rejecting connections at ~150 active backends. Why might this happen below the cap?

Quiz

Why does Postgres use a process-per-connection model instead of threads?

Recall before you leave
  1. 01
    What shared-memory structures scale with max_connections and why does raising it hurt throughput?
  2. 02
    How should a senior engineer allocate a max_connections budget on a primary Postgres instance?
  3. 03
    Why is the throughput degradation past (cores × 2) non-linear rather than linear?
Recap

Every Postgres connection forks a backend via the postmaster: ~10 MB private memory, a proc array slot, a lock table entry. Connection startup costs ~5 ms. Shared-memory structures are pre-allocated to max_connections at startup — doubling it doubles fixed overhead. Past (cores × 2) active backends, context-switch and proc-array scan overhead causes throughput to degrade non-linearly. The Postgres community’s guidance for 25 years: cap max_connections at 200–500; multiplex above that with a pooler. Raising max_connections to 1,000 without a pooler is almost always the wrong response to pool exhaustion — it moves the symptom while worsening the underlying throughput profile.

Connected lessons
appears again in258
Continue the climb ↑Pooler landscape 2026, serverless connection storms, and the full failure-mode taxonomy
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.