PostgreSQLI · Architecture and the Process ModelArchitecture
The background processes and what each one is responsible for
What you'll learn
- Name each background process and the single responsibility it holds
- Map a production symptom to the process that owns it
- Read the statistics view that reports on each process
- Recognise which processes exist in which PostgreSQL versions
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
A cluster with no clients connected is still busy. Pages are being written, the write-ahead log is being flushed, dead rows are being reclaimed and statistics are being updated, all by processes that no client asked for and that no query is waiting on.
These processes matter to an operator for a specific reason: they are where a large fraction of unexplained I/O comes from. When a host shows write activity that no application accounts for, the answer is almost always one of the seven processes below, and knowing which one narrows the investigation immediately.
The inventory
flowchart TD
P["postmaster\nlistens, forks, supervises"]
P --> C["checkpointer\nbounds recovery time"]
P --> B["background writer\nkeeps clean buffers ready"]
P --> W["walwriter\nflushes WAL buffers"]
P --> A["autovacuum launcher\nschedules workers"]
A --> AW["autovacuum worker\nvacuums one table"]
P --> L["logical replication launcher\nstarts subscription workers"]
P --> IO["io worker x3\nasynchronous reads (18+)"]
checkpointer
Writes every dirty buffer to disk and records a point in the write-ahead log from which recovery may begin. It exists to bound recovery time: without checkpoints, crash recovery would have to replay the entire log from the beginning of time.
It is also the largest single source of write I/O on most clusters, and
its work arrives in waves. The parameters that shape it —
checkpoint_timeout, max_wal_size, checkpoint_completion_target —
trade recovery time against write smoothness, and Part XII works
through the trade properly.
Its statistics moved in PostgreSQL 17. They are now in
pg_stat_checkpointer, and the columns older documentation names on
pg_stat_bgwriter no longer exist there.
$ psql -U postgres -c '\d pg_stat_checkpointer' View "pg_catalog.pg_stat_checkpointer"
Column | Type | Collation | Nullable
---------------------+--------------------------+-----------+----------
num_timed | bigint | |
num_requested | bigint | |
num_done | bigint | |
restartpoints_timed | bigint | |
restartpoints_req | bigint | |
restartpoints_done | bigint | |
write_time | double precision | |
sync_time | double precision | |
buffers_written | bigint | |
slru_written | bigint | |
stats_reset | timestamp with time zone | |num_timed counts checkpoints that happened because
checkpoint_timeout elapsed. num_requested counts those forced
early, usually because WAL volume reached max_wal_size. A cluster
where num_requested dominates is checkpointing on volume rather than
on schedule, which means checkpoints are arriving faster than intended
and each one is competing with the workload that caused it.
background writer
Writes some dirty buffers ahead of time so that backends looking for a free buffer find a clean one and do not have to write it themselves. It is a latency smoother, not a durability mechanism: it makes the read-causes-a-write path from the previous lesson rarer.
$ psql -U postgres -c '\d pg_stat_bgwriter' View "pg_catalog.pg_stat_bgwriter"
Column | Type | Collation | Nullable
------------------+--------------------------+-----------+----------
buffers_clean | bigint | |
maxwritten_clean | bigint | |
buffers_alloc | bigint | |
stats_reset | timestamp with time zone | |walwriter
Flushes the write-ahead log buffers to disk periodically, so that committing backends usually find their WAL already written. Without it, every commit would do its own flush.
It matters most for synchronous_commit = off workloads, where it
becomes the mechanism by which data eventually reaches disk, and it
defines the window of transactions that can be lost in a crash. Part XII
covers that trade explicitly.
autovacuum launcher and workers
The launcher is a scheduler. At intervals of
autovacuum_naptime it examines the cluster and starts workers for
tables whose dead-row counts have crossed their thresholds. The workers
do the actual vacuuming and analysing, and they are the processes you
will see appear and disappear.
The launcher is always present; workers are transient. Seeing no autovacuum worker at a given instant is normal. Seeing the maximum number of workers continuously, for hours, is the signature of autovacuum failing to keep up, which is Part VIII’s central subject.
PostgreSQL 18 changed the worker configuration in an operationally
useful way. autovacuum_max_workers is now sighup context, so it can
be raised on a running cluster, up to the autovacuum_worker_slots
ceiling set at startup.
$ psql -U postgres -c "SELECT name, setting, context FROM pg_settings WHERE name LIKE 'autovacuum%worker%' ORDER BY name" name | setting | context
-------------------------+---------+------------
autovacuum_max_workers | 3 | sighup
autovacuum_worker_slots | 16 | postmaster
(2 rows)On PostgreSQL 16 and earlier, autovacuum_max_workers is postmaster
context and raising it needs a restart. If you are writing a runbook
that must work across an estate spanning versions, that difference is
the sort of detail that decides whether the procedure works at 03:00.
logical replication launcher
Starts and supervises the workers that apply logical replication subscriptions. On a cluster with no subscriptions it does nothing at all, and its presence in the process list is not evidence that logical replication is configured.
io workers (PostgreSQL 18 and later)
New in 18. They execute asynchronous I/O requests submitted by
backends, so that a backend can issue a read and continue working
rather than blocking until the page arrives. The count is governed by
io_workers, and the mechanism as a whole by io_method, which
defaults to worker.
$ psql -U postgres -c "SELECT name, setting, context FROM pg_settings WHERE name IN ('io_method','io_combine_limit','effective_io_concurrency') ORDER BY name" name | setting | context
--------------------------+---------+------------
effective_io_concurrency | 16 | user
io_combine_limit | 16 | user
io_method | worker | postmaster
(3 rows)effective_io_concurrency defaulting to 16 is itself a change in 18;
on earlier releases the default was far lower. A migration that
compares I/O behaviour before and after an upgrade to 18 is comparing
two different concurrency settings unless this is accounted for.
Mapping a symptom to a process
This is the table worth remembering, because it converts a vague complaint into a specific first query.
| Symptom | Likely owner | First thing to read |
|---|---|---|
| Periodic write spikes, latency rising in waves | checkpointer | pg_stat_checkpointer.num_timed vs num_requested |
| Write I/O with no application activity | checkpointer or autovacuum worker | pg_stat_activity filtered to backend_type |
| Backends stalling on buffer allocation | background writer capped | pg_stat_bgwriter.maxwritten_clean |
| Commit latency higher than expected | walwriter, or the storage below it | pg_stat_io where object = 'wal' |
| Table growing while rows are deleted | autovacuum not keeping up | pg_stat_user_tables.n_dead_tup, last_autovacuum |
| Sustained maximum autovacuum workers | autovacuum losing the race | pg_stat_progress_vacuum |
| Unexplained reads at high queue depth | io workers, doing their job | pg_stat_io by backend_type |
The single most useful habit is to split pg_stat_activity by
backend_type before concluding anything about load. A cluster that
looks busy is often busy with maintenance, and that is a completely
different investigation from a cluster busy with queries.
psql -U postgres -c \
"SELECT backend_type, count(*), count(*) FILTER (WHERE state = 'active') AS active
FROM pg_stat_activity
GROUP BY backend_type
ORDER BY count(*) DESC"
Production discipline
- Split activity by
backend_typebefore diagnosing load. Maintenance load and query load look identical in a host-level CPU or I/O graph and require opposite responses. - Check
num_requestedagainstnum_timedwhenever checkpoints are suspected. Volume-triggered checkpoints mean the configuration and the workload disagree. - Audit monitoring queries against the target version before an
upgrade. The
pg_stat_bgwritercolumn removals in 17 break dashboards silently, and a blank panel is easy to attribute to the wrong cause. - Do not read a transient autovacuum worker as a problem. Read a persistently saturated worker pool as one.
- Record which processes your version should show. io workers from 18; no stats collector from 15. An unexpected process name is usually a version difference, not an intrusion.
Cross-course references
- Observability for Production Sysadmins — Part LIX (Database observability) covers exporting these statistics, and Part XCIII (Upgrades) covers the pattern of validating that monitoring survived a version change.
- Linux for Production Sysadmins — Part XLI (Disk performance) covers measuring the write behaviour these processes generate from outside the database.
- Ceph & Distributed Storage — Part LXXIX (Slow ops) covers what a checkpoint write burst looks like from the storage platform’s side.
Quiz
Knowledge check · 6 questions
Q1. After upgrading from PostgreSQL 16 to 18, a Grafana panel showing checkpoint counts goes blank while the database itself is healthy. What is the most likely cause?
Q2. pg_stat_checkpointer shows num_requested far exceeding num_timed. What does that indicate?
Q3. Which of these are true of the PostgreSQL 18 background process set? Select all that apply.
Q4. If the background writer process exits unexpectedly, the postmaster restarts it without forcing the whole cluster into crash recovery.
Q5. A host shows sustained write I/O but the application team reports no unusual activity. Give the single query that best narrows the cause, and name the two background processes most likely responsible.
Q6. Identify what the evidence supports and what it rules out, then state the next measurement.
A cluster running PostgreSQL 18 shows application write latency rising every few minutes in a sawtooth pattern and returning to normal in between. pg_stat_checkpointer reports num_timed at 41 and num_requested at 2,847 since the last statistics reset nine days ago. checkpoint_timeout is the default and max_wal_size is 1 GB. The application recently began a bulk data import that runs continuously. pg_stat_bgwriter shows maxwritten_clean rising steadily.
Passing score: 75%. Answers are checked in this browser.