Skip to main content
RunBook Academy

PostgreSQLIV · Connections, Sessions and PoolingConnections

Connection exhaustion and the storm that follows

Advanced⏱ ~25 minpsql

What you'll learn

  • Trace the feedback loop that turns slowness into a connection storm
  • Distinguish exhaustion caused by leaks, by slowness and by genuine demand
  • Recover access to a saturated cluster
  • Choose a remediation that breaks the loop rather than feeding it

Prerequisites

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

Connection exhaustion looks like a capacity problem and is usually a consequence of something else. Understanding the loop matters because the intuitive response — allow more connections — feeds it.

The loop

flowchart TD
    A["Something slows down\nlock, storage, a bad plan"] --> B["Each request holds its\nconnection for longer"]
    B --> C["Pool saturates;\nnew requests wait"]
    C --> D["Application timeouts fire"]
    D --> E["Clients retry and reconnect"]
    E --> F["More backends: more memory,\nmore contention, more forks"]
    F --> A

Every arrow is an application or a database behaving as designed. The loop is emergent, and it converges on an outage rather than away from one, because each turn adds load to a system that was already struggling.

The critical property is that the first arrow is not connections. Something else slowed down. Treating the visible saturation as the cause leads to a remediation aimed at the fifth box.

Telling the three cases apart

Exhaustion has three common causes with different evidence and opposite remediations.

Causestate breakdownxact_ageWhat to do
LeakMany idle or idle in transaction, few activeSome very oldFind the code path; bound with timeouts
SlownessMany active, waitingUniformly risingFind what they wait on; reduce arriving work
Genuine demandMany active, progressingShort and stablePool, or add capacity

One query separates them:

psql -U postgres -c \
  "SELECT state,
          count(*),
          max(now() - xact_start)   AS oldest_xact,
          max(now() - state_change) AS longest_in_state
     FROM pg_stat_activity
    WHERE backend_type = 'client backend'
    GROUP BY state ORDER BY count(*) DESC"

A cluster with 190 idle and 4 active is leaking or over-pooled. A cluster with 190 active, all waiting on the same wait_event, has a bottleneck the connections are queued behind. The second case is where adding connections does the most harm.

Getting back in

If you set reserved_connections and granted pg_use_reserved_connections to an operator role, as the earlier lesson recommended, you connect normally. If not, the options narrow.

Read-only / Safethe two refusals, and what each leaves you
$ psql -h 127.0.0.1 -U app -d postgres -c 'SELECT 1'
-- reserved slots remain; a privileged role can still connect
FATAL:  remaining connection slots are reserved for roles with privileges
      of the "pg_use_reserved_connections" role

-- nothing remains at all, for anyone
FATAL:  sorry, too many clients already

The first message is the better one: capacity exists that your role is not entitled to. Connect as a superuser, or as a role holding pg_use_reserved_connections.

The second means every slot including the reservations is consumed. There is no privileged route in, and the remaining options are to wait for a disconnection — which a storm will immediately consume — or to reduce the arriving load from outside the database.

Terminating sessions, carefully

-- Ask politely: cancel the running query, leave the session connected
SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE pid = 41822;

-- End the session entirely
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid = 41822;

pg_cancel_backend ends the current statement and the session survives, its transaction rolled back to the point of the cancelled statement. It does nothing to an idle session, because there is no statement to cancel.

pg_terminate_backend ends the session. Its transaction is rolled back, the connection drops, and the application learns about it at its next attempt to use that connection.

Production discipline

  1. Read the state breakdown before acting. Idle-heavy and active-heavy exhaustion have opposite remediations.
  2. Capture pg_stat_activity to a table before terminating anything. The identifying columns are the only route to prevention.
  3. Reduce arriving load from outside the database first. Scaling the application down during a storm is counter-intuitive and usually the fastest route to a responsive database.
  4. Lower a pooler’s database-side pool during a storm, not raise it. Queuing outside the database is the point.
  5. Prefer pg_cancel_backend to pg_terminate_backend where a statement is the problem, and target a specific identified session rather than a state.
  6. Set reserved_connections before you need it. Once the second refusal message appears there is no privileged route in.

Cross-course references

  • Observability for Production Sysadmins — Part XX (Alert quality) covers alerting on connection headroom and rising transaction age rather than on the exhaustion itself, which arrives too late to act on.
  • Kubernetes for Production Sysadmins — Part LXXXII (HPA) covers autoscaling that reacts to latency, which during a database storm scales the application up and makes the loop worse.
  • Linux for Production Sysadmins — Part LXXXI (Incident) covers evidence capture before remediation, which is the discipline this lesson applies to sessions.

Quiz

Knowledge check · 6 questions

  1. Q1. During a connection storm, which action is most likely to make the database responsive again quickly?

  2. Q2. Why is a connection storm self-reinforcing rather than self-limiting?

  3. Q3. Which columns should be captured before terminating sessions during an incident? Select all that apply.

  4. Q4. pg_cancel_backend will end an idle session and free its connection slot.

  5. Q5. A cluster shows 190 sessions active and all waiting on the same wait event, with connection errors from the application. Explain why raising max_connections is the wrong response.

  6. Q6. Sequence the response and justify the order.

    At 14:02 an index was dropped as part of a cleanup change. At 14:06 API latency rises. At 14:09 the connection pool saturates and the application begins returning errors. At 14:11 the platform autoscaler adds eight application replicas in response to the latency. At 14:13 PostgreSQL is refusing all connections with 'sorry, too many clients already' and the on-call engineer cannot connect. pg_stat_activity was last successfully sampled at 14:08 and showed 140 sessions active, nearly all waiting on IO:DataFileRead, running the same SELECT against the orders table.

Passing score: 75%. Answers are checked in this browser.