You've set checkpoint_completion_target to 0.9 and adjusted bgwriter_delay. But you have no idea whether any of it is doing what you expect. That's what pg_stat_bgwriter is for.
Run this and actually read it:
SELECT * FROM pg_stat_bgwriter;The Counters That Tell You What's Writing
buffers_checkpoint: Dirty pages written by checkpoint. High absolute numbers aren't alarming—checkpoints are supposed to write pages. But when this number is doing almost all the work, your bgwriter isn't helping.
buffers_clean: Dirty pages written by the background writer proactively between checkpoints. If this is near zero relative to buffers_checkpoint, bgwriter_delay is too high or bgwriter_lru_maxpages too low—your checkpoints are doing burst writes instead of steady ones.
buffers_backend: Dirty pages that a backend process had to write itself because neither the checkpoint nor the bgwriter had gotten to them yet. Each occurrence means a query was blocked waiting for a page to be written to disk. If buffers_backend / (buffers_checkpoint + buffers_clean + buffers_backend) exceeds about 5%, your background write infrastructure isn't keeping up.
buffers_backend_fsync: Times a backend had to call fsync itself. Under normal operation this should be zero. If it's climbing, you have an I/O saturation problem that no amount of Postgres tuning will fix.
buffers_alloc: Total buffer allocations. Useful as a denominator for other ratios.
The Ratio That Actually Matters
SELECT
buffers_backend::float / NULLIF(buffers_checkpoint + buffers_clean + buffers_backend, 0) AS backend_write_ratio,
buffers_clean::float / NULLIF(buffers_checkpoint + buffers_clean, 0) AS bgwriter_contribution
FROM pg_stat_bgwriter;backend_write_ratio above 0.05 means your backends are doing disk writes that should have been done in the background. bgwriter_contribution near 0 means your bgwriter is barely contributing.
The Checkpoint Timing Gap
checkpoint_write_time: Milliseconds spent writing pages during checkpoints.
checkpoint_sync_time: Milliseconds spent fsync-ing pages during checkpoints.
If checkpoint_sync_time is large relative to checkpoint_write_time, your storage is slow at fsync. This gap is a better indicator of storage fsync performance than anything you'll see in the OS.
Tuning bgwriter
bgwriter_delay: Milliseconds between bgwriter activity cycles. Default 200ms. Reducing to 50-100ms makes it run more frequently.
bgwriter_lru_maxpages: Maximum pages written per cycle. Default 100. Increase if your write workload is bursty.
Adjust in the direction the counter ratios indicate—if bgwriter_contribution is near zero, decrease bgwriter_delay first.
Resetting for a Fresh Baseline
SELECT pg_stat_reset_shared('bgwriter');The counters accumulate since the last statistics reset. If you've just changed bgwriter settings, reset to get a clean measurement window. Wait an hour under normal load, then re-read.
What pg_stat_bgwriter Doesn't Tell You
It won't tell you which queries triggered backend writes, which tables are generating the most dirty pages, or actual I/O latency. For that you need pg_stat_io (Postgres 16+) and track_io_timing. It also doesn't distinguish between shared_buffers evictions and checkpoint-driven writes.
What it does tell you is precise: whether your background write infrastructure is keeping up. The buffers_backend ratio is the single most actionable number in the output.
Building something? List it on builds.anethoth.com. More posts at anethoth.com.