Skip to main content
RunBook Academy

PostgreSQLI · Architecture and the Process ModelArchitecture

The process model: one backend per connection

Foundation⏱ ~25 minpsqlps

What you'll learn

  • Describe what the postmaster does and what it deliberately does not do
  • Explain what happens at the operating-system level when a client connects
  • Identify every process in a running cluster from ps and from pg_stat_activity
  • Predict the resource cost of raising the connection count on a real host

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.

PostgreSQL does not use a thread pool. When a client connects, the server creates a new operating-system process to serve that one connection, and that process lives until the connection ends. A thousand connections means a thousand processes.

Almost everything an operator finds surprising about PostgreSQL’s resource behaviour follows from this. It is why connections are expensive rather than cheap, why max_connections cannot simply be raised to a comfortable number, why per-session memory settings multiply in a way that catches people out, and why connection poolers are a normal part of a production deployment rather than an optimisation for very large systems.

What the postmaster does

The first process is the postmaster. It is the parent of everything else and it has a narrow job:

  • bind the listening socket and accept incoming connections;
  • perform authentication against pg_hba.conf;
  • fork a backend process for each accepted connection;
  • start and, if they die, restart the background processes;
  • own shared memory and coordinate startup and shutdown;
  • carry out crash recovery when the cluster did not shut down cleanly.

It does not parse SQL. It does not read your tables. It never executes a query. If you find the postmaster consuming CPU, something unusual is happening, because in steady state it is asleep in accept().

Its practical significance is that it is the process whose death takes the cluster with it, and the process whose PID appears in postmaster.pid. Everything else is replaceable at runtime.

The processes in a running cluster

Here is a PostgreSQL 18.6 cluster at rest, before any client has connected. The indentation of ps output is doing real work: every process has the postmaster as its parent.

Read-only / Safea PostgreSQL 18.6 cluster with no clients connected
$ ps -eo pid,ppid,args --sort=pid
    PID    PPID COMMAND
    1       0 postgres
   74       1 postgres: io worker 0
   75       1 postgres: io worker 1
   76       1 postgres: io worker 2
   77       1 postgres: checkpointer
   78       1 postgres: background writer
   80       1 postgres: walwriter
   81       1 postgres: autovacuum launcher
   82       1 postgres: logical replication launcher

Two things in that list are specific to the version this course targets. The three io worker processes are new in PostgreSQL 18 and belong to the asynchronous I/O subsystem introduced in that release; they do not exist on 17 or earlier. Conversely, if you are reading older material that mentions a stats collector process, it was removed in PostgreSQL 15, and statistics are now kept in shared memory instead. Recognising which processes should be present is a version question, and getting it wrong sends you looking for a fault that is not there.

Now connect two clients and look again, this time from inside the database rather than from the shell.

Read-only / Safethe same cluster with two client connections
$ psql -U postgres -c 'SELECT pid, backend_type, state, wait_event_type, wait_event FROM pg_stat_activity ORDER BY backend_type, pid'
 pid |         backend_type         | state  | wait_event_type |     wait_event
-----+------------------------------+--------+-----------------+---------------------
81 | autovacuum launcher          |        | Activity        | AutovacuumMain
78 | background writer            |        | Activity        | BgwriterMain
77 | checkpointer                 |        | Activity        | CheckpointerMain
227 | client backend               | active | Timeout         | PgSleep
234 | client backend               | active |                 |
74 | io worker                    |        | Activity        | IoWorkerMain
75 | io worker                    |        | Activity        | IoWorkerMain
76 | io worker                    |        | Activity        | IoWorkerMain
82 | logical replication launcher |        | Activity        | LogicalLauncherMain
80 | walwriter                    |        | Activity        | WalWriterMain
(10 rows)

The two new rows, PIDs 227 and 234, are client backends. They appeared when the clients connected and they will disappear when the clients disconnect. The pid column is a real operating-system PID: the same number ps shows, the same number you would pass to kill, and the same number that appears in the server log for that session. That correspondence is what makes it possible to follow a single session from a log line to a process to a pg_stat_activity row without guessing.

What a connection actually costs

A connection is a process, and a process is not free. The cost has three parts, and only one of them is obvious.

Creation cost. The postmaster must fork, and the new backend must attach to shared memory, load catalogue entries and initialise its local caches. On a healthy system this is on the order of a few milliseconds — negligible for a connection held for hours, and significant for an application that opens a connection per HTTP request. That pattern turns a cheap query into a mostly-fork workload.

Memory cost. Each backend has private memory that is not shared with any other: catalogue and relation caches, the query plan it is executing, and any working memory the query needs for sorts and hashes. The baseline is small, but it is per process, and the working memory is governed by work_mem, which is where the multiplication happens. Part XI is devoted to this because it is the single most common cause of a PostgreSQL host running out of memory.

Scheduling and contention cost. Every backend that wants a shared resource — a buffer, a lock, a WAL insertion slot — contends with every other. Beyond a certain concurrency the additional processes do not add throughput; they add contention, and total throughput falls. The point where that happens is a property of the workload and the hardware, not a number that can be quoted here, but the shape of the curve is reliable: throughput rises, plateaus, then declines.

That third cost is why the answer to “the database is slow, shall we raise max_connections?” is so often no. Raising the ceiling does not create capacity. It removes the error that was preventing the system from becoming more overloaded.

Shared memory: what the processes have in common

If every backend is a separate process, they need somewhere to cooperate. That is shared memory, allocated once by the postmaster at startup and mapped into every child.

Read-only / Safethe largest shared memory allocations on a default cluster
$ psql -U postgres -c 'SELECT name, pg_size_pretty(allocated_size) AS size FROM pg_shmem_allocations ORDER BY allocated_size DESC LIMIT 10'
         name         |  size
----------------------+---------
Buffer Blocks        | 128 MB
<anonymous>          | 4637 kB
XLOG Ctl             | 4110 kB
AioHandleIOV         | 2784 kB
AioHandle            | 1566 kB
AioHandleData        | 1392 kB
Buffer Descriptors   | 1024 kB
transaction          | 517 kB
Checkpointer Data    | 512 kB
Checkpoint BufferIds | 320 kB
(10 rows)

Buffer Blocks is the shared buffer pool — the value of shared_buffers, which was 128 MB on this default cluster. Everything else is bookkeeping: the WAL control structures, the transaction state, the checkpointer’s work list, and in PostgreSQL 18 the handles that track asynchronous I/O in flight.

The operational point is that this region is fixed at startup. shared_buffers is a postmaster-context parameter: changing it requires a restart, not a reload, because the allocation happens before any backend exists. Part III covers how to read that requirement off the server before planning a change, and Part XI covers how to choose the value.

Following one session across three views

The reason the process model is worth this much attention early is that it makes a session traceable. During an incident you can move between the operating system and the database in both directions, and each direction answers a different question.

# From inside PostgreSQL: which sessions exist, and what are they doing?
psql -U postgres -c \
  "SELECT pid, usename, application_name, client_addr, state,
          now() - xact_start AS xact_age, left(query, 60) AS query
     FROM pg_stat_activity
    WHERE backend_type = 'client backend'
    ORDER BY xact_start NULLS LAST"

# From the operating system: what is that specific backend consuming?
ps -o pid,ppid,etime,rss,pcpu,args -p 227

# Which client socket does it belong to?
ss -tnp 2>/dev/null | grep 227 || true

The xact_age column in the first query is the one to look at first in almost any incident. A transaction that has been open for hours is the root cause of a startling number of apparently unrelated problems — vacuum not reclaiming space, replication conflicts, wraparound risk — and Part VII and Part VIII both return to it.

Production discipline

  1. Treat a connection as a process, because it is one. Capacity planning for connections is capacity planning for processes and their private memory, not for lightweight handles.
  2. Never kill -9 a PostgreSQL backend. It forces crash recovery for the entire cluster. Use pg_terminate_backend(), and understand what it is doing before you use that either.
  3. Know which processes your version should have. The io workers exist from 18 onwards; the stats collector was removed in 15. An inventory that does not match the version sends you hunting a phantom fault.
  4. Read pg_stat_activity before changing max_connections. The question is what is holding the connections. The ceiling is the last thing to adjust, not the first.
  5. Record the backend PID when you investigate a session. It is the join key between the server log, ps, and pg_stat_activity, and it is stable for the life of the connection.

Cross-course references

  • Linux for Production Sysadmins — Part VI (Processes) covers fork(), process accounting and signals, which is the layer this lesson sits directly on top of. Part XXXVII (Resources) covers the limits that cap how many backends a host can actually carry.
  • Observability for Production Sysadmins — Part LIX (Database observability) covers exporting the connection and activity metrics this lesson reads by hand.
  • Docker & Containers — Part XV (Resource controls) covers the cgroup limits that decide when a containerised backend meets the OOM killer.

Quiz

Knowledge check · 6 questions

  1. Q1. A monitoring script uses kill -9 to clear a long-running PostgreSQL backend. What is the consequence?

  2. Q2. You are reading a PostgreSQL 18 cluster's process list and see three processes named 'io worker'. What should you conclude?

  3. Q3. The postmaster process parses and executes queries on behalf of connected clients.

  4. Q4. shared_buffers can be increased on a running cluster with a configuration reload, because it is shared memory rather than per-backend memory.

  5. Q5. Explain why raising max_connections is usually the wrong first response to connection exhaustion, naming the two costs it increases.

  6. Q6. Explain the mechanism connecting these observations and identify what to examine next.

    A reporting service was deployed that opens a new PostgreSQL connection for each incoming HTTP request and closes it on response. Under load, the database host shows high system CPU relative to user CPU, the PostgreSQL log records a steady stream of connection authorised and disconnection messages, application latency is dominated by a fixed overhead on every request, and pg_stat_activity rarely shows more than a handful of active sessions at any instant.

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