postgres://internals

Replication

// the problem

One server is a single point of failure and a single source of read capacity. Replication fixes both: the WAL you met earlier isn't just for crash recovery — stream it to another machine, replay it there, and you have a live, read-only standby that can take over if the primary dies. The only question that really matters is: when the primary fails, how much of the newest data might be gone? That's the sync-vs-async trade-off.

Write on the primary, stream the WAL to the standby, and watch the lag. Then pull the plug — promote the standby — in async vs sync mode and see what survives.

fig.01 · streaming replication
primary lsn
0
standby lsn
0
lag
0
mode
async

primary

accepts writes → WAL

no writes yet

standby

replays WAL · read-only

nothing replayed yet

async: the primary commits immediately; the standby trails by the lag — a failover loses the un-streamed tail

It's just WAL, shipped

Physical (streaming) replication reuses the machinery from the WAL lesson: every change is already a WAL record on the primary. A WAL sender process on the primary streams those records to a WAL receiver on the standby, which replays them into its own copy of the data files — a byte-for-byte clone that trails by the replication lag, usually milliseconds.

There are actually four positions to track, because a record moves through stages on the standby: it's sent, then written to the standby's WAL, then flushed (fsync'd), then replayed into the data files. pg_stat_replication on the primary shows all four per standby — sent_lsn, write_lsn, flush_lsn, replay_lsn — and lag is simply pg_current_wal_lsn() minus the standby's replay_lsn.

Standbys are hot standbys: fully queryable, read-only. So replication buys you two things at once — high availability (promote a standby on failure) and read scaling (send read traffic to replicas).

Async vs sync: the data-loss dial

The dial is when a commit is allowed to return:

  • Asynchronous (the default): the primary commits and returns immediately, and the standby catches up a moment later. Fast, but if the primary dies before a commit has streamed, promoting the standby loses that commit. You saw the gap grow as lag in the explorer.
  • Synchronous: a commit waits until at least one standby confirms it has the WAL record, then returns. Zero committed data lost on failover — at the cost of round-trip latency on every commit (and it stalls if the sync standby is down).

// why it matters · the same synchronous_commit dial, now spanning machines

synchronous_commit picks how far a commit waits before returning. In the WAL lesson it was local; with a synchronous standby it extends across the network, and each level waits for a later point on the standby:

  • remote_write — the standby has received the WAL into memory (survives a Postgres crash there, but not an OS crash).
  • on — the standby has flushed (fsync'd) the WAL to disk. The usual "sync replication" setting: no committed data lost even if the standby's OS crashes.
  • remote_apply — the standby has replayed it, so a read routed to that standby is guaranteed to see the commit (read-your-writes across replicas).

Which standbys count is set by synchronous_standby_names, including quorum rules like ANY 2 (s1, s2, s3). Most systems run async and accept a tiny loss window; go sync only when losing even one committed transaction is unacceptable.

Replication slots keep the WAL around

A standby that falls behind (or disconnects) still needs the WAL it hasn't consumed yet. A replication slot makes the primary retain WAL until the standby confirms it — so a lagging replica can always catch up. Create one and inspect it right here; with no standby attached it shows active = false — exactly the shape of a forgotten slot:

sql · live postgrescreate a slot, inspect it, drop it
⌘/ctrl + enter

// gotcha · a forgotten slot fills your disk

That active = false is the danger. A slot for a standby that's gone forever pins WAL forever: the primary can't recycle it, so pg_wal grows until the disk fills — a classic outage. Monitor pg_replication_slots (watch wal_status slide from reserved toward lost) and drop dead ones.

wal_level must be high enough to log the detail replication needs, and these are the views you'd watch on a real primary:

sql · live postgreseditable — run it
⌘/ctrl + enter

Logical replication: rows, not bytes

Physical replication copies the whole cluster byte-for-byte. Logical replication instead decodes the WAL into row-level changes (INSERT / UPDATE / DELETE). The publisher declares a publication; a subscriber elsewhere creates a subscription that applies those changes. Declare one live:

sql · live postgrespublish one table's row changes
⌘/ctrl + enter

Because it ships rows instead of bytes, logical replication can target selected tables, replicate between different major versions, or feed an entirely different system — the foundation of change-data-capture and zero-downtime major-version upgrades. It needs wal_level = logical, and each published table needs a replica identity (its primary key by default) so the subscriber can match rows for UPDATE/DELETE.

Your turn

// challengewrite it yourself

Show this server's WAL level — it must be at least 'replica' to feed standbys.

⌘/ctrl + enter
// challengewrite it yourself

How many standbys are currently streaming from this server? (Zero here — no replicas are connected.)

⌘/ctrl + enter

// what you now understand

  • 01A WAL sender on the primary streams records to a WAL receiver on the standby, which replays them into a byte-for-byte hot standby (queryable, read-only) — high availability + read scaling.
  • 02A record moves sent → written → flushed → replayed on the standby; pg_stat_replication shows all four LSNs, and lag = pg_current_wal_lsn() − replay_lsn. A standby can be promoted on failure (failover).
  • 03Async commits return immediately (a failover loses the un-streamed tail). Sync (synchronous_commit = remote_write / on / remote_apply) waits for the standby to receive / flush / replay — each stronger and slower; synchronous_standby_names picks which standbys count (incl. quorum ANY 2 of 3).
  • 04Replication slots make the primary retain WAL until a standby confirms it (active=true). A forgotten slot (active=false) pins WAL forever and fills the disk — monitor wal_status and drop dead ones.
  • 05wal_level must be 'replica' for physical replication, 'logical' for logical decoding; pg_stat_replication and pg_replication_slots show the state.
  • 06Logical replication decodes WAL into row changes via publications/subscriptions (each table needs a replica identity) — replicate selected tables across major versions or into other systems: the basis of CDC and zero-downtime upgrades.

// self-test

You run asynchronous replication. The primary's disk dies, and you promote the standby. What's the risk?

// self-test

A replica was decommissioned but its replication slot was never dropped. Weeks later the primary's disk is full. Why?

// go deeper

nextRow-Level Security