Checkpoints Occurring Too Frequently
The log line 'checkpoints are occurring too frequently (N seconds apart)' with the HINT 'Consider increasing the configuration parameter "max_wal_size"',...
In 10 seconds
The server log keeps printing 'checkpoints are occurring too frequently (N seconds apart)' and the disks show a sawtooth of write spikes every few seconds. Throughput sags during each spike and WAL volume balloons. Nothing is 'broken' in the sense of an error or a crash -- the database is checkpointing far more often than it should because the write rate is outrunning max_wal_size. You need to confirm that WAL volume (not the clock) is driving the checkpoints, see the exact interval and I/O cost the server is paying, and raise max_wal_size so the same workload checkpoints on the gentle timed schedule instead.
Why This Happens
PostgreSQL writes a checkpoint for one of two reasons. A TIMED checkpoint fires every checkpoint_timeout (default 5 minutes) -- the healthy, gentle cadence. A REQUESTED checkpoint fires when the volume of WAL written since the last checkpoint grows large enough that, to stay within max_wal_size, a checkpoint must start now.
The trigger is not max_wal_size exactly: PostgreSQL aims to finish the checkpoint before the WAL would overflow, so it starts when WAL since the last checkpoint reaches roughly max_wal_size / (2 + checkpoint_completion_target). With the default completion_target of 0.9 that is about max_wal_size / 2.9. Set max_wal_size to 64MB and that threshold is only ~22MB of WAL -- so a write burst that generates 1.1GB of WAL trips the threshold over and over, firing a requested checkpoint every fraction of a second. Each requested checkpoint is expensive twice over: it flushes ALL dirty shared buffers to disk in one burst (the I/O spike), and immediately afterwards every page that gets modified must write a FULL-PAGE IMAGE into WAL again (the WAL amplifier), because full_page_writes protects against torn pages after a checkpoint. So frequent checkpoints both spike your I/O and inflate your WAL, which generates yet more WAL pressure -- a vicious circle.
The cure is not a query or a VACUUM; it is configuration. Raise max_wal_size so the WAL threshold sits comfortably above your busiest write window. The same burst then never reaches the threshold, no requested checkpoint fires, and checkpoints fall back to the calm timed schedule with checkpoint_completion_target spreading each one's writes over most of the interval. The same burst then never reaches the threshold, no requested checkpoint fires, and checkpoints fall back to the calm timed schedule with checkpoint_completion_target spreading each one's writes over most of the interval.
Diagnose
Free Tier -- Basic Approach
The on-call smoke alarm: one query against the cumulative statistics that tells you instantly whether checkpoints are timer-driven (healthy) or WAL-driven (max_wal_size too small).
checkpoints_timed | checkpoints_req | verdict
-------------------+-----------------+-------------------------------------------------------
0 | 32 | WAL-driven: max_wal_size too small for the write rate
(1 row)
Reading pg_stat_bgwriter, checkpoints_timed is 0 and checkpoints_req is 32, so the verdict is unambiguous: 'WAL-driven: max_wal_size too small for the write rate'. This is the metric to graph and alert on -- a rising checkpoints_req rate while checkpoints_timed stays flat is the early warning, long before the log fills with 'too frequently'. (On PG17+ the same two numbers live in pg_stat_checkpointer as num_requested / num_timed.)
Fix
Raise max_wal_size so WAL volume no longer drives checkpoints. Run the IDENTICAL burst again: this time it forces zero requested checkpoints -- the storm is gone, no restart, no lock.
max_wal_size
--------------
8GB
(1 row)
INSERT 0 800000
INSERT 0 800000
INSERT 0 800000
INSERT 0 800000
INSERT 0 800000
pg_sleep
----------
(1 row)
checkpoints_timed | checkpoints_req | write_time_s | buffers_checkpoint
-------------------+-----------------+--------------+--------------------
0 | 0 | 0.0 | 0
(1 row)
ALTER SYSTEM SET max_wal_size = '8GB' + pg_reload_conf lifts the requested-checkpoint threshold to roughly 8192 / 2.9 â 2.8 GB -- well above the burst's ~1.1GB of WAL. After a single quiescing CHECKPOINT and a counter reset (so the comparison starts from the same clean baseline as the break phase), the same five inserts run again and the counters read checkpoints_timed 0, checkpoints_req 0, write_time 0.0, buffers_checkpoint 0: the workload no longer triggers a checkpoint of either kind within the window. The change is online -- max_wal_size is SIGHUP-reloadable, no restart and no lock.
Verify
Prove the storm is actually over: the post-fix burst added NO new 'too frequently' warning to the log, and the requested-checkpoint counter stayed at zero.
== new "too frequently" warnings caused by the post-fix burst ==
warnings_before_fix: 225
warnings_after_fix: 225
new_warnings_from_fixed_burst: 0
== counters after the post-fix burst ==
checkpoints_timed | checkpoints_req
-------------------+-----------------
0 | 0
(1 row)
max_wal_size
--------------
8GB
(1 row)
Counting the 'checkpoints are occurring too frequently' lines in the server log before and after the fixed burst gives 225 and 225 -- a delta of 0, so the burst that previously logged dozens of warnings now logs none. The counters confirm it: checkpoints_req is still 0 after the burst, with max_wal_size showing 8GB. (The 225 baseline is the accumulated warnings from the break phase; what matters is that the fixed burst contributes zero new ones.)
⪠Rollback
There is nothing destructive to roll back -- max_wal_size is a SIGHUP-reloadable soft limit, changed online with ALTER SYSTEM SET max_wal_size = '...'; SELECT pg_reload_conf; and it takes effect immediately with no restart and no lock. It is a soft target, not a preallocation: setting it to 8GB does not reserve 8GB of disk up front; WAL grows toward it only as the workload demands and is recycled at each checkpoint. The only real consideration is the trade-off it encodes: a larger max_wal_size means fewer, larger checkpoints (less I/O churn, lower write amplification) but a longer crash-recovery replay window and a higher steady-state WAL footprint on disk. If you over-set it and disk headroom becomes a concern, simply lower it again with ALTER SYSTEM SET + reload -- the change is fully reversible. Never try to 'fix' frequent checkpoints by lowering checkpoint_timeout or issuing manual CHECKPOINTs; both make the storm worse.
ðŠ Version Matrix (PG 14--18)
The behaviour is identical on every supported major version: at max_wal_size=64MB the same WAL burst forces a stack of requested checkpoints on all five. The one honest version nuance is WHERE you read the counter -- PG17 renamed and moved the view.
â Common Mistakes
Stop this incident before it pages you
The free runbook clears today's failure. Pro adds the standing guardrails -- the alert query, the dashboard, and the managed-service knobs -- so it never becomes an incident again.
- â A ready-to-paste alert query with the right thresholds.
- â The topology change that prevents recurrence.
- â The exact knobs on RDS, Azure Flexible Server & Cloud SQL.
Frequently Asked Questions
What does 'checkpoints are occurring too frequently' actually mean?
It means two REQUESTED checkpoints (checkpoints forced by WAL volume, not by the timer) fired closer together than checkpoint_warning, which defaults to 30 seconds. The '(N seconds apart)' is the measured interval; N of 0 or 1 means they are essentially back-to-back. The accompanying HINT -- 'Consider increasing the configuration parameter max_wal_size' -- is the server telling you the fix directly.
How do I confirm WAL volume, not the timer, is driving my checkpoints?
Compare the two counters. On PGâ€16 read pg_stat_bgwriter.checkpoints_req vs checkpoints_timed; on PG17+ read pg_stat_checkpointer.num_requested vs num_timed. If the requested count dominates while the timed count stays flat, max_wal_size is too small for your write rate. In this recipe one write burst produced checkpoints_req = 32 with checkpoints_timed = 0.
Why does raising max_wal_size fix it instead of a query?
Because the problem is a configuration mismatch, not corrupt or missing data. A requested checkpoint fires when WAL since the last checkpoint reaches about max_wal_size / 2.9. Raising max_wal_size lifts that threshold above your busiest write window, so the same workload no longer trips it. In the recipe, the identical 1.1GB burst forced 32 requested checkpoints at 64MB and zero at 8GB.
Won't a big max_wal_size waste disk or slow recovery?
max_wal_size is a soft target, not a reservation -- WAL only grows toward it as needed and is recycled at each checkpoint. The genuine trade-off is recovery time: fewer, larger checkpoints mean a longer WAL replay window after a crash, and a larger steady-state WAL footprint. Size it to comfortably absorb your peak write window, not arbitrarily large, and keep checkpoint_completion_target high (0.9 default) so each checkpoint's writes are spread out.
I'm on PostgreSQL 17 and pg_stat_bgwriter no longer has checkpoints_req -- where did it go?
PG17 split the checkpoint statistics into a dedicated pg_stat_checkpointer view. checkpoints_timed and checkpoints_req became num_timed and num_requested there, and the old columns were removed from pg_stat_bgwriter. Update any alerts and dashboards to read pg_stat_checkpointer on 17 and later; the behaviour is identical, only the view moved.
References
- PostgreSQL Docs -- WAL Configuration (max_wal_size, checkpoints)
- PostgreSQL Docs -- Write Ahead Log parameters (checkpoint_timeout, checkpoint_completion_target, checkpoint_warning)
- PostgreSQL Docs -- pg_stat_bgwriter / pg_stat_checkpointer
- PostgreSQL 17 Release Notes -- split of checkpoint stats into pg_stat_checkpointer
- PostgreSQL Docs -- SQL CHECKPOINT