---
title: 'Consistency Models'
source: 'https://academia.sh/en/courses/introduction-to-system-design/consistency-models'
course: 'Introduction to System Design'
language: en
updated: '2026-08-23T07:01:26+00:00'
license: 'CC BY-SA 4.0'
---

# Consistency Models

Separating strong, weak, and eventual consistency: stating that distributed consistency is a different question from transaction isolation, measuring the same event sequence read under three models for the number of times two clients see different values, the number of rounds needed to converge, and the number of monotonic-read violations, and showing that sticky routing fixes monotonic reads while widening the gap between clients.

The previous lesson collapsed the staleness window to a single number: 0 or `D` rounds. That
number is a window's width; it says nothing about what can happen inside it. In the same
`D`-round window, two clients querying the same shipment can see different statuses; one can see
a sequence that goes backward instead of forward; a third can fail to find the event it just
wrote on its next query. These are separate phenomena, and none substitutes for another.

What defines the inside of the window is called a **consistency model**: what the system promises
about reads. This lesson names three models and reads the same event sequence with all three,
counting the difference between their promises.

## A Model, Not an Isolation Level

The word consistency has appeared in two separate places in this repository. In the Data Modeling
and Relational Theory course it was one of the ACID properties; in the Advanced SQL course it was
the measure of isolation levels. The question here is different, and neither substitutes for the
other: transaction isolation governs whether concurrent transactions see each other's half-finished
work; distributed consistency governs when an acknowledged write becomes visible across replicas.
A system can run the strictest isolation level while being only eventually consistent; the two
are chosen separately. Isolation levels are not retold here.

How replication works was also established in the Relational Database Administration course.
What gets measured here is the promise sitting on top of that same replication.

## Three Models

**Strong consistency**: every read sees the most recently acknowledged write. The system looks
single-copy from the outside. Its cost is that the local replica verifies its held value is the
newest.

**Eventual consistency**: a read may not see the latest write, but once writing stops, all
replicas are guaranteed to converge on the same value. The term was introduced in the Data
Modeling and Relational Theory course; what is added here is that convergence is not automatic —
it requires a repair mechanism that resends a lost propagation.

**Weak consistency**: no promise is made at all. A read may see a stale value, and replicas may
never converge. This is not a looser form of eventual consistency; it is the absence of the
convergence guarantee, and the difference is measurable.

## Three-Replica Setup

The model sets up a three-zone replica set. Writes arrive at zone `b34` and propagate to `b35`
and `b06`; the propagation latencies and a loss rule are model parameters. Two clients read the
same tracking number every round and are routed to different zones in turn.

```js
// consistency/model.mjs — three-replica in-process model. A round is an abstract step; latency,
// the loss rule, and the repair interval are model parameters, not measured durations.
const LATENCY = { b35: 1, b06: 3 };          // rounds for a propagation to reach its target
const LOST = (write) => write % 5 === 0;     // propagation to b06 is lost on these writes
const REPAIR = 2;                            // resend interval in the repaired models
const ROUTE = { A: ["b34", "b35", "b06"], B: ["b06", "b35", "b34"] };
const STICKY = { A: ["b35"], B: ["b06"] };   // sticky routing: the client stays on one replica

export function run({ rounds: N, writeEnd: W, model, sticky = false }) {
  const version = { b34: 0, b35: 0, b06: 0 };
  const seen = { A: 0, B: 0 };
  const trace = { A: [], B: [] };
  let flight = [], messages = 0, differing = 0, violation = 0, converged = null;

  for (let round = 1; round <= N; round++) {
    if (round <= W) {
      version.b34 = round; // the carrier event is written to b34, the carrier is acknowledged right away
      for (const h of ["b35", "b06"]) {
        messages += 1;
        flight.push({ target: h, write: round, arrives: round + LATENCY[h], lost: h === "b06" && LOST(round) });
      }
    }
    const arriving = flight.filter((m) => m.arrives === round);
    flight = flight.filter((m) => m.arrives !== round);
    for (const m of arriving) {
      if (m.lost === false) { version[m.target] = Math.max(version[m.target], m.write); continue; }
      if (model === "weak") continue; // a loss is not repaired: there is no convergence guarantee
      messages += 1;
      flight.push({ ...m, arrives: round + REPAIR, lost: false });
    }

    const read = (client) => {
      const route = sticky ? STICKY[client] : ROUTE[client];
      const zone = route[(round - 1) % route.length];
      if (model !== "strong") return version[zone];
      if (zone !== "b34") messages += 2; // the local replica verifies the latest write with b34
      return version.b34;
    };
    const value = { A: read("A"), B: read("B") };
    if (value.A !== value.B) differing += 1;
    for (const c of ["A", "B"]) {
      if (value[c] < seen[c]) violation += 1;
      seen[c] = Math.max(seen[c], value[c]);
      trace[c].push(value[c]);
    }
    if (round > W && converged === null && version.b34 === version.b35 && version.b35 === version.b06) {
      converged = round - W;
    }
  }
  return { differing, violation, messages, converged, version, trace };
}
```

The three models are separated in the code by two lines: whether a lost propagation gets resent,
and whether a read is verified against `b34`. A **monotonic-read violation** is a client seeing a
version older than one it has already seen.

```js
// consistency/measure.mjs — the same event sequence read with three models: diff, violation, convergence, messages
import { run } from "./model.mjs";

const ROUNDS = 20, WRITE_END = 10;
const pad = (x, n) => String(x).padStart(n);
console.log(`${ROUNDS} rounds, one write per round through round ${WRITE_END}; no writes after that.`);
console.log("two clients read the same tracking number every round, routed to different replicas.");
console.log();
console.log("model           | differing val | monotonic viol. | converged     | msgs | final version b34/b35/b06");
console.log("----------------|---------------|------------------|---------------|------|--------------------------");
const CONFIGS = [["strong", false], ["eventual", false], ["eventual", true], ["weak", false]];
const results = {};
for (const [model, sticky] of CONFIGS) {
  const r = run({ rounds: ROUNDS, writeEnd: WRITE_END, model, sticky });
  const label = sticky ? `${model}+sticky` : model;
  results[label] = r;
  console.log(`${label.padEnd(15)} | ${pad(`${r.differing}/${ROUNDS}`, 13)} | ${pad(r.violation, 16)} | ` +
    `${pad(r.converged === null ? "not converged" : `${r.converged} round`, 13)} | ${pad(r.messages, 4)} | ` +
    `${r.version.b34}/${r.version.b35}/${r.version.b06}`);
}
console.log();
for (const label of Object.keys(results)) {
  console.log(`${label.padEnd(15)} A: ${results[label].trace.A.join(" ")}`);
  console.log(`${label.padEnd(15)} B: ${results[label].trace.B.join(" ")}`);
}
```

```sh
node consistency/measure.mjs
```

```
20 rounds, one write per round through round 10; no writes after that.
two clients read the same tracking number every round, routed to different replicas.

model           | differing val | monotonic viol. | converged     | msgs | final version b34/b35/b06
----------------|---------------|------------------|---------------|------|--------------------------
strong          |          0/20 |                0 |       5 round |   76 | 10/10/10
eventual        |          9/20 |                8 |       5 round |   22 | 10/10/10
eventual+sticky |         13/20 |                0 |       5 round |   22 | 10/10/10
weak            |         13/20 |               12 | not converged |   20 | 10/10/9

strong          A: 1 2 3 4 5 6 7 8 9 10 10 10 10 10 10 10 10 10 10 10
strong          B: 1 2 3 4 5 6 7 8 9 10 10 10 10 10 10 10 10 10 10 10
eventual        A: 1 1 0 4 4 3 7 7 6 10 10 9 10 10 10 10 10 10 10 10
eventual        B: 0 1 3 1 4 6 4 7 9 7 10 10 9 10 10 10 10 10 10 10
eventual+sticky A: 0 1 2 3 4 5 6 7 8 9 10 10 10 10 10 10 10 10 10 10
eventual+sticky B: 0 0 0 1 2 3 4 4 6 7 8 9 9 9 10 10 10 10 10 10
weak            A: 1 1 0 4 4 3 7 7 6 10 10 9 10 10 9 10 10 9 10 10
weak            B: 0 1 3 1 4 6 4 7 9 7 10 10 9 10 10 9 10 10 9 10
```

## Reading the Numbers

The strong row delivers exactly what it promises: the two clients never saw different values in
any round (0/20), monotonic violations were 0, and both traces are exactly the write sequence
itself. The cost shows up in the message column: 76 messages, roughly four times the cheapest
row. That count comes from reads, not writes — every read landing on the local replica adds a
round trip to `b34`. In a read-heavy flow, the cost grows with the number of reads.

The eventual row shows the limit of the promise. Convergence takes 5 rounds: five rounds after
writing stops, all three replicas settle on version 10. Convergence happened, but inside the
window the clients saw different values in 9 of 20 rounds and produced 8 monotonic violations.
The traces show this directly: client A starts `1 1 0` — in round three it gets a response that
looks as though it had never seen the shipment's status at all, because that round routed to zone
`b06`. For a tracking query, this means the status shown on screen goes backward.

The weak row's only difference is the absence of a mechanism, and the result shows in the last
column: the replicas stayed at `10/10/9`, and convergence never happened. The tenth write is lost
on its way to `b06` and never resent, so that zone shows the shipment one step behind forever.
The convergence guarantee costs two messages here (20 versus 22); what it buys is the absence of
a permanent divergence. Treating eventual consistency as a model with no guarantee at all means
missing those two messages.

## Session Guarantees

A monotonic violation can be fixed on an axis separate from the consistency model. In the
eventual+sticky row, the model stayed the same; the only change is that the client is routed to
the same replica every round — the read-path application of the **sticky routing** pattern
introduced in the Caching, Queues and Asynchronous Processing course. Violations dropped from 8
to 0, because a single replica's version number never goes backward. Promises of this kind are
called **session guarantees**; the two best known are monotonic reads (a client never sees an
older value than one it has already seen) and read-your-writes (a client sees its own write on
its next read).

The same row also says something second and more important: the differing-value count rose from
9 to 13. Once sticky routing pins a client to a single replica, A always reads `b35` and B always
reads `b06`, and because the two zones have different latencies, the two nearly diverge every
round. A session guarantee fixes what one client sees over time; it does not fix the gap between
clients — it can widen it.

This maps to two separate requirements in the tracking service. A user querying the same shipment
repeatedly needs the status not to go backward; sticky routing provides that. End-of-day billing
basing itself on the same instant for every shipment cannot be provided by a session guarantee —
that needs strong consistency, at the cost of a round trip per read. The next lesson ties these
two requirements to separate patterns.

## Summary

- Distributed consistency is a question separate from transaction isolation: one governs whether
  concurrent transactions see each other, the other governs when an acknowledged write shows up
  across replicas.
- In the same twenty-round sequence, strong produced 0/20 differing values and 0 violations at a
  cost of 76 messages; eventual produced 9/20 differing values and 8 violations at a cost of 22
  messages.
- Eventual consistency's guarantee is convergence, and it is not free: with repair removed (weak),
  the replicas stayed at 10/10/9 and never converged; repair cost two messages.
- Convergence took 5 rounds; during that time the promise is only "eventually the same," saying
  nothing about what reads will see.
- Sticky routing dropped monotonic violations from 8 to 0 but raised the differing-value count
  from 9 to 13; a session guarantee does not fix the gap between clients.

## Next Step

All three lessons up to this point carried the same implicit assumption: every replica is up, and
only the link or the latency between them causes trouble. When a replica goes down entirely, the
question changes: now a replacement must be found, requests must be routed to it, and the
requests dropped during that transition must be counted. How the transition is handled is
a choice of pattern: one replica takes writes while the other waits, or all of them take writes at
once. The next lesson measures these two patterns: the failover round and the requests dropped
during it in an active–passive setup, and the number of conflicting writes and the lost-write
count a resolution rule produces in an active–active setup.
