WAL and recovery
Checkpoints
A checkpoint writes every dirty page in shared buffers to the data files, so crash recovery only needs WAL from that point on.
In plain words
Changed pages live in memory for a while. A checkpoint is the moment PostgreSQL makes sure all of them are safely in the data files. After that, if the server crashes, recovery only has to replay the WAL written since the checkpoint.
Four settings control it. checkpoint_timeout decides how often one starts (and max_wal_size can start one early). checkpoint_completion_target decides how slowly the pages are written, so the disk is not flooded. checkpoint_flush_after nudges the operating system to send writes to disk in small steady pieces. checkpoint_warning only warns you in the log if checkpoints come too close together.
For DBAs and operators
When a checkpoint starts
- time:
checkpoint_timeouthas passed since the previous one started (default 5min). - wal: the WAL written since the last checkpoint reaches about
max_wal_size/ (1 +checkpoint_completion_target), roughly 540 MB with the defaults (1GB, 0.9). That way the WAL kept for recovery plus the WAL written during the next checkpoint stays nearmax_wal_size. - Explicit:
CHECKPOINT, shutdown, end of recovery,pg_backup_start(),DROP DATABASEandCREATE DATABASE … STRATEGY FILE_COPY(the defaultWAL_LOGstrategy needs none).
How it paces itself
The checkpointer compares its progress with two clocks: elapsed time as a share of checkpoint_timeout, and WAL written as a share of that WAL trigger distance. It aims to finish when the larger of the two reaches checkpoint_completion_target (0.9). If WAL is growing fast, it speeds up.
Reading the log line
LOG: checkpoint complete: wrote 2850 buffers (17.4%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 1 recycled; write=269.912 s, sync=0.012 s, total=269.931 s; sync files=27, longest=0.003 s, average=0.001 s; distance=23456 kB, estimate=24310 kB; lsn=0/6BD8C930, redo lsn=0/6BD8C8D8write is the paced write phase, sync is the final fsync of the files. A large sync time usually means a lot of data was still in the OS page cache, which checkpoint_flush_after is meant to prevent. distance is the WAL generated since the previous checkpoint.
Tuning in practice
- Look at
pg_stat_checkpointer(PostgreSQL 17+):num_requestedmuch higher thannum_timedmeansmax_wal_sizeis too small for the write load. - Longer intervals mean fewer full-page images in WAL and less I/O, but longer crash recovery.
- Keep
log_checkpoints = on(the default since PostgreSQL 15).
SELECT num_timed, num_requested, write_time, sync_time, buffers_written
FROM pg_stat_checkpointer;For developers: inside the source
Where it lives
checkpointer.c:CheckpointerMain()(the wait loop),RequestCheckpoint(), the pacing functionsCheckpointWriteDelay()andIsCheckpointOnSchedule(), andCheckpointerShmemStruct(ckpt_started,ckpt_done,ckpt_flagsand the queue of fsync requests forwarded by backends).xlog.c:CreateCheckPoint(),CheckPointGuts(),CreateRestartPoint()(the standby variant) andCalculateCheckpointSegments(), which turnsmax_wal_sizeandcheckpoint_completion_targetintoCheckPointSegments.bufmgr.c:BufferSync(), the write phase.sync.c:ProcessSyncRequests(), the fsync phase;pg_control.h: theCheckPointrecord andControlFileData.
Call flow
CheckpointerMain() timeout or RequestCheckpoint()
CreateCheckPoint(flags)
REDO point: XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO)
wait for backends with DELAY_CHKPT_START
CheckPointGuts() CLOG, SUBTRANS, MultiXact, ... then
CheckPointBuffers() → BufferSync()
ProcessSyncRequests() fsync every touched file
XLogInsert(XLOG_CHECKPOINT_ONLINE); XLogFlush()
UpdateControlFile() under ControlFileLock
KeepLogSeg() → RemoveOldXlogFiles()Since PostgreSQL 17 an online checkpoint fixes its REDO point by inserting an XLOG_CHECKPOINT_REDO record; a shutdown checkpoint computes it directly under all WAL insertion locks. From that moment RedoRecPtr is advanced, so every page modified afterwards gets a full-page image.
The write phase
BufferSync() scans all buffer headers once and sets BM_CHECKPOINT_NEEDED on each dirty permanent buffer (all dirty buffers with CHECKPOINT_FLUSH_ALL). It sorts them as CkptSortItems by tablespace, relation, fork and block, and writes them through SyncOneBuffer() → FlushBuffer(), using a binary heap to balance progress across tablespaces. After every buffer it calls CheckpointWriteDelay(flags, progress): if the checkpoint is not CHECKPOINT_IMMEDIATE and IsCheckpointOnSchedule() says it is ahead of both the time and the WAL clock, it naps 100 ms (wait event CheckpointWriteDelay). checkpoint_flush_after works through ScheduleBufferTagForWriteback() and IssuePendingWritebacks().
Observe and experiment
pg_stat_checkpointerandlog_checkpoints(text built byLogCheckpointStart()/LogCheckpointEnd());pg_buffercache_summary()frompg_buffercacheshows dirty buffers before and after.- Injection points
checkpoint-before-old-wal-removalandcreate-restart-pointare used by the TAP testssrc/test/recovery/t/046_checkpoint_logical_slot.pl,047_checkpoint_physical_slot.pland041_checkpoint_at_promote.pl(server built with-Dinjection_points=true). storage/buffer/READMEexplains pins, content locks and the buffer header state that the write phase relies on.
Parameters that shape this
checkpoint_timeout max_wal_size min_wal_size checkpoint_completion_target checkpoint_flush_after checkpoint_warning log_checkpoints full_page_writes
Related topics
Write-ahead log (WAL) Checkpointer Background writer Crash recovery Shared buffers