Lesson 0009 · Durability

The WAL & checkpoints

Postgres promises your commit survives a crash without flushing every data page — by writing the change to a log first. One log, redo-only, and the reason InnoDB needs two where Postgres needs one.

~11 minWarm-up recall + quiz + InnoDB contrastCheat sheet: here

Warm-up: recall 0008 & the storage pillar first

Closed book. The second one is the hinge for today's big contrast.

Postgres picks a Seq Scan over your index. The most accurate framing is:

The planner is cost-based (0008). A Seq Scan won on estimated cost — fairly (selectivity/hardware) or not (stale stats).

Where does Postgres keep the old version of a row after an UPDATE?

No undo log — old versions live in the heap as dead tuples (0001/0004), reclaimed by VACUUM. Hold that: it's why Postgres needs only ONE log for durability.

Here's the problem durability has to solve. A committed transaction must survive a crash — but its changes might only be sitting in dirty pages in memory, not yet on disk. The naive fix (flush every changed data page at every commit) would be brutally slow: those pages are scattered, so it's random I/O on the hot path of every write.

The one idea: log before data

Write-Ahead Logging flips the order. Before a data page may be written to disk, the small log record describing that change must be flushed first. PostgreSQL Docs: "WAL's central concept is that changes to data files… must be written only after those changes have been logged, that is, after WAL records describing the changes have been flushed to permanent storage."

Why that helps: the WAL is one sequential file. At commit, you flush only the WAL — not the scattered data pages — and you're safe. PostgreSQL Docs: "Using WAL results in a significantly reduced number of disk writes, because only the WAL file needs to be flushed to disk to guarantee that a transaction is committed, rather than every data file changed by the transaction." A sequential append to one file beats random writes to many — the same trade every durable database makes.

COMMIT PATH ┌────────────────────────────────────────────────────────────┐ │ 1. change pages in the buffer pool (still dirty, in memory) │ │ 2. append WAL records describing the changes │ │ 3. flush WAL to disk ← the only forced write at commit │ │ 4. report COMMIT ok │ └────────────────────────────────────────────────────────────┘ dirty data pages are written later, lazily. If we crash first, the WAL still has the change and we REDO it on restart.
Recovery = roll forward (REDO)

After a crash, Postgres replays the WAL to re-apply any committed change that hadn't reached the data files yet. PostgreSQL Docs: "any changes that have not been applied to the data pages can be redone from the WAL records. (This is roll-forward recovery, also known as REDO.)" You met this exact mechanism as InnoDB's redo log in MySQL lesson 0007 — the WAL is Postgres's redo log.

Checkpoints — so recovery isn't the whole history

If recovery replayed all WAL ever written, restart would take forever and WAL would grow without bound. A checkpoint fixes both: periodically Postgres flushes all dirty data pages to disk and writes a checkpoint record. PostgreSQL Docs: "Checkpoints are points in the sequence of transactions at which it is guaranteed that the heap and index data files have been updated with all information written before that checkpoint. At checkpoint time, all dirty data pages are flushed to disk…"

Two consequences follow directly. Recovery only needs to replay WAL from the last checkpoint: PostgreSQL Docs: "the crash recovery procedure looks at the latest checkpoint record to determine the point in the WAL (known as the redo record) from which it should start the REDO operation." And WAL older than that can be thrown away: PostgreSQL Docs: "after a checkpoint, WAL segments preceding the one containing the redo record are no longer needed and can be recycled or removed."

The checkpoint trade-off

Frequent checkpoints → short crash recovery and less WAL to keep, but more constant data-page I/O (and I/O spikes as dirty pages are flushed). Infrequent checkpoints → cheaper steady-state, but a longer replay after a crash and more WAL retained. Tuning checkpoint frequency is trading recovery time against runtime write overhead — the same dial you saw on the InnoDB side.

The durability dial: synchronous_commit

By default, COMMIT waits for the WAL to be flushed to disk before it returns — full durability. PostgreSQL Docs: "The local behavior of all non-off modes is to wait for local flush of WAL to disk." Turn synchronous_commit = off and commit returns before the flush — much faster under write load, at a price. And here is the subtle, important part:

off loses transactions, but never corrupts

PostgreSQL Docs: "setting this parameter to off does not create any risk of database inconsistency: an operating system or database crash might result in some recent allegedly-committed transactions being lost, but the database state will be just the same as if those transactions had been aborted cleanly." So synchronous_commit=off risks a small window of recent commits on a crash, but the database stays consistent — unlike turning off fsync, which really can corrupt. It's the Postgres analogue of InnoDB's innodb_flush_log_at_trx_commit dial from MySQL 0007: trade a sliver of durability for throughput, deliberately.

The keystone contrast: why InnoDB needs two logs and Postgres needs one

This is where the whole course clicks together. InnoDB keeps two logs: a redo log (roll forward for durability) and an undo log (roll back a transaction, and serve old row versions for MVCC). Postgres has only the WAL — pure redo. It needs no undo log because, from the very first lesson, old row versions live in the heap (never update in place), and VACUUM — not an undo log — cleans them up.

JobInnoDBPostgreSQL
Roll forward after crash (durability)Redo logWAL (redo)
Old row versions for MVCCUndo logThe heap (dead tuples)
Undo an aborted transactionUndo logMark tuples not-visible; heap cleanup
Reclaim old versionsPurge threadsVACUUM / autovacuum
Flush-at-commit dialinnodb_flush_log_at_trx_commitsynchronous_commit

So the choice you learned in lesson 0001 — keep versions in the heap — is exactly what lets Postgres drop the undo log. One design decision, echoing all the way down to the durability layer.

Check yourself

From memory. Two items reach back on purpose.

WAL's central rule is:

The log record describing a change must be flushed to durable storage before the data page it changes. That's what makes commit cheap and crash recovery possible.

At commit, the only thing that must be flushed to disk is:

Only the sequential WAL must be flushed to guarantee durability — the scattered dirty data pages are written lazily later. That's the big reduction in disk writes.

A checkpoint lets crash recovery:

A checkpoint flushes all dirty pages, so recovery only replays WAL from the last checkpoint's redo record — and older WAL can be recycled.

synchronous_commit = off risks:

off returns commit before the WAL flush, so a crash can lose a small window of recent transactions — but the database stays consistent, as if those had aborted cleanly. (fsync=off is the one that risks corruption.)

Postgres needs no undo log (unlike InnoDB) because: (recall 0001)

Never-update-in-place keeps old versions inline in the heap (cleaned by VACUUM), so the WAL only needs to do redo. InnoDB's undo log does the MVCC job the heap does here.
Optional — when you have an instance

A lab to run later — peek at the WAL and force a checkpoint:

SHOW wal_level;                 -- replica (default): enough for crash recovery + streaming
SHOW synchronous_commit;        -- on by default (full durability)
SELECT pg_current_wal_lsn();    -- current write position in the WAL

INSERT INTO some_table DEFAULT VALUES;   -- generate some WAL
SELECT pg_current_wal_lsn();    -- the LSN advanced

CHECKPOINT;                     -- force dirty pages to disk now (superuser)

Watch the LSN move as you write, and note that CHECKPOINT is what lets old WAL be recycled. Bring anything surprising to your teacher.

Primary source — read this next

PostgreSQL Docs — 28.4 Write-Ahead Logging for the core idea, then 28.5 WAL Configuration for checkpoints and tuning. For the byte-level picture, Suzuki's "Internals of PostgreSQL," ch. 9 (WAL).