Skip to content
BytePatterns

Strong vs Eventual Consistency: Consistency Models Explained

9 min readBytePatterns

Strong vs eventual consistency, and the models between them: read-your-writes, monotonic reads and causal order, what each prevents, and what each one costs.

Once data lives on more than one machine, the copies are not always identical. A write lands on one node and reaches the others a little later. The question a consistency model answers is not "are the copies the same?" but "what is a read allowed to return while they are not?" Strong and eventual consistency are the two ends of that answer, and most real systems, and most good interview answers, live on the rungs in between.

The problem it solves

Replication buys availability and read throughput, and it brings anomalies that users notice:

  • You post a comment, refresh, and it is gone: your read hit a replica that had not applied your write yet.
  • You refresh twice and a counter goes backwards: the second read hit a replica further behind than the first.
  • A reply appears in a thread before the message it answers.
  • Two users see different "latest" prices at the same moment.

Each consistency model rules out some of these and pays for it in latency, availability or coordination. The design skill is picking the weakest model whose anomalies your users would never see.

The intuition

Think of it as a ladder, from weakest promise to strongest:

  • Eventual consistency. If writes stop, all copies converge. Until then a read may return any recent value, including one older than a write that already succeeded. It orders nothing.
  • Session guarantees, promises about one client's view:
    • Read-your-writes: you never read a version older than your own last write.
    • Monotonic reads: once you have seen a version, you never see an older one.
    • Monotonic writes and writes-follow-reads complete the classic set of four.
  • Causal consistency. If one write could have been influenced by another (a reply and its message), everyone sees them in that order. Unrelated writes may still appear in different orders to different people.
  • Strong consistency (linearizability). The system behaves as if there were a single copy: every read returns the latest completed write, for everyone.

The costs climb with the ladder. Session guarantees are cheap: the client carries a version token, the highest version it has written or read, and any replica at least that fresh may answer. Causal consistency needs each write to carry its dependencies, and a replica holds a write back until its causes have arrived. Strong reads need coordination on every read, confirming with the leader or a quorum that no newer write exists, which adds a round trip and, as the CAP theorem says, means refusing to answer during a partition. Many stores let you choose per request, as of September 2026, so a checkout can pay for strong reads while a feed does not.

Watch it run

The animation reads one key with three copies and asks, at each step, what a read may return. The client writes v2; the leader accepts it and acknowledges. Replication is asynchronous: replica A applies it quickly, while B is behind by one write. Under eventual consistency, a read routed to B returns v1: not corrupt, just older than a write that already succeeded. That is how a page contradicts the person using it, when your own comment vanishes on refresh. Read-your-writes fixes exactly that: the session carries its own version, so a lagging replica cannot answer it, and the read comes back v2. Causal goes one rung further: a reply may not arrive anywhere the message it answers has not, so B holds the reply until v2 is there. Strong means every read confirms with the leader or a quorum first, so no copy can answer alone, at the cost of one more round trip. And that is the bill: lose the leader and the strong read has nobody to ask, so it waits rather than answer. So pick the weakest rung whose anomalies a user would never notice, and write the choice down.

Consistency Models

Step 1 of 10

One key, three copies. The interesting question is not where it is stored — it is what a read is allowed to return.

The same interactive animation as the lesson — step through it with the controls.

The code

A toy model: a leader's log, replicas that have applied a prefix of it, and three read modes. Reads return the version number they saw:

class Cluster:
    """Toy model: a leader's log, replicas that apply it asynchronously."""
    def __init__(self, replicas=("A", "B")):
        self.log = ["v1"]                          # version k is log[k - 1]
        self.applied = {r: 1 for r in replicas}    # how far each replica has got
        self.leader_up = True

    def write(self, value):
        self.log.append(value)                     # accepted and acknowledged
        return len(self.log)

    def catch_up(self, replica, upto=None):
        self.applied[replica] = upto or len(self.log)

    def read(self, replica, mode, session):
        if mode == "strong":                       # confirm with the leader first
            if not self.leader_up:
                raise TimeoutError("no leader to confirm with")
            v = len(self.log)
        elif mode == "session" and self.applied[replica] < session["seen"]:
            fresh = [a for a in self.applied.values() if a >= session["seen"]]
            v = fresh[0] if fresh else len(self.log)   # a fresh replica, else the leader
        else:
            v = self.applied[replica]              # eventual: whatever it has
        session["seen"] = max(session["seen"], v)
        return v

c = Cluster()
me = {"seen": c.write("v2")}                       # the session remembers version 2
c.catch_up("A")                                    # A applies it; B lags
print(c.read("B", "eventual", {"seen": 0}))        # 1
print(c.read("B", "session", me))                  # 2
print(c.read("B", "strong", me))                   # 2
c.leader_up = False
print(c.read("B", "session", me), c.read("B", "eventual", {"seen": 0}))   # 2 1
try:
    c.read("B", "strong", me)
except TimeoutError as e:
    print("strong read:", e)                       # strong read: no leader to confirm with

With the leader down, the session read still succeeds because replica A is fresh enough; only the strong read stalls. Next, a one-pass checker for the two session anomalies, cross-checked against the pair-by-pair definitions on 3,000 seeded random runs with three replicas:

def session_anomalies(history):
    """One pass over a session's ops: ('w', version) or ('r', version)."""
    wrote = read = 0
    ryw = mono = False
    for kind, v in history:
        if kind == "w":
            wrote = max(wrote, v)
        else:
            ryw |= v < wrote                       # older than my own write
            mono |= v < read                       # older than something I already read
            read = max(read, v)
    return ryw, mono

def session_anomalies_brute(history):
    """The definitions, pair by pair."""
    pairs = [(a, b) for i, a in enumerate(history) for b in history[i + 1:] if b[0] == "r"]
    return (any(a[0] == "w" and b[1] < a[1] for a, b in pairs),
            any(a[0] == "r" and b[1] < a[1] for a, b in pairs))

import random
rng = random.Random(32)
ok, seen = True, {"eventual": [0, 0], "session": [0, 0]}
for _ in range(3_000):
    for mode in ("eventual", "session"):
        c, me, history = Cluster(("A", "B", "C")), {"seen": 0}, []
        for _ in range(rng.randint(1, 12)):
            step = rng.random()
            if step < 0.3:
                v = c.write("x")
                me["seen"] = max(me["seen"], v)
                history.append(("w", v))
            elif step < 0.6:                       # replication moves forward only
                r = rng.choice("ABC")
                c.catch_up(r, rng.randint(c.applied[r], len(c.log)))
            else:
                history.append(("r", c.read(rng.choice("ABC"), mode, me)))
        fast = session_anomalies(history)
        ok &= fast == session_anomalies_brute(history)
        seen[mode] = [n + f for n, f in zip(seen[mode], fast)]
print(ok, seen)   # True {'eventual': [1765, 166], 'session': [0, 0]}

Eventual reads broke read-your-writes in 1,765 runs and monotonic reads in 166; the version token broke neither. Last, causal delivery, checked exhaustively over every arrival order of four writes where a thanks depends on an answer, which depends on a question:

from itertools import permutations

deps = {"question": [], "answer": ["question"], "thanks": ["answer"], "other": []}

def deliver(order, causal):
    """Apply writes as they arrive; causal mode holds one until its causes are in."""
    visible, held, states = set(), [], []
    for w in order:
        held.append(w)
        progress = True
        while progress:
            progress = False
            for h in held:
                if not causal or all(d in visible for d in deps[h]):
                    visible.add(h); held.remove(h); progress = True
                    states.append(set(visible))
                    break
    return states

def broken(states):                                # an effect visible without its cause
    return any(d not in s for s in states for w in s for d in deps[w])

orders = list(permutations(deps))                  # every arrival order: 24
print(sum(broken(deliver(o, causal=False)) for o in orders),
      sum(broken(deliver(o, causal=True)) for o in orders),
      all(deliver(o, causal=True)[-1] == set(deps) for o in orders))   # 20 0 True

Applying on arrival shows an effect before its cause in 20 of 24 orders; holding writes back shows none, and every replica still ends with all four.

The complexity

  • Eventual reads: one replica, no coordination, O(1) network hops.
  • Session reads: the same, plus a version comparison; occasionally a redirect to a fresher replica.
  • Causal: metadata per write (dependencies or vector clocks) and buffering on replicas.
  • Strong reads: a round trip to the leader or a quorum on every read, and no answer while it is unreachable.

Where it goes wrong

  • Calling eventual consistency "wrong". Stale is not corrupt; the question is whether a user can notice.
  • Session tokens that do not travel. Read-your-writes breaks across devices unless the version token goes with the user, not the browser tab.
  • Assuming a cache is consistent. A cache in front of a strong database serves eventual reads.
  • Strong everywhere. Paying a round trip on every read of data nobody minds seeing a second late.
  • Not writing it down. The next team assumes a stronger model than you built.

When it shows up in interviews

As "strong vs eventual consistency?", as follow-ups in replication and key-value store designs ("what does a user see right after posting?"), and in any case where money or inventory must never be double-counted, which is where strong reads earn their cost.

How to say it in an interview

"Consistency models say what a read may return while replicas disagree. Eventual means copies converge but a read can be stale. Session guarantees fix what users notice: read-your-writes and monotonic reads, done cheaply by having the client carry the highest version it has seen so only a fresh-enough replica answers. Causal keeps replies behind the messages they answer. Strong, or linearizable, behaves like one copy but costs a leader or quorum round trip per read and stalls when that's unreachable. I'd choose per feature: read-your-writes for a user's own posts, strong reads for balances and stock, eventual for counts and feeds."