max_wal_size
Maximum WAL size before checkpoint is forced. Too small triggers checkpoint storms: I/O spikes every few seconds, degrading write throughput.
Tier 2 — Production Guide — Operational guidance is available; evidence scope is stated where relevant.
What We Observed
box
cl_wal_storm (--memory=1g, PG 17.10), pgbench TPC-B scale 5, -c 8 -j 4 -T 60
storm low
- max wal size
64MB
- tps
1569
- latency avg ms
5.092
- checkpoint events
25 requested vs 1 timed (storm ratio = 25:1)
- storm messages
checkpoints are occurring too frequently (2 seconds apart)
- source file
no storm tuned
- max wal size
1GB (default)
- tps
1474
- latency avg ms
2.708
- note
No storm messages. Checkpoints on timed schedule.
- source file
historical curve
- 64MB tps
944
- 64MB storm warnings
24
- 128MB tps
965
- 128MB storm warnings
33
- source
wal_curve.txt (2026-06-19 lab capture)
curve note
Checkpoint storm is threshold behavior: below critical WAL size → storm; above → stable.
What Was Expected
- framing
Too small max_wal_size forces checkpoint every time WAL fills. Each checkpoint is I/O-intensive: all dirty buffers flushed to disk. Under write workload: repeated storms degrade tps, spike I/O, and create checkpointing overhead that competes with normal writes.
- storm evidence
- setting
max_wal_size=64MB, pgbench -c 8 -j 4 -T 60
- container
cl_wal_storm (--memory=1g)
- log lines
checkpoints are occurring too frequently (15s apart); (2s apart); (2s apart)
- pg stat checkpointer
num_requested=25, num_timed=1 (storm: 25 forced checkpoints in 60s)
- source file
- historical evidence
- log
2026-06-19: checkpoints occurring too frequently (6 seconds apart); (2 seconds apart); (1 second apart)
- source
- source file
Common Pitfalls
- Treating max_wal_size as a HARD cap on WAL on disk. It is a soft checkpoint trigger; WAL can briefly exceed it under a spike, and replication slots / wal_keep_size can hold far more — out-of-disk WAL is a separate failure.
- Leaving the 1GB default on a write-heavy server. Sustained high WAL rates blow through 1GB in seconds, firing volume-driven checkpoints; the only signal is 'checkpoints are occurring too frequently' in the log.
- Tuning max_wal_size without checkpoint_timeout. Whichever trigger fires first wins; a huge max_wal_size still checkpoints every checkpoint_timeout, so they must be raised together.
- Judging the setting by total WAL MB. Total MB is confounded by throughput (a storming server does less work); use the FPI fraction (wal_fpi/wal_records) and pg_stat_checkpointer.num_requested to see amplification and volume-triggering.
- Oversizing 'as large as possible'. Past the time-driven crossover there is no throughput gain (plateau in the curve) and WAL can occupy more disk between checkpoints; target the crossover plus headroom.
- Forgetting it is reload-only and cannot run in a transaction. ALTER SYSTEM SET must be its own statement (not combined with pg_reload_conf in one psql -c), then reload — no restart needed.
- Ignoring wal_compression. With frequent checkpoints, full-page images dominate WAL; leaving compression off magnifies the write amplification the storm already causes.
Tuning Guidance
Unlock Incident Pro
The free sections tell you what to do. Pro is the measured proof — the exact failure signature, safe-ceiling formula, and per-workload defaults, each captured on real PG 14–18 runs.
- Tuning Guidance
max_wal_size is the SOFT upper bound on how much write-ahead log (WAL) may accumulate between automatic checkpoints. It is a trigger, not a hard cap: when WAL written since the last checkpoint approaches max_wal_size, PostgreSQL starts a checkpoint to flush dirty buffers so old WAL can be recycled. Checkpoints are therefore fired by whichever comes FIRST — time (checkpoint_timeout, default 5min) or volume (max_wal_size). Set it too small and the volume trigger dominates: checkpoints fire constantly, throughput collapses, and every checkpoint forces full-page images into the WAL (write amplification). Set it large enough and checkpoints become purely TIME-driven and spread out, which is almost always what you want on a write-heavy box. It is 'soft' because under a write spike WAL can briefly exceed it; the real hard limits are disk space and the separate wal_keep_size / replication-slot retention (covered elsewhere).
Measured literally from pg_settings on PG 14/15/16/17/18: setting=1024, unit=MB => effective default 1024MB (1GB) on ALL FIVE versions (boot_val=1024 AND reset_val=1024 everywhere — no auto-tuning, no version drift). vartype=integer, min_val=2 (2MB floor), max_val=2147483647 (MB => effectively unbounded). context='sighup' => changed with a config RELOAD, no restart and no session/role override (it is one cluster-wide value, unlike work_mem). default_changed=false and boot_fallback_changed=false across 14-18: this is one of the rare GUCs whose compiled fallback and effective default are identical and constant on every supported version. short_desc: 'Sets the WAL size that triggers a checkpoint.' Related fixed defaults observed: checkpoint_timeout=300s (5min), checkpoint_completion_target=0.9, min_wal_size=80MB, wal_compression=off.
max_wal_size is structurally IDENTICAL across PG 14-18: effective default 1024MB (1GB), boot_val=reset_val=1024, min 2MB, max 2147483647MB, context=sighup, unit=MB (see _guc_matrix_machine for literal per-version rows). There is NO cross-version difference at all — not even the boot_val/reset_val split seen on shared_buffers. The surrounding checkpoint machinery (checkpoint_timeout=300s, checkpoint_completion_target=0.9, min_wal_size=80MB) is also constant in shape across versions. Bottom line: treat '1GB default, sighup/reload context, fires a checkpoint when WAL since last checkpoint nears this size' as a constant from 14 through 18.
context=sighup, so applying a change is a ONE-STEP, reload-only operation — no outage: ALTER SYSTEM SET max_wal_size = '4GB'; then SELECT pg_reload_conf(); (or SIGHUP / pg_ctl reload / systemctl reload postgresql). Verify with SHOW max_wal_size;. IMPORTANT LAB GOTCHA proven here: ALTER SYSTEM cannot run inside a transaction block, and psql wraps multiple -c statements (or a multi-statement single -c) in an implicit transaction — so ALTER SYSTEM SET ...; SELECT pg_reload_conf(); in ONE -c fails with 'ERROR: ALTER SYSTEM cannot run inside a transaction block'. Send ALTER SYSTEM as its OWN statement, then reload separately. There is no per-session override; the new value takes effect immediately on reload for subsequent checkpoint decisions. BOUNDARY PROOF (captured literal): an out-of-range / invalid value is rejected at set time -- ALTER SYSTEM SET max_wal_size = 1; -> ERROR: 1 MB is outside the valid range for parameter "max_wal_size" (2 MB .. 2147483647 MB). It is context=sighup, so a session-level set is refused -- SET max_wal_size = ... -> ERROR: parameter "max_wal_size" cannot be changed now; change it in postgresql.conf / ALTER SYSTEM and reload (pg_reload_conf), no restart.
References
- PostgreSQL 18 docs — Write Ahead Log / Checkpoints: max_wal_size, min_wal_size, checkpoint_timeout, checkpoint_completion_target (postgresql.org/docs/18/runtime-config-wal.html)
- PostgreSQL 18 docs — WAL Configuration & full-page writes / FPI amplification after checkpoints (postgresql.org/docs/18/wal-configuration.html)
- PostgreSQL 18 docs — pg_stat_checkpointer (num_timed vs num_requested) and pg_stat_wal (wal_fpi, wal_records, wal_bytes) (postgresql.org/docs/18/monitoring-stats.html)
- PostgreSQL 18 docs — pg_settings (boot_val vs reset_val semantics) (postgresql.org/docs/18/view-pg-settings.html)
- Lab ground truth: truth/pg_settings_pg14.18.tsv (this repo)
- Lab captures: wal_curve_hi.txt, wal_storm.txt} (this repo)
Technical Reference
- Category
- Write-Ahead Log / Checkpoints
- Type
- integer (MB)
- Blast radius
- cluster
- Severity
- High
- Context
- sighup
- Versions
- 14, 15, 16, 17, 18
- Reviewed
- 2026-06-19
Severity scale: 1 Minimal · 2 Low · 3 Moderate · 4 High · 5 Critical.