The Runtime Theory
ApplicationInternalsstorage

What happens after COMMIT on the primary?

A step-by-step walk from WAL flush on the primary, through walsender streaming and walreceiver receipt, replay, hot-standby visibility, flush acks, and failover.

The Runtime Theory Team4 min read07 steps

layer stack

Application

HWHardware
KKernel
RTRuntime
APPApplication
SYSSystem
CLIClient
NETNetwork
TLSCrypto
SRVServer

adjacent altitudes in this subsystem are still being traced

trace spine

  1. 01 COMMIT flushes WAL on the primary
  2. 02 Walsender streams the records
  3. 03 Walreceiver writes and acks the flush
  4. 04 Replay applies changes on the standby
  5. 05 Hot standby: reads proceed, snapshots respected
  6. 06 Synchronous commit waits for the ack
  7. 07 Failover: promotion and the new timeline
On this page

Replication is the durability escape hatch: if you can't trust one machine's disk, keep a second machine's disk warm. The elegant trick of WAL-based replication is that it reuses everything from crash recovery — the standby is just a primary that never stops recovering. The machinery: two background processes, one network socket, and a promise about when "COMMIT" means "safe everywhere."

Step 1 — COMMIT on the primary

The commit path runs exactly as in the commit trace: WAL buffer appended, fsync, commit record durable. The primary has now paid the durability price. Replication's job is to make the standby pay it too — from the primary's log, not by re-running SQL.

Step 2 — the walsender

Each standby connects to the primary and is served by a dedicated walsender process. The handshake: the standby reports its last received LSN; the walsender positions itself there and starts streaming WAL records over a TCP connection — as they're written. Records are sent in XLOG_DATA messages, typically batched with a configurable delay (wal_sender_timeout, 60s default, is a liveness check, not a batching knob — the stream sends as fast as the network allows). The standby is, in effect, a client reading the primary's write-ahead log in real time.

Step 3 — the walreceiver

On the standby, the walreceiver process receives the records, writes them to the standby's own WAL files, and fsyncs them (controlled by wal_receiver_create_temp_slot and standby fsync behavior — a standby that doesn't fsync received WAL can still lose data). Then it sends a flush ack: "records up to LSN X are safe on my disk." That ack is the message the entire sync/async distinction hangs on.

Step 4 — replay

The standby's startup process replays the WAL — the exact same code path as crash recovery (that's the reuse trick). The standby is permanently in recovery mode; the only difference is the WAL keeps arriving instead of ending. Replay applies page deltas to the standby's buffer pool; dirty pages are checkpointed normally.

Step 5 — hot standby: reading during replay

On hot standby, the startup process shares the data files with live readers. This is the hard part: a reader's snapshot must never see a half-replayed page or a row whose commit record hasn't replayed yet. The rules:

  • Pages: the buffer pool's page LSN tracking ensures a reader never fetches a page with redo still pending (page_in_hot_stop re-fetch logic).
  • Rows: a visible row version's commit status is consulted via the CLOG as of the snapshot — transactions still in progress on the primary are invisible on the standby until their commit record replays.
  • The price: replay must be careful — one slow reader can force the startup process to wait (max_standby_streaming_delay kicks in for queries that block replay, killing them after a timeout). Read replicas are "eventually consistent with a hard cap."

Step 6 — synchronous commit

With synchronous_standby_names = 'replica1', the COMMIT changes character: after the primary's fsync, the transaction waits for the standby's flush ack (or apply ack with remote_apply) before returning to the client. Cost: one extra network RTT on every commit — ~0.5-5ms on typical LANs. The guarantee: if the primary dies after this commit returned, the standby has the record and recovery will include it. With remote_apply, the standby has applied it — the newly-promoted node can serve it immediately. Async mode: commit returns after the primary's fsync only; a failover can lose the last few hundred ms of commits.

Step 7 — failover

When the primary dies: the standby runs pg_ctl promote (or pg_promote()): it stops replaying, writes an end-of-recovery record, and starts a new timeline (the timeline ID is part of every WAL record header). The history file records the switch. Then the old primary, if it comes back, finds its timeline is stale — the new primary's history forbids it from serving writes (it becomes the new standby or gets re-cloned via pg_rewind, which rolls its tail back to the divergence point using WAL). Split-brain protection is your configuration's job (fencing, synchronous_commit = remote_apply), not the database's.

What it costs

  • Async streaming: ~0 commit latency overhead; lag bounded by network + replay speed.
  • Sync commit (flush ack): +1 RTT per commit (0.5-5ms LAN, worse over WAN).
  • remote_apply: +1 RTT and replay delay — commit latency equals replay time.
  • Steady-state overhead: one walsender per standby (a few % CPU), WAL volume unchanged (replication carries the same bytes the primary wrote anyway).
sql
-- the state, live
SELECT pid, state, sync_state, replay_lag
  FROM pg_stat_replication;
  pid   |   state   | sync_state | replay_lag
--------+-----------+------------+------------
 41203  | streaming | sync       | 00:00:00.032

replay_lag: 32ms — 32 milliseconds of data the standby doesn't have yet. Every high-availability decision in Postgres is a conversation about that number: how big may it grow (async), whether commits wait for it (sync), and whether the app can survive a failover into its gap (it's the size of your loss window).