Background processes
Checkpointer
The checkpointer is a single background process. It sleeps most of the time, wakes up to run checkpoints (or restartpoints on a standby), and collects the list of files that must be flushed to disk.
In plain words
The checkpointer is the process that actually does the work of a checkpoint. Every few minutes (or sooner when a lot of WAL is written) it copies all changed pages from shared memory to the data files and asks the operating system to make them durable. Then it writes a small "checkpoint done" note into the WAL and into the pg_control file.
It works slowly on purpose: it spreads the writes over most of the time until the next checkpoint so the disks are not flooded.
It has a second, quieter job. Whenever any process writes a page to a data file, the write first lands in the operating system cache. Instead of forcing it to disk right away, that process hands a note to the checkpointer saying "this file needs an fsync". At the end of the checkpoint the checkpointer flushes every file on its list in one go.
There is exactly one checkpointer, and it also runs on standby servers, where it performs restartpoints instead of checkpoints.
For DBAs and operators
Its main loop
The checkpointer sleeps on its latch (wait_event = CheckpointerMain). It wakes up when:
checkpoint_timeouthas elapsed since the last checkpoint started (reasontime);- a backend notices that WAL since the last checkpoint is approaching
max_wal_sizeand sets a request flag (wal); - someone runs
CHECKPOINT, a base backup starts, a database is dropped or created withSTRATEGY FILE_COPY, or the server shuts down (immediate force wait,shutdown immediate…); - its sync request queue is filling up and must be absorbed.
During the write phase it sorts the dirty buffers by file and block, writes them in that order, and calls CheckpointWriteDelay between writes to stay on schedule (shown as wait_event = CheckpointWriteDelay). While waiting it keeps absorbing sync requests. Then it fsyncs every file on its pending list, writes the checkpoint WAL record, updates pg_control, and removes or recycles WAL segments older than the redo point that nothing else (replication slots, wal_keep_size, archiving) still needs.
fsync request absorption
Backends, the background writer and I/O workers write pages without fsyncing. Each write to a segment file registers a sync request in a shared-memory queue. The checkpointer absorbs these into a private hash table, de-duplicating them (thousands of writes to the same file become one fsync). If the queue is full, the writer first tries to compact it; if that fails it must fsync the file itself. Those fsyncs show up in pg_stat_io as fsyncs for client backend and should stay near zero.
Restartpoints on a standby
A standby cannot write checkpoint records. Instead its checkpointer performs restartpoints: it flushes dirty buffers and advances the recovery start point, but only at positions where the primary wrote a checkpoint record. If the primary checkpoints rarely, the standby's pg_wal can grow beyond max_wal_size.
Monitoring
-- PostgreSQL 17+ (before that: pg_stat_bgwriter.checkpoints_*)
SELECT num_timed, num_requested, num_done,
restartpoints_timed, restartpoints_req, restartpoints_done,
write_time, sync_time, buffers_written, slru_written
FROM pg_stat_checkpointer;
SELECT pid, wait_event FROM pg_stat_activity
WHERE backend_type = 'checkpointer';num_done and slru_written are new in PostgreSQL 18; num_timed/num_requested also count checkpoints that were skipped because the server was idle. In ps the process is postgres: checkpointer.
If it dies
The checkpointer is attached to shared memory, so if it is killed the postmaster performs a full crash restart:
LOG: checkpointer process (PID 4478) was terminated by signal 9: Killed LOG: terminating any other active server processesAt shutdown it is the last regular process to exit: it writes the shutdown checkpoint and the statistics file. Very slow shutdowns are often just a big shutdown checkpoint; running CHECKPOINT just before a planned restart makes the restart fast.
For developers: inside the source
Where it lives
checkpointer.c:CheckpointerMain(), request handling,CheckpointWriteDelay()and the fsync request queue.xlog.c:CreateCheckPoint(),CreateRestartPoint(),CheckPointGuts(),RemoveOldXlogFiles().bufmgr.c:CheckPointBuffers()→BufferSync().sync.c:RegisterSyncRequest(),RememberSyncRequest(),ProcessSyncRequests().
Shared state and requests
CheckpointerShmemStruct holds ckpt_started, ckpt_done, ckpt_failed and ckpt_flags (protected by the spinlock ckpt_lck), two condition variables start_cv / done_cv, and the requests[] array of CheckpointerRequest (a FileTag plus type), protected by CheckpointerCommLock and sized Min(NBuffers, MAX_CHECKPOINT_REQUESTS). RequestCheckpoint(flags) ORs flags such as CHECKPOINT_IMMEDIATE, CHECKPOINT_FORCE, CHECKPOINT_WAIT or CHECKPOINT_CAUSE_XLOG into ckpt_flags, sets the checkpointer's latch and, with CHECKPOINT_WAIT, sleeps on the CVs until ckpt_done catches up. The WAL-volume trigger comes from XLogCheckpointNeeded(), checked in XLogWrite() when a segment fills.
Call flow of one checkpoint
CheckpointerMain()wakes fromWaitLatch(… WAIT_EVENT_CHECKPOINTER_MAIN), callsAbsorbSyncRequests(), reads and clearsckpt_flags, incrementsckpt_started.CreateCheckPoint()(orCreateRestartPoint()during recovery) fixes the redo pointer and callsCheckPointGuts().CheckPointGuts()flushes the relation map, replication slots, logical snapshot files, then the SLRUs (CheckPointCLOG(),CheckPointMultiXact()…) and finallyCheckPointBuffers().BufferSync()marks dirty buffersBM_CHECKPOINT_NEEDED, sorts them asCkptSortItems by tablespace/relation/fork/block, balances tablespaces with a binary heap and writes each viaSyncOneBuffer(), callingCheckpointWriteDelay()after every buffer. That function sleeps 100 ms at a time unlessIsCheckpointOnSchedule()says it is behind on time or WAL.ProcessSyncRequests()fsyncs every file in the privatependingOpshash table.- Back in
CreateCheckPoint(): insert the checkpoint record, updatepg_control,RemoveOldXlogFiles().
fsync forwarding
When a backend writes a block, md.c calls RegisterSyncRequest(), which in a normal backend becomes ForwardSyncRequest() into the shared array. If the array is full it tries CompactCheckpointerRequestQueue() (removing duplicates) before giving up, in which case the caller fsyncs the file itself. The checkpointer drains the array with AbsorbSyncRequests() → RememberSyncRequest(), deduplicating by FileTag.
Experimenting
- DTrace/SystemTap probes
buffer__checkpoint__start,buffer__checkpoint__sync__start,buffer__checkpoint__donebracket the phases inCheckPointGuts()(build with--enable-dtrace). log_checkpoints = onpluspg_stat_iorows forbackend_type = 'checkpointer';pg_buffercacheto watch dirty buffers drain.- TAP tests touching checkpoints and restartpoints:
045_archive_restartpoint.pl,041_checkpoint_at_promote.pl. The buffer READMEREADMEexplains buffer header locking.
Parameters that shape this
checkpoint_timeout max_wal_size checkpoint_completion_target checkpoint_flush_after checkpoint_warning log_checkpoints min_wal_size wal_keep_size
Related topics
Checkpoints Background writer Write-ahead log (WAL) Crash recovery Shared buffers Startup process Postmaster Hot standby and recovery conflicts