Background processes
WAL writer
The WAL writer wakes up every wal_writer_delay and writes and flushes WAL that backends have not flushed themselves. It matters most when synchronous_commit is off.
In plain words
Every change is first described in a WAL record in the WAL buffers (shared memory). Before PostgreSQL tells you "COMMIT", it normally makes sure that record is flushed to pg_wal on disk. The backend running your transaction does that flush itself.
The WAL writer is a helper that flushes WAL in the background, every wal_writer_delay (200 ms). With normal commits it has little to do. It becomes important when synchronous_commit = off: then COMMIT returns before the WAL is flushed, and the WAL writer is the one that makes those commits durable shortly afterwards.
That gives much faster commits, at a price: if the server crashes, the last fraction of a second of "committed" transactions can be lost (the database stays consistent; it is as if those transactions never happened). Try the selector below and watch the "not yet durable" counter.
For DBAs and operators
One round of XLogBackgroundFlush()
- Target = the end of the last completely filled WAL page. If that is already flushed, target = the latest asynchronous commit (so a partially filled page is written for it).
- If
wal_writer_flush_after = 0, orwal_writer_delayhas passed since its last flush, or at leastwal_writer_flush_after(1MB) of WAL is unflushed: write and fsync up to the target. - Otherwise only write it to the kernel (cheap) and fsync later.
When an asynchronous commit happens, the backend wakes the WAL writer immediately if it was hibernating or if unflushed WAL has already reached wal_writer_flush_after (or that setting is 0). The documented worst case for an async commit to become durable is three times wal_writer_delay. After 50 idle rounds it hibernates (sleep × 25, wait_event = WalWriterMain either way).
Why it helps with synchronous commit too
Large transactions (bulk loads, CREATE INDEX) fill WAL buffers quickly. If the buffers wrap around before anyone wrote them out, the inserting backend must write WAL itself, counted in pg_stat_wal.wal_buffers_full. The WAL writer reduces that by writing filled pages in the background. A large wal_buffers_full is a sign to raise wal_buffers (default -1 = 1/32 of shared_buffers, max 16MB auto).
Monitoring (PostgreSQL 18)
SELECT wal_records, wal_fpi, wal_bytes, wal_buffers_full
FROM pg_stat_wal;
-- WAL write / fsync counts per process type moved to pg_stat_io in 18
SELECT backend_type, context, writes, fsyncs, write_time, fsync_time
FROM pg_stat_io
WHERE object = 'wal'
ORDER BY writes DESC;In PostgreSQL 18 the old wal_write, wal_sync, wal_write_time and wal_sync_time columns were removed from pg_stat_wal; use pg_stat_io (timings need track_wal_io_timing). If client backend has many WAL fsyncs and walwriter few, commits are synchronous and small, which is normal.
Choosing synchronous_commit
| Value | COMMIT waits for | Lost on crash |
|---|---|---|
on (default) | local flush (+ sync standby flush if configured) | nothing |
local | local flush only | nothing locally |
off | nothing | up to ~3 × wal_writer_delay of commits |
synchronous_commit can be set per transaction (SET LOCAL synchronous_commit = off) for unimportant writes such as session logs, while money transfers stay safe. Unlike fsync = off, it never risks corruption.
Process facts
One process, postgres: walwriter in ps, backend_type = 'walwriter'. Started by the postmaster after recovery ends, so it does not run on a standby. Killing it with signal 9 causes a crash restart: LOG: WAL writer process (PID 3151) was terminated by signal 9: Killed.
For developers: inside the source
Where it lives
walwriter.c:WalWriterMain(), a short loop aroundXLogBackgroundFlush().xlog.c: everything else:XLogBackgroundFlush(),XLogWrite(),XLogFlush(),XLogSetAsyncXactLSN(),AdvanceXLInsertBuffer().xact.c:RecordTransactionCommit()decides between a synchronousXLogFlush()and the asynchronous path.
Key state in XLogCtlData
LogwrtRqst(Write/Flushrequests) andasyncXactLSN, the newest async commit, under the spinlockinfo_lck.logInsertResult,logWriteResult,logFlushResult:pg_atomic_uint64positions read withRefreshXLogWriteResult().WalWriterSleeping: true while the WAL writer hibernates.WALWriteLock: the LWLock any process must hold to callXLogWrite().
Call flow
An asynchronous commit: RecordTransactionCommit() → XLogSetAsyncXactLSN(XactLastRecEnd), which advances asyncXactLSN and sets the WAL writer's latch (ProcGlobal->walwriterProc) if it is sleeping or if wal_writer_flush_after blocks are unflushed. Each WAL writer round:
WalWriterMain()callsXLogBackgroundFlush().- Target =
LogwrtRqst.Writerounded down to a page; if already flushed, useasyncXactLSNand setflexible = false. - Decide whether to also fsync (time since
lastflush≥WalWriterDelay, or ≥WalWriterFlushAfterblocks). WaitXLogInsertionsToFinish(), takeWALWriteLock,XLogWrite(), release.WalSndWakeupProcessRequests()wakes walsenders, thenAdvanceXLInsertBuffer(InvalidXLogRecPtr, …, true)pre-initialises free WAL buffer pages, taking work off backends' insert path.
If it returns false for LOOPS_UNTIL_HIBERNATE (50) rounds, the loop calls SetWalWriterSleeping(true) and waits WalWriterDelay * HIBERNATE_FACTOR (25) with WAIT_EVENT_WAL_WRITER_MAIN.
Compare a synchronous commit: XLogFlush() uses LWLockAcquireOrWait(WALWriteLock, …), so concurrent committers wait for one flusher instead of queuing to flush again (group commit); commit_delay adds a pg_usleep() there.
Experimenting
wal_debugexists only in builds compiled withWAL_DEBUG; thenXLogBackgroundFlush()logsxlog bg flush request write …; flush: ….pg_stat_iowithobject = 'wal'andbackend_type = 'walwriter';pg_walinspectto look at the records themselves.- The WAL section of
READMEexplains the write-ahead rule and the insert/write/flush positions.
Parameters that shape this
wal_writer_delay wal_writer_flush_after synchronous_commit wal_buffers commit_delay wal_sync_method track_wal_io_timing
Related topics
Write-ahead log (WAL) Synchronous replication Checkpointer Background writer Crash recovery Postmaster