DB Internals AtlasPostgreSQL 18.6 Home About

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_timeout has 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 near max_wal_size.
  • Explicit: CHECKPOINT, shutdown, end of recovery, pg_backup_start(), DROP DATABASE and CREATE DATABASE … STRATEGY FILE_COPY (the default WAL_LOG strategy 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/6BD8C8D8

write 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_requested much higher than num_timed means max_wal_size is 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 functions CheckpointWriteDelay() and IsCheckpointOnSchedule(), and CheckpointerShmemStruct (ckpt_started, ckpt_done, ckpt_flags and the queue of fsync requests forwarded by backends).
  • xlog.c: CreateCheckPoint(), CheckPointGuts(), CreateRestartPoint() (the standby variant) and CalculateCheckpointSegments(), which turns max_wal_size and checkpoint_completion_target into CheckPointSegments.
  • bufmgr.c: BufferSync(), the write phase.
  • sync.c: ProcessSyncRequests(), the fsync phase; pg_control.h: the CheckPoint record and ControlFileData.

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_checkpointer and log_checkpoints (text built by LogCheckpointStart()/LogCheckpointEnd()); pg_buffercache_summary() from pg_buffercache shows dirty buffers before and after.
  • Injection points checkpoint-before-old-wal-removal and create-restart-point are used by the TAP tests src/test/recovery/t/046_checkpoint_logical_slot.pl, 047_checkpoint_physical_slot.pl and 041_checkpoint_at_promote.pl (server built with -Dinjection_points=true).
  • storage/buffer/README explains 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