Consistency models
During the last over of an IPL final, a fan refreshes the score: 124/3. They refresh again: 120/3. The score went backwards. The scorer entered the update once, the database stored it once, and the fan's two refreshes happened to land on two replicas, one of them a second behind.
Was that a bug? It depends entirely on what the system promised. A consistency model is that promise, stated precisely: which values a read is allowed to return while copies of the data disagree. "Strongly consistent" is not precise enough for a design review, and an interviewer who asks what it means wants one of the names on this page.
A system is described as strongly consistent. What exactly would you expect a read to return, right after someone else's write finished? And is that the same thing as a serializable database?
A promise about reads#
The strong vs eventual consistency article gives the two ends of the scale. This chapter fills in the rungs between them, each one a cheaper promise than the one above. From strongest to weakest:
Linearizable: the system behaves as if there is a single copy. Sequential: everyone agrees on one order of events, which may lag real time. Causal: everyone sees causes before their effects. Session guarantees: each user sees a sensible history of their own. Eventual: copies agree once writes stop.
Linearizability: as if there were one copy#
Linearizability makes a replicated object look like a single copy. Two rules:
- Once a write has been acknowledged, every read that starts after that moment returns that value or a newer one.
- Once any read has returned the new value, every read that starts after it returns the new value too, even if the write has not been acknowledged yet.
Operations that overlap in time can land either way. That is the whole definition, and it is about real time: the clock on the wall decides what "after" means.
Here is one write and four reads. Click the reads to change what they returned, and see which promises the history keeps.
- ✗LinearizableBilal at 50 ms and Chen at 45 ms started after the write was acknowledged at 30 ms, so must see 124/3.
- ✗SequentialBilal saw the new score, then the old one. No single order of events allows that.
- ✓Read your writesAsha sees her own update.
- ✗Monotonic readsBilal saw 124/3, then 120/3. The score went backwards for him.
- ✓EventualOnly promises that copies agree once writes stop. Any of these reads is allowed.
The default history is the cricket fan's. Bilal saw 124/3 and then 120/3, which breaks monotonic reads, sequential consistency and linearizability at once. Fix Bilal's second read, and leave Chen reading the old score after the write was acknowledged: sequential consistency is now kept and linearizability still is not. That gap is real time, and it is what you pay for.
When you actually need it#
Most features do not. A few cannot work without it:
Locks and leader election. "Who holds the lock?" must have one answer at a time, or two holders act at once. This is why etcd and ZooKeeper are linearizable.
Uniqueness. Two people registering the same username in the same second must not both succeed. Checking a stale copy lets them.
Check-then-act on a single value. "Book seat 42 if it is free" or "debit if the balance covers it" must read the latest value, or two requests both see it free.
Two channels racing. A user is sent an SMS saying "your payment is confirmed", taps it, and the app reads a replica that has not caught up and shows "payment pending". The SMS channel was faster than replication. Linearizable reads (or reading from the primary) fix it.
The cheaper promises#
Sequential consistency keeps one order of events that every client agrees on and that respects each client's own order, but it may run behind real time. Everyone sees the same story, possibly a little late.
Causal consistency only orders events that could have caused each other. If Bilal posts a comment and Chen replies to it, nobody may see the reply without the comment. Two unrelated comments can appear in different orders to different readers.
Causal consistency has a special place: it is the strongest model a system can offer while staying available on both sides of a network partition (a 2011 result by Mahajan, Alvisi and Dahlin). Anything stronger means some requests must wait or fail during a partition, which is the CAP trade made precise.
Session guarantees are promises to one client about its own experience. Douglas Terry and colleagues named four in 1994 for the Bayou system: read your writes (you see your own changes), monotonic reads (your view never goes backwards), monotonic writes (your writes apply in the order you made them) and writes follow reads (a write you make after reading something is ordered after it).
They are cheap and fix most of what users notice. Two ways to build them:
- Sticky routing: keep each session on one replica. Simple, and it breaks when that replica fails over.
- A token: after every read or write, give the client the log position it has seen. Later reads go to any replica that has caught up to that position, or wait until one has. MongoDB's causally consistent sessions work this way.
// After a write, remember how far the log had got.
const { rows } = await primary.query('SELECT pg_current_wal_lsn() AS lsn')
session.minLsn = rows[0].lsn
// Before reading from a replica, check it has replayed at least that far.
const { rows: r } = await replica.query(
'SELECT pg_last_wal_replay_lsn() >= $1::pg_lsn AS caught_up',
[session.minLsn],
)
const db = r[0].caught_up ? replica : primary // read your writes, without always using the primaryLinearizable is not serializable#
The most common mix-up in this area, and an interviewer's favourite.
Serializability is about transactions: several reads and writes across many rows. It promises the result is the same as running the transactions one at a time in some order. Any order. It says nothing about real time. (It is the strongest of the isolation levels.)
Linearizability is about single operations on a single object, and it is all about real time: after a write finishes, later reads see it.
They are independent. A database can be serializable and still serve your transaction a snapshot from two seconds ago off a replica: some serial order exists, it just is not the real-time one. A key value store can be linearizable per key and have no transactions at all.
Having both is called strict serializability: transactions in one order, and that order matches real time. Spanner offers it and calls it external consistency, which is exactly what the TrueTime commit wait buys.
What your database actually gives you#
| Choice | What you gain | What you pay | Pick it when |
|---|---|---|---|
| Postgres or MySQL with async replicas | Reads on the primary see every committed write. Each replica, read alone, never goes backwards. | Replica reads are only eventually consistent, and switching replicas can go backwards. | Route reads that must be fresh to the primary, or use an LSN token as above. |
| DynamoDB | Strongly consistent reads within a region if you set ConsistentRead, at twice the read cost. | Eventually consistent reads by default, and global tables replicate between regions with last writer wins. | Turn on strong reads for the few lookups that need them, not the whole table. |
| Cassandra | Tunable per query. QUORUM writes and reads usually read your writes. | QUORUM alone is not linearizable. Concurrent writes resolve by timestamp, and a failed write can surface later. | Use lightweight transactions (IF NOT EXISTS, built on Paxos) for the rows that need a single answer. |
| MongoDB | readConcern "linearizable" for single document reads, and causally consistent sessions. | Linearizable reads are slower and go to the primary. The defaults are weaker. | Sessions for user-facing consistency, linearizable reads for the rare check-then-act. |
| etcd, ZooKeeper, Spanner | Linearizable by default; Spanner is strictly serializable. | Every write waits for a majority, and in Spanner, for commit wait too. | Coordination data, and data where real-time order is the product. |
Kyle Kingsbury's Jepsen project has tested dozens of databases under network partitions and clock faults, and many failed to keep the guarantees their documentation claimed, at least in the versions tested. Before you rely on a consistency claim for something that matters, read the database's own definition and look for a Jepsen report on it.
- etcdLinearizable reads by default
- DynamoDBConsistentRead: true for strong reads in a region
- CassandraConsistency level per query; LWT for linearizable
- MongoDBreadConcern levels and causal sessions
- SpannerExternal consistency: strict serializability
- JeJepsenIndependent tests of what databases really guarantee
Interview replay#
Checkpoint#
1. A write is acknowledged at 10:00:00.030. A read on another client starts at 10:00:00.045 and returns the old value. Which model does this break?
2. Your database is serializable. Can a transaction still read data that is two seconds old?
3. A feed shows replies before the comments they answer. Which is the cheapest model that prevents it?
- Linearizable = one copy, in real time. Needed for locks, uniqueness and check-then-act.
- Causal is the strongest model that stays available in a partition.
- Session guarantees (read your writes, monotonic reads) fix most user-visible weirdness, cheaply.
- Serializable ≠ linearizable. Both together is strict serializability.
A consistency model is the precise promise about what a read may return while copies disagree. Linearizable means the system behaves like one copy in real time: once a write is acknowledged, every later read sees it, and I need that for locks, unique names and check-then-act on a value. Sequential gives one agreed order that may lag real time. Causal only guarantees causes before effects, like a comment before its reply, and it is the strongest model that stays available during a partition. Session guarantees, read your writes and monotonic reads, protect each user's own view, and I build them with a log-position token rather than sending everything to the primary. Serializable is a different thing: transactions in some order, not necessarily real time, and both together is strict serializability, which Spanner offers. I pick the weakest model each operation can live with, and I check the database's actual guarantee, ideally against a Jepsen report.