What this module covers: Module P-10 established that under partition you must choose, and that even without partition you trade latency against consistency. This module is the menu you're choosing from. It builds the models as a hierarchy — linearizable, sequential, causal, the session guarantees, eventual — and anchors each to the bug report it produces rather than to its formal definition. Then the quorum arithmetic: why R + W > N gives overlap, worked for N=3, and the four places it stops helping. Finally the dials you actually turn in production — Cassandra's consistency levels, DynamoDB's strong versus eventual reads — and why session guarantees are what most products genuinely need.
"Eventual Consistency" Is a Promise About the Future, Not About Now
Module P-5 left you with a set of symptoms rather than a theory. A user changes their display name and the next page render shows the old one. A support agent marks a ticket resolved, refreshes, and it's open again. An order confirmation page says "you have no orders". The diagnosis in each case was replication lag, and the remedies were ad hoc: route that read to the primary, add a delay, cache the value client-side.
Those remedies work, but they're guesses, because "replication lag" names a cause rather than a contract. What you actually need is the ability to say: this read is allowed to be stale, this one is not, and this one may be stale but must never go backwards. That sentence is a consistency model. A consistency model is a contract between the storage system and the application about which orderings of operations an observer is permitted to see.
The critical framing: models are defined by what they forbid. Eventual consistency forbids almost nothing — it promises only that if writes stop, replicas converge. It says nothing about how long, nothing about what you see in the meantime, and nothing about whether two observers see the same thing. Every stronger model is eventual consistency plus a specific prohibition, and every prohibition costs coordination, which costs latency or availability or both.
Analogy: consistency models are the promises a postal service makes. Eventual consistency promises only that every postcard arrives eventually, in whatever order the sorting office produces. Causal consistency promises that if your reply quotes my postcard, mine is delivered first — the sorting office reads the quotes and orders accordingly. Linearizability promises there is exactly one mailbox in the world, everyone queues at it in real time, and nobody may write until the person ahead of them has finished. The single mailbox is the fastest to reason about and the one with the longest queue.
The Hierarchy, and What Each Model Forbids
Read this table as a ladder. Each row down permits something the row above forbids, and in exchange stops requiring some coordination.
Model
Forbids
Permits
What it costs
Linearizable
Any read returning a value older than a write that has already completed. All observers see one global order consistent with real time.
Nothing about staleness — there is no staleness.
Consensus or a single leader on the read path. Every operation pays at least one round trip to a majority; cross-region that is Mumbai↔Virginia RTT ~180 ms. Unavailable under partition (Module P-10).
Sequential
Replicas applying operations in different orders. Everyone agrees on one total order.
That order need not match real time — your read may be arbitrarily stale, as long as it's stale in an order everyone agrees on.
A total order still needs agreement on ordering, so most of linearizability's coordination without its recency benefit. Rarely a deliberate production choice.
Causal
Seeing an effect before its cause. If write B was influenced by write A, no observer sees B without A.
Concurrent writes — writes with no causal relationship — appearing in different orders to different observers.
Tracking causal dependencies: vector clocks or version vectors on every value, which grow with the number of writers. Concurrent writes surface as siblings the application must resolve.
Read-your-writes
A client failing to observe its own completed write.
That client seeing nothing of other clients' writes. Other clients seeing nothing of this one.
Per-session routing or a write position token carried with the session. Loses free load balancing across replicas.
Monotonic reads
Reads going backwards. Once you've seen a value, you never see an older one.
Never seeing the newest value at all. Arbitrary staleness, as long as it's non-decreasing.
Pinning a session to a replica, or tracking the highest position observed. A replica failure resets the guarantee.
Monotonic writes
A client's own writes being applied out of the order it issued them.
Other clients' writes interleaving arbitrarily.
Serialising a session's writes, which removes the option of firing independent writes concurrently.
Eventual
Nothing about ordering. Only: if writes stop, replicas converge.
Everything above. Reads that jump forwards and backwards, effects before causes, your own write invisible to you.
Nothing at coordination time. You pay in application complexity and user-visible weirdness.
Three things this table hides that matter:
The hierarchy is not a single line. Linearizable implies sequential implies causal, and causal implies all four session guarantees. But the session guarantees are incomparable with each other — read-your-writes does not imply monotonic reads, and a system can provide monotonic reads without read-your-writes. That's why they're usually specified as a set.
There's a fourth session guarantee the industry names less often: writes follow reads (sometimes "session causality"). It forbids your write being ordered before a write you read prior to making it. The concrete case: you read a comment, reply to it, and your reply must not be ordered before the comment you were replying to. This is causal consistency scoped to one session.
Causal is the strongest model that stays available under partition. This is the useful practical result sitting on top of Module P-10: if you insist on remaining available to every client during a network partition, causal consistency is the ceiling. Anything stronger requires refusing service to the minority side. That makes causal the natural target for a system that has genuinely chosen availability, rather than treating "eventual" as the only alternative to "strong".
A distinction people conflate constantly: linearizability is about a single object over time. Serializability (Module P-1) is about multiple objects within a transaction. They are orthogonal. Snapshot isolation gives you a consistent view across many rows and is deliberately not linearizable — a snapshot read is stale by construction. When someone says "we need strong consistency", the first question is whether they mean recency on one key or atomicity across several, because the mechanisms are different and so are the bills.
Every Violation Has a Bug Report
Formal definitions do not survive contact with a standup. What survives is the symptom. Learn the models through the tickets they generate, because that's the direction you'll be reasoning in — from a screenshot to a mechanism.
The bug report
Model violated
The mechanism underneath
"A reply appears above the comment it's replying to."
Causal
Comment and reply were written to different partitions or regions and replicated independently. The reply's replication finished first.
"I changed my profile photo, the page reloaded, and it's the old one."
Read-your-writes
The write went to the primary, the follow-up read went to a replica that hasn't replayed it (Module P-5).
"A colleague's new photo showed up, then reverted, then came back."
Monotonic reads
Two reads load-balanced to two replicas with different lag. The second replica was further behind than the first.
"I deleted the tag then renamed the item, and now the tag is back on the renamed item."
Monotonic writes
The two writes took different paths and were applied in the opposite order.
"The counter went 41, 43, 42, 44."
Monotonic reads
Same as above, on a value that changes fast enough for the reordering to be obvious.
"The transfer debited one account and hasn't credited the other yet."
Not a consistency violation — an atomicity one
This is Module A-3's problem (2PC, saga, outbox), not a replication problem. Reaching for a stronger consistency level will not fix it.
"Two users both claimed the last ticket."
Linearizability, specifically read-modify-write
A quorum read followed by a quorum write is not atomic. This needs compare-and-set, which needs consensus (Module A-1) or a lock (Module A-2).
That last row is the one worth internalising. No consistency level makes read-then-write safe. You can read at the strongest level available and write at the strongest level available and still lose an update, because the two operations are separate and something can happen between them. Uniqueness, "claim the last one", and balance checks are a different class of problem requiring a different mechanism.
Why this matters in production: the most expensive debugging sessions in distributed systems come from treating a consistency-model violation as a bug in the code that produced the symptom. The reply-above-the-comment ticket gets assigned to whoever owns the comments UI, who adds a client-side sort by timestamp, which papers over it until clock skew (Module A-1) makes the timestamps lie too. Naming the violated model tells you which layer to fix and which team owns it.
R + W > N: The Arithmetic, and What It Actually Buys
Quorum replication is the standard way to get a tunable guarantee out of a leaderless system. You have N replicas per key. A write must be acknowledged by W of them before the client is told it succeeded. A read must collect responses from R of them and returns the value with the highest version.
The claim is that if R + W > N, the read set and the write set must overlap by at least one replica, so the read is guaranteed to see the latest completed write. The arithmetic is pigeonhole, not magic:
text
Three replicas, two of them hold the write, you ask two of them — you cannot pick two replicas out of three and miss both members of a two-member set. The read gets at least one copy of the newest version, and because reads return the highest version seen, that's the one you get.
Worked for N = 3:
R
W
R + W
Overlap
Read tolerates
Write tolerates
What you're buying, and what it costs
1
1
2
none
2 down
2 down
Lowest latency both ways, maximum availability. Reads can miss the newest write entirely — this is plain eventual consistency.
1
3
4
1
2 down
0 down
Fast reads, single-replica read latency. Any one replica unavailable makes the key unwritable. Write latency is the slowest of three.
2
2
4
1
1 down
1 down
The usual default. Both paths survive one failure; both pay the second-fastest replica's latency.
3
1
4
1
0 down
2 down
Fast writes, useful for write-heavy ingestion. Any one replica unavailable makes the key unreadable — usually the wrong trade, because reads outnumber writes.
2
1
3
none
1 down
2 down
Cheap, and gives you no overlap guarantee at all. A common misconfiguration, because 2 + 1 "feels like" a quorum.
3
3
6
3
0 down
0 down
Strongest overlap, zero failure tolerance on either path. One node in maintenance takes the key offline.
The two rows worth staring at are R=2, W=2 and R=2, W=1. They differ by one acknowledgement on the write path and that single acknowledgement is the entire guarantee. Latency difference is small — one extra same-datacentre round trip at ~0.5 ms — and teams shave it under pressure without registering that they've dropped from "overlap guaranteed" to "overlap by luck".
Also note what W buys on the durability axis, separately from consistency: W = 1 means the write exists on exactly one disk when the client is told it succeeded. If that node's disk dies before replication catches up, the write is gone, and the client was told it committed. That's a durability decision masquerading as a latency decision.
Where Quorums Stop Helping
R + W > N is a real guarantee about a narrow thing: a read will observe the latest completed write. Four gaps sit around that sentence, and every one of them produces production behaviour that looks like the quorum is broken when it is working exactly as specified.
1. It is not linearizability. The guarantee covers completed writes. It says nothing about a write that is in flight. Consider N=3, W=2, R=2, with a write that has reached replica 1 but not yet 2 or 3:
Reader A reads replicas 1 and 2, sees the new version on 1, returns the new value.
Reader B, starting after A finished, reads replicas 2 and 3, sees only the old version, returns the old value.
The value went forwards and then backwards for two observers in real-time order. That is a linearizability violation, and it happens with the quorum condition fully satisfied. Cassandra mitigates part of this with read repair — the reader writes the newer value back to the lagging replicas — but read repair on a QUORUM read is not atomic with the read, so it narrows the window rather than closing it.
2. There is no rollback for a partial write. If a coordinator dies after reaching one replica out of the required two, the client receives an error. The write is not undone. It sits on that one replica, and read repair may eventually spread it to the others. So a write your application was told failed can become durable minutes later. Any retry logic that assumes a failed write left no trace is wrong here — this is precisely why Module P-16's idempotency keys are not optional in quorum systems.
3. Concurrent writes still need conflict resolution. Quorums decide how many replicas participate. They say nothing about which write wins when two clients write the same key with no causal relationship between them. The options, with their costs:
Resolution strategy
Behaviour
Cost
Last-write-wins by timestamp
Highest timestamp survives, the other is discarded.
Silently loses data, and correctness now depends on clock synchronisation across nodes (Module A-1). Two writes within the clock skew window resolve arbitrarily.
Version vectors, return siblings
Both versions are kept and handed to the application to merge.
Every read path must handle multi-value responses. Vector size grows with the number of distinct writers, and sibling explosion is a real operational failure mode.
CRDTs
The data type is designed so any merge order converges to the same result.
Only works for operations that fit the algebra — counters, sets, registers. You cannot express "only if balance ≥ amount". Covered in Module A-9.
Consensus per operation
One order, agreed.
Full linearizability price, plus unavailability of the minority side. Cassandra's lightweight transactions are this, and they are roughly an order of magnitude more expensive than a normal write.
4. Sloppy quorums weaken it further, deliberately. In a strict quorum, W must be satisfied by nodes from the key's home replica set. Under a sloppy quorum, when home replicas are unreachable, the coordinator writes to whichever N reachable nodes it can find and attaches a hint — "this belongs to node 3, deliver it when node 3 returns". That's hinted handoff.
The availability win is significant: writes keep succeeding through a partition or a rolling restart. The cost is that R + W > N no longer implies overlap, because the W acknowledgements came from nodes that a subsequent read of the home replica set will never consult. You have W acknowledgements and zero guarantee. Until the hints drain, the system is plainly eventual regardless of the level you configured.
Why this matters in production: the quorum condition is frequently cited in design reviews as though it delivers strong consistency. It delivers one property — overlap for completed writes — and if the reviewer cannot also state what happens to an in-flight write, a failed write, two concurrent writes, and a write during a partition, the design has four unexamined failure modes with a reassuring inequality painted over them.
The Dials You Actually Turn
Most engineers meet consistency models as configuration values rather than as theory. Two systems dominate the vocabulary.
Cassandra sets consistency per query, on the read and the write independently. The important levels, with N = 3 in a single datacentre:
Level
Replicas required
Gives you
The cost
ANY (writes only)
Any node, including a hint holder
Highest write availability
The write may exist only as a hint. No read at any level is guaranteed to find it until the hint is delivered. Effectively unbounded staleness.
ONE
1
Lowest latency, tolerates 2 failures
No overlap guarantee. Reads may return arbitrarily old data even after a QUORUM write.
QUORUM
⌊N/2⌋ + 1 = 2
R + W > N when used on both sides
One extra round trip; loses availability if 2 of 3 replicas are down. In a multi-region cluster, QUORUM counts replicas across all regions, so it can force a cross-region round trip.
LOCAL_QUORUM
Majority within the local datacentre
Quorum semantics at local latency
Overlap holds only within the region. A read in another region can miss the write completely. This is the default for multi-region deployments and the reason cross-region convergence needs its own treatment (Module A-9).
ALL
3
Strongest overlap available without consensus
Zero failure tolerance for that key. One node restarting makes the operation fail. Still not linearizable, for the in-flight-write reason above.
The trap in that table is LOCAL_QUORUM. It is almost always the right choice — paying ~180 ms of Mumbai↔Virginia RTT on every write is not viable for an interactive path — and it means your global consistency story is eventual no matter what the local level says. Teams configure LOCAL_QUORUM for latency and then reason about correctness as if they'd configured QUORUM.
DynamoDB exposes a simpler switch: reads are eventually consistent by default, and you can request a strongly consistent read per operation. The costs are concrete:
A strongly consistent read consumes roughly twice the read capacity of an eventually consistent one for the same data, so the bill scales with how often you insist.
Strongly consistent reads are not available on global secondary indexes. A GSI is maintained asynchronously; if your access pattern goes through an index, you get eventual consistency and no dial to change it. This is a data-modelling constraint (Module F-14), discovered late and expensively.
Strong consistency is scoped to the region. In a multi-region configuration, replication between regions is asynchronous, so a strongly consistent read in one region tells you nothing about a write accepted in another.
Strongly consistent reads cannot be served from whichever replica is nearest or least loaded, so they lose the tail-latency smoothing that reading from any replica provides (Module F-4).
The generalisable point across both systems: the consistency dial is a per-operation decision, and treating it as a global setting is how you end up paying for strong consistency on paths that never needed it while running eventual consistency on the one path that did. The dial belongs next to the query, chosen from the requirement — which is exactly the "consistency tolerance" number Module F-1 made you write down per operation rather than per system.
Session Guarantees Are What Your Product Actually Wants
Here is the observation that resolves most real arguments about consistency: users do not perceive global state. They perceive their own session's history. A user who sees their own change take effect immediately, never sees a value go backwards, and never sees a reply before its parent, will describe the system as consistent — even if another user on the other side of the world is reading data that's several seconds stale, which is invisible to both of them.
That's why the session guarantees are the pragmatic centre of gravity. They cost dramatically less than linearizability because they require no agreement between nodes — only that reads for a given session are routed with knowledge of what that session has already observed.
The implementation is a position token. In Postgres, the write-ahead log position (LSN) is exactly that token:
typescript
The same token, kept as "the highest position I have returned to this session", gives monotonic reads: never serve a read from a replica behind that mark. Both guarantees, one mechanism, no coordination between nodes.
The costs, named:
The token has to live somewhere the next request can find it. A signed cookie or a field in the session store works. In-process memory does not survive the request landing on a different instance, and a serverless function has no affinity at all (Module F-10) — the token must travel with the request.
Falling back to the primary under lag is a load-shifting behaviour, not a failure. When replication lag spikes, primary read load rises exactly when the primary is already struggling. Cap it: after some threshold, serve stale and say so in the UI, rather than converting a lag incident into a primary overload incident (Module A-8).
The cruder version — pin the session to one replica by hashing the session ID — gets monotonic reads for free and inherits every cost of sticky routing from Module F-9: uneven load, a guarantee that resets when that replica is replaced, and a lag spike that affects a fixed subset of users rather than everyone a little.
Session guarantees say nothing across users. Two users in a shared document, a chat, or a collaborative board are one logical session in the product's mind and separate sessions in the storage layer's. Shared-state features are where session guarantees stop being sufficient and you need causal delivery or a CRDT (Module A-9).
Why this matters in production: the default reflex when a staleness bug is reported is "route reads to the primary". It works, and it silently discards the reason the replicas exist — offloading reads. One endpoint moved to the primary is fine; the pattern spreading to a dozen endpoints over a year is how a read-replica architecture quietly becomes a single-node architecture with expensive idle machines attached, discovered during the next traffic peak.
Where This Shows Up in Your Stack
PostgreSQL:synchronous_commit is the consistency dial, and its values are not interchangeable. remote_write waits for the replica's OS to receive the WAL; on waits for a flush to the replica's disk; only remote_apply waits until the replica has applied it and it is visible to readers there. A team chasing read-your-writes and setting remote_write gets durability they didn't ask for and none of the visibility they did. synchronous_standby_names = 'ANY 1 (r1, r2, r3)' is a write quorum in Postgres form — W=2 including the primary, with the cost that every commit waits for the fastest replica.
Node.js: the position token is application state, so it must be explicit. Storing it in module-level memory works in development on one process and fails the moment there are two instances or one restart. Put it in the signed session cookie or the session store, and set an upper bound on how long you'll honour it — an hour-old LSN forces a needless primary read.
Redis: replication is asynchronous, so reading from a replica breaks read-your-writes by default, and a failover can lose acknowledged writes. WAIT numreplicas timeout returns how many replicas acknowledged — useful, but it is an acknowledgement count, not linearizability, and it does not make the preceding write atomic with anything. Treated properly in Module P-12; the summary is that a Redis replica is an eventual-consistency cache of a cache.
Cassandra / DynamoDB: set the level per query from the requirement, not once in a config file. Audit the write paths that were dropped to ONE or LOCAL_ONE for latency, and confirm that no correctness argument anywhere depends on a quorum read seeing them.
Summary
A consistency model is defined by what it forbids, not by how "strong" it sounds — and every prohibition is paid for in coordination, which becomes latency, availability, or both.
The hierarchy is linearizable → sequential → causal → session guarantees → eventual, but the session guarantees are incomparable with one another, so they're specified as a set. Causal is the strongest model that remains available under partition — the practical ceiling for a system that has genuinely chosen AP (Module P-10).
Linearizability is about one object over time; serializability is about many objects in a transaction. They're orthogonal, and conflating them sends you after the wrong mechanism.
Learn the models by their bug reports — reply before its parent is causal, own change invisible is read-your-writes, a value reverting on refresh is monotonic reads, two users claiming the last ticket is read-modify-write and no consistency level fixes it.
R + W > N guarantees overlap ≥ R + W − N for completed writes, and that is all it guarantees. With N=3, R=2, W=2 is the balanced point; R=2, W=1 is the common misconfiguration that feels like a quorum and provides no overlap.
Quorums are not linearizability. In-flight writes can be observed then unobserved; a failed partial write is never rolled back and may become durable later, which is why idempotency keys (Module P-16) are mandatory here; concurrent writes still need last-write-wins, version vectors, CRDTs, or consensus, each with a named cost; and sloppy quorums with hinted handoff satisfy W from nodes a later read will never consult, voiding the inequality in exchange for availability.
Cassandra's levels trade round trips for tolerance:ONE is fast and gives no overlap, QUORUM gives overlap and loses availability at two failures, ALL gives zero failure tolerance and still isn't linearizable, and LOCAL_QUORUM — nearly always the right multi-region choice, since a cross-region write would pay ~180 ms — makes your global story eventual whatever the local level says.
DynamoDB's strongly consistent read costs roughly double the capacity, is unavailable on global secondary indexes, is region-scoped, and forfeits nearest-replica tail smoothing. The GSI restriction is a data-modelling constraint, not a tuning one.
Session guarantees are the pragmatic middle ground because users perceive their own history, not global state. A write-position token — Postgres LSN, or a version stamp — buys read-your-writes and monotonic reads with no inter-node coordination. Its costs: the token must survive the next request, lag spikes shift read load onto the primary, and it says nothing about state shared between users.
The next module, Distributed Caching with Redis, takes the same question one layer up. A cache is a replica you chose to run with weaker guarantees than the database, and every model in this table applies to it — with the added complication that the cache and the database can disagree, and nothing in either of them is responsible for noticing. Beyond that, Module A-1 supplies the missing machinery: you cannot order operations without clocks, and physical clocks are not trustworthy enough to do it, which is why logical clocks and consensus exist. Module A-9 takes causal consistency and CRDTs to the case where the replicas are 180 ms apart and neither side can wait.
Knowledge Check
A user is viewing a colleague's profile. The colleague's newly uploaded photo appears, then reverts to the old one after a refresh, then reappears on the next refresh. Reads are load-balanced across three asynchronous replicas. Which guarantee is being violated, and what is the proportionate fix?
A team runs Cassandra with N=3 and writes and reads both at QUORUM. They observe a key returning the new value, then the old value on a subsequent read from a different client, and later the new value again. They conclude the quorum condition is not being honoured. What is the correct explanation?
After a successful checkout, the confirmation page reads the order list from a read replica and shows "you have no orders". It resolves within a second. Which remedy does the module put forward, and why does it prefer that one?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
overlap ≥ R + W − N
N = 3, R = 2, W = 2 → overlap ≥ 2 + 2 − 3 = 1
// Read-your-writes across a primary and a pool of async replicas.// The session carries the WAL position its last write reached.asyncfunctionwriteProfile(session: Session, userId:string, name:string){const{ rows }=await primary.query(`UPDATE profiles SET display_name = $2 WHERE id = $1
RETURNING pg_current_wal_lsn() AS lsn`,[userId, name],);// The token is the whole mechanism: the position a later read must reach. session.minLsn = rows[0].lsn;}asyncfunctionreadProfile(session: Session, userId:string){const target = session.minLsn;if(target){for(const replica of replicas){// pg_last_wal_replay_lsn() is *applied*, not merely received —// received-but-unapplied is the subtle bug in this whole pattern.const{ rows }=await replica.query(`SELECT pg_last_wal_replay_lsn() >= $1::pg_lsn AS caught_up`,[target],);if(rows[0].caught_up)returnqueryProfile(replica, userId);}// Cost named: every read in the lag window falls back to the primary,// so a lagging replica set converts read load into primary load.returnqueryProfile(primary, userId);}returnqueryProfile(pickReplica(), userId);}