DB Internals AtlasPostgreSQL 18.6 Home About

Background processes

Background writer

The background writer wakes up every bgwriter_delay, guesses how many buffers backends will need soon, and writes dirty ones ahead of time so that queries rarely have to write pages themselves.

In plain words

Shared buffers are a fixed number of page slots. When a query needs a page that is not cached, its backend must free a slot: it walks around the buffers like the hand of a clock, skipping recently used pages, and takes the first one that nobody needs. If that page is dirty (changed but not yet saved), the backend must write it to disk first, and your query waits.

The background writer exists to avoid that wait. It wakes up regularly (every bgwriter_delay, 200 ms by default), looks at how many buffers backends took recently, and cleans about that many (times bgwriter_lru_multiplier) just ahead of the clock hand. When the backend arrives there, it finds clean slots.

It is capped by bgwriter_lru_maxpages per round so it never floods the disk. When the server is idle it hibernates and is woken up by the next backend that needs a buffer.

Try the sliders: raise the load or lower the cap and watch the red "backend had to write" squares appear.

For DBAs and operators

One round of BgBufferSync()

  1. Read how many buffers were allocated since the last round (recent_alloc) and where the clock hand is.
  2. Smooth it: if the new value is higher it is taken at once, otherwise the average decays over 16 rounds.
  3. upcoming = smoothed_alloc × bgwriter_lru_multiplier, but at least a minimum so that the whole pool is scanned at least once every 2 minutes.
  4. Scan forward from where the last round stopped (or from the clock hand if the hand has overtaken it). A buffer with usage_count = 0 and no pins is "reusable"; if it is dirty, write it. Stop when enough reusable buffers are ahead of the hand, or when bgwriter_lru_maxpages pages were written; the latter increments maxwritten_clean.

It does not decrement usage counts (only the backends' clock sweep does) and never writes recently used pages: those are written by the checkpointer, or by a backend when it finally evicts them. It also asks the kernel to start writing its data out every bgwriter_flush_after (512kB) on Linux, to avoid big write bursts later. With wal_level replica or higher it also logs a running-transactions snapshot into WAL every 15 seconds, which helps standbys and logical decoding get started.

Hibernation

If a round finds nothing to do and no buffers were allocated, it sleeps for bgwriter_delay × 50 instead (wait_event = BgwriterHibernate, otherwise BgwriterMain). The first backend that allocates a buffer wakes it up.

Monitoring

SELECT buffers_clean, maxwritten_clean, buffers_alloc, stats_reset
  FROM pg_stat_bgwriter;

-- who writes buffers? (PostgreSQL 16+)
SELECT backend_type, context, writes, evictions
  FROM pg_stat_io
 WHERE object = 'relation' AND writes > 0;

Since PostgreSQL 17 pg_stat_bgwriter has only these columns; checkpoint counters moved to pg_stat_checkpointer and the old buffers_backend / buffers_backend_fsync moved to pg_stat_io (rows for client backend).

Tuning

  • maxwritten_clean growing steadily means the cap stopped the writer often: raise bgwriter_lru_maxpages (for example 400 to 1000) or the multiplier.
  • Many backend writes in pg_stat_io (context normal) with a busy, write-heavy workload: same advice, or lower bgwriter_delay (for example 50ms).
  • An aggressive writer may write the same hot page several times; that is extra I/O. Setting bgwriter_lru_maxpages = 0 disables background writing (the process still runs).
  • Backend writes in context bulkread/bulkwrite/vacuum come from ring buffers and are not something the background writer can avoid.

Process facts

One process, postgres: background writer in ps, backend_type = 'background writer'. It starts with the postmaster (also during recovery on a standby). If killed with signal 9 the postmaster performs a crash restart: LOG: background writer process (PID 6069) was terminated by signal 9: Killed.

For developers: inside the source

Where it lives

  • bgwriter.c: BackgroundWriterMain(), the sleep/hibernate logic and the standby snapshot logging.
  • bufmgr.c: BgBufferSync(), the algorithm the simulation shows, and SyncOneBuffer().
  • freelist.c: the clock sweep (StrategyGetBuffer()), StrategySyncStart() and StrategyNotifyBgWriter().
  • README: buffer replacement strategy and buffer header locking.

Shared state

BufferStrategyControl (freelist.c) holds the clock hand nextVictimBuffer (a pg_atomic_uint32), completePasses, the allocation counter numBufferAllocs and bgwprocno, all read together under the spinlock buffer_strategy_lock. Each round, StrategySyncStart(&complete_passes, &num_buf_alloc) returns the hand position and resets the allocation count; that is the recent_alloc fed into the estimate.

One round

  1. BackgroundWriterMain() calls can_hibernate = BgBufferSync(&wb_context), then pgstat_report_bgwriter() and pgstat_report_wal(true).
  2. BgBufferSync() compares its own position with the hand (next_to_clean, next_passes), estimates reusable buffers ahead from a smoothed density, and updates smoothed_alloc with fast-attack/slow-decline:
    if (smoothed_alloc <= (float) recent_alloc)
        smoothed_alloc = recent_alloc;
    else
        smoothed_alloc += ((float) recent_alloc - smoothed_alloc) /
            smoothing_samples;          /* 16 */
  3. The scan target is at least min_scan_buffers = NBuffers / (scan_whole_pool_milliseconds / BgWriterDelay), with scan_whole_pool_milliseconds = 120000.
  4. The loop calls SyncOneBuffer(next_to_clean, true, wb_context). With skip_recently_used = true a pinned buffer or one with non-zero usage count is skipped; a clean one returns BUF_REUSABLE, a written one BUF_WRITTEN. Hitting bgwriter_lru_maxpages increments maxwritten_clean.
  5. Writes are queued in a WritebackContext; bgwriter_flush_after controls when sync_file_range()-style writeback hints are issued.

Sleeping

It normally waits BgWriterDelay ms (WAIT_EVENT_BGWRITER_MAIN). If BgBufferSync() returned true, it registers itself with StrategyNotifyBgWriter(MyProcNumber) and sleeps BgWriterDelay * HIBERNATE_FACTOR (50) with WAIT_EVENT_BGWRITER_HIBERNATE; the next StrategyGetBuffer() sets its latch. Separately, every LOG_SNAPSHOT_INTERVAL_MS (15000) it calls LogStandbySnapshot() if WAL has advanced since the last one.

Experimenting

  • Compile with -DBGW_DEBUG to get a DEBUG1 line per round from BgBufferSync() (recent_alloc, smoothed, density, wrote …).
  • pg_buffercache_summary() / pg_buffercache_usage_counts() show dirty buffers and usage-count distribution; pg_stat_io rows for background writer count its writes.

Parameters that shape this

bgwriter_delay bgwriter_lru_maxpages bgwriter_lru_multiplier bgwriter_flush_after shared_buffers

Related topics

Shared buffers Checkpointer Checkpoints Postmaster I/O workers Memory architecture