PostgreSQLI · Architecture and the Process ModelArchitecture
The process model: one backend per connection
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
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.
$ 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 launcherTwo 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.
$ 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.
$ 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
- 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.
- Never
kill -9a PostgreSQL backend. It forces crash recovery for the entire cluster. Usepg_terminate_backend(), and understand what it is doing before you use that either. - 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.
- Read
pg_stat_activitybefore changingmax_connections. The question is what is holding the connections. The ceiling is the last thing to adjust, not the first. - Record the backend PID when you investigate a session. It is the
join key between the server log,
ps, andpg_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
Q1. A monitoring script uses kill -9 to clear a long-running PostgreSQL backend. What is the consequence?
Q2. You are reading a PostgreSQL 18 cluster's process list and see three processes named 'io worker'. What should you conclude?
Q3. The postmaster process parses and executes queries on behalf of connected clients.
Q4. shared_buffers can be increased on a running cluster with a configuration reload, because it is shared memory rather than per-backend memory.
Q5. Explain why raising max_connections is usually the wrong first response to connection exhaustion, naming the two costs it increases.
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.