Checkpoint Amplification
partialSummary
PostgreSQL's checkpoint process periodically flushes all dirty shared buffer pages to disk, causing a predictable I/O storm at each checkpoint interval that spikes disk utilisation and elevates write transaction latency for the duration of the flush.
Description
A PostgreSQL checkpoint is a consistency point: all dirty pages in shared_buffers are written to their data files, ensuring that a crash can recover from that checkpoint point without replaying WAL from the very beginning. Checkpoints occur either at checkpoint_timeout (default 5 minutes) or when WAL has grown by max_wal_size (default 1GB) since the last checkpoint, whichever comes first.
The checkpoint process flushes dirty pages using the bgwriter and checkpointer background processes. The total I/O volume is bounded by the number of dirty pages in shared_buffers at checkpoint time. With shared_buffers = 8GB and a write-heavy workload, the entire 8GB may be dirty by checkpoint time, requiring 8GB of disk writes in a short window.
checkpoint_completion_target controls how to spread this I/O over time: it is the fraction of the checkpoint interval the checkpointer has to finish writing all dirty pages. This default is version-scoped and the value matters: PostgreSQL 13 and earlier default to 0.5; PostgreSQL 14 (released September 2021) and every version since default to 0.9. An instance running the current default is not the aggressive case; the aggressive case is a pre-14 instance, or any instance where an operator has manually set completion_target back down toward 0.5.
At completion_target = 0.5 (pre-14 default, or a manually lowered value), the checkpoint must complete within 50% of the interval. For checkpoint_timeout = 5min, that is 2.5 minutes: all 8GB of dirty pages must be flushed in 2.5 minutes, 8GB / 150s = 53MB/s of checkpoint I/O, added to the baseline write workload.
At completion_target = 0.9 (the PostgreSQL 14+ default), the same 8GB is spread over 4.5 minutes: 8GB / 270s = 30MB/s, a lower, more sustained I/O rate instead of a concentrated burst. This is the current sane default. The failure mode's periodic I/O-spike signature is most pronounced on a pre-14 instance still running 0.5, or on any 14+ instance where completion_target has been manually lowered.
full_page_writes = on (default, safety-critical) causes the first write to any page after a checkpoint to write the full 8KB page image to WAL, not just the changed bytes. This is necessary for partial write protection during crashes but doubles or triples WAL size for write-heavy workloads. After a checkpoint, the first modification of each page in shared_buffers generates a 8KB WAL record instead of a few hundred bytes. For a working set of 100,000 pages all modified after a checkpoint, this generates 800MB of WAL records before the normal MVCC overhead.
The I/O pattern is periodic: quiet between checkpoints, then a burst at checkpoint time. This is visible in disk I/O metrics as a regular spike every checkpoint_timeout seconds, correlating with elevated write query latency.
MySQL's InnoDB storage engine has its own checkpoint mechanism, and it is a mechanically different design from PostgreSQL's, not the same knob under a different name. InnoDB uses fuzzy checkpointing: instead of flushing all dirty pages in a bounded window tied to a wall-clock checkpoint interval, InnoDB's page cleaner threads continuously flush dirty pages from the buffer pool in the background, paced against redo log capacity rather than a timer. innodb_log_file_size (or, from MySQL 8.0.30 onward, the combined innodb_redo_log_capacity that replaces the old file-size-times-file-count sizing) bounds how much redo log can exist before InnoDB is forced into a synchronous checkpoint to reclaim log space; a larger redo log gives fuzzy checkpointing more room to spread flushes over time and lowers the odds of a throughput-halting forced checkpoint. innodb_max_dirty_pages_pct (default 90) is the second lever: the page cleaner flushes more aggressively as the buffer pool's dirty page fraction approaches this ceiling, aiming to keep dirty pages bounded on an ongoing basis rather than to complete a batch within a fixed fraction of an interval. There is no InnoDB equivalent of checkpoint_completion_target, because InnoDB is not targeting "finish this flush within X% of an interval"; it is targeting "keep dirty pages and redo log utilisation under a continuous ceiling." An operator who tunes PostgreSQL checkpoint behaviour and assumes the same knob, or the same mental model, carries over to InnoDB will end up tuning the wrong parameter.
Characteristics
Triggers
- ·large shared_buffers with high dirty-page fraction at checkpoint time
- ·checkpoint_completion_target left at the pre-PostgreSQL-14 default of 0.5, or manually lowered from the 14+ default of 0.9, concentrating checkpoint I/O into a shorter window
- ·full_page_writes = on (required for crash safety) amplifying WAL after each checkpoint
- ·frequent forced checkpoints from max_wal_size exceeded (aggressive write workloads)
- ·low checkpoint_timeout (5 min default) causing frequent checkpoint I/O storms
Detection Signals
Mitigation Strategies
ALTER SYSTEM SET checkpoint_completion_target = 0.9; SELECT pg_reload_conf(). On PostgreSQL 14 and later this is already the default, so check pg_settings before assuming action is needed; the setting only needs changing if it was manually lowered. On PostgreSQL 13 and earlier, this is a required change: the shipped default is 0.5. Either way, 0.9 spreads checkpoint writes over 90% of the checkpoint interval instead of 50%, reducing peak I/O spike at checkpoint time. Takes effect immediately without restart. Does not reduce total checkpoint I/O volume; spreads the same work more evenly.
Set checkpoint_timeout = '15min' or '30min'. Fewer checkpoints per hour means fewer I/O bursts. However: longer checkpoint intervals increase WAL retained for crash recovery (max_wal_size may also need increasing) and extend crash recovery time. Trade-off: more time between checkpoints increases recovery time after a crash.
Smaller shared_buffers means fewer dirty pages to flush per checkpoint. Counter-intuitive: reducing shared_buffers from 16GB to 8GB on a write- heavy workload can reduce checkpoint I/O amplitude at the cost of slightly reduced cache hit rate. Measure cache hit rate before and after.
On Linux, enable HugeTLB (2MB pages) for shared_buffers by setting huge_pages = on in postgresql.conf and allocating sufficient hugepages at the OS level. Reduces checkpoint overhead from TLB invalidations during page dirty-tracking, which is a CPU cost that compounds with large shared_buffers.
Recovery Steps
- 1.Confirm checkpoint I/O is the driver: check pg_stat_bgwriter.checkpoint_write_time trend
- 2.Check the PostgreSQL major version and current checkpoint_completion_target via SHOW checkpoint_completion_target; on 14+ it should already read 0.9
- 3.If it is below 0.9 (pre-14 instance, or manually lowered), set checkpoint_completion_target = 0.9 via ALTER SYSTEM and SELECT pg_reload_conf()
- 4.If checkpoint_timeout is 5 minutes (default), increase to 15 minutes
- 5.Monitor pg_stat_bgwriter for the next several checkpoint cycles to confirm reduced peak I/O
- 6.If I/O spikes persist: profile disk utilisation during and outside checkpoint windows to quantify remaining headroom
- 7.Consider storage tier upgrade if checkpoint I/O still saturates after tuning
Estimated recovery time: Tuning changes (checkpoint_completion_target, checkpoint_timeout) take effect within minutes without a database restart. I/O pattern improvement is visible within the next 1–2 checkpoint cycles (5–30 minutes). No service interruption required for these configuration changes.
Affected Systems
Patterns
Technologies
Basis
Precisely understood PostgreSQL checkpoint mechanics with exact parameter values and measurable I/O patterns; periodic I/O spike fingerprint is unambiguous in production monitoring. InnoDB fuzzy-checkpointing mechanism and the named knobs are documented in the MySQL Reference Manual; the exact default of innodb_redo_log_capacity was not asserted for that reason.
Sources & Claims
checkpoint_completion_target default changed from 0.5 to 0.9 in PostgreSQL 14
pendingofficial documentation · PostgreSQL 14 release notes; PostgreSQL runtime config docs, checkpoint_completion_target
storage-engine-internals-spine batch 2
InnoDB uses fuzzy checkpointing, continuously flushing dirty buffer pool pages paced against redo log capacity, rather than PostgreSQL's fixed-interval checkpoint model
pendingofficial documentation · MySQL 8.0 Reference Manual, InnoDB Buffer Pool and Checkpoint
storage-engine-internals-spine batch 2
innodb_max_dirty_pages_pct default is 90
pendingofficial documentation · MySQL 8.0 Reference Manual: innodb_max_dirty_pages_pct system variable
storage-engine-internals-spine batch 2
MySQL 8.0.30 introduced innodb_redo_log_capacity as a combined sizing variable, replacing the older innodb_log_file_size / innodb_log_files_in_group pair; the exact default capacity value is not asserted here and should be confirmed against the MySQL version in use
pendingofficial documentation · MySQL 8.0 Reference Manual: innodb_redo_log_capacity system variable
storage-engine-internals-spine batch 2; default value intentionally withheld pending confirmation
Run This Failure
Blast radius analysis for this failure mode within each scenario that carries it.
Related Architecture Knowledge
Inbound: affects this entity
Write-heavy transactional workloads trigger frequent PostgreSQL checkpoints that flush large numbers of dirty pages to disk simultaneously, causing I/O spikes that interrupt query execution and increase write amplification beyond the WAL baseline.
Tradeoffs
- ·Larger max_wal_size means longer crash recovery time: trade checkpoint frequency for recovery time
- ·Disabling full page writes (off recommended only with storage-level checksum) reduces WAL size but risks corruption
- ·Checkpoint amplification is inherent to PostgreSQL's MVCC architecture: cannot be fully eliminated