Skip to main content
RunBook Academy

PostgreSQLIV · Connections, Sessions and PoolingConnections

What a connection costs

Intermediate⏱ ~25 minpsqlps

What you'll learn

  • Break a connection's cost into establishment, memory and contention
  • Measure a backend's real memory footprint rather than quoting a figure
  • Explain why throughput falls beyond a concurrency plateau
  • Recognise the workload shapes that make connections expensive

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.

Every capacity conversation about PostgreSQL connections stalls in the same place: somebody quotes a per-connection memory figure from an article, somebody else disputes it, and the discussion ends without a measurement. The figure is workload-dependent and the only useful version of it is the one from your own server.

This lesson breaks the cost into three parts that behave differently, and shows how to measure each.

Establishment

PostgreSQL 18’s granular connection log reports the components directly.

Read-only / Safethe cost of one connection, broken down
$ docker logs rbpg-base18
LOG:  connection received: host=[local]
LOG:  connection authenticated: user="postgres" method=trust (.../pg_hba.conf:117)
LOG:  connection authorized: user=postgres database=shop application_name=psql
LOG:  connection ready: setup total=1.498 ms, fork=0.445 ms, authentication=0.127 ms

On an idle local cluster: a fork of 0.445 ms, authentication of 0.127 ms, and 1.498 ms total. The roughly 0.9 ms unaccounted for is the backend attaching to shared memory, registering itself, setting its database and populating its local caches.

Those numbers grow in ways worth predicting. Authentication that consults an external directory — LDAP, or a PAM stack reaching off the host — can take tens of milliseconds and is subject to that system’s availability. Fork cost rises with memory pressure on the host. And setup total rises when the catalogue is large.

When establishment cost matters: an application opening a connection per request. The measured breakdown above is 1.498 ms of setup, of which 0.445 ms is the fork itself. At 2,000 requests per second that is 2,000 forks per second and roughly three seconds of connection-establishment work arriving every second — work that has to be absorbed somewhere, and that a pool removes entirely. That is the shape covered in the connection-storm material later in this part.

When it does not: an application holding a pool. A connection opened once and used for hours has amortised its establishment cost to nothing.

Memory

Read-only / Safeone backend's resident memory, from the operating system
$ ps -o pid,ppid,etime,rss,args -p 124
    PID    PPID     ELAPSED   RSS COMMAND
  124       1       00:03 15360 postgres: postgres postgres [local] SELECT

15 MB is the floor for a session that has done almost nothing, and quoting it as the per-connection cost would be wrong in both directions.

It is an overstatement because a large part of that RSS is shared pages inherited from the postmaster through fork() — the buffer pool mapping among them — and counting it once per backend double-counts memory the host allocated once.

It is an understatement because it excludes everything the session has not yet done. A backend grows as it touches catalogue entries, and it grows sharply while executing a query that allocates work_mem for sorts and hashes. Part XI works that arithmetic through in full; the short version is that work_mem is per operation, so one query can allocate it several times over.

Contention

The third cost has no configuration remedy and is the reason the other two are not the whole story.

Every backend that wants a shared resource contends with every other: a buffer in the pool, an entry in the lock table, a slot to insert a WAL record. Below some level of concurrency, adding backends adds throughput. Above it, adding backends adds contention, and total throughput falls.

flowchart LR
    A["Low concurrency\nthroughput rises\nwith each backend"] --> B["Plateau\nadditional backends\nadd no throughput"]
    B --> C["Beyond the plateau\ncontention dominates\ntotal throughput FALLS"]

The position of the plateau is a property of the workload and the hardware, not a number that can be quoted. What is reliable is the shape: it exists, and past it more connections make the database slower.

This is the mechanism behind the single most common wrong response in this part of the course.

Which workloads make connections expensive

Workload shapeEstablishmentMemoryContention
Long-lived pool, few connectionsNegligiblePredictableLow
Connection per HTTP requestDominantLow eachLow
Many idle connections from many app replicasNegligibleDominantLow
High concurrency of active queriesNegligibleHighDominant
Serverless functions scaling on demandDominantModerateSpiky

The middle row is the one most estates actually have: dozens of application replicas, each with its own pool, most connections idle most of the time. The cost there is memory and connection slots rather than contention, and the remedy is a pooler that lets many application connections share fewer database connections.

Production discipline

  1. Measure per-backend memory on your own server, at peak. A figure from an article is not evidence about your workload, and RSS at rest understates a backend running a real query.
  2. Read the state breakdown before proposing any connection-count change. Idle-heavy and active-heavy clusters need opposite responses.
  3. Treat the concurrency plateau as real even though its position is unknown. Past it, more connections reduce total throughput.
  4. Cost establishment separately from memory. A connection-per-request application has a problem that a larger ceiling cannot fix and a pooler can.
  5. Remember an idle connection still holds a slot, its memory and its caches, so a pool sized far above what the application uses is a standing cost.

Cross-course references

  • Linux for Production Sysadmins — Part VI (Processes) covers fork and resident set accounting, and Part XL (Memory performance) covers why summing RSS across processes overstates real consumption.
  • Kubernetes for Production Sysadmins — Part XII (Resources) covers the per-pod limits that decide how many application replicas exist, which is the input to the connection count.
  • Observability for Production Sysadmins — Part LIX (Database observability) covers exporting the connection state breakdown so the plateau is visible before it is reached.

Quiz

Knowledge check · 6 questions

  1. Q1. pg_stat_activity shows 4 sessions active and 190 idle against a max_connections of 200. The application reports connection errors. What does this evidence support?

  2. Q2. Why is a backend's resident set size a poor figure to multiply by max_connections for capacity planning?

  3. Q3. What does an idle connection outside a transaction still consume? Select all that apply.

  4. Q4. Beyond a certain level of concurrency, adding more connections reduces the total throughput of a PostgreSQL cluster.

  5. Q5. An application opens a connection per HTTP request at 2,000 requests per second. Name the component of connection cost that dominates and explain why raising max_connections does not help.

  6. Q6. Cost the proposal and state what you would measure before agreeing to it.

    A platform team is consolidating twelve application services onto one PostgreSQL cluster. Each service runs eight replicas, and each replica opens a pool of twenty connections. They propose max_connections of 2000 to accommodate the arithmetic with headroom, on a host with 64 GB of memory. The current cluster runs max_connections of 200 with a peak of 60 concurrent sessions of which fewer than ten are typically active. work_mem is at the 4MB default.

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