I4 · Part 2 · Intermediate
Consistency models
Every consistency model is just a rule about which stale reads you are willing to let a user see.
07On this page
- Consistency is a contract about what a read may return
- Replication lag is the concrete thing underneath all of it
- The two poles, and the useful middle
- Quorums: R + W N, with the numbers worked out
- Linearizability vs serializability — telling them apart for good
- Choosing per operation: model, cost, and what it looks like to a user
- Recognizing it in an unseen problem
Consistency is a contract about what a read may return
Once data exists in more than one place — a replica, a cache, a second region — "what is the current value?" stops having a single answer. A consistency model is the contract you offer the application about which of the possible answers a read is allowed to produce. Stronger models forbid more answers, which costs coordination, which costs latency and availability.
Interviewers push on this because it is the one topic where candidates reliably say something confidently wrong ("we'll use eventual consistency for speed") without noticing they just permitted a user to see their own comment disappear. The senior move is to pick a model per operation and justify it in product terms.
Replication lag is the concrete thing underneath all of it
"Eventual consistency" sounds abstract until you attach numbers to it. Lag is the wall-clock delay between a write committing on the primary and being visible on a given replica, and its distribution is wildly skewed — the median is boring and the tail is where your bugs live.
| Setup | Typical lag | Tail behaviour |
|---|---|---|
| Same-AZ async replica | 0.5-2 ms | Tens of ms under write bursts |
| Cross-AZ, same region | 2-10 ms | 100 ms+ during compaction or vacuum |
| Cross-region (us-east → eu-west) | Roughly 50-150 ms; the floor is the one-way delay of about 35-45 ms (half of a 70-90 ms round trip), plus apply time | Seconds if the link saturates |
| Any replica during a bulk write, index build, or long transaction | seconds to minutes | Single-threaded replay (classic MySQL) can fall hours behind and never catch up until write traffic drops |
| Read-through cache with 60 s TTL | up to 60 s | This is replication lag too. People forget caches are replicas |
The operationally important property: lag is not bounded. Any design that says "the replica is only a few milliseconds behind, so it's fine" is a design that breaks during the exact incident where correctness matters most. If you need a bound, you must enforce one — by reading the primary, by waiting for a version token, or by refusing to serve from a replica whose lag exceeds a threshold.
The two poles, and the useful middle
Strong consistency (informally: every read sees the latest committed write) requires the read to be coordinated with the write. The usual ways are to send the read through the leader after it has confirmed it is still the leader (Raft's ReadIndex, or a clock-based leader lease), or, in a leaderless register, to read a quorum and write the newest value back to a quorum before returning (the ABD algorithm). A plain "read R replicas and take the newest" is not enough, as the warning below shows. Cost: at least one extra round trip, and unavailability whenever the leader or a quorum is unreachable. Eventual consistency promises only that if writes stop, replicas converge. Cost: nothing; guarantee: nearly nothing. Any stale value is legal, in any order, on any read.
Between them sit the session guarantees — the models that actually ship in real products, because they fix the anomalies users can perceive without paying for global coordination.
| Model | Forbids | How it's implemented |
|---|---|---|
| Read-your-writes | You post a comment and it isn't there on refresh | Route a user's reads to the primary for N seconds after their write; or return a version token (Postgres LSN, MySQL GTID) with the write and have the read wait for a replica that has caught up to it |
| Monotonic reads | Refreshing shows a comment, then it vanishes, then it's back | Sticky routing: hash the user to one replica so they never move backwards in the replication stream |
| Consistent prefix | Seeing the reply "because he's late" before the question "why?" | Preserve write order per partition; don't shard causally-related rows across independently-replicated partitions |
| Monotonic writes | Your second write being applied before your first (set a name, then fix a typo, and the typo wins) | Apply one client's writes in the order issued: per-session sequence numbers, or route a session's writes through one leader or one partition log |
| Writes-follow-reads | Replying to a post and having the reply replicate somewhere the post has not reached yet | Carry the version you read (token or dependency) on the write, and apply the write only where that version is already present |
| Causal consistency | Any effect visible before its cause, across users | Track happens-before with vector clocks or dependency metadata and delay applying an update until its dependencies have landed |
Say it like this → "I'll serve reads from
replicas, but pin a user to the primary for about 500 ms after any write
of theirs — that buys read-your-writes for the person who cares, while
everyone else still gets the cheap replica read. If the lag distribution
makes a fixed window unreliable, I'll upgrade to returning the write's
LSN and having the replica wait for it."
⚠ Read-your-writes breaks on a second device, and on the client's own cache
Sticky-to-primary usually keys on the session or connection. Alice
updates her profile on her phone, opens her laptop, and the laptop's
session hits a lagging replica — the anomaly is back. If cross-device
read-your-writes matters, the sticky key has to be the user,
not the session, and the version token has to travel with the user
(stored server-side or in a cookie), not with the connection.
Quorums: R + W > N, with the numbers worked out
In a leaderless or quorum-replicated store (Dynamo, Cassandra, Riak) you
choose three numbers: N replicas hold each key, a write must be
acknowledged by W of them, a read must gather R of them. If
R + W > N, the read set and the write set must overlap in
at least one node by pigeonhole — so at least one responding replica has
the newest write, and version stamps let the reader pick it.
| N, W, R | Overlap? | Write tolerance | Read tolerance | Character |
|---|---|---|---|---|
| 3, 2, 2 | Yes (4 > 3) | 1 node down | 1 node down | The default. Balanced, survives one failure on both paths |
| 3, 3, 1 | Yes (4 > 3) | 0 — any node down blocks writes | 2 nodes down | Read-optimised: 1-replica reads are fast and local, writes are brittle |
| 3, 1, 3 | Yes (4 > 3) | 2 nodes down | 0 | Write-optimised: fast durable-ish writes, fragile reads |
| 3, 1, 1 | No (2 < 3) | 2 down | 2 down | Pure eventual consistency. Lowest latency, highest availability, a read can miss a write entirely |
| 5, 3, 3 | Yes (6 > 5) | 2 down | 2 down | Survives two failures on both paths; ~1.5x the write cost. Standard for critical data |
Latency follows directly: a W=2 write waits for the second-fastest of three replicas, so it inherits that node's p95, not the fastest node's. Raising W or R moves you further into the tail of the slowest responder — which is why quorum systems tune these per-query rather than globally.
Dry run: N=3, W=2, R=2 — and what breaks it
| Time | Operation | Nodes contacted | Resulting state | Read/write sets overlap? | Result |
|---|---|---|---|---|---|
| t0 | write v1 | acks from R1, R2 (W=2) | R1=v1, R2=v1, R3=v0 | — | Write succeeds |
| t1 | read | contacts R2, R3 (R=2) | sees v1 (R2), v0 (R3) | Yes — R2 is in both the write set {R1,R2} and the read set {R2,R3} | Returns v1 — correct |
| t2 | write v2 | acks from R1, R3 (W=2) | R1=v2, R2=v1, R3=v2 | — | Write succeeds |
| t3 | read | contacts R1, R2 (R=2) | sees v2 (R1), v1 (R2) | Yes — R1 is in both {R1,R3} and {R1,R2} | Returns v2 — correct |
| t4 | write v3, during a partition — R1 and R2 unreachable (a Riak or original-Dynamo style store) | sloppy quorum acks from R3 (real member) + R4 (hinted handoff, not in the preference list) | R3=v3; hint queued on R4 | The acking set {R3,R4} is no longer a subset of {R1,R2,R3} | Write "succeeds" (W=2 met) |
| t5 | read, partition healed, hint not yet delivered | contacts R1, R2 (R=2) | sees v2 (R1), v1 (R2) | No — write set was {R3,R4}, read set is {R1,R2}: disjoint | Returns v2 — v3 is silently missed |
The pigeonhole argument only holds while every acking node is a genuine member of the key's preference list. The moment a sloppy quorum lets an outsider count toward W, the read set and write set can miss each other entirely — which is exactly why R + W > N is a normal-operation guarantee, not a partition-tolerant one. Which systems do this? Riak and the original Dynamo paper use sloppy quorums by design. Cassandra does not: its hinted handoff lets a hint satisfy a write only at consistency level ANY, so at ONE, QUORUM or higher the acks must come from real replicas and the overlap argument holds.
⚠ R + W > N does not give you linearizability
It guarantees the newest committed value is in the read set. It
does not order concurrent writes (two writers at W=2 can each succeed on
a different pair and produce siblings), it does not make a failed write
disappear (a write that reached one node and then failed can still be
read later and repaired into existence), and sloppy quorums with
hinted handoff — used by Riak and the original Dynamo, and by Cassandra
only at CL=ANY — accept W acks from nodes that aren't even in the key's
preference list, which breaks the overlap argument outright. Even with
strict quorums, last-write-wins timestamps can pick the wrong winner under
clock skew. Quorums buy you strong-ish reads under normal operation, not
consensus. For real linearizability you need a consensus-backed log (Raft,
Paxos) with reads confirmed by the leader (log, ReadIndex or a lease), or
a read-repair-before-return protocol such as ABD. In Cassandra that means
lightweight transactions and SERIAL reads, not QUORUM plus QUORUM.
Linearizability vs serializability — telling them apart for good
They sound like synonyms, they come from different fields, and mixing them up is the fastest way to lose credibility on this topic.
- Linearizability is about single objects and real time. Every operation appears to take effect instantaneously at some point between its invocation and its response, and that point respects wall-clock order: if write W completes before read R begins, R must see W. It says nothing about multi-key transactions. It is a recency guarantee — the C in CAP.
- Serializability is about multi-object transactions and says nothing about real time. Concurrent transactions produce a result equivalent to some serial order — and that order need not match the order they actually happened in. A transaction that committed an hour ago may legally be ordered after one committing now. It is an isolation guarantee — the I in ACID.
- Strict serializability is both: a serial order that also respects real time. Spanner (via TrueTime) is the system that uses the term "external consistency" for it; FoundationDB documents strictly serializable transactions too. CockroachDB deliberately claims only serializability, because clock skew can produce a "causal reverse" anomaly between transactions that touch disjoint keys. Strict serializability is the strongest practical model.
The one-liner worth memorising: linearizability is about recency of a
single key; serializability is about the illusion of one-at-a-time
transactions. A database can be serializable and still let you read
stale data: a transaction run against a lagging follower snapshot, or one
serialised at an old timestamp, is a valid serial order that is not the
newest one. (Postgres sidesteps this: a hot standby refuses SERIALIZABLE
transactions outright.) And linearizable does not mean "no transactions":
etcd offers atomic multi-key transactions and documents strict
serializability for its default reads, while a single register with
compare-and-set is linearizable with no multi-object transactions at all.
ZooKeeper is the cautionary case: writes go through a leader in a total
order, but reads are served locally by whichever server you are connected
to and can be stale unless you call sync() first.
Between these sits snapshot isolation, which many databases ship
as the strongest level they call "serializable" or as the default
"repeatable read". It prevents dirty and non-repeatable reads, but still
allows write skew (two doctors each see the other on call and both
go off call), lost updates in some engines, and phantoms for range
checks. The fix is a true serializable level, or an explicit constraint,
SELECT ... FOR UPDATE, or a conditional write. Isolation
levels get their own chapter; the point here is that "transactional" does
not automatically mean "serializable".
Choosing per operation: model, cost, and what it looks like to a user
| Model | Guarantee | Cost | Real product example |
|---|---|---|---|
| Eventual | Replicas converge if writes stop | None — local read, local write, survives partitions | Like count, view count, follower count. Off by 3 for 200 ms; nobody can tell, and nobody is harmed. Rendering it as "1.2k" makes the staleness literally invisible |
| Monotonic reads | Never move backwards in time | Sticky routing — mild load imbalance, awkward on replica failover | An infinite feed. Items reappearing or vanishing while scrolling reads as a bug even when values are individually fine |
| Read-your-writes | You always see your own effects | Primary reads for a short window, or LSN-waiting on read | Posting a comment, editing a profile, uploading an avatar. The cheapest model that makes the product feel correct — and the default you should reach for on any write-then-read flow |
| Causal | Effects never precede their causes | Dependency metadata on every write; real complexity | Threaded comments and chat. A reply must not be visible before the message it replies to |
| Linearizable (single key) | Reads see the latest committed write, in real-time order | Leader round trip; unavailable during leader loss (CP) | Bank balance display, seat inventory, feature-flag kill switch. Being 200 ms stale is a wrong number on a screen someone will act on |
| Strict serializable / transactional | Multi-key transactions in a real-time-respecting serial order | Consensus per commit; cross-region commits cost an inter-region RTT | Username reservation, seat booking, money transfer. Two users claiming @ada concurrently must produce exactly one winner — this needs a uniqueness constraint or a compare-and-set, not a read-then-write |
The three-question test for any field in your design
- If a user sees a value 5 seconds old, what happens? Nothing → eventual. Confusing → session guarantees. Wrong decision or lost money → strong.
- Can two concurrent writes both "win"? If the answer must be no (username, seat, inventory), no amount of replication tuning helps — you need a single serialisation point: a uniqueness constraint, a conditional write, or consensus.
- Who notices the staleness — the writer or a third party? Only the writer → read-your-writes is enough and it's cheap. Everyone → you're in strong-consistency territory.
Say it like this → "I'd mix models rather than
pick one. Counters and feeds go eventual off replicas. Anything the user
just wrote gets read-your-writes via primary-pinning. Balance and
inventory read from the leader, and the actual decrement is a
conditional update inside a transaction, so two concurrent buyers can't
both take the last seat. That's three models in one system and each one
is justified by what breaks if I go weaker."
Recognizing it in an unseen problem
- Signals: the moment your diagram grows a read replica, a cache, or a second region, you have chosen a consistency model — the only question is whether you chose it deliberately. "Multi-region", "read replicas", "global users" are all consistency prompts in disguise.
- The naive design says "we'll use eventual consistency, it's more available" and then puts a write-then-read flow on top of it. Almost every user-visible consistency bug is a read-your-writes violation, not a deep causality problem.
- Distinguishing it from CAP hand-waving: CAP is a statement about behaviour during a network partition, which is rare. Replication lag is present every single second of normal operation. Talk about lag; mention CAP only if partitions are actually in scope.
- Uniqueness and inventory are not consistency-tunable. If two concurrent operations must not both succeed, no choice of R and W saves you — you need a single point of serialisation. Say that explicitly; it's a common trap.
- Quantify. "Cross-region lag is ~100 ms, so a European read of a US write can be stale for a tenth of a second — fine for a like count, not for a payment confirmation" is worth more than three paragraphs of theory.
- The pitfall to avoid: using "strongly consistent" and "serializable" interchangeably. If you only remember one thing: linearizability = recency of one key; serializability = transactions appear one-at-a-time.
Before you move on
- Explain, using the pigeonhole argument, why R + W > N guarantees a reader sees the newest write during normal operation.
- Trace what happens to that guarantee when a sloppy quorum with hinted handoff is in play during a partition, as in the dry run above.
- State the difference between linearizability and serializability in one sentence each.
- Pick a consistency model per operation for a concrete feature (a like count versus a username reservation, say) and justify each choice.
- Explain why "we'll use eventual consistency" is an incomplete answer without also naming which specific operations need read-your-writes or strong consistency.
Sources and further reading
- etcd API guarantees — atomic multi-key transactions, strict serializability, and the opt-in stale serializable reads
- ZooKeeper programmer's guide — the stated guarantees, why reads can be stale, and when to call sync
- Cassandra architecture: Dynamo — hinted handoff, and that a stored hint satisfies only consistency level ANY
- PostgreSQL hot standby — SERIALIZABLE is not available on a standby
- CockroachDB: living without atomic clocks — why it claims serializability, not external consistency, and the causal reverse anomaly
- Spanner paper (OSDI 2012) — external consistency, TrueTime and commit wait
- Spanner timestamp bounds — strong versus stale reads, and when stale reads can still block
- Jepsen: strong serializability — the formal definition, and why it cannot stay available through a partition
- FoundationDB transaction manifesto — strictly serializable transactions as a design goal
Claims in this chapter were checked against these sources in October 2026. Figures marked "planning figure" are rules of thumb, not guarantees.
5 questions drawn from 8, scored out of 5. This is practice: it runs in your browser, saves nothing and does not change what you have marked read.
Finished reading?
0 of 36 chapters read in this series