DB Internals AtlasPostgreSQL 18.6 Home About

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_timeout has elapsed since the last checkpoint started (reason time);
  • a backend notices that WAL since the last checkpoint is approaching max_wal_size and sets a request flag (wal);
  • someone runs CHECKPOINT, a base backup starts, a database is dropped or created with STRATEGY 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.

LOG: checkpoint starting: time LOG: checkpoint complete: wrote 522 buffers (3.2%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 2 recycled; write=53.912 s, sync=0.008 s, total=53.937 s; sync files=42, longest=0.004 s, average=0.001 s; distance=25430 kB, estimate=25430 kB; lsn=0/30346C8, redo lsn=0/3034690

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.

LOG: restartpoint starting: time LOG: restartpoint complete: wrote 1204 buffers (7.3%), wrote 0 SLRU buffers; 0 WAL file(s) added, 1 removed, 2 recycled; … LOG: recovery restart point at 0/5000060 DETAIL: Last completed transaction was at log time 2026-10-22 15:40:12.03+00.

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 processes

At 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

  1. CheckpointerMain() wakes from WaitLatch(… WAIT_EVENT_CHECKPOINTER_MAIN), calls AbsorbSyncRequests(), reads and clears ckpt_flags, increments ckpt_started.
  2. CreateCheckPoint() (or CreateRestartPoint() during recovery) fixes the redo pointer and calls CheckPointGuts().
  3. CheckPointGuts() flushes the relation map, replication slots, logical snapshot files, then the SLRUs (CheckPointCLOG(), CheckPointMultiXact() …) and finally CheckPointBuffers().
  4. BufferSync() marks dirty buffers BM_CHECKPOINT_NEEDED, sorts them as CkptSortItems by tablespace/relation/fork/block, balances tablespaces with a binary heap and writes each via SyncOneBuffer(), calling CheckpointWriteDelay() after every buffer. That function sleeps 100 ms at a time unless IsCheckpointOnSchedule() says it is behind on time or WAL.
  5. ProcessSyncRequests() fsyncs every file in the private pendingOps hash table.
  6. Back in CreateCheckPoint(): insert the checkpoint record, update pg_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__done bracket the phases in CheckPointGuts() (build with --enable-dtrace).
  • log_checkpoints = on plus pg_stat_io rows for backend_type = 'checkpointer'; pg_buffercache to watch dirty buffers drain.
  • TAP tests touching checkpoints and restartpoints: 045_archive_restartpoint.pl, 041_checkpoint_at_promote.pl. The buffer README README explains 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