Write-Ahead Logging: How WAL Guarantees Durability
A transaction is durable when, after COMMIT returns, its effects survive a crash. Flushing every changed data page to disk at commit would be ruinously slow and random. PostgreSQL instead uses Write-Ahead Logging (WAL): before any change reaches a data file, a compact, sequential description of that change is written and flushed to the WAL. The rule (the "write-ahead" rule) is enforced in the buff
Grounded in the source
Where this comes from in postgres/postgres
Each citation was checked against the REL_17_STABLE branch: real file path, symbol present, exact line number. Follow any link to read the code.
Primary symbol: CommitTransaction · line 2188
Primary symbol: XLogFlush · line 2778
Primary symbol: XLogInsert · line 474
Primary symbol: XLogRecPtr · line 21
The one thing to understand first
A transaction is durable when, after COMMIT returns, its effects survive a crash. Flushing every changed data page to disk at commit would be ruinously slow and random. PostgreSQL instead uses Write-Ahead Logging (WAL): before any change reaches a data file, a compact, sequential description of that change is written and flushed to the WAL. The rule (the "write-ahead" rule) is enforced in the buffer manager and is the foundation of crash recovery.
PostgreSQL makes the log durable, not the data — the data pages catch up later. That single inversion explains why commits are fast, why a crash is recoverable, why replication is just "ship the log," and why a stuck WAL consumer can fill your disk.
The LSN: a byte address in the log
Every WAL record has a Log Sequence Number (LSN) — a 64-bit monotonically increasing byte offset into the logical WAL stream, represented as XLogRecPtr in src/include/access/xlogdefs.h. Each data page header stores the LSN of the last WAL record that modified it (pd_lsn). The buffer manager will not flush a dirty page to disk until the WAL up to that page's pd_lsn has been flushed — that single check is the write-ahead guarantee.
SELECT pg_current_wal_lsn(); -- current insert position
SELECT pg_walfile_name(pg_current_wal_lsn()); -- which 16MB segmentInserting a WAL record
Modifying code (heap, btree, etc.) builds a WAL record and calls XLogInsert() (src/backend/access/transam/xloginsert.c). The record describes the change as a redo action: enough information to re-apply it during recovery. Records are appended into shared WAL buffers; XLogInsert returns the LSN of the end of the record.
The first time a page is modified after a checkpoint, PostgreSQL writes a full-page image (FPI) into WAL. This protects against torn pages — a partial 8KB write during a crash — because recovery can restore the entire page from the log. Full-page writes are why WAL volume spikes right after a checkpoint.
What happens at COMMIT
On commit, RecordTransactionCommit() (src/backend/access/transam/xact.c) writes a commit WAL record and then calls XLogFlush() to force the WAL up to the commit LSN out to durable storage via fsync. Only after that flush succeeds does the backend report success to the client. The data pages themselves may still be dirty in shared buffers — they will be written lazily by the background writer or the next checkpoint.
The cost of that fsync is why synchronous_commit = off is so much faster: it lets the backend return before the flush, trading a tiny window of potential loss for throughput. It does not risk corruption — only the loss of the most recent commits.
Checkpoints bound recovery time
A checkpoint (CreateCheckPoint() in src/backend/access/transam/xlog.c) flushes all dirty buffers and records a checkpoint WAL record. After a crash, recovery only needs to replay WAL from the last checkpoint's redo point forward, because everything before it is known to be on disk. checkpoint_timeout and max_wal_size control how often this happens, trading steady-state I/O against recovery time.
Crash recovery: REDO
On startup after an unclean shutdown, StartupXLOG() reads the control file (pg_control) to find the redo point, then replays each WAL record by dispatching to that resource manager's rm_redo callback (for example, heap_redo). Each record is idempotent against its target page: the redo routine checks the page LSN and skips records already reflected on the page. When replay reaches the end of valid WAL, the database is consistent and opens for connections.
Layer 3 — Watch it happen on your own database
-- See WAL generation rate and replay position
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')) AS total_wal;
SELECT slot_name, restart_lsn, pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;Run an UPDATE, then call pg_current_wal_lsn() again — the LSN advances by the size of the WAL you just generated. The second query is the one you reach for in an incident: a slot whose retained bytes keep climbing is the reason your disk is filling.
Layer 4 — The levers this hands you
- WAL is also the replication stream. Streaming replication ships these same records to standbys, which run the redo path continuously — so WAL volume is replication bandwidth.
- Archiving WAL enables PITR. A base backup plus archived WAL lets you replay to any point in time;
archive_command/archive_libraryis what makes that possible. - WAL bloat has real causes. An unconsumed replication slot or failing
archive_commandprevents segment recycling and can fill the disk — cap the damage withmax_slot_wal_keep_size. - **
synchronous_commitis a throughput dial, not a safety switch.** Turning itoffskips the commitfsyncwait and risks only the last few commits — never corruption.
Layer 5 — What an Oracle DBA should expect vs what they get
WAL is Oracle's redo log under a different name, with a few sharp differences:
- One log, not redo + UNDO. Oracle separates redo (for roll-forward) from UNDO (for rollback and read consistency). PostgreSQL's WAL is roll-forward only; rollback and MVCC are handled by dead tuples in the heap, so there is no UNDO log to size or tune.
- Full-page images instead of a fractured-block fix-up. The first change to a page after each checkpoint writes the whole page into WAL to defeat torn writes — which is why Postgres WAL volume spikes right after a checkpoint. Oracle's equivalent concern (fractured blocks) is handled differently; the post-checkpoint WAL surge surprises Oracle DBAs.
- LSN is your SCN. The
XLogRecPtrLSN plays the role of Oracle's SCN as the global progress marker, but it is a literal byte offset into the log — you can subtract two LSNs to get bytes, which is how you measure replication lag and WAL rate directly. - No separate archiver process by default tuning. Archiving is a command/library you configure, not a background ARCn you manage; getting
archive_commandright (and monitored) is the Postgres equivalent of babysitting the archive destination.
Key takeaway
Durability in PostgreSQL is a property of the log, not the data files. XLogInsert() appends a redo description, XLogFlush() forces it to disk at commit, and the dirty data pages are written lazily afterward. Crash recovery replays WAL forward from the last checkpoint's redo point. Internalize "make the log durable, ship the log, replay the log" and replication, PITR, and WAL-disk incidents all become the same story told three ways.
Interview lens
How this shows up in interviews
What an interviewer is actually testing, the same mechanism seen from three roles, and the gap between a shallow answer and one that shows real depth.
What the interviewer is testing
Whether you understand why WAL exists — turn scattered random page flushes into one sequential flush — and the exact write-ahead rule and commit/fsync path.
Questions you should be able to answer
- ▸After COMMIT returns, what guarantees durability if the changed data pages aren't on disk yet?
- ▸State the write-ahead rule.
- ▸What does synchronous_commit = off actually trade away?
Database engineer
You treat commit latency as WAL fsync latency: put pg_wal on fast storage and tune wal_buffers, commit_delay, and synchronous_commit deliberately.
Software engineer
You know a returned COMMIT means the WAL record is flushed, not that data files are updated; with synchronous_commit=off a crash can lose the last moments even though COMMIT returned.
Feature builder
You set per-transaction synchronous_commit=off for non-critical high-volume writes (telemetry) to cut latency, keeping it on for money movements.
Shallow answer
'WAL is the transaction log used for recovery.' Names it but misses the write-ahead rule and why it's fast.
Answer that shows depth
Before any change reaches a data file, a compact redo record describing it is written to WAL and, at commit, fsync'd — the write-ahead rule, enforced in the buffer manager. Durability comes from that single sequential flush, not from writing scattered 8KB pages; the pages are written later by the checkpointer/bgwriter and, after a crash, reconstructed by replaying WAL from the last checkpoint. synchronous_commit=off returns COMMIT before the WAL flush, trading a small bounded window of possibly-lost-but-still-consistent transactions for much lower commit latency.
Pro lab notebook
The captured run behind every query
Every SQL query above is real and the source citations are verified. Pro unlocks the captured psql output — each query re-run against a real PostgreSQL 17.10 lab, with its verbatim result.
2 real lab transcripts for this lesson
The explanation and the SQL above are free — so are the verified source citations. What Pro unlocks is the proof: every query in Write-Ahead Logging: How WAL Guarantees Durability re-run against a real PostgreSQL 17.10 instance, with its verbatim psql output captured line for line.
What Pro unlocks here
- Every query in this lesson re-run against a real PostgreSQL 17.10 lab
- The verbatim psql output — page headers, row versions, plan nodes, catalog rows
- The corrected form of any query the source lesson got wrong, re-captured
- A copy-ready notebook you can replay yourself to confirm every claim
How this was verified
This explanation is anchored to PostgreSQL's source tree — the files linked above, on the REL_17_STABLE branch — and cross-checked against the official documentation for versions 15 through 18. Every SQL query shown was executed and its output captured in a throwaway PostgreSQL 17.10 lab; where a query needed correcting to run, that correction is noted inline.
Official documentation: §30.3 Write-Ahead Logging (WAL)