studyDDIAChapter 6 · Replication

Your Own Write, Gone from the Replica

Write to the leader, immediately read a follower, and your own submission looks lost — a real Postgres standby returns count 0 for the comment you just posted, and reappears only after it catches up ~5 s later. You close the anomaly by routing reads-after-writes back to the leader.


Concept

Write to the leader, then read from a follower — the classic scale-out pattern, since data is read far more than written. But followers replicate asynchronously, so for a moment the follower hasn't seen your write yet. Read it in that window and your own submission looks lost — the comment you just posted isn't there. You will write to a real Postgres primary, immediately read a real streaming replica, watch your write come back as count 0, and see it reappear only after the replica catches up ~5 seconds later. Then you close the anomaly by routing reads-after-writes back to the leader.

Provenance

Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 6 — Replication, §"Reading your own writes":

if the user views the data shortly after making a write, the new data may not yet have reached the replica. To the user, it looks as though the data they submitted was lost.

The book names the anomaly and the guarantee that fixes it (read-your-writes / read-after-write consistency). Here you reproduce it on real streaming replication and measure the window.

Prerequisites

The surprise is about the gap between a leader's commit and a follower's apply. These help; each line notes where it shows up.

  1. Leader-follower (single-leader) replication — writes go to one leader; followers copy its change log and serve reads. Reading a follower offloads the leader — the whole point.
  2. Asynchronous replication — the leader commits and replies to the client without waiting for followers, so a follower is always slightly behind. That "behind" is the whole exercise.
  3. Replication lag — the delay between a leader commit and a given follower reflecting it. Here we set it to 5 s (recovery_min_apply_delay) so it's large and obvious instead of a sub-millisecond race.
  4. Read-your-writes consistency — the guarantee that a user always sees their own recent writes (others' can lag). The fix in Step 3.
Where to learn the prerequisites The anomaly and its fixes (#1–#4): DDIA §"Reading your own writes" and §"Solutions for Replication Lag". How to make it real (#3): the PostgreSQL manual on streaming replication, pg_stat_replication, and recovery_min_apply_delay. If only one is new, make it #2 — async replication is why the window exists at all.
Environment these numbers came from The anomaly (immediate replica read misses the write) is 100% reproducible; the catch-up is ~5 s by construction. Captured on macOS 26.5.2 (Darwin 25.5.0), arm64, Docker 29.6.2, official postgres:16 (PostgreSQL 16.14): one primary + one async standby streaming over a Docker network, standby lagged 5 s via recovery_min_apply_delay.

Mental model

setup_replication.sh stands up a primary (leader) and a standby (follower) with real Postgres streaming replication. The standby is deliberately held 5 seconds behind (recovery_min_apply_delay = 5s) so the lag is easy to see. You write a comment to the primary and immediately read it back from both nodes; the follower hasn't applied it yet. The setup is scripted (it's just plumbing); the writes and reads in the Steps you run by hand.

Setup

cd study/ddia/ch06/code
bash setup_replication.sh     # ~30 s: primary + a fresh and a 5s-lagged standby

# handles for the two nodes:
#   primary (leader):        docker exec ddia-ch6-primary psql -U postgres -d study
#   standby (5s follower):   docker exec ddia-ch6-standby  psql -U postgres -d study

Do not read the output yet — make each prediction first.


Step 1 · write to the leader, read both (the surprise)

Predict. Insert a comment on the primary, then immediately run the same SELECT count(*) WHERE id = 1 on the primary and on the standby. What does each return?

# on the PRIMARY (leader):
docker exec ddia-ch6-primary psql -U postgres -d study -c \
  "INSERT INTO comments VALUES (1, 'my first comment');"

# then immediately, on the PRIMARY and the STANDBY:
docker exec ddia-ch6-primary psql -U postgres -d study -c "SELECT count(*) FROM comments WHERE id=1;"
docker exec ddia-ch6-standby psql -U postgres -d study -c "SELECT count(*) FROM comments WHERE id=1;"
What does each node return?
PRIMARY SELECT count(*) WHERE id=1 -> 1 (your write is here) STANDBY SELECT count(*) WHERE id=1 -> 0 (your write is MISSING -- looks lost)

The leader has it; the follower returns 0. To a user who just posted the comment and gets routed to a follower for the read, it looks as though their submission vanished — the exact "understandably unhappy" moment the book describes.

Step 2 · measure the window

Predict. How long until the standby reflects the write? (We set the standby's apply delay to 5 s.)

# poll the standby until the row appears
until docker exec ddia-ch6-standby psql -U postgres -d study -tAc \
  "SELECT count(*) FROM comments WHERE id=1;" | grep -q 1; do sleep 0.2; done
docker exec ddia-ch6-standby psql -U postgres -d study -c "SELECT body FROM comments WHERE id=1;"
How long until the standby catches up?
--- after ~5s the standby catches up --- STANDBY SELECT body WHERE id=1 -> my first comment

~5 seconds. The write was never lost — it just hadn't been applied on the follower yet. Real replicas lag by microseconds to seconds under load; we made it 5 s so you can see the window, but the anomaly is the same at any lag > 0.

Step 3 · the fix: read your own writes from the leader

Predict. How do you guarantee a user always sees their own recent write?

Observe. Route reads that might depend on a user's own recent write to the leader (or to a follower known to be caught up past that write's position):

docker exec ddia-ch6-primary psql -U postgres -d study -c "SELECT body FROM comments WHERE id=1;"
#  ->  my first comment      (the leader always has it)

Read-your-writes consistency is exactly this: the write's author reads from a source guaranteed to include it. Everyone else can keep reading the cheap, slightly-stale followers.


What you should see

Why

Single-leader replication scales reads by letting many followers serve queries, but it keeps writes correct by funneling them through one leader. To keep the leader fast, replication is asynchronous: the leader appends the change to its write-ahead log, commits, and answers the client immediately, then streams the log to followers, which apply it a little later. That "a little later" is replication lag, and during it a follower's state is an earlier version of the database. A read routed there sees the past — including, briefly, a past in which your just-submitted write doesn't exist yet. Nothing is broken and nothing is lost; you simply read a replica before it caught up. We forced the gap to 5 s with recovery_min_apply_delay, but under normal load it's just small enough to slip through in real traffic and confuse exactly the user who wrote the data.

The fix follows from naming the guarantee you actually need. You don't need every follower to be current — that would throw away the whole benefit of async replication. You need the narrow promise that the author of a write sees their own write: read-your-writes consistency. Implement it by routing reads-after-writes to the leader, or to a follower you've confirmed has replayed past the write's log position (Postgres exposes the position as an LSN), or by having the client remember the timestamp/LSN of its last write and only reading replicas that have reached it. Everyone else keeps hitting the cheap stale followers, which is fine — they never wrote the data, so they can't tell it's a few milliseconds old.

The boundary — lag is the price of async scale-out, not a bug Asynchronous followers exist because you want cheap reads and a fast leader; the lag is the cost you accepted for that. So the answer is rarely "make it synchronous" (that reintroduces the latency and availability cost you were avoiding) but "pick the consistency you actually need and route accordingly." Read-your-writes is one such targeted guarantee; the next exercise (monotonic reads) is another, for a different anomaly the same lag produces when a user's reads bounce between replicas. Synchronous replication is a valid choice for data where any staleness is unacceptable — but then you're back to waiting for a follower on every commit.

Go deeper

  1. Read your writes by LSN. After the insert, capture pg_current_wal_lsn() on the primary; poll the standby's pg_last_wal_replay_lsn() until it passes that LSN, then read the standby. Predict whether you can get a fresh read from the follower this way (you can) — the without-hitting-the-leader version of the fix.
  2. Sync vs async latency. Add synchronous_standby_names so the primary waits for the standby to confirm each commit. Predict how much a single INSERT's latency grows — the cost you pay to erase the lag window.
  3. Two windows. Lower recovery_min_apply_delay to 500ms and re-run. The anomaly still happens, just harder to catch by hand — which is exactly why it slips into production unnoticed.

Sources: DDIA 2e, Ch. 6, §"Reading your own writes", §"Solutions for Replication Lag" · the PostgreSQL streaming replication docs (pg_stat_replication, recovery_min_apply_delay).