Replication and Read Scaling
A primary streams its changes to replicas that serve reads; asynchronous replication is fast and lagging, synchronous is consistent and slow, and the "I saved it and see the old value" bug is replication lag meeting a naive read route.
How it works
The primary accepts all writes and appends them to its WAL. Replicas connect, stream that WAL, and replay it, ending up with a copy of the primary a short time behind. Reads can go to any replica; writes must go to the primary. This scales reads almost linearly — ten replicas, roughly ten times the read capacity — and gives you a machine ready to promote if the primary fails.
Streaming replication ships WAL bytes and produces a physical copy. Logical replication ships decoded row changes, which lets you replicate a subset of tables, across major versions, or into a different system entirely — the basis of change-data-capture and zero-downtime upgrades.
Lag, and reading your own writes
Asynchronous replication — the default — means the primary acknowledges a commit without waiting for replicas. So for a window of milliseconds to seconds, a replica is behind. A user updates their profile (write → primary), the page reloads and reads their profile (read → replica), and the replica has not replayed the write yet: they see the old value, having just changed it. Nothing is broken; this is what asynchronous means.
Four ways to route around it. Read from the primary after a write for a few seconds (simple, slight extra primary load, the window is a guess). Wait for the replica to catch up to the write’s WAL position before reading (exact, needs plumbing). Synchronous replication so the commit does not return until a replica has the write (no lag to read, but every write pays a round trip and a slow replica slows every commit). Or accept the staleness where it does not matter, which is most reads.
1-- primary: require one named replica to confirm before COMMIT returns2synchronous_standby_names = 'ANY 1 (replica_a, replica_b)';3 4-- per-transaction: this one must be durable on a replica; others stay async5SET synchronous_commit = 'on'; -- for the transaction that must not be lostFailover
When the primary dies, a replica is promoted to primary and the application is pointed at it. Automated by tools (Patroni, cloud managed services) that use consensus to avoid two primaries. The cost of asynchronous replication shows here: any writes the old primary had not yet streamed are lost on failover. Synchronous replication to at least one replica bounds that loss to zero for the synchronous replica, which is why financial systems pay its latency.
The number to monitor is lag, in both bytes (pg_wal_lsn_diff) and seconds. Alert on it, not just on replica uptime — a replica that is up but an hour behind is serving hour-old reads and is useless as a failover target.
Key points
- Primary takes writes and streams WAL; replicas replay it and serve reads. Reads scale near-linearly.
- Async is fast and lagging; sync is consistent and slower. Lag causes the "read your own write" bug.
- Route around lag: read-from-primary-after-write, wait-for-LSN, synchronous commit, or accept staleness.
- Failover promotes a replica; async replication can lose the last unsent writes. Monitor lag in bytes and seconds.
Replication lag: read your own writes
Reads → any replica: Every read goes to a replica. Cheapest and simplest, and it shows the user their old profile for as long as the lag lasts.
When to use — and when not
- Read-heavy workloads; high availability; offloading reporting from the primary.
- Scaling writes — replicas do not help; that is partitioning and sharding.
- Reads that must be strictly current, unless you route them to the primary or wait for the LSN.
Failure modes
- Naive read routing showing users their own stale writes.
- A lagging replica used as a failover target, losing data on promotion.
- Synchronous replication to a slow replica stalling all commits.
See how this works internally →
Descend one layer: the same topic explained from the machinery up.