Skip to main content
RunBook Academy

PostgreSQLXII · WAL, Checkpoints and Crash RecoveryWAL

Write-ahead logging: the durability contract

Intermediate⏱ ~30 minpsql

What you'll learn

  • State the write-ahead rule and what it guarantees
  • Explain what full_page_writes protects against and why it costs what it does
  • Distinguish the settings that trade latency from those that void the contract
  • Choose a wal_level from what the cluster actually needs

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.

Part VI established that a commit waits for one fsync. This part is about what that fsync is writing and why it is enough.

The rule

A change is written to durable WAL before the data page it describes is written to disk.

That is the whole contract, and everything else in this part is a consequence of it.

It works because the WAL is sequential and the data pages are not. One sequential write, synced once, records what happened to a hundred scattered pages. Those pages can then be written whenever it is convenient — by a checkpoint, by the background writer, or by a backend that needs the buffer — because if the server dies first, the WAL contains everything needed to redo them.

Read-only / Safethe contract, tested
$ docker kill --signal=KILL rbpg-stor && docker start rbpg-stor
LOG:  database system was interrupted; last known up at 2026-08-27 21:01:01 UTC
LOG:  database system was not properly shut down; automatic recovery in progress
LOG:  redo starts at 7/CB35CEA8
LOG:  invalid record length at 7/CF4461A0: expected at least 24, got 0
LOG:  redo done at 7/CF446178 system usage: CPU: user: 0.09 s, system: 0.03 s, elapsed: 0.12 s
LOG:  database system is ready to accept connections

-- SELECT count(*) FROM crashtest;
300000

Every committed row survived. 65 MB of WAL was replayed in 0.12 seconds.

What full_page_writes protects against

A PostgreSQL page is 8 KiB. Storage typically writes in 512-byte or 4 KiB units. If the power fails mid-write, the page on disk can be part old and part new — a torn page.

WAL cannot repair that, because a WAL record describes a change to a page and assumes the rest of the page is intact. Applying “set byte 1200 to X” to a half-written page produces something worse.

full_page_writes solves it: the first modification of a page after each checkpoint writes the entire page to WAL. Recovery can then restore the whole page rather than patching it.

Read-only / Safewhat that costs, measured across a checkpoint boundary
$ psql -U postgres -c "CHECKPOINT" -c "UPDATE churn_default SET v=v+1 WHERE id BETWEEN 1 AND 20000"   # repeated, measuring pg_current_wal_lsn and wal_fpi
  round 1: WAL 9217 kB   full page images: 473
round 2: WAL 6123 kB   full page images: 2
round 3: WAL 6138 kB   full page images: 2

50% more WAL in the first round, and 473 full page images against 2.

Three consequences that come up repeatedly:

WAL volume is not a flat function of write volume. It spikes after every checkpoint. A monitoring alert on WAL rate will fire on the spike, not on a problem.

Shorter checkpoint intervals mean more spikes. This is the largest argument against setting checkpoint_timeout very low, and lesson XII-04 develops the trade.

Freezing is disproportionately expensive, because it modifies every page. Lesson VIII-05 measured a first freeze generating roughly the table’s own size in WAL, and this is the mechanism.

The settings, and which ones void the contract

SettingTradesVoids the contract?
synchronous_commit = offLoses recent commits on crashNo — bounded, recoverable
wal_compressionCPU for WAL volumeNo
wal_buffersMemory for fewer flushesNo
commit_delayLatency for batchingNo
full_page_writes = offTorn page protectionYes, on ordinary storage
fsync = offThe write-ahead rule itselfYes

The first four are engineering decisions with understood costs. The last two are the removal of the property that makes recovery work, and lesson VI-06 made the distinction: synchronous_commit = off risks a bounded number of recent transactions, fsync = off risks unbounded corruption.

wal_level

LevelRecords enough forCost
minimalCrash recovery onlyLeast WAL
replica (default)Crash recovery, archiving, physical replicationModerate
logicalAll of the above, plus logical decodingMost

minimal is a genuine optimisation for a cluster that will never be replicated, archived or backed up physically — a build-time data transformation, a scratch instance. It also disallows WAL archiving and replication entirely, so a cluster set to minimal cannot have a standby added without a restart, and cannot be backed up with pg_basebackup.

logical is required for logical replication and for change-data-capture tools. It adds row-level detail to WAL and therefore volume, so setting it “just in case” is a real, ongoing cost.

Changing wal_level requires a restart. Choose it when the cluster is built, and revisit it only with a maintenance window.

What to take from this

  • The rule: WAL is durable before the data page it describes. That is what makes crash recovery possible.
  • Measured: 300,000 committed rows survived a SIGKILL; 65 MB replayed in 0.12 s.
  • invalid record length at the end of redo is normal, not corruption.
  • full_page_writes costs 50% more WAL in the round after a checkpoint. Use wal_compression rather than turning it off.
  • fsync = off and full_page_writes = off remove the guarantee. Everything else on the list trades latency or CPU.
  • An LSN is a byte offset. Subtracting two of them answers most WAL questions.

Cross-course references

  • Linux for Production Sysadmins — Part XIII (Disks and block devices) covers whether a flush reaches durable media, which is the assumption the whole contract rests on.
  • Ceph & Distributed Storage — Part XII (BlueStore) covers the same write-ahead idea implemented one layer down, and Part II (Storage performance fundamentals) covers measuring the flush latency it costs.

Quiz

Knowledge check · 6 questions

  1. Q1. During an incident the recovery log shows 'invalid record length at 7/CF4461A0: expected at least 24, got 0' followed by 'redo done'. What should be concluded?

  2. Q2. Why does the first UPDATE of a set of pages after a checkpoint generate substantially more WAL than identical updates that follow?

  3. Q3. A cluster is set to wal_level = minimal to reduce WAL volume. What has this precluded?

  4. Q4. Which settings trade latency, CPU or memory without removing the durability guarantee? Select all that apply.

  5. Q5. An LSN such as 7/CF446178 is a byte offset into the WAL stream, so subtracting two LSNs gives the number of bytes between them.

  6. Q6. Explain the write-ahead rule and why it makes a scattered set of page writes recoverable from one sequential log.

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