physical-replication — streaming to a physical standby
Physical (aka streaming, aka binary) replication ships raw WAL bytes from primary to standby. The standby applies them via the same recovery machinery used at startup — replaying block-level changes. Sibling to logical replication but MUCH simpler code path: no decoding, no output plugin, no per-row conflict handling.
The file map
| File |
KB |
Role |
replication/walsender.c |
134 |
Primary side. The walsender is a backend spawned per replication connection. Reads WAL, sends it over the connection, handles feedback + syncrep signaling + timeline history. |
replication/walreceiver.c |
47 |
Standby side. The walreceiver is a distinct aux process on the standby. Establishes libpq connection to primary's walsender, receives WAL, writes to local pg_wal, signals startup process to replay. |
replication/walreceiverfuncs.c |
11 |
SQL-callable helpers on the standby side. |
replication/slot.c |
97 |
Replication slot infrastructure — physical AND logical share this file for the base mechanics (create/drop/persistence). |
replication/slotfuncs.c |
28 |
SQL-callable slot functions. |
replication/syncrep.c |
35 |
Synchronous replication — primary waits for confirmed standby write / flush / apply based on synchronous_commit. |
access/transam/xlogrecovery.c |
— |
The startup process's WAL replay engine. Called on the standby to consume WAL from walreceiver. |
backup/basebackup*.c |
— |
pg_basebackup server-side implementation — makes a physical copy for a new standby. |
The two processes
Walsender on the primary:
- Spawned per replication connection.
- Runs a state machine:
WALSNDSTATE_STARTUP → _BACKUP → _CATCHUP → _STREAMING → _STOPPING.
- In STREAMING state, sends chunks of WAL as
XLogData messages plus periodic KeepAlive.
- Receives
HotStandbyFeedback from standby and updates pg_stat_replication view.
- If synchronous, participates in the
syncrep.c signaling.
Walreceiver on the standby:
- One instance total (not per-slot).
- Connects to primary via libpq, negotiates the streaming protocol.
- Writes received WAL to local
pg_wal/ files.
- Reports progress back via feedback +
pg_stat_wal_receiver.
- Cooperates with the startup process which does the actual replay.
Physical vs logical slots
Physical slot:
- Reserves WAL for the standby's use — primary won't recycle WAL under
restart_lsn.
- No catalog_xmin (no need — physical replay doesn't need catalog snapshots).
- Simpler: just a
restart_lsn for the primary to track.
- Created by
pg_create_physical_replication_slot('name').
Logical slot (contrast):
- Reserves WAL AND catalog snapshots.
- Has an output plugin.
- SLRU (
pg_replslot/<name>/) spill space.
Streaming protocol
Walsender sends messages tagged with:
'w' — WAL data chunk (with LSN + timestamp).
'k' — KeepAlive (empty; probes liveness).
Walreceiver replies with:
'r' — receipt confirmation (LSNs: written, flushed, applied).
'h' — HotStandbyFeedback (xmin + catalog_xmin, if hot_standby_feedback = on).
Message boundaries + framing are the shared replication/pgoutput and libpq protocol layers.
Synchronous replication semantics
synchronous_commit on the primary controls what happens before COMMIT returns to the client:
| Setting |
Wait for |
off |
Nothing (fastest, unsafe on crash). |
local |
Local WAL flushed to disk. |
remote_write |
Primary's WAL flushed AND standby has written to memory (not necessarily to disk). |
on (default) |
Primary AND standby WAL flushed to disk. |
remote_apply |
Primary + standby flushed + standby applied. |
Which standby? Configured via synchronous_standby_names — either a name list or a quorum specification (PG 10+: ANY 2 (a, b, c)).
Hot standby
When hot_standby = on, the standby accepts read-only queries during recovery. Complications:
- Snapshot conflicts — a long standby query may prevent the primary from removing tuples the query needs. Solutions:
max_standby_streaming_delay — cancel long queries if they block replay.
hot_standby_feedback — send xmin back to primary; primary keeps those tuples alive at the cost of freezing pressure.
- Locking conflicts — same story for locks needed by replay.
Common patch shapes
Add a new replication feedback field
- Extend
HotStandbyFeedback message structure in libpq/pqcomm.h etc.
- Sender updates (walreceiver) — extend the send.
- Receiver updates (walsender) — parse and store new field.
- View update —
pg_stat_replication gets a new column.
- Docs.
Change walsender state machine
Careful — the state transitions are checked in many places. Consider:
- The transition itself.
- Every check of the state.
- Failure paths that must reset state cleanly.
Extend synchronous_standby_names semantics
Existing supports named priority and quorum. Extensions might add per-region grouping, latency-based selection, etc. Would touch syncrep.c SyncRepGetSyncStandbysQuorum + related.
Debug "standby is falling behind"
pg_stat_replication on primary — see write_lag, flush_lag, replay_lag.
pg_stat_wal_receiver on standby — connection state, last_msg_receipt_time.
pg_wal_replay_pause_lsn() / pg_last_wal_replay_lsn() — how far the standby has applied.
- Check for long queries on standby (via
pg_stat_activity) blocking replay under high concurrency.
Pitfalls
synchronous_commit = off is per-transaction — you can set it locally in a session. Async transactions still write WAL, just don't wait.
synchronous_standby_names with unreachable standby BLOCKS commits — a misconfigured name = your primary waits forever. Set synchronous_commit = local under emergency to unblock.
hot_standby_feedback = on retains xmin — long standby queries + feedback + high write rate → wraparound risk on primary. Monitor.
- Physical slot never expires — a dropped standby's slot pins WAL forever.
pg_drop_replication_slot is manual.
- Walreceiver reset on error — a transient network error may cause walreceiver to reconnect from scratch, replaying overlap. Usually fine; occasionally shows up as a spike in replay activity.
max_standby_streaming_delay = -1 means "wait forever for the query to finish". A standby with this set + a stuck query = replay stalls.
hot_standby = off allows recovery to catch up faster — no snapshot conflicts. Sometimes recommended for CPU-heavy replay windows.
primary_conninfo password can leak into logs — use ~/.pgpass or a service definition instead.
- Base backup via pg_basebackup includes UNLOGGED tables' init fork but not their main fork — standby will re-init unlogged tables on promotion. Common surprise for test setups.
archive_command can conflict with slot-based replication — both retain WAL; consider which method is authoritative.
Related corpus
- Subsystem:
replication (the parent doc — covers both physical + logical high-level).
- Sibling skill:
logical-replication (very different code paths despite shared filenames).
- Idioms:
walsender-state-machine, wal-page-write-flush, wal-buffer-state, crash-recovery-startup, replication-origin-tracking (logical-specific but adjacent).
- Sessions:
2026-06-02-replication-synthesis.md — comprehensive replication doc write.
Corpus-chain shortcut
python3 scripts/corpus-chain.py --file src/backend/replication/walsender.c
python3 scripts/corpus-chain.py --file src/backend/replication/walreceiver.c
python3 scripts/corpus-chain.py --idiom walsender-state-machine
Boundary
Use this skill for streaming replication + walsender/walreceiver + physical slots + syncrep + hot standby.
Don't use for:
- Logical replication —
src/backend/replication/logical/ — completely separate code paths. See logical-replication skill.
- WAL insertion / write / flush on the primary — those are
access/transam/xlog.c machinery; use wal-buffer-state idiom or wal-and-xlog skill.
- Recovery / crash restart —
access/transam/xlogrecovery.c — used at startup regardless of replication.
- Backup tools —
pg_basebackup invocation lives in src/bin/pg_basebackup/.
pg_receivewal — external walreceiver; different implementation than backend walreceiver.
1---2name: physical-replication3description: PostgreSQL's physical streaming replication — walsender (primary side, `replication/walsender.c`) + walreceiver (standby side, `replication/walreceiver.c`) + slots (`replication/slot.c`) + synchronous replication (`syncrep.c`). Covers the streaming protocol variant, hot standby, standby feedback, synchronous commit, WAL sender/receiver state machines, and the physical-slot semantics that differ from logical slots. Loads when the user asks about streaming replication, hot standby, `pg_basebackup`, synchronous_commit levels, walsender/walreceiver state, primary_conninfo, physical replication slots, standby feedback (`hot_standby_feedback`), or `pg_wal_replay_*` recovery-progress functions. Skip when the ask is about logical replication (`replication/logical/`) — sibling but very different code path.4---56# physical-replication — streaming to a physical standby78Physical (aka streaming, aka binary) replication ships raw WAL bytes from primary to standby. The standby applies them via the same recovery machinery used at startup — replaying block-level changes. Sibling to logical replication but MUCH simpler code path: no decoding, no output plugin, no per-row conflict handling.910## The file map1112| File | KB | Role |13|---|---:|---|14| `replication/walsender.c` | 134 | **Primary side.** The walsender is a backend spawned per replication connection. Reads WAL, sends it over the connection, handles feedback + syncrep signaling + timeline history. |15| `replication/walreceiver.c` | 47 | **Standby side.** The walreceiver is a distinct aux process on the standby. Establishes libpq connection to primary's walsender, receives WAL, writes to local pg_wal, signals startup process to replay. |16| `replication/walreceiverfuncs.c` | 11 | SQL-callable helpers on the standby side. |17| `replication/slot.c` | 97 | Replication slot infrastructure — physical AND logical share this file for the base mechanics (create/drop/persistence). |18| `replication/slotfuncs.c` | 28 | SQL-callable slot functions. |19| `replication/syncrep.c` | 35 | Synchronous replication — primary waits for confirmed standby write / flush / apply based on `synchronous_commit`. |20| `access/transam/xlogrecovery.c` | — | The startup process's WAL replay engine. Called on the standby to consume WAL from walreceiver. |21| `backup/basebackup*.c` | — | pg_basebackup server-side implementation — makes a physical copy for a new standby. |2223## The two processes2425**Walsender** on the primary:26- Spawned per replication connection.27- Runs a state machine: `WALSNDSTATE_STARTUP` → `_BACKUP` → `_CATCHUP` → `_STREAMING` → `_STOPPING`.28- In STREAMING state, sends chunks of WAL as `XLogData` messages plus periodic `KeepAlive`.29- Receives `HotStandbyFeedback` from standby and updates `pg_stat_replication` view.30- If synchronous, participates in the `syncrep.c` signaling.3132**Walreceiver** on the standby:33- One instance total (not per-slot).34- Connects to primary via libpq, negotiates the streaming protocol.35- Writes received WAL to local `pg_wal/` files.36- Reports progress back via feedback + `pg_stat_wal_receiver`.37- Cooperates with the startup process which does the actual replay.3839## Physical vs logical slots4041Physical slot:42- Reserves WAL for the standby's use — primary won't recycle WAL under `restart_lsn`.43- No catalog_xmin (no need — physical replay doesn't need catalog snapshots).44- Simpler: just a `restart_lsn` for the primary to track.45- Created by `pg_create_physical_replication_slot('name')`.4647Logical slot (contrast):48- Reserves WAL AND catalog snapshots.49- Has an output plugin.50- SLRU (`pg_replslot/<name>/`) spill space.5152## Streaming protocol5354Walsender sends messages tagged with:5556- `'w'` — WAL data chunk (with LSN + timestamp).57- `'k'` — KeepAlive (empty; probes liveness).5859Walreceiver replies with:6061- `'r'` — receipt confirmation (LSNs: written, flushed, applied).62- `'h'` — HotStandbyFeedback (xmin + catalog_xmin, if `hot_standby_feedback = on`).6364Message boundaries + framing are the shared `replication/pgoutput` and libpq protocol layers.6566## Synchronous replication semantics6768`synchronous_commit` on the primary controls what happens before COMMIT returns to the client:6970| Setting | Wait for |71|---|---|72| `off` | Nothing (fastest, unsafe on crash). |73| `local` | Local WAL flushed to disk. |74| `remote_write` | Primary's WAL flushed AND standby has written to memory (not necessarily to disk). |75| `on` (default) | Primary AND standby WAL flushed to disk. |76| `remote_apply` | Primary + standby flushed + standby applied. |7778Which standby? Configured via `synchronous_standby_names` — either a name list or a quorum specification (PG 10+: `ANY 2 (a, b, c)`).7980## Hot standby8182When `hot_standby = on`, the standby accepts read-only queries during recovery. Complications:8384- Snapshot conflicts — a long standby query may prevent the primary from removing tuples the query needs. Solutions:85 - `max_standby_streaming_delay` — cancel long queries if they block replay.86 - `hot_standby_feedback` — send xmin back to primary; primary keeps those tuples alive at the cost of freezing pressure.87- Locking conflicts — same story for locks needed by replay.8889## Common patch shapes9091### Add a new replication feedback field9293- Extend `HotStandbyFeedback` message structure in `libpq/pqcomm.h` etc.94- Sender updates (walreceiver) — extend the send.95- Receiver updates (walsender) — parse and store new field.96- View update — `pg_stat_replication` gets a new column.97- Docs.9899### Change walsender state machine100101Careful — the state transitions are checked in many places. Consider:102- The transition itself.103- Every check of the state.104- Failure paths that must reset state cleanly.105106### Extend synchronous_standby_names semantics107108Existing supports named priority and quorum. Extensions might add per-region grouping, latency-based selection, etc. Would touch `syncrep.c` `SyncRepGetSyncStandbysQuorum` + related.109110### Debug "standby is falling behind"111112- `pg_stat_replication` on primary — see write_lag, flush_lag, replay_lag.113- `pg_stat_wal_receiver` on standby — connection state, last_msg_receipt_time.114- `pg_wal_replay_pause_lsn()` / `pg_last_wal_replay_lsn()` — how far the standby has applied.115- Check for long queries on standby (via `pg_stat_activity`) blocking replay under high concurrency.116117## Pitfalls118119- **`synchronous_commit = off` is per-transaction** — you can set it locally in a session. Async transactions still write WAL, just don't wait.120- **`synchronous_standby_names` with unreachable standby BLOCKS commits** — a misconfigured name = your primary waits forever. Set `synchronous_commit = local` under emergency to unblock.121- **`hot_standby_feedback = on` retains xmin** — long standby queries + feedback + high write rate → wraparound risk on primary. Monitor.122- **Physical slot never expires** — a dropped standby's slot pins WAL forever. `pg_drop_replication_slot` is manual.123- **Walreceiver reset on error** — a transient network error may cause walreceiver to reconnect from scratch, replaying overlap. Usually fine; occasionally shows up as a spike in replay activity.124- **`max_standby_streaming_delay = -1`** means "wait forever for the query to finish". A standby with this set + a stuck query = replay stalls.125- **`hot_standby = off` allows recovery to catch up faster** — no snapshot conflicts. Sometimes recommended for CPU-heavy replay windows.126- **`primary_conninfo` password can leak into logs** — use `~/.pgpass` or a service definition instead.127- **Base backup via pg_basebackup includes UNLOGGED tables' init fork but not their main fork** — standby will re-init unlogged tables on promotion. Common surprise for test setups.128- **`archive_command` can conflict with slot-based replication** — both retain WAL; consider which method is authoritative.129130## Related corpus131132- **Subsystem**: `replication` (the parent doc — covers both physical + logical high-level).133- **Sibling skill**: `logical-replication` (very different code paths despite shared filenames).134- **Idioms**: `walsender-state-machine`, `wal-page-write-flush`, `wal-buffer-state`, `crash-recovery-startup`, `replication-origin-tracking` (logical-specific but adjacent).135- **Sessions**: `2026-06-02-replication-synthesis.md` — comprehensive replication doc write.136137## Corpus-chain shortcut138139```140python3 scripts/corpus-chain.py --file src/backend/replication/walsender.c141python3 scripts/corpus-chain.py --file src/backend/replication/walreceiver.c142python3 scripts/corpus-chain.py --idiom walsender-state-machine143```144145## Boundary146147**Use this skill** for streaming replication + walsender/walreceiver + physical slots + syncrep + hot standby.148149**Don't use** for:150- **Logical replication** — `src/backend/replication/logical/` — completely separate code paths. See `logical-replication` skill.151- **WAL insertion / write / flush** on the primary — those are `access/transam/xlog.c` machinery; use `wal-buffer-state` idiom or `wal-and-xlog` skill.152- **Recovery / crash restart** — `access/transam/xlogrecovery.c` — used at startup regardless of replication.153- **Backup tools** — `pg_basebackup` invocation lives in `src/bin/pg_basebackup/`.154- **`pg_receivewal`** — external walreceiver; different implementation than backend walreceiver.