Bonus I
Replication Lag and Eventual Consistency in Practice
Your write succeeded. Your user disagrees.
Every engineering team eventually receives this ticket: "I changed my profile name, but I still see the old one." The developer checks the database — the new value is right there. They refresh — it looks fine now. Ticket closed: works for me.
But nothing was fixed, because nothing was broken. The user simply caught the system in a window we've been circling for three articles now: Part 6 called it stale cache data, Bonus F called it the latency-vs-consistency trade, Part 7 called it eventual consistency. This article is about the place where that window becomes visible: replication lag.
Why Replicas Exist at All
Blog 5 introduced replication: keeping copies of your database on multiple servers, for two good reasons — survival (if the primary dies, a replica takes over) and read scaling (most systems read far more than they write, and replicas absorb that read traffic).
Writes
↓
PRIMARY ← the source of truth (Blog 5)
↙ ↘
REPLICA A REPLICA B ← derived copies
↑ ↑
Reads Reads
One rule from Blog 5 still governs everything: the primary is the source of truth. Everything else is a derived copy. And derived copies have a property we already know intimately from caching — they can be behind.
The Simple Mental Model: The Original and the Photocopy
Keep this picture for the rest of your career. The primary database is the original document, kept in the records room. Replicas are photocopies on people's desks. Photocopies are wonderful — fifty people can read them at once without crowding around the original. But the moment someone edits the original, every photocopy in the building is temporarily outdated, until a fresh copy reaches each desk.
Now widen the lens, because your system has more photocopies than you think:
| The photocopy | Fresh copy arrives via |
|---|---|
| Read replicas | Replication stream |
| Caches (Part 6) | TTL expiry or invalidation |
| Search indexes | Async indexing pipeline |
| Analytics projections | Batch or stream jobs |
| Async read models | Event consumers (Part 7) |
Different desks. Different delivery schedules. Same fundamental situation: one original, many copies, each catching up at its own speed.
(And if two desks both edit their copies? That's a conflict — a different beast, waiting for us in Part 19: Collaboration Tools.)
Why the Copies Fall Behind
Replication lag is the gap between the original changing and a copy reflecting it. Where does the gap come from? The copy must replay changes — writes arrive at the replica and are re-applied, usually in order, so a burst of writes creates a backlog. The network is in the middle, especially across regions, where physics charges rent. The replica has a day job — it's busy serving reads while trying to catch up. And big transactions clog the pipe — one bulk update can stall everything behind it.
Under light load, lag might be milliseconds. Under heavy write bursts: seconds, sometimes worse.
Synchronous vs Asynchronous Replication
The deepest design choice hides here, and you've already met it wearing different clothes.
| Synchronous | Asynchronous | |
|---|---|---|
| Write is "done" when… | Replica(s) confirm too | Primary confirms alone |
| Write latency | Higher — wait for the slowest confirmer | Low |
| Lag window on that replica | None | Real, and variable |
| If a replica is slow or down | Writes suffer or stall | Writes don't care |
| Bonus F label | Consistency (EC) | Latency (EL) |
Recognize it? Sync vs. async replication is the ELC dial from Bonus F, installed at the storage layer. Bonus F warned that async read replicas are "the most common accidental EL choice." This article is what that accident looks like up close.
The Classic Stale Read Problem
Let's watch the ticket from the introduction actually happen. A user updates their profile name from "Amit" to "Amit Singh."
t = 0.0s Write "Amit Singh" → PRIMARY ✓
App returns: "Profile updated!"
t = 0.3s User refreshes the page
Read → REPLICA B (currently 2 seconds behind)
Replica B still holds: "Amit"
t = 2.0s Replica B catches up → "Amit Singh"
The user, at t = 0.3s, sees "Amit" — three hundred milliseconds after being told the update succeeded.
Now the four sentences that untangle every incident of this shape: the write path succeeded, the primary has the new name. The read path was stale — it landed on a copy that hadn't caught up. The database was not "wrong" — every component did exactly its job. The architecture allowed a consistency window, and nobody had decided whether this read was allowed to fall inside it.
The bug isn't in the database. The bug is in the assumption: "the write succeeded, so every read is fresh." That assumption is false in any system with more than one copy of the data — which is to say, every system you will ever design at scale.
Read-Your-Writes: The Consistency Users Expect
Here's the human truth underneath the technical one: users happily tolerate other people's changes arriving late. They do not tolerate their own changes disappearing. "I just typed it. Where did it go?"
Even a system that is comfortably eventually consistent globally usually needs one specific guarantee locally — read-your-writes: a user who just made a change must see that change.
The practical toolbox, roughly from bluntest to sharpest: read from the primary right after a write (simplest, costs primary load if overused); sticky routing (pin this user's reads to the primary, or one up-to-date replica, for a short window after their write); session-based consistency (the session remembers "this user wrote at time T," and serves them fresh data until the copies pass T); version or timestamp checks (the client carries the version it wrote, and a replica may answer only if it has caught up to that version, otherwise escalate to primary); lag-aware routing (never route to replicas more than X seconds behind); and honest UI (when immediate consistency genuinely isn't possible, say so — "Updating…" beats a silent lie).
One sharp edge worth knowing: sticky sessions live on one device. The user who edits on their phone and checks on their laptop just left your sticky session behind. Cross-device read-your-writes needs version tokens or primary reads, not routing tricks.
Monotonic Reads: Never Go Backward
There's a failure worse than stale. A user tracks their order:
Refresh 1 → "Order Shipped" (served by an up-to-date replica)
Refresh 2 → "Order Packed" (served by a laggier replica)
Time just ran backward. Stale is forgivable — the user assumes the system is "catching up." Backward is alarming — the user assumes the system is broken, and quietly stops trusting every status you show them, everywhere, forever.
The guarantee they expect has a name: monotonic reads — once a user has seen a state, never show them an older one.
The practical approaches: pin a user's session to one replica for a short period (one desk's photocopy may be behind, but it never regresses); track the last-seen version (the client remembers "I've seen version 42"; the system never serves it version 41); never serve older than observed (same rule, enforced server-side per session); and route critical state transitions to the primary (order status, payment state, anything users refresh anxiously).
Notice both of these guarantees — read-your-writes and monotonic reads — are session-sized. You're not making the whole system strongly consistent. You're making one user's experience coherent while the copies catch up around them. That's usually all the consistency the moment actually requires.
Where Lag Shows Up Outside Databases
Here's the widening realization: eventual consistency is not a database topic. It appears anywhere data is copied and moved — which, in a modern system, is everywhere. Cache lag is Part 6's stale data, where the TTL is the lag window you signed up for. CDN lag is a purge taking time to propagate across edge locations worldwide. Search index lag means a seller creates a listing and for a few seconds search can't find it — not a bug, an async indexing pipeline doing exactly what it was built to do. Analytics and dashboard lag means the numbers are minutes or hours behind, by design. Reporting lag means yesterday's batch job answers today's question. Event-driven projection lag is read models built from Part 7's events — their lag is literally consumer lag, the vital sign we already monitor. And notification lag is the "order delivered" push arriving after you've opened the door.
Same photocopy story, different desks. Part 7's consumer lag is replication lag, wearing event-driven clothes.
When Lag Is Acceptable vs Dangerous
Bonus F gave us the deciding question: is this a money, safety, trust, or convenience problem? Apply it:
| Comfortable with lag | Dangerous with lag |
|---|---|
| Analytics dashboards | Payment confirmation |
| Recommendations | Wallet balance before spending |
| Public catalog browsing | Inventory at final booking |
| Search indexing (seconds) | Account permissions and access control |
| Notification delivery (seconds) | Compliance / audit-critical writes |
| Follower counts, like counts | Medical or safety-critical data |
Two rows deserve a highlight. Permissions: an employee is removed from a project, but a lagging permission copy keeps their access alive for another minute. That's not a stale read — that's a security hole with a timestamp. Final booking: Part 6 said never cache the confirmation check. The replication twin: never read the confirmation from a replica. Browsing can lag; confirming cannot.
The pattern, one more time: the business impact decides the consistency requirement, never the other way around.
Designing Around Replication Lag
You can't eliminate lag. You can decide where it's allowed to live.
Decide. Define allowed staleness per data type — the same per-data discipline as Part 6's TTLs and Bonus F's table. Document consistency expectations in the design: one line per dataset, "Order status: ≤5s, monotonic per session, transitions read primary."
Route. Critical reads go to the primary — confirmations, balances, permissions. Safe, read-heavy traffic goes to replicas — browsing, catalogs, feeds, this is what replicas are for. Lag-aware routing pulls a replica out of rotation once it's beyond its threshold.
Contain. Use cache TTLs with lag in mind — a 60s TTL on top of a 5s-lagging replica is up to 65 seconds of staleness; copies stack, so does their lag. Version numbers or timestamps on records are the backbone of read-your-writes and monotonic guarantees. Idempotent event consumers (Part 7) ensure projections that may replay don't double-apply.
Reveal. Show "processing…" states where consistency is genuinely delayed — honesty is a design tool. Monitor lag: most databases expose a "seconds behind primary" style metric, Part 7's consumer lag covers your projections, index freshness covers search. Alert on thresholds — lag you don't watch is lag your users discover for you.
And notice this is just C.R.E.D again: Clarify which reads follow a user's own write. Requirements — allowed staleness, per data type. Estimate the lag windows under real write load. Design the routing, versioning, and monitoring to match.
A good architect does not ask only, "Is the write successful?" A good architect asks, "Where will the next read come from, and how far behind can that place be?"
Common Mistakes Engineers Make with Replication Lag
Assuming a successful write means every read is fresh — the founding myth of this entire bug family. Blindly reading from replicas after writes — the Amit problem, shipped to production as a default connection setting. Using replicas for correctness-critical reads — balances, permissions, confirmations should be source-of-truth only. Ignoring read-your-writes — users forgive global lag; they don't forgive their own edits vanishing. Ignoring monotonic reads — once users see time move backward on one screen, they distrust every screen you own. Treating search index lag as a bug — it's a design property with a window; know the window, state the window. Hiding delayed consistency from the UI — a silent stale screen reads as broken, an honest "updating…" reads as working. Not monitoring lag — if you can't chart it, your users are your monitoring. And stacking caches on replicas without doing the math — TTL plus replica lag is one staleness budget, not two separate ones.
From My Journey: An Architectural Lesson
In many systems I've watched evolve, the sequence repeats almost word for word. The system grows, reads outnumber writes, and the team does the sensible things: add read replicas, add caches, add a search index, maybe async read models for the heavy screens. Performance improves. Everyone is happy.
Then the tickets start. "I changed it, but I still see the old value." Intermittent. Unreproducible. Closed and reopened. The instinct is to hunt for the failure — which write was lost, which service misbehaved.
And the realization, when it lands, is oddly calming: nothing failed. The system now holds multiple truth-like copies of the same data, and each copy catches up at its own speed. The write went to one place; the read came from another; the gap between them was always part of the design — just never written down.
That's the trade in its plainest form. Replicas and projections buy scale and latency. They cost a consistency window. Some data can live in that window comfortably. Some data cannot — and deciding which is which is the architecture.
Eventual consistency is not a vague theory. It is the visible delay between the source of truth changing and every useful copy catching up. The write path tells you whether the change was accepted. The read path tells you whether the user can see it yet.
Key Takeaways
- Replicas, caches, indexes, and projections are photocopies of one original — each catching up on its own schedule.
- Replication lag is where eventual consistency stops being theory and becomes a user-visible window.
- Async replication buys write latency and availability at the price of that window — the ELC dial at the storage layer.
- A successful write guarantees the original changed — it says nothing about where the next read lands.
- Read-your-writes and monotonic reads are session-sized guarantees: make one user's experience coherent, not the whole system strongly consistent.
- Lag is acceptable for browsing, analytics, and recommendations — dangerous for money, permissions, and confirmations.
- Staleness stacks: cache TTL on top of replica lag is one combined budget.
- Define allowed staleness per data type, route critical reads to the primary, and monitor lag before users report it.
Interview Lens
The classic probe: "You added read replicas. A user updates their profile and immediately sees the old name. What happened, and how do you fix it?" This is a favorite because it checks whether you've operated replicas or just drawn them.
The strong answer names the window and the routing rule: "The read hit a lagging replica — a read-your-writes violation. Default reads go to replicas, but for N seconds after a user's own write, their reads are pinned to the primary, or gated on a version token so a replica may answer only once it's caught up. Critical state like payments always reads primary."
Expect follow-ups: "How do you monitor it?" — replica lag metric, alert thresholds, plus consumer lag for async projections. "What about across devices?" — sticky sessions don't travel; version tokens or primary reads do. "What if the replica never catches up?" — lag-aware routing pulls it from rotation; that's a capacity signal, not a consistency one.
The differentiator: name the guarantees — read-your-writes and monotonic reads. Candidates who use the vocabulary signal they know these are known, solved patterns, not novel bugs.
Real-World Engineering Lens
Find out where your reads actually go. It's usually one line in a database client config, decided years ago, felt on every request since. Audit it this week.
List the reads that happen right after a user's own write — profile pages, settings screens, "post comment then reload." Those are your read-your-writes surface. Route or gate them deliberately.
Put "seconds behind primary" on the main dashboard, next to CPU. Alert below the threshold where users would notice.
Do the compounded staleness math. Walk one piece of data through every copy it lives in — replica, cache, CDN, index — and add up the worst case. That number should be a decision, not a surprise.
Rehearse it in staging. Introduce artificial replica lag and click through your critical flows. Whatever looks broken is your design backlog.
What's Next
Every copy in this article — replicas, indexes, projections, caches — is ultimately the same thing: bytes, living somewhere, being shipped somewhere else.
Which raises a question we've been postponing since the series began: where exactly do all these bytes live? And why does it matter whether they live in object storage, block storage, or a file system?
Blog 8 — Storage Systems. The main series resumes with the layer everything else in this course quietly stands on: object, block, file, and distributed storage.
Question for Readers
Right now, without checking, do you know whether your application's reads go to the primary or to replicas by default?
Go look. It's probably one config line. Then ask the sharper question: which of those reads happen immediately after a user's own write?
That list is your read-your-writes surface — and most teams have never written it down.