Cookbook/ Configuration & Tuning/ Checkpoints Occurring Too Frequently
Configuration & Tuning · Runbook

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"',...

🧩 5 steps ⏱ ~15 min 📊 Intermediate 🐘 PostgreSQL 14--18 Last reviewed June 26, 2026

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

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).

OUTPUT -- captured on real PostgreSQL
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.)

This is a Pro lesson

Get every Learning Pathway and cookbook recipe -- grounded in PostgreSQL source code, with diagnostics, fixes, and prevention for each topic.

Continue this lesson to learn:

  • Advanced Approach -- Production-Safe
  • All 36 Learning Pathway lessons
  • 170+ cookbook recipes
  • Source-grounded diagnostics & fixes

Secure checkout Cancel anytime Source-grounded

FIX

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.

OUTPUT -- captured on real PostgreSQL
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

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.

OUTPUT -- captured on real PostgreSQL
== 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

✗Reading checkpoints_req without comparing it to checkpoints_timed. A high checkpoints_req is only alarming relative to checkpoints_timed: req >> timed means WAL volume is driving checkpoints (raise max_wal_size); req ≈ 0 with timed climbing is the healthy timer-driven pattern.
✗Assuming max_wal_size is the trigger value. The requested-checkpoint threshold is roughly max_wal_size / (2 + checkpoint_completion_target) -- about max_wal_size/2.9 at the default 0.9 -- not max_wal_size itself. That is why a 1.1GB burst still forced a checkpoint at 2GB but none at 8GB in this recipe.
✗Lowering checkpoint_timeout to 'spread the load'. That does the opposite: a shorter timeout makes checkpoints MORE frequent. To reduce checkpoint frequency you raise max_wal_size (and optionally checkpoint_timeout), never lower it.
✗Issuing manual CHECKPOINT commands to 'get ahead of it'. A CHECKPOINT is itself a requested checkpoint; sprinkling them in adds to the very storm you are trying to stop and pollutes the checkpoints_req counter.
✗Reading the wrong view after upgrading to PG17. PostgreSQL 17 moved the checkpoint counters out of pg_stat_bgwriter into a new pg_stat_checkpointer view (checkpoints_timed/checkpoints_req became num_timed/num_requested). Monitoring that still queries pg_stat_bgwriter.checkpoints_req on PG17+ silently breaks -- the column is gone.
🔒 Pro · Prevent

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.
Unlock Pro$24.99/mo · $199/yr

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

Keep going

Related & next steps