Fundamentals
Strong, eventual, causal, and read-your-writes consistency. Understand the spectrum from linearizability to eventual consistency and when each is appropriate.
When data lives on multiple machines, a fundamental question arises: when one node updates a value, when do other nodes see that update? The answer depends on the consistency model the system implements. Choosing the right consistency model is one of the most impactful decisions in system design — it affects correctness, performance, availability, and user experience.
Imagine you post a photo on social media. You hit "post" and immediately refresh your profile. If you do not see your own photo, that feels broken — even though technically the data might still be propagating to the replica your request hit. Now imagine you transfer money between bank accounts. If the system shows inconsistent balances even briefly, money could be double-spent.
Different use cases demand different guarantees. Consistency models give us a vocabulary to discuss these guarantees precisely.
Consistency models form a spectrum from strongest to weakest:
Strongest ◄─────────────────────────────────────────► Weakest
Linearizability → Sequential → Causal → Read-your-writes → Eventual
│ │ │ │ │
"Behaves as "All nodes "Cause "You see your "Eventually
one copy" agree on before own writes" converges"
order" effect"Stronger models are easier to reason about but harder to implement efficiently. Weaker models allow better performance and availability but push complexity onto the application developer.
Linearizability is the gold standard of consistency. It guarantees that every operation appears to take effect at a single, instantaneous point in time between its invocation and completion. The system behaves as if there is exactly one copy of the data.
If client A writes a value and client B reads it after the write completes, client B is guaranteed to see the new value. There is a total order of all operations that is consistent with real time.
# With linearizability, this always works correctly
# Thread A: # Thread B:
account.balance = 100 #
# write completes at time T #
# read at time T+1
assert account.balance == 100 # Always trueLinearizability typically requires coordination between nodes on every operation. Common approaches:
Linearizability is expensive. Every read and write may need to contact multiple nodes and wait for acknowledgment. During network partitions, linearizable systems must sacrifice availability (this is the CP choice from CAP). Latency increases, especially across geographic regions.
Sequential consistency is slightly weaker than linearizability. It guarantees that all nodes see operations in the same order, but this order does not need to correspond to real-time ordering. Operations from a single client are always seen in the order they were issued.
The practical difference: with linearizability, if client A's write finishes before client B's read starts (in wall-clock time), B must see the write. With sequential consistency, B might not — as long as all nodes agree on some total order that is consistent with each client's individual ordering.
# Sequential consistency allows this scenario:
# Client A writes x=1, then x=2
# Client B writes x=3
# All nodes might see: x=1, x=3, x=2 (wrong under linearizability)
# But all nodes MUST agree on the same order
# And A's operations must appear in order: x=1 before x=2Causal consistency captures the idea that if operation A could have influenced operation B, then everyone must see A before B. Operations that are not causally related (concurrent operations) can be seen in any order.
# Causal consistency example: social media comments
# Alice posts: "I got the job!"
# Bob reads Alice's post, then comments: "Congratulations!"
# Causal consistency guarantees:
# Anyone who sees Bob's comment MUST also see Alice's post.
# You will never see "Congratulations!" without the post it refers to.
# However, Carol might post an unrelated status update concurrently,
# and different users might see Carol's post and Alice's post in different orders.
# That is fine — they are not causally related.Causal consistency is typically implemented using vector clocks or version vectors. Each node maintains a vector of logical timestamps, one per node. When a node performs an operation, it increments its own timestamp. When it receives data from another node, it merges the vectors by taking the component-wise maximum.
# Simplified vector clock
class VectorClock:
def __init__(self, num_nodes, node_id):
self.clock = [0] * num_nodes
self.node_id = node_id
def increment(self):
self.clock[self.node_id] += 1
def merge(self, other_clock):
for i in range(len(self.clock)):
self.clock[i] = max(self.clock[i], other_clock[i])
self.increment()
def happens_before(self, other):
"""Returns True if self causally precedes other."""
return (all(s <= o for s, o in zip(self.clock, other.clock))
and any(s < o for s, o in zip(self.clock, other.clock)))Causal consistency is a sweet spot for many applications. It prevents anomalies that confuse users (seeing a reply without the original message) while allowing much better performance than linearizability. It does not require global coordination — only tracking dependencies.
This is a session-level guarantee: after you write a value, you will always see that value (or a more recent one) in subsequent reads within the same session. Other users might still see stale data.
Without read-your-writes, a user could update their profile name, refresh the page, and see the old name. The user thinks the update failed and tries again. This creates a terrible user experience even though the system is technically working correctly.
# Read-your-writes with client-tracked timestamps
class ReadYourWritesClient:
def __init__(self):
self.last_write_timestamp = 0
def write(self, key, value):
result = self.primary.write(key, value)
self.last_write_timestamp = result.timestamp
return result
def read(self, key):
# Find a replica that is caught up to our last write
for replica in self.replicas:
if replica.replication_position >= self.last_write_timestamp:
return replica.read(key)
# Fall back to primary if no replica is caught up
return self.primary.read(key)Eventual consistency is the weakest useful guarantee: if no new writes are made to a value, all replicas will eventually converge to the same value. There is no bound on how long "eventually" takes (though in practice it is usually milliseconds to seconds).
Eventual consistency enables the highest availability and lowest latency. Writes succeed on a single node and propagate asynchronously. Reads never block waiting for replication. This is the foundation of AP systems.
`ONE` is eventually consistent. Higher levels like `QUORUM` provide stronger guarantees.Eventually Consistent Replication:
Time ──►
Node A: [x=1]────[x=2]──────────────────[x=2]
│ │
write x=2 (converged)
│ │
Node B: [x=1]──────┼───[x=1]──[x=2]────[x=2]
│ │ │
async stale receives
repl. read update
│
Node C: [x=1]──────┼──────────[x=1]──[x=2]──[x=2]
│ │
stale receives
read update┌────────────────────┬──────────────┬─────────────┬──────────────────────┐
│ Model │ Performance │ Complexity │ Best For │
├────────────────────┼──────────────┼─────────────┼──────────────────────┤
│ Linearizability │ Lowest │ Highest │ Financial data, │
│ │ │ │ distributed locks │
├────────────────────┼──────────────┼─────────────┼──────────────────────┤
│ Sequential │ Low │ High │ Multi-processor │
│ │ │ │ memory models │
├────────────────────┼──────────────┼─────────────┼──────────────────────┤
│ Causal │ Medium │ Medium │ Social media, │
│ │ │ │ collaborative apps │
├────────────────────┼──────────────┼─────────────┼──────────────────────┤
│ Read-your-writes │ Medium-High │ Low │ User-facing apps │
│ │ │ │ with profile edits │
├────────────────────┼──────────────┼─────────────┼──────────────────────┤
│ Eventual │ Highest │ Lowest │ Analytics, caching, │
│ │ │ │ DNS, CDN │
└────────────────────┴──────────────┴─────────────┴──────────────────────┘