Postgres pg_stat_bgwriter: Understanding How Your Database Writes to Disk
The view that tells you whether your background writer is keeping up, whether your checkpoints are spread out properly, and whether your backends are doing write work they should not be doing.
Postgres has three processes that can write dirty buffers from shared_buffers back to disk: the checkpointer, the background writer, and the backends themselves. Which one does the work has substantial performance consequences, and the pg_stat_bgwriter view is the single place that tells you the breakdown. Reading it well is one of the operational skills that separates intermediate from senior Postgres operators.
What the view shows
pg_stat_bgwriter has a small number of columns and is global (one row, not per-database). The columns split into two groups: checkpoint counters and buffer-write counters.
The checkpoint counters tell you how checkpoints are happening: checkpoints_timed counts checkpoints triggered by the checkpoint_timeout setting, checkpoints_req counts checkpoints triggered by other means (usually filling max_wal_size). The ratio between them is one of the most important diagnostic signals in the view: a healthy system has checkpoints_timed dominating, and a system where checkpoints_req is more than 10-20 percent of total checkpoints is checkpointing under pressure rather than on schedule.
The buffer-write counters tell you who wrote what. buffers_checkpoint is dirty buffers written by the checkpointer during checkpoint operations, buffers_clean is dirty buffers written by the background writer between checkpoints, buffers_backend is dirty buffers written by backend processes when they needed a clean buffer and none was available. The healthy ratio is checkpoint-dominated, with bgwriter contributing a moderate amount, and backend writes minimized.
The backend-write problem
The buffers_backend counter is the one that most often signals a tuning problem. When a backend process needs to bring a page into shared_buffers and there is no clean buffer available, the backend itself has to write a dirty buffer to disk to make room. This is the slowest case for both the backend (which now has to wait for the write before continuing) and the system (which loses the batching benefits of letting the bgwriter or checkpointer handle writes).
A high buffers_backend rate relative to buffers_checkpoint and buffers_clean usually means one of three things: the background writer is configured too conservatively (bgwriter_lru_maxpages is too low or bgwriter_delay is too high), shared_buffers is too small for the working set so eviction pressure is constant, or checkpoint frequency is too high so the checkpointer is occupying I/O bandwidth that the bgwriter could otherwise use.
The diagnostic query is straightforward:
SELECT
checkpoints_timed, checkpoints_req,
buffers_checkpoint, buffers_clean, buffers_backend,
ROUND(100.0 * buffers_backend / NULLIF(buffers_checkpoint + buffers_clean + buffers_backend, 0), 1) AS pct_backend_writes,
ROUND(100.0 * checkpoints_req / NULLIF(checkpoints_timed + checkpoints_req, 0), 1) AS pct_req_checkpoints
FROM pg_stat_bgwriter;Both percentages should be low. pct_backend_writes above 20 percent indicates the bgwriter is not keeping up. pct_req_checkpoints above 10-15 percent indicates max_wal_size is too small for the write rate.
Checkpoint write spreading
The checkpointer in modern Postgres versions spreads its writes across the checkpoint_completion_target fraction of the checkpoint interval. The default of 0.9 means the checkpointer aims to finish writing dirty buffers 90 percent of the way through the interval, smoothing the I/O over time rather than producing a burst at the start of the checkpoint.
The buffers_checkpoint counter combined with the checkpoint_write_time and checkpoint_sync_time columns (also in pg_stat_bgwriter) lets you verify the spreading is working. checkpoint_write_time should be a substantial fraction of the checkpoint interval; checkpoint_sync_time should be a small fraction. A system where checkpoint_sync_time is large is bottlenecked on fsync calls, which usually means either many separate files (lots of relations being written) or slow storage.
Configuration knobs that matter
The settings that affect what pg_stat_bgwriter shows include max_wal_size (raise to reduce checkpoints_req), checkpoint_completion_target (keep at 0.9 unless you have specific reasons), bgwriter_lru_maxpages (raise to let the bgwriter do more work between cycles, default 100 is conservative for modern systems), bgwriter_delay (lower to make the bgwriter cycle more often, default 200ms), and shared_buffers (raise if the working set does not fit, but the conventional 25-percent-of-RAM rule is usually right).
The interaction between these settings is the part that gets misconfigured. Raising max_wal_size to reduce req checkpoints is straightforward and almost always helps; raising bgwriter_lru_maxpages helps if buffers_backend is high but does nothing if it is already low; lowering shared_buffers helps only if the OS page cache is being starved of memory the database is sitting on without using.
The reset story
pg_stat_bgwriter is reset by pg_stat_reset_shared('bgwriter'). The stats_reset column shows when the last reset happened. Resetting periodically is sometimes useful for isolating the effect of a configuration change, but the cumulative counters work fine if you compute deltas between samples.
For production monitoring, the right pattern is to sample pg_stat_bgwriter on a schedule (every 1-5 minutes) and store the snapshot, then compute deltas to get rates. The raw cumulative counters are not directly useful because they grow without bound; the rate of growth is what tells you whether the system is healthy.
What the view does not show
pg_stat_bgwriter is a global summary, not a per-relation or per-query view. It tells you that something is generating lots of dirty buffers but not what. To find the source, combine it with pg_stat_user_tables (which tables are being updated heavily), pg_stat_statements (which queries are generating the writes), and pg_stat_io in Postgres 16+ (which provides a finer breakdown of read and write activity).
The view also does not show I/O latency. It tells you how many buffers were written but not how long each write took. For latency, combine pg_stat_bgwriter with OS-level metrics (iostat, /proc/diskstats) or with the pg_stat_io view which has timing columns.
Our use across products
Our four products (DocuMint, CronPing, FlagBit, WebhookVault) currently run on SQLite, where the closest analog is the WAL checkpoint behavior controllable via wal_autocheckpoint and the pragma wal_checkpoint operations. The Postgres migration plan includes pg_stat_bgwriter in the standard monitoring dashboard from day one, with alerts on backend-write percentage and requested-checkpoint percentage as the two leading indicators of write tuning problems.
Deeper observation
A theme in Postgres operational tooling is that the views expose just enough information to diagnose problems before they become outages, but the discipline of reading them well is largely uncodified. pg_stat_bgwriter has eight columns and a thirty-year history, and most teams running production Postgres do not look at it routinely. The information is there; the operator culture of using it is what makes the difference between systems that quietly accumulate write inefficiency and systems that catch tuning problems early. The features Postgres exposes are dense, but the meaning has to be unpacked by every operator who runs it.