PostgreSQLXII · WAL, Checkpoints and Crash RecoveryWAL
Background writing and the PostgreSQL 18 I/O subsystem
What you'll learn
- Distinguish the three processes that write dirty buffers and when each does
- Read maxwritten_clean and act on it
- Explain what the PostgreSQL 18 asynchronous I/O subsystem changes
- Use pg_stat_io and pg_aios to attribute I/O correctly
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
Three different processes write dirty buffers out of the pool, for three different reasons, and one of them is a symptom rather than a mechanism.
Who writes what
| Writer | When | Reported in |
|---|---|---|
| Checkpointer | On schedule, spread across the interval | pg_stat_checkpointer.buffers_written |
| Background writer | Continuously, ahead of demand | pg_stat_bgwriter.buffers_clean |
| Backends | When a backend needs a buffer and none is clean | pg_stat_io, context normal |
The third is the one to care about. A backend writing a dirty buffer is a query waiting on a write it did not ask for, and it is the direct cost of a pool with nothing clean in it.
$ psql -U postgres -x -c "SELECT * FROM pg_stat_bgwriter"buffers_clean | 97984
maxwritten_clean | 570
buffers_alloc | 2131374
stats_reset | 2026-08-27 18:31:22.55921+00maxwritten_clean counts how often the background writer stopped
early because it hit bgwriter_lru_maxpages. 570 occurrences means it
was repeatedly capped while there was still work to do.
The PostgreSQL 18 asynchronous I/O subsystem
This is the largest change to PostgreSQL’s I/O behaviour in years. From the release notes:
Add an asynchronous I/O subsystem … This feature allows backends to queue multiple read requests, which allows for more efficient sequential scans, bitmap heap scans, vacuums, etc. This is enabled by server variable
io_method, with server variablesio_combine_limitandio_max_combine_limitadded to control it. This also enableseffective_io_concurrencyandmaintenance_io_concurrencyvalues greater than zero for systems withoutfadvise()support. The new system viewpg_aiosshows the file handles being used for asynchronous I/O.
$ psql -U postgres -c "SELECT name, setting, boot_val FROM pg_settings WHERE name IN ('io_method','io_workers','io_combine_limit','io_max_combine_limit')" name | setting | boot_val
----------------------+---------+----------
io_combine_limit | 16 | 16
io_max_combine_limit | 16 | 16
io_method | worker | worker
io_workers | 3 | 3$ SELECT name, setting, enumvals FROM pg_settings WHERE name IN ('io_method','io_combine_limit','io_max_combine_limit'); name | setting | enumvals
----------------------+---------+------------------------
io_combine_limit | 16 |
io_max_combine_limit | 16 |
io_method | worker | {sync,worker,io_uring}Three values on this build. enumvals is the right way to ask, because
the list depends on how the binary was compiled — a build without
liburing will not offer io_uring at all.
worker — the default — uses dedicated I/O worker processes, of which
there are three by default. sync reverts to the previous synchronous
behaviour, which is the comparison to make if you suspect a regression.
io_uring uses the Linux io_uring interface and is available only where
the build and kernel support it.
io_combine_limit is how many blocks may be merged into one I/O
request: 16 blocks is 128 kB. Raising it can help large sequential
reads on storage that rewards large requests, and
io_max_combine_limit bounds what io_combine_limit may be set to.
pg_stat_io
pg_stat_io attributes I/O by who did it, to what, and in what context.
PostgreSQL 18 added byte-level columns. From the release notes:
Add
pg_stat_iocolumns to report I/O activity in bytes … The new columns areread_bytes,write_bytes, andextend_bytes. Theop_bytescolumn, which always equalledBLCKSZ, has been removed.
and
Add WAL I/O activity rows to
pg_stat_io… This includes WAL receiver activity and a wait event for such writes.
$ SELECT backend_type, object, context, reads,
pg_size_pretty(read_bytes::numeric) AS read,
writes, pg_size_pretty(write_bytes::numeric) AS written,
extends, pg_size_pretty(extend_bytes::numeric) AS extended
FROM pg_stat_io
WHERE reads > 0 OR writes > 0 OR extends > 0
ORDER BY reads DESC NULLS LAST LIMIT 8; backend_type | object | context | reads | read | writes | written | extends | extended
--------------------+----------+----------+-------+---------+--------+---------+---------+----------
client backend | relation | normal | 68667 | 537 MB | 461 | 3688 kB | 1995 | 16 MB
autovacuum worker | relation | vacuum | 11155 | 111 MB | 9466 | 74 MB | 0 | 0 bytes
walsender | wal | normal | 1164 | 140 MB | 7986 | 86 MB | |
client backend | relation | vacuum | 905 | 113 MB | 0 | 0 bytes | 0 | 0 bytes
client backend | relation | bulkread | 827 | 56 MB | 0 | 0 bytes | |
background worker | relation | bulkread | 811 | 54 MB | 0 | 0 bytes | |
standalone backend | relation | normal | 479 | 4336 kB | 1038 | 8304 kB | 598 | 5464 kB
autovacuum worker | relation | normal | 296 | 3392 kB | 2 | 16 kB | 18 | 160 kBSeveral things are readable here that no other view offers.
The walsender / wal row is the 18 feature doing its job: WAL I/O
now appears in pg_stat_io, attributed to the process performing it. On
a primary with a standby attached, that row is replication reading WAL
to ship it. On a cluster that has just crash-recovered, the equivalent
row is startup / wal — the recovery in lesson XII-06 reading WAL to
replay it.
context = bulkread is the ring buffer from lesson XI-02, keeping
large scans out of the main pool. Separating it from normal shows how
much of your read volume is scans versus working-set access.
extend_bytes of 94 MB on a client backend is relation extension —
a table growing. It is not a read or a write in the ordinary sense and
it was invisible before this column existed.
Writes attributed to client backend in normal context are the
symptom from the top of this lesson: backends writing dirty buffers
because nothing was clean.
-- the specific query: are backends doing the checkpointer's work?
SELECT backend_type, context, writes, pg_size_pretty(write_bytes) AS written
FROM pg_stat_io
WHERE backend_type = 'client backend' AND writes > 0;
What to take from this
- Three writers: checkpointer on schedule, background writer continuously, backends when desperate. The third is the symptom.
maxwritten_cleanrising meansbgwriter_lru_maxpagesis capping the writer. Measured here at 570.- The background writer is among the least impactful things to tune. Tune it on evidence.
- PostgreSQL 18’s asynchronous I/O subsystem is controlled by
io_method, withworkerthe default andsyncthe way to test whether it is implicated. effective_io_concurrencynow defaults to 16 and works differently. Pre-18 advice about it describes another mechanism.pg_stat_ioattributes I/O by backend type, object and context, with byte columns new in 18.pg_aiosis a live view for one question.
Cross-course references
- Linux for Production Sysadmins — Part XLI (Storage Performance) covers the queue depth and completion behaviour that PostgreSQL 18’s I/O methods are trying to exploit.
- Observability for Production Sysadmins — Part LIX (Database
observability) covers the
pg_stat_ioseries that make this subsystem’s behaviour visible, which is where the olderpg_stat_bgwritercolumns went.
Quiz
Knowledge check · 6 questions
Q1. pg_stat_io shows a client backend with a substantial writes figure in the normal context. What does that indicate?
Q2. A cluster upgraded to PostgreSQL 18 shows a change in read-path performance. Which setting is the appropriate diagnostic first step?
Q3. Querying pg_aios during a period of heavy sequential scanning returns zero rows. What does that mean?
Q4. Which observations are available from pg_stat_io that other views do not provide? Select all that apply.
Q5. Guidance about effective_io_concurrency written for PostgreSQL 15 applies unchanged to PostgreSQL 18.
Q6. How would you establish whether the background writer needs tuning on a given cluster?
Passing score: 75%. Answers are checked in this browser.