Copying data across nodes with replication
Open in the interactive app: practice, quiz, mock interview →In one minute
- Replication keeps copies of the same data on several nodes for availability, read scaling, and geographic locality.
- The main designs are single-leader (one writer, many followers), multi-leader (several writers that exchange changes), and leaderless (clients write to a quorum of replicas).
- The key decision is when a write counts as committed: after the leader alone (asynchronous), after one or more followers (semi-synchronous), or after a majority (consensus).
- Asynchronous replication means replica lag, which surfaces as stale reads and, on failover, lost writes.
How it works
Single-leader replication
All writes go to the leader. It records each change in a log (PostgreSQL's WAL, MySQL's binlog) and followers stream and replay that log in order. Followers serve reads, so read capacity scales with replica count while write capacity does not. Log shipping can be physical (byte-level page changes, needs identical versions) or logical (row-level changes, works across versions and feeds change data capture).
Synchronous, semi-synchronous, asynchronous
With asynchronous replication the leader commits locally and acknowledges the client; followers catch up later. Writes are fast, but if the leader dies, anything not yet shipped is lost. With synchronous replication the leader waits for a follower to confirm durability. No acknowledged write is lost, but one slow follower stalls every write, so systems usually wait for any one of several followers (semi-synchronous, or "any 1 of 3"). Read the fine print on what happens when no follower answers. MySQL's semi-synchronous source waits rpl_semi_sync_source_timeout (10 seconds by default) and then quietly reverts to asynchronous replication until a replica catches up (MySQL docs), so the "no loss" promise lapses exactly when things go wrong. PostgreSQL instead blocks commits until a synchronous standby returns. Consensus-based systems (Raft in etcd, CockroachDB, Spanner's Paxos groups) wait for a majority, which tolerates a minority of failures without losing data or availability. In Raft, an entry is committed once the leader has stored it on a majority, and a candidate cannot win an election unless its log is at least as up to date as a majority's, which is what makes promotion safe (Ongaro and Ousterhout).
Lag and its symptoms
Replica lag is the gap between the leader's log position and what a follower has applied. It is usually milliseconds and spikes to minutes under heavy writes, long-running replica queries, or network trouble. Visible symptoms:
- Read-your-writes violations: a user saves a profile and the next page, served by a replica, shows the old value.
- Non-monotonic reads: two requests hit two replicas and time appears to go backwards.
- Causality violations: a reply is visible before the message it answers.
These are the session guarantees Terry and colleagues named in 1994: read your writes, monotonic reads, writes follow reads, and monotonic writes (Terry et al.). Fixes include routing a user's reads to the leader for a few seconds after a write, tracking the write's log position and reading only from replicas that have applied it, or pinning a sequence of related reads to one replica so time never runs backwards.
Failover
When the leader fails, a follower is promoted. The hard parts are detection (a timeout too short causes needless failovers, too long extends downtime), choosing the most up-to-date follower, and fencing the old leader so it cannot keep accepting writes after a network partition heals. Two nodes both believing they are leader is split brain, and it corrupts data.
Multi-leader and leaderless
Multi-leader setups accept writes in several regions and exchange changes asynchronously, so concurrent writes to the same row conflict and need a resolution rule (last-writer-wins, merge functions, CRDTs). Leaderless systems (Dynamo-style, Cassandra) write to W of N replicas and read from R; with R + W > N a read overlaps the latest write, though edge cases around sloppy quorums and concurrent writes remain. Amazon's Dynamo paper describes a common setting of N=3, R=2, W=2, with hinted handoff and Merkle-tree anti-entropy to repair replicas that missed writes (DeCandia et al.).
Down the stack
Layer 1The contract
PostgreSQL makes the commit contract explicit with synchronous_commit. With local, a commit is acknowledged once the primary's WAL is flushed; standbys may lag, and failover can lose anything not yet shipped. With remote_write, a standby has received the WAL and written it to its OS, so it survives a standby process crash but not a standby power loss. With on (when synchronous standbys are configured), a standby has flushed it to disk. With remote_apply, a standby has replayed it, so a read on that standby immediately after commit sees the write. Each step up adds latency and narrows what a failover can lose. These levels only bite when synchronous_standby_names is set; with it empty, every non-off value behaves like on locally (Postgres docs).
Layer 2The mechanism
Changes are WAL records stored in segment files of 16 MB by default, set at initdb time (Postgres docs). On the primary, a walsender process per standby streams records; on the standby, walreceiver writes them and the startup process replays them. The standby reports three positions back: written, flushed, and replayed LSN, which the primary exposes in pg_stat_replication as write_lsn, flush_lsn, replay_lsn and the matching lag times. synchronous_standby_names = 'ANY 1 (s1, s2)' makes commits wait for whichever of two standbys confirms first, while FIRST 1 (s1, s2) waits for the highest-priority one that is connected (Postgres docs). Replication slots stop the primary from deleting WAL a standby still needs, which protects the standby but can fill the primary's disk if a standby disappears. Since Postgres 13, max_slot_wal_keep_size caps that retention; its default of -1 still means unlimited, so set it. Postgres 17 added sync_replication_slots, which copies logical failover slots to a physical standby so change-data-capture consumers can continue after a promotion. MySQL's equivalent is the binlog with GTIDs. Its semi-synchronous plugin (renamed rpl_semi_sync_source and rpl_semi_sync_replica; the old master and slave names are deprecated) waits for one replica to acknowledge receipt. By default it waits after the binlog is synced but before the storage engine commits (AFTER_SYNC), so no other session can see a write the replica has not received.
Layer 3The machine
A synchronous commit costs a local WAL fsync (roughly 50 µs to 1 ms on SSDs), a network round trip (about 0.5 ms in one availability zone, 1 to 2 ms across zones, 60 to 150 ms across regions), and the standby's own fsync. WAL volume spikes after each checkpoint because the first change to any 8 KB page writes the full page image, so lag often follows the checkpoint cycle. Replay in PostgreSQL runs in a single startup process (Postgres docs), so a primary writing with 32 cores can produce WAL faster than one core on the standby replays it. Since Postgres 15, recovery_prefetch (default try) reads ahead in the WAL and issues read-ahead advice for pages not yet in memory, which hides some disk latency but not the single-core limit (Postgres docs). Across regions, TCP throughput is limited by the bandwidth-delay product: filling 1 Gbps at 70 ms needs about 8.75 MB in flight, and smaller socket buffers silently cap the stream.
Why it's built this way
- Disks and machines fail, so data must exist on more than one node.
- Concurrent writers on several nodes create conflicts, so most systems use one leader and ship its log.
- Shipping the log the database already writes for crash recovery is cheaper than shipping data, so replication reuses the WAL.
- Waiting for a remote acknowledgement adds at least one round trip per commit, so asynchronous is the default and synchronous is opt-in.
- Two leaders would diverge, so promotion needs fencing or a majority vote.
Diagram
sequenceDiagram
participant C as Client
participant L as Leader
participant F1 as Follower 1
participant F2 as Follower 2
C->>L: write x = 5
L->>L: append to log (LSN 101)
L->>F1: stream LSN 101
L->>F2: stream LSN 101
F1-->>L: applied 101
L-->>C: commit ok (semi-sync, 1 ack)
F2-->>L: applied 101 (later)
C->>F2: read x with min LSN 101Worked example
An e-commerce database takes 4,000 writes per second at about 1 KB of WAL each, so 4 MB per second of log. It has one leader and three followers in the same region, plus one cross-region follower 70 ms away.
With async replication, normal lag is under 50 ms locally. During a nightly bulk import of 30 GB, WAL volume rises to 60 MB per second, and the cross-region follower, limited to 40 MB per second of bandwidth, falls 20 minutes behind. Had the region failed then, failing over to that replica would lose 20 minutes of orders.
The team makes three changes: semi-synchronous commit with "any 1 of the 2 local followers", which adds about 1 ms per commit and guarantees zero acknowledged loss on single-node failure; throttling the import to 20 MB per second; and an alert when any replica's lag exceeds 30 seconds. Reads that must reflect the user's own writes carry the commit LSN in the session and are routed to a replica at or past it.
In the wild
- GitHub, 2012: automatic failover to a cold replica, then split brain. A schema migration overloaded the active MySQL node, health checks failed, and Pacemaker promoted a replica whose InnoDB buffer pool was cold. It performed so badly that the role flipped back. The next day a segfault partitioned the cluster and two masters were elected, one stale; 16 private repositories were briefly visible to the wrong users. GitHub made failover human-initiated afterwards (GitHub).
- GitHub, 2018: 43 seconds of partition, 24 hours of repair. Network maintenance cut the US East hub from the primary data center for 43 seconds. Orchestrator promoted West Coast primaries before they had all of East's writes, so both coasts held writes the other lacked. GitHub restored from backup and re-replicated rather than discard data, ran degraded for 24 hours 11 minutes, reconciled leftover writes by hand, and stopped cross-region promotion (GitHub).
- Shopify, 2021: monotonic reads across lagging replicas. Replicas with different lag let one request see a row in one table but not its related row in another. Shopify rejected waiting on every replica and GTID-based causal reads as too costly to run, and instead hashed a
consistent_read_idin its ProxySQL fork to pin a series of reads to one replica (Shopify).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Async single-leader | Fast writes, simple | Data loss on failover, stale reads | Some loss tolerable, read scaling needed |
| Semi-sync (any 1 of N) | No acknowledged loss on one failure | Extra round trip per commit | Losing committed writes is unacceptable |
| Consensus (majority) | Automatic, safe failover | Higher write latency, needs 3+ nodes | Strong consistency with high availability |
| Multi-leader | Local writes in each region | Conflicts, complex resolution | Multi-region writes, offline clients |
| Leaderless quorum | No failover, tunable R and W | Weaker guarantees, repair needed | High write availability, simple data model |
Pitfalls in production
- Silent lag. Replicas fall hours behind and nobody notices until failover. Alert on lag in both bytes and seconds.
- Long queries on replicas. An analytics query conflicts with replay and either cancels or stalls replication. Postgres waits
max_standby_streaming_delay(30 seconds by default) before cancelling the query, andhot_standby_feedbackavoids cancellations at the cost of bloat on the primary. Use a dedicated replica for analytics with its own settings. - Split brain after failover. The old leader returns and accepts writes. Fence it with leases, STONITH, or by revoking its access before promotion.
- Failover drills never run. The first real failover reveals stale DNS, broken runbooks and a cold buffer cache on the new leader. Rehearse failover regularly in production-like conditions.
- Sync quietly becomes async. MySQL semi-sync falls back after its timeout, so you think you have zero data loss and don't. Alert on the semi-sync status variables, not just on lag.
- Assuming replicas are backups. A bad
DELETEreplicates instantly. Keep point-in-time recovery backups separately.
Leadership lens
- Write down the recovery point (how much data you may lose) and recovery time objectives; they choose async versus sync, not taste.
- Ask which reads can tolerate staleness and which cannot, and design routing explicitly for the latter.
- Prefer managed or consensus-based failover over hand-written scripts; promotion and fencing are where homegrown tooling fails.
- Budget cross-region bandwidth for peak write volume, not average.
Check yourself
Why does synchronous replication to a single named follower reduce availability?
Show answer
Every commit waits for that follower, so if it is slow or down, writes stall even though the leader is healthy.
A user edits a post and sees the old version on refresh. What is happening and how do you fix it?
Show answer
The read hit a lagging replica; route that user's reads to the leader briefly, or to a replica that has applied the write's log position.
Why does
R + W > Nnot make a leaderless store linearizable on its own?Show answer
Concurrent writes, sloppy quorums, and partial write failures can still let reads see different values, so extra mechanisms are needed for strict ordering.
Sources and further study
Primary
- Ongaro and Ousterhout, In Search of an Understandable Consensus Algorithm (Raft, 2014): majority commit and the election restriction that keeps promotion from losing committed entries.
- DeCandia et al., Dynamo (SOSP 2007): leaderless quorums, sloppy quorums, hinted handoff and anti-entropy.
- Terry et al., Session Guarantees for Weakly Consistent Replicated Data (1994): the original definitions of read your writes and monotonic reads.
Systems docs
- PostgreSQL: Replication settings:
synchronous_standby_names, slot limits, and standby conflict settings. - PostgreSQL: Hot Standby: how replay conflicts with standby queries and how cancellations work.
- MySQL 8.4: Semisynchronous Replication: wait points, acknowledgement counts and the fallback to async.
Books
- Designing Data-Intensive Applications, Martin Kleppmann, chapter 5 "Replication" (1st edition) or chapter 6 (2nd edition): leaders and followers, replication lag, multi-leader and leaderless.
- Database Internals, Alex Petrov, chapter 11 "Replication and Consistency" and chapter 12 "Anti-Entropy and Dissemination".
- Understanding Distributed Systems, Roberto Vitillo, chapter "Replication": state machine replication and consistency models in short form.
Other curricula
- donnemartin/system-design-primer: presents master-slave and master-master replication with their disadvantages (lag, conflicts, failover complexity), but not commit levels or session guarantees.
Watch
- Distributed Systems 5.1: Replication (Martin Kleppmann): a Cambridge lecture introducing replication and the consistency problems it creates.
Free resources
- PostgreSQL: High Availability and Replication: streaming replication, synchronous commit, and failover in Postgres.
- MySQL 8.4: Replication: binlog formats, GTIDs, and semi-synchronous replication (8.0 reached end of life in April 2026).
- Jepsen: Consistency Models: precise definitions of the guarantees replicas can offer.
- Raft: the consensus algorithm behind many majority-replicated databases, with a visualization.
- Kleppmann: Please stop calling databases CP or AP: why replication guarantees need more nuance than CAP.