Consistency Models

Causal Consistency: Never Show an Effect Before Its Cause

Preserve the order of operations that are causally related; allow any order for operations that are genuinely concurrent. It removes the anomalies that make users think a system is broken, it remains available during a partition, and there is a theorem saying nothing stronger can do both.

▶ Run the lab

The question this answers

The question

How much ordering can I keep while still accepting writes on both sides of a partition?

The guarantee — the property claimed, and its scope

Causal consistency: if operation A causally precedes operation B — same process, or B read a value A wrote, or transitively so — then every process observes A before B. Operations that are concurrent (neither precedes the other) may be observed in different orders by different processes. Convergence to a single value for concurrent writes is *not* implied; adding it gives causal+ consistency.

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.

What a node knows — observation versus inference

A replica knows the dependencies attached to each update it has received and whether it has already applied them. That is genuine local knowledge: it can decide "I may not show this yet" without asking anyone. What it cannot know is whether a dependency it has not seen is in flight, lost, or does not exist — so an update whose dependencies never arrive is held indefinitely, and detecting that requires a timeout, which is a guess.

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.

What guarantee?What does a node know?How does it work?What can fail?How does it fail?Where is coordination?What holds under failure?How does it recover?How would you know?What is the simpler thing?
causal consistencyhappened-beforevector clocksavailability

The anomaly it exists to prevent

The canonical example is a comment thread. Alice writes "Has anyone seen my keys?" Bob reads it and replies "They are on the table." Under eventual consistency, a third user Carol can receive Bob's reply before Alice's question — and sees a reply to nothing. Nothing failed; the two updates simply took different paths and Carol's replica applied them in arrival order.

This is qualitatively worse than staleness. Missing both messages is fine. Seeing only the question is fine. Seeing only the answer is incoherent — it presents the user with a world that could not have happened, and it is the class of anomaly that makes people describe a system as broken rather than slow.

Causal consistency forbids exactly this and nothing more. It says: if B depends on A, nobody sees B without A. It does not say everyone sees things in the same order, and it does not say you see the latest state. It targets precisely the anomalies that violate a user's model of cause and effect.

A reply arriving before the message it replies toprotocol
Alice (replica 1)Bob (replica 2)Carol (replica 3)question: deliveredquestionquestion: delayedquestiondelayedreply: deliveredreplypost "seen my keys?" (write) at t=0post "seen my keys?"reads it, replies "on the table" (write) at t=3reads it, replies "on the table"sees the reply — cause not yet present (read) at t=5sees the reply — cause not yet presentquestion finally arrives (write) at t=12question finally arrivest=0time →t=12
delivereddelayed (dashed, long)duplicated (×2)dropped — stops short, never arriveswriteread
Bob's reply causally depends on Alice's question, but reached Carol first. Causal consistency requires Carol's replica to *hold* the reply until the question arrives — the dependency is carried with the update, so the replica can make that decision locally.

How dependencies are tracked, and why that is the expensive part

The happened-before relation is Lamport's, and this domain develops it in Happens-Before: The Only Ordering You Actually Have. Operationally, each update carries metadata describing what it depends on, and a replica delays applying an update until those dependencies are satisfied locally. Two mechanisms dominate. Vector clocks — one counter per writer — precisely capture the relation but grow with the number of writers, which for a client-per-writer system is unbounded. Explicit dependency lists — "this update depends on these specific versions" — are compact when dependencies are few and grow with a session's read history.

The metadata cost is the reason causal consistency is rare in production databases despite being theoretically attractive. Research systems (COPS, Eiger, and their descendants) demonstrated it at scale by aggressively pruning dependencies — dropping transitively-implied ones, and truncating at session boundaries. Every practical implementation is a compromise on what it tracks, and that compromise is where its real guarantee lives.

Note where this connects: a session floor as used in Read-After-Write: Letting a User See Their Own Change is the degenerate one-writer case of the same idea. Widen it from a scalar to a vector and from one session to all sessions, and you have causal consistency. That is the clearest way to see why session guarantees are cheap and causal consistency is not.

1on receive(update):
2 # deps: the versions this update causally depends on
3 if all(local_version(k) >= v for k, v in update.deps):
4 apply(update)
5 flush_pending_that_now_have_deps()
6 else:
7 # We may NOT show this yet. Holding it is the guarantee.
8 pending.add(update)
9
10# The honest problem: a dependency that never arrives.
11# Nothing distinguishes "in flight" from "lost". A timeout is a guess,
12# and both choices are bad: drop it (violate causality later) or hold it
13# forever (unbounded memory, and the update is invisible).
A replica deciding locally whether an update may be shown yet

The theorem that makes it interesting

Causal consistency matters not because it is convenient but because of where it sits. It is available: a replica can accept a read or a write using only local state plus dependencies it already holds, so it keeps working during a partition. And there is a result — from the line of work by Mahajan, Alvisi and Dahlin, and refined by Attiya and others — that causal consistency is essentially the strongest model that can be provided by an always-available, convergent system.

That gives the consistency landscape a natural dividing line. Below it: eventual consistency and session guarantees, all available. Above it: sequential consistency, linearizability, strict serializability, none of which can be available during a partition. Causal consistency is the ceiling on the available side, which is why it is the interesting answer to "how much can I have without giving up availability?"

What it still does not give you is any global invariant. Two users can concurrently claim the same username with full causal consistency — the two claims are concurrent, so nothing orders them, and no amount of causality repairs that. Invariants need coordination, and coordination is exactly what availability forbids. See Start From the Invariant, Not From the Architecture and CAP: What the Theorem Actually Says.

  • Available: a replica answers from local state plus satisfied dependencies, so partitions do not block it.
  • Convergent when paired with a merge rule for concurrent writes — that pairing is "causal+".
  • The ceiling for available models: nothing stronger is achievable while staying available and convergent.
  • Still no invariants: concurrent operations are unordered, so uniqueness and limits remain out of reach.

Key points

  • Causally-related operations are seen in order by everyone; concurrent operations may be seen in any order.
  • It forbids the anomalies that read as incoherent — an effect visible without its cause — rather than merely reducing staleness.
  • A replica enforces it locally by holding updates whose dependencies it has not yet applied.
  • It is the strongest model compatible with remaining available during a partition.
  • It provides no global invariants: concurrent claims to the same username are still both valid.

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.

How it works
  • Each operation is tagged with metadata identifying the operations it causally depends on — a vector clock or an explicit dependency set.
  • When a replica receives an update, it checks whether every dependency has been applied locally.
  • If yes, it applies the update and re-checks any pending updates that were waiting on it.
  • If no, it buffers the update and does not make it visible to readers.
  • Concurrent updates — neither depending on the other — are combined by a merge rule to give convergence, producing causal+ consistency.
What can fail at the boundary
  • Dependency metadata grows without bound as the number of writers or the session history grows.
  • A dependency is lost in transit, and the dependent update is buffered indefinitely.
  • Aggressive pruning of dependencies drops a real one, and a causality violation becomes possible again.
  • Concurrent updates need a merge rule, and a bad one destroys updates or prevents convergence.
  • Dependencies established outside the system — a user telling a colleague to refresh — are invisible to it entirely.
How it fails — what an operator sees
  • Buffered updates that never appear: an update sits pending because a dependency was lost. The operator sees a write acknowledged and permanently invisible on some replicas, with no error anywhere and growing pending-buffer memory.
  • Metadata bloat: per-update dependency vectors grow with the writer population until they dominate payload size. Observed as network and storage growth uncorrelated with user data volume.
  • Visibility latency spikes: during a partition, updates accumulate pending on the far side and become visible in a burst after heal. Users experience a period of apparent staleness followed by a flood.
  • False causality from over-tracking: a session that read a large page acquires dependencies on everything it saw, and its next write is delayed on all of them — a write blocked behind unrelated data.
  • Out-of-band causality violation: a user posts, phones a colleague, and the colleague refreshes and sees nothing. The dependency existed in the real world and never entered the system, so the guarantee legitimately does not cover it.
Where coordination is required
  • No agreement between replicas is required — the enforcement is local, which is exactly why availability is preserved.
  • The cost is metadata on every operation and buffering on the read path, so it is paid in bytes and visibility latency rather than in blocking.
  • Anything that needs a global decision remains outside the model and requires real coordination. See Coordination Couples Availability.
What still holds under failure
  • Reads and writes remain available on every reachable replica during a partition.
  • Updates that depend on the unreachable side buffer and become visible after heal, in the right order.
  • Concurrent updates on the two sides converge after heal if a merge rule exists; without one, replicas diverge permanently despite causality being respected.
How it recovers
  • Detect: monitor pending-buffer depth and the age of the oldest pending update — a growing oldest-age is the signal that a dependency will never arrive.
  • Contain: bound the buffer and make eviction explicit and alarming rather than silent, since evicting a pending update knowingly breaks the guarantee.
  • Recover: after a partition, dependencies arrive and pending updates flush in causal order automatically.
  • Reconcile: for concurrent updates, run the merge; for evicted pending updates, this is a data-repair task the system cannot do for you.
  • Verify: an end-to-end probe that writes a dependent pair and asserts no replica ever shows the second without the first.
How you would know
  • Pending-buffer depth and oldest-pending age per replica.
  • Dependency metadata size per update, as a fraction of payload — the direct measure of the model's overhead.
  • Visibility latency: time from write acknowledgement to visibility at each replica, distinct from replication lag.
  • Count of pending updates evicted by a buffer bound, which is a count of deliberate guarantee violations.
  • Results of a causal-pair probe asserting effect-never-before-cause.
When it helps
  • Social and collaborative features where messages, replies, edits and reactions have obvious causal relationships.
  • Multi-region systems that must accept writes everywhere but cannot tolerate incoherent orderings.
  • Any product where users interpret an ordering violation as data corruption rather than as slowness.
  • Systems already carrying version metadata for conflict detection, where the incremental cost is small.
When it hurts
  • Independent-key workloads with no meaningful causal relationships — the metadata buys nothing.
  • Very large writer populations, where vector clocks grow past the point of practicality.
  • Systems needing invariants, which causal consistency does not provide and cannot be extended to provide while staying available.
  • Single-region deployments where linearizability is affordable and simpler to reason about.
Simpler alternatives

An effect arriving before its cause

An effect arriving before its cause
A posts. B reads the post and replies — so the reply causally follows the post. C replicates the two items over different streams, and the fast one wins.
post visible at C
step 6
reply visible at C
step 3
held for its dependency
not held
C's view
incoherent
Two independent streams, two different lags, one incoherent readersimplified
Replica AReplica BReplica Creplicate post: deliveredreplicate postreplicate post (slow stream): delayedreplicate post (slow stream)delayedreplicate reply (fast stream): deliveredreplicate reply (fast stream)user posts (write) at t=0user postsuser reads the post, then replies (write) at t=2user reads the post, then repliesreply visible (read) at t=3reply visiblepost visible (read) at t=6post visiblet=0time →t=6
delivereddelayed (dashed, long)duplicated (×2)dropped — stops short, never arriveswriteread
C applied the reply before the post. Nothing was lost and nothing failed — the two updates simply took different paths, and no rule required them to arrive in order.
C, step 3:
  reply: "congratulations!"   ← in reply to a post that is not here
  (no post)

C, step 6:
  post: "we shipped it"
  reply: "congratulations!"
Causality violated
This is not staleness. Staleness is being behind, which users tolerate; this is being incoherent, which reads as a bug in the product. The two updates are not concurrent — B literally read the post before writing the reply — so there is a fact of the matter about their order, and the system ignored it.
Causal consistency says: if A causally precedes B — same process, or B read what A wrote, or transitively so — everyone observes A before B. Operations that are genuinely concurrent may still be observed in different orders by different processes, and convergence on a single value for them is not implied (adding it gives causal+). It is the strongest model known to be compatible with remaining available during a partition, which makes it the interesting ceiling for available systems. The cost is dependency tracking: every update carries what it depends on, that metadata grows with the number of writers, and an update whose dependencies never arrive is held indefinitely — a liveness problem that needs its own detection. And it buys no global invariant at all: two concurrent claims to the same username are both causally fine.
simplifiedTwo items on independent replication streams with fixed delays; dependency tracking is modelled as “hold until the dependency is applied”, which is the mechanism, not the metadata scheme. Real systems track dependencies with vector-ish structures whose size is the actual engineering cost.

What people believe, and what is true

Claim

Causal consistency means everyone sees the same order.

Reality

Only for causally-related operations. Concurrent operations may legitimately be observed in different orders by different processes — that freedom is what keeps it available.

Claim

Causal consistency makes replicas converge.

Reality

It orders causally-related operations; it says nothing about concurrent ones. Convergence needs a merge rule on top, and the combination is called causal+.

Claim

If we track causality we can enforce uniqueness.

Reality

Two concurrent claims are unordered by definition, so causality has nothing to say about them. Invariants need coordination, which is precisely what an available system declines to do.

Claim

It is basically eventual consistency with better ordering.

Reality

It adds a genuine safety property — an effect is never visible without its cause — which eventual consistency lacks entirely. That safety property is checkable in finite time; eventual consistency has nothing checkable.

Go deeper

Only the levels this lesson can honestly fill — a missing level is a claim nobody had.

Overview

If B happened because of A, nobody sees B without A. Things that happened independently may be seen in any order.

Practical

Attach dependency metadata to updates and let replicas hold updates whose dependencies have not arrived. Then watch two numbers: the pending-buffer age (a dependency that never arrives is a permanently invisible write) and the metadata size (which grows with writers and is what makes this expensive).

Advanced

Causal consistency is the partial order of Lamport's happened-before, made into a visibility rule. Its significance is positional: it is available and convergent, and it is essentially the strongest model that is both — so it marks the boundary between the models that survive a partition and those that cannot. Everything above the line (sequential, linearizable, strictly serializable) requires coordination and therefore gives up availability, which is CAP restated in terms of models rather than systems. See Happens-Before: The Only Ordering You Actually Have, Vector Clocks: Buying Concurrency Detection at O(N) and CAP: What the Theorem Actually Says.

Apply it

Interview questions
  • 💬 A user sees a reply to a comment that is not visible. Which guarantee is missing, and how would a replica enforce it locally?
  • 💬 Why is causal consistency compatible with availability during a partition when linearizability is not?
  • 💬 What happens to an update whose causal dependency is lost in transit? What are your two options, and why are both bad?