Module A-10·40 min read

Replication is not a backup. Understanding the consistency guarantees of each replication mode is non-negotiable.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module 10 — Replication, High Availability, and the CAP Trade-offs

What this module covers: Replication is not a backup. High availability is not the same as durability. Failover is not free. This module covers the full replication stack in Postgres — how WAL streaming works at the protocol level, exactly what each synchronous_commit mode guarantees, how failover happens and what it costs, how to build an HA cluster that survives real failures, and how CAP theorem applies concretely to every Postgres HA configuration you will encounter in production.


Replication Is Not a Backup

This is the first thing to establish because conflating them causes real disasters.

Replication propagates changes from primary to standby in near-real-time. If you DROP TABLE transactions on the primary, that statement replicates to every standby within milliseconds. All copies lose the table simultaneously.

Backup is a point-in-time snapshot that is not affected by live changes. A backup from yesterday still has the table after you accidentally drop it today.

You need both. They solve different problems:

  • Replication solves availability — keeping a hot standby ready to take over if the primary fails
  • Backup solves recoverability — restoring to a known good state after data corruption, accidental deletion, or application bugs

With that established: here is how replication works.


Physical Replication: The Full Protocol

Physical (streaming) replication was introduced in Postgres 9.0 and is the foundation of all Postgres HA setups. Every byte on the standby is an exact copy of the primary — same heap files, same indexes, same system catalog.

The Connection Sequence

  1. The standby's WAL receiver process connects to the primary using a replication connection (a special connection type — replication=true in the connection string)
  2. The primary's WAL sender process handles this connection
  3. They negotiate: the standby sends its current replay LSN, the primary starts streaming WAL from that point
  4. WAL records flow continuously from primary to standby
sql

The four LSN columns track where the standby is in the pipeline:

  • sent_lsn — WAL sent by the primary's WAL sender
  • write_lsn — WAL written to standby's disk (received but not fsynced)
  • flush_lsn — WAL fsynced to standby's disk (durable on standby)
  • replay_lsn — WAL applied to standby's heap files (visible to queries on standby)

The gap between each adjacent pair is a different form of lag. A healthy streaming standby has all four within a few KB of each other.

The Standby's Startup Process

On the standby, the startup process runs in continuous recovery mode (Module 3). It reads WAL from pg_wal/ (written by the WAL receiver) and applies each record to the heap, exactly as it would during crash recovery — except it never stops. New WAL keeps arriving and being applied.

This is why a standby cannot accept write queries: it is perpetually replaying, and accepting writes would conflict with the recovery process. It can accept read queries (with hot_standby = on) because reads only need visibility snapshots, which the startup process maintains.

hot_standby_feedback

When a query runs on the standby, it holds an MVCC snapshot. If the primary vacuums and freezes tuples that the standby's query still needs, the standby query will get:

ERROR: could not serialize access due to concurrent update

hot_standby_feedback prevents this by having the standby report its oldest active transaction XID to the primary. The primary's autovacuum respects this and does not reclaim tuples that are still needed by standby queries.

ini

The cost: hot_standby_feedback = on can delay autovacuum on the primary if long-running standby queries hold old snapshots. This is the same dead tuple accumulation problem as long-running transactions on the primary (Module 4) — except it originates from the standby.


Synchronous Replication Modes: Exact Semantics

Module 3 introduced the synchronous_commit settings. Here is the full precision of what each one guarantees and costs.

The Five Modes

ini

synchronous_commit = on (default)

  • Guarantee: WAL flushed to primary disk before COMMIT returns
  • What it does NOT guarantee: standby has received or applied the transaction
  • If primary crashes and is unrecoverable: promote standby → standby may be missing recent transactions
  • Latency added: none beyond local fsync (~1ms on SSD)

synchronous_commit = remote_write

  • Guarantee: WAL written (not fsynced) to standby's OS buffer before COMMIT returns
  • What it does NOT guarantee: standby's data survives a standby crash (OS buffer not yet flushed)
  • If standby crashes immediately after: up to one fsync cycle of data at risk on standby
  • Latency added: network RTT to standby (~1ms LAN, ~5ms same-region cloud)

synchronous_commit = remote_apply

  • Guarantee: transaction applied and visible on standby before COMMIT returns
  • What it does NOT guarantee: nothing — this is the strongest mode
  • Read-after-write from standby is consistent: a client that committed on primary can immediately query standby and see the committed data
  • Latency added: network RTT + standby apply time (~2–10ms)

synchronous_commit = local

  • Guarantee: WAL flushed to primary disk; standby is completely async regardless of synchronous_standby_names
  • Use: when you have a sync standby configured but specific transactions should not wait for it
  • Latency added: same as on (local fsync only)

synchronous_commit = off

  • Guarantee: COMMIT returns immediately; WAL flushed within wal_writer_delay (200ms default)
  • Risk: up to 200ms of committed transactions lost on crash
  • Latency added: none — fastest possible commit

Quorum Commit (Postgres 10+)

ini

Quorum commit is the production-correct setup for high availability: the primary does not stall if one standby is down or lagging, as long as the quorum is met. With ANY 2 (s1, s2, s3): one standby can be down for maintenance without affecting commit latency.


Replication Slots: Guarantees and Risks

A replication slot guarantees that the primary retains WAL until the standby confirms it has consumed everything up to that point.

sql

The WAL Accumulation Risk

As covered in Module 4: a replication slot that becomes inactive (standby goes down, network partition) retains all WAL from its restart_lsn forward. This accumulation is unbounded — Postgres will fill your disk before dropping WAL that a slot needs.

ini

When a slot is invalidated due to max_slot_wal_keep_size, the standby must perform a new base backup to reconnect — it cannot resume streaming from where it left off.

Standbys Without Slots

A standby can connect without a replication slot. In this case:

  • The primary does not retain WAL specifically for this standby
  • If the standby falls too far behind (beyond wal_keep_size), the standby gets: ERROR: requested WAL segment has already been removed
  • The standby must take a new base backup

The trade-off: slots guarantee WAL retention (risk: disk fill). No slot provides no guarantee but no disk fill risk.

For production: use slots with max_slot_wal_keep_size set. The worst case (slot invalidated) requires a base backup. The alternative worst case (no slot, disk full) requires emergency intervention on the primary.


Point-in-Time Recovery (PITR)

PITR allows restoring a Postgres cluster to any point in time, not just the latest backup. It requires:

  1. A base backup (full physical copy of $PGDATA)
  2. Continuous WAL archiving from the backup point to the target recovery time
ini

Recovery Target Modes

ini

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.