WAL and recovery
Write-ahead log (WAL)
Before a changed page may reach the data files, a record describing the change must be in the write-ahead log. A commit is durable once its WAL is flushed to disk.
In plain words
Imagine a shop that keeps a diary. Before anything on the shelves is moved, the clerk writes one line in the diary: "moved box 7 to shelf 3". If the lights go out, the next morning someone reads the diary and redoes every line, and the shop is back exactly as it was. PostgreSQL's diary is the write-ahead log (WAL).
Every change (insert, update, delete, index change, new table) becomes a small WAL record (temporary and unlogged tables are the exception: they skip the WAL). Each record has an address called an LSN (log sequence number), which is simply its byte position in the never-ending log, written like 0/2E09378.
Records are first collected in a small memory area (the WAL buffers) and then written to files in the pg_wal directory, 16 MB each by default. When you COMMIT, PostgreSQL (with the default synchronous_commit = on) waits until your records are safely on disk. Writing the log is fast because it is one sequential stream; the scattered table pages can be written later.
Old WAL files are not kept forever: after a checkpoint they are no longer needed for crash recovery, and PostgreSQL renames them for reuse or deletes them, unless they still have to be archived or are being kept for replicas (by a replication slot or wal_keep_size).
For DBAs and operators
Records and LSNs
A WAL record has a header (total length, xid, previous record pointer, resource manager, CRC) followed by block references and data. The LSN is a 64-bit byte position printed as two hex halves. pg_waldump decodes them (output shortened):
$ pg_waldump -p $PGDATA/pg_wal 000000010000000000000002
rmgr: Heap len (rec/tot): 61/61, tx: 768, lsn: 0/02000038, prev 0/01FFFFD0,
desc: INSERT off: 46, flags: 0x00, blkref #0: rel 1663/5/16439 blk 273
rmgr: Btree len (rec/tot): 64/64, tx: 768, lsn: 0/02000078, prev 0/02000038,
desc: INSERT_LEAF off: 256, blkref #0: rel 1663/5/16445 blk 170
rmgr: Transaction len (rec/tot): 34/34, tx: 768, lsn: 0/020000B8, prev 0/02000078,
desc: COMMIT 2026-10-22 15:34:09.112233 UTCFrom buffer to segment
Backends copy records into wal_buffers (default -1 = 1/32 of shared_buffers, at most one segment). At commit the backend calls XLogFlush(): it writes and fsyncs (per wal_sync_method) everything up to its commit record. Concurrent committers share one flush ("group commit"); commit_delay can widen that window. The WAL writer flushes in the background every wal_writer_delay. pg_stat_wal.wal_buffers_full counts the times a backend had to write because the buffers were full.
Segment file names
Segments are wal_segment_size (16 MB unless set at initdb --wal-segsize). The 24-hex-digit name is timeline (8) + high 32 bits of the LSN (8) + segment number within that 4 GB range (8):
SELECT pg_current_wal_lsn(), -- 0/2E09378
pg_walfile_name(pg_current_wal_lsn()), -- 000000010000000000000002
pg_walfile_name_offset(pg_current_wal_lsn()); -- (…0002, 14717816)
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0'));synchronous_commit
on(default): wait for the local flush (and, with sync standbys, their flush; see synchronous replication).off: return at once; the WAL writer flushes within about 3 ×wal_writer_delay. A crash can lose the last acknowledged transactions but never corrupts data.local,remote_write,remote_apply: only differ when synchronous standbys are configured.
wal_level
minimal skips WAL for bulk loads into tables created or truncated in the same transaction (no archiving or replication possible); replica (default) logs enough for archiving, streaming and hot standby; logical adds the information needed for logical decoding. wal_compression (pglz, lz4, zstd) compresses only full-page images.
Recycling and removal
At the end of each checkpoint, segments older than the checkpoint's REDO point are recycled (renamed to future names) or removed, keeping between min_wal_size and max_wal_size. Extra WAL is retained by wal_keep_size, by replication slots (capped by max_slot_wal_keep_size) and by a failing archive_command (look for .ready files in pg_wal/archive_status). Never delete files in pg_wal by hand.
SELECT wal_records, wal_fpi, pg_size_pretty(wal_bytes), wal_buffers_full
FROM pg_stat_wal; -- PG18: write/sync timings moved to pg_stat_ioPostgreSQL 18 source trail: xloginsert.c states the WAL-before-data rule; RelationNeedsWAL() captures relation-level WAL exceptions.
For developers: inside the source
Where it lives
src/backend/access/transam/xloginsert.c(linked in the source trail): the record-building APIXLogBeginInsert(),XLogRegisterData(),XLogRegisterBuffer(),XLogInsert(), andXLogRecordAssemble(), which chains the pieces together and decides per block whether a full-page image is needed.xlog.c:XLogInsertRecord()and the shared state (XLogCtlData,XLogCtlInsert), plusXLogFlush(),XLogWrite(),XLogBackgroundFlush()(the WAL writer's work) and segment recycling.xlogrecord.h: the on-disk format, anXLogRecordheader (xl_tot_len,xl_xid,xl_prev,xl_info,xl_rmid,xl_crc) followed byXLogRecordBlockHeaderentries and the payload.rmgrlist.h: the resource managers (Heap, Btree, Transaction, XLOG …) with their redo and desc callbacks; the desc functions inrmgrdesc/produce whatpg_waldumpprints.
Writing a record
Every WAL-logged change follows the pattern in the "Write-Ahead Log Coding" section of access/transam/README (simplified from heap_insert()):
START_CRIT_SECTION();
/* ... change the page ... */
MarkBufferDirty(buffer);
XLogBeginInsert();
XLogRegisterData(&xlrec, SizeOfHeapInsert);
XLogRegisterBuffer(0, buffer, REGBUF_STANDARD);
recptr = XLogInsert(RM_HEAP_ID, info);
PageSetLSN(page, recptr);
END_CRIT_SECTION();Insertion has two phases. ReserveXLogInsertLocation() advances Insert->CurrBytePos under the insertpos_lck spinlock (the only fully serialized step), then CopyXLogRecordToWAL() copies the bytes into the WAL buffers while holding one of NUM_XLOGINSERT_LOCKS (8) WAL insertion locks, so several backends copy at once. If a checkpoint moved RedoRecPtr after the record was assembled, XLogInsertRecord() returns InvalidXLogRecPtr and XLogInsert() loops to reassemble it with the now-required page images.
Commit and flush
xact.c: RecordTransactionCommit() → XactLogCommitRecord() → XLogFlush(XactLastRecEnd) when synchronous_commit is not off (or the commit drops relations); otherwise it only calls XLogSetAsyncXactLSN(). XLogFlush() calls WaitXLogInsertionsToFinish(), then takes WALWriteLock with LWLockAcquireOrWait(): a backend that had to wait and finds its LSN already flushed by someone else returns without writing. That is group commit. commit_delay is a pg_usleep() before taking the lock, only when at least commit_siblings other transactions are active. XLogWrite() writes and syncs (issue_xlog_fsync()), marks finished segments for the archiver (XLogArchiveNotifySeg()) and requests a checkpoint when XLogCheckpointNeeded() says CheckPointSegments was exceeded.
Observe and experiment
pg_walinspect:pg_get_wal_records_info(),pg_get_wal_block_info(),pg_get_wal_stats()from SQL;pg_waldumpoffline.wal_consistency_checking = 'heap,btree'adds a page image to every such record and compares it after replay (verifyBackupPageConsistency()), the standard way to test a new redo routine on a standby.wal_debugexists only when the server is compiled withWAL_DEBUGdefined (seepg_config_manual.h).- TAP tests:
src/test/recovery/t/039_end_of_wal.pl,018_wal_optimize.pl(thewal_level = minimalskip),src/bin/pg_waldump/t/.
Extension hooks
Extensions can log their own pages with generic WAL (generic_xlog.c: GenericXLogStart(), GenericXLogRegisterBuffer(), GenericXLogFinish(), which logs page deltas), or register a custom resource manager with RegisterCustomRmgr(), which only works from a library in shared_preload_libraries; test_custom_rmgrs is a minimal example.
Parameters that shape this
wal_level synchronous_commit wal_buffers wal_writer_delay wal_compression max_wal_size min_wal_size wal_keep_size max_slot_wal_keep_size wal_sync_method commit_delay wal_recycle wal_init_zero fsync
Related topics
Checkpoints Full-page writes Crash recovery WAL writer Archiving and point-in-time recovery Replication slots Streaming replication