The question this answers
A user saves a change, the page reloads, and the change is gone. Nothing failed — so what guarantee was missing?
Read-your-writes: within a single session, any read issued after a write by that same session observes that write, or a later state. It says nothing about writes made by other sessions, and nothing about whether two reads in the session move forward in time together — that is Monotonic Reads: Never Let Time Run Backwards.
Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.
A follower knows its own applied position. If the client presents a position it requires — "I need at least 4,912,338" — the follower can *verify* whether it satisfies the request rather than guess. This is the rare case in this domain where a node can answer a freshness question soundly, and it is why position-based routing beats timing heuristics.
A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.
The bug, and why it never appears in the error rate
A user edits their profile. The write goes to the leader and is acknowledged. The page reloads, the read is load-balanced to a follower that is 300ms behind, and the profile shows the old value. The user tries again. Sometimes it works. Support cannot reproduce it. Every component behaved exactly as designed, which is why no monitor fires and why this bug survives for months.
The guarantee that was missing has a name, and naming it is most of the fix: the system offered eventual consistency where the interface implicitly promised read-your-writes. Once stated that way, the solutions are obvious and cheap — the difficulty was never technical, it was that nobody had written down what the read path promised.
Four ways to get it, in increasing order of honesty
These are not equivalent. The first two are heuristics that work until they do not; the last two are actual guarantees, and the difference is whether the follower can *verify* the requirement or merely hope.
The token approach is the one worth internalising, because it generalises: the client carries a small piece of causal knowledge — "I have seen up to here" — and every replica can check it locally. That idea reappears as vector clocks, as consistent-prefix reads, and as the causal metadata in Causal Consistency: Never Show an Effect Before Its Cause.
| Mechanism | How it works | Where it fails |
|---|---|---|
| Read from the leader after writingtypical | Route reads to the leader for N seconds after a write by this session | Leader load; and "N seconds" is a guess that lag can exceed |
| Sticky sessions to one replicatypical | Pin a session to the replica that served it | That replica may not be the one that got the write; rebalancing breaks the pin |
| Write-position tokenprotocol | Leader returns its log position; client sends it with reads; a replica behind that position redirects or waits | Requires the position to survive in the client or session store; adds a wait when replicas lag |
| Read your own writes from a local cachetypical | The client displays what it wrote, independent of the read path | Only fixes the writer's own view, and diverges if the write is later rejected or transformed server-side |
Getting "the same session" right is harder than it sounds
The guarantee is scoped to a session, so the scope has to be defined, and the obvious definitions leak. A user on a phone and a laptop is two sessions but one person, and they will absolutely notice if the laptop does not show what the phone just saved. A logged-out user identified by a cookie loses the session on cookie clear. Server-side, a session that spans multiple services needs the position token to be propagated through every hop, exactly like a trace context or a deadline. See Pass the Remaining Budget Down, Not a Fresh One for the same plumbing problem.
The pragmatic answer is usually to scope the guarantee to the *user* rather than the connection, storing the last-written position against the user identity in a fast shared store. That is a small piece of coordination — but a much smaller one than making every read linearizable, which is the alternative people reach for when they cannot name what they actually need.
1// After any write, the leader tells us where the write landed.2const { position } = await leader.write(userId, patch)3await sessionStore.setMinPosition(userId, position) // survives device + process4 5// On read, the requirement travels with the request.6async function readProfile(userId: string) {7 const required = await sessionStore.getMinPosition(userId)8 const replica = pickReplica()9 10 // The replica KNOWS its own applied position — this is not an inference.11 if (replica.appliedPosition() >= required) return replica.read(userId)12 13 // Otherwise: wait briefly, or fall back to the leader. Both are honest;14 // silently serving the stale value is the only wrong answer.15 return replica.waitFor(required, { timeoutMs: 50 })16 .then(() => replica.read(userId))17 .catch(() => leader.read(userId))18}Key points
- Read-your-writes is a per-session guarantee: your own writes are visible to you, and nothing is claimed about anyone else's.
- The bug it fixes produces zero errors, which is why it survives in production for months.
- Position tokens are the honest mechanism — a replica can verify "am I past position P" locally; timing heuristics only hope.
- It is dramatically cheaper than linearizable reads and fixes the majority of user-visible staleness complaints.
- Defining "the session" is the real work: scope it to the user, not the connection, or a second device breaks it.
The chain, answered
Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.
- • The write is accepted at the leader, which returns a monotonic position identifying where it landed in the log.
- • The client or session store retains that position as the session's freshness floor.
- • Every subsequent read for that session carries the floor.
- • A replica compares the floor against its own applied position; if it is behind, it waits, redirects to the leader, or returns an explicit "too stale" rather than a stale value.
- • The floor advances on each write and can be aged out once the whole fleet is provably past it.
- • The position token is lost — cookie cleared, new device, session store eviction — and the guarantee silently reverts to eventual consistency.
- • A replica is behind the floor and no fallback is defined, so the read either blocks or serves stale data.
- • The token is not propagated across a service hop, so an internal call reads from a replica without the requirement.
- • Reads after a failover reference a position from a log the new leader does not share.
- • The write is acknowledged and later rolled back, leaving a floor that references a position that no longer means what the client thinks.
- • "My change did not save": user-visible stale reads immediately after a write, with a zero error rate and a support ticket that engineering cannot reproduce because their own replica is fast.
- • Leader overload after a naive fix: routing all post-write reads to the leader for 10 seconds moves a large read fraction onto the one node you were trying to protect, and latency degrades globally.
- • Cross-device inconsistency: the user's phone shows the new value and their laptop shows the old one, because the guarantee was scoped to a connection rather than to the user.
- • Post-failover token mismatch: reads block or error because the session floor references a log position the promoted leader never had, appearing as a burst of timeouts confined to recently-active users.
- • Silent degradation: the token store is unavailable, the read path falls back to "no requirement", and the guarantee disappears without any signal.
- • Very little: the read path needs a per-session position, not agreement between replicas. No node has to talk to another to satisfy it.
- • The session store is a small shared dependency, and its availability becomes part of the guarantee — plan for what happens when it is down (fall back to the leader, not to stale reads).
- • Compare with linearizable reads, which require the reader to confirm leadership or read a quorum on every request. Read-your-writes gets most of the perceived benefit for a tiny fraction of the cost. See Coordination Couples Availability.
- • If replicas lag, reads for recently-writing sessions get slower or move to the leader; correctness is preserved and latency degrades — usually the right direction.
- • If the session store fails, the guarantee is lost but no incorrect data is produced *if* the fallback is the leader.
- • Sessions that have not written recently are unaffected, so the blast radius is limited to active writers.
- • Detect: measure the rate of reads that had to wait or redirect because a replica was behind the floor — this is the direct signal of the guarantee doing work.
- • Contain: cap the wait, and prefer redirecting to the leader over blocking, so replica lag becomes leader load rather than user-visible latency.
- • Recover: as replicas catch up, redirects fall automatically; there is no separate recovery step.
- • Reconcile: after a failover, invalidate session floors that reference the old leader's log rather than letting them block forever.
- • Verify: an end-to-end test that writes then immediately reads through the public path, run continuously against production, catches this class of regression when nothing else does.
- • Rate and latency of reads that waited for a replica to catch up, and the rate that redirected to the leader.
- • Fraction of reads carrying a freshness floor at all — a drop means the token is being lost somewhere.
- • Leader read load attributable to post-write routing, so the fix does not quietly recreate a single-node bottleneck.
- • A synthetic write-then-read probe measuring how often the read observes the write on first attempt.
- • Support ticket volume tagged "did not save", which is the metric the business actually feels.
- • Any interface where a user writes and then immediately sees a view of what they wrote — which is nearly every form, ever.
- • Systems that have moved reads to replicas and are now seeing unreproducible "it did not save" reports.
- • Cases where full linearizability is too expensive but the user-visible symptom must go away.
- • Reads that are not tied to a session at all — analytics, exports, public feeds — where the token machinery is pure overhead.
- • Systems where all reads already go to the leader; you would be adding plumbing for a guarantee you already have for free.
- • Cases where the requirement is actually stronger — one user must see another user's write — since read-your-writes does not provide it and will not fix the complaint.
- • Route all reads to the leader. Simple, correct, and the right answer until leader read load actually becomes a problem.
- • Render the write optimistically on the client and skip the re-read entirely — no distributed guarantee needed for the common case.
- • Full linearizable reads if the requirement is genuinely cross-session recency. See Linearizability: An Operation Is an Interval, Not a Point.
- • Bounded-staleness reads, where the replica refuses to answer if it is more than X behind — coarser than a per-session floor but far simpler to operate.
- • Cache the write result in the session and serve it for the immediate follow-up read, accepting that it only covers the writer.
The user saved it, reloaded, and it was gone
t=0 PUT /doc → leader 200 OK (position 1) t=2 GET /doc → near 200 OK, the change is there
| node | applied position | would serve | satisfies “≥ 1”? |
|---|---|---|---|
| leader | 1 | new | yes — verified locally |
| near← this read | 1 | new | yes — verified locally |
| far | 0 | old | no |
What people believe, and what is true
Read-your-writes means the data is consistent.
It means *your* writes are visible to *you*. Another user's write made a moment ago may still be invisible, and two of your own reads may still go backwards without Monotonic Reads: Never Let Time Run Backwards.
Sticky sessions solve it.
Stickiness pins you to a replica, not to a replica that has your write. If the write went to the leader and your sticky replica is behind, you still read stale.
This requires strong consistency.
It is one of the cheapest guarantees available — a per-session position and a comparison. Reaching for linearizability here is paying global coordination for a local problem.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
Whatever else a reader misses, they should see their own writes. That is a per-session promise, and it is cheap.
Practical
Have the write return a log position, store it against the user (not the connection), send it with reads, and let replicas either satisfy it or hand off to the leader. Never let a replica serve a value it knows is behind the requirement.
Advanced
The token is a minimal causal context: it encodes exactly the part of the happened-before relation this session depends on. Generalise it from one position to a vector and you have causal consistency; generalise the scope from one session to all sessions and you are paying for linearizability. Seeing read-your-writes as the smallest useful projection of causality is what makes Session Guarantees: The Underrated Middle Ground and Causal Consistency: Never Show an Effect Before Its Cause feel like one idea rather than three. See Happens-Before: The Only Ordering You Actually Have and Vector Clocks: Buying Concurrency Detection at O(N).
Apply it
- 💬 Users report that their profile edit sometimes does not stick, but your error rate is zero. Diagnose it and give three fixes ranked by cost.
- 💬 Why is "read from the leader for five seconds after a write" not a guarantee?
- 💬 The user writes on their phone and reads on their laptop. Does your read-your-writes implementation cover that? What has to change if not?