---
title: 'Consistency Models'
source: 'https://academia.sh/en/courses/nosql/consistency-models'
course: 'Non-Relational Data Models'
language: en
updated: '2026-08-23T07:00:46+00:00'
license: 'CC BY-SA 4.0'
---

# Consistency Models

Translating combinations of settings into consistency models: measuring which read and write concern gives which model by running the same member session under six settings, separating the settings that preserve session guarantees using read-your-writes failures and monotonic-violation counts, defining causal consistency over two collections that sit on separate shards and counting reads where the causal link breaks, and translating each guarantee's cost into a number of turns waited.

The previous lesson's two tables counted settings but never named them. The promise `w=1` with a
`local` read gives is not the promise `w=majority` with a `majority` read gives, and the
combinations between the two promise separate things as well. The names for these promises —
strong consistency, eventual consistency, monotonic reads, read-your-writes — were defined and
measured in the Introduction to System Design course; the definitions are not repeated here.

This lesson's job is translation. A store does not select a consistency model; it selects a
setting. The triple of write concern, read concern, and read direction corresponds to a model
name, and whether it earns that name can be counted. This lesson builds that mapping, adds a model
off that list — causal consistency — and runs one member session under six settings.

## Causal Consistency

There is a wide range between eventual and strong consistency, and its most useful point is
**causal consistency**. Its promise: if a write was made after seeing another write's result,
every read that sees the second also sees the first; unlinked writes can be seen in different
orders. This is where it departs from strong consistency — it orders only the linked writes, not
all of them.

In the library database, the link is concrete: a loan record is written, and once acknowledged,
the overdue fine tied to it is written, citing it because that loan's existence was seen. The
break is one kind of read: **the fine is visible, the loan record it cites is not** — a fine whose
source cannot be found.

On a single replica set this cannot happen, because a secondary applies the primary's log in
order: a member that has applied the fine has also applied the loan. The break comes from the two
collections sitting on **separate shards** — the previous two lessons built this arrangement, the
shard key chosen per collection, each shard its own replica set with its own member distances. The
reading session reads the two collections from two different members; the ordering guarantee
lives inside a cluster, not between clusters.

## The Mechanism

The mechanism is again a model: no real cluster is built, a turn is an abstract step, member
distance is a parameter in turns.

**NS21 — member distances on the fine shard's replica set are 1, 2, 3, and 4 turns; the loan
shard's cluster is NS19's (3, 5, 6, 8 turns).** Reason: the fine collection is kept in a single
site, the loan collection is spread across branches. **NS22 — one loan transaction happens every
eight turns, and the fine record is sent one turn after its loan record's acknowledgement.**
Reason: a fine is written only after the record is seen processed. Both assumptions are linear: if
the distances grow, the breach window grows in the same proportion.

```js
// session/model.mjs — session guarantees and causality are a MODEL: a turn is an abstract step,
// member distance is a parameter in turns, no real cluster is built. The loan collection and the
// fine collection sit on SEPARATE SHARDS; each shard is its own replica set with its own
// distances. A fine record is sent AFTER the loan record is ACKNOWLEDGED: the link is causal.
export const DELAY = { loan: [0, 3, 5, 6, 8], fine: [0, 1, 2, 3, 4] };
export const OPS = 12, GAP = 8, TURNS = 108, MAJORITY = 3, SECONDARY = [1, 2, 3, 4];

// derived in Lesson 3: ack turns is twice the (w-1)th value in the secondaries' distance ranking
export const ackTurns = (shard, w) =>
  w <= 1 ? 0 : 2 * DELAY[shard].slice(1).sort((a, b) => a - b)[w - 2];

// The largest seq a member has seen as of turn t. A majority read is further bounded by the
// point where the write reaches a majority and the primary learns it (Lesson 3's commit point).
const visible = (writes, shard, member, t, read) => writes.reduce((s, y) =>
  y.sent + DELAY[shard][member] <= t
  && (read !== "majority" || y.sent + ackTurns(shard, MAJORITY) <= t) ? y.seq : s, 0);

// "primary" is a single member; "sticky" binds the session to the nearest secondary on both
// shards; "rotating" goes to a different secondary each turn, crossing the two shards in opposite
// directions. Every fourth turn the loan read jumps to the farthest secondary — where the monotonic violation appears.
const pickMember = (direction, t, shard) => direction === "primary" ? 0 : direction === "sticky" ? SECONDARY[0]
  : shard === "fine" ? SECONDARY[t % 4] : SECONDARY[(3 * t) % 4];

export function run({ w, read = "local", direction = "primary", token = false }) {
  const ack = ackTurns("loan", w), loan = [], fine = [];
  for (let j = 1; j <= OPS; j += 1) {            // the loan schedule is independent of the setting
    const g = 1 + (j - 1) * GAP;
    loan.push({ seq: j, sent: g, ack: g + ack });
    fine.push({ seq: j, sent: g + ack + 1 });
  }
  const sees = (y, p, u, t) => visible(y, p, u, t, read);
  // With the session token on, a read from a member that has not caught up is not answered: the
  // member is waited on until it does. The number of turns waited is the guarantee's cost.
  const catchUp = (y, p, u, t, target) => {
    let x = t;
    while (x <= TURNS && sees(y, p, u, x) < target) x += 1;
    return x - t;
  };

  let rywMiss = 0, writeWait = 0;                  // the writing session: reads its own record back at ack time
  for (const y of loan) {
    const t = y.ack, m = pickMember(direction, t, "loan");
    if (sees(loan, "loan", m, t) >= y.seq) continue;
    if (token) writeWait += catchUp(loan, "loan", m, t, y.seq); else rywMiss += 1;
  }

  // the reading session: each turn reads the fine collection first, then the loan record it cites
  let monoViol = 0, causalBreak = 0, stale = 0, readWait = 0, waited = 0, seenSeq = 0;
  for (let t = 1; t <= TURNS; t += 1) {
    const fc = sees(fine, "fine", pickMember(direction, t, "fine"), t);
    const m = pickMember(direction, t, "loan");
    let lo = sees(loan, "loan", m, t), tr = t;     // tr: the turn the answer is given
    if (token) {
      const b = catchUp(loan, "loan", m, t, Math.max(fc, seenSeq));
      if (b > 0) { waited += 1; readWait += b; tr = t + b; lo = sees(loan, "loan", m, tr); }
    }
    if (lo < seenSeq) monoViol += 1;
    if (fc > lo) causalBreak += 1;                 // the fine is visible, the loan it cites is not
    if (lo < loan.filter((y) => y.ack <= tr).length) stale += 1;  // measured against what was acked by answer time
    seenSeq = Math.max(seenSeq, lo);
  }
  return { rywMiss, monoViol, causalBreak, stale, wait: writeWait + readWait,
    writeWait, readWait, waited, ack };
}
```

```js
// session/measure.mjs — same member session under six settings: which setting gives which guarantee
import { run, ackTurns, DELAY, SECONDARY, OPS, GAP, TURNS, MAJORITY } from "./model.mjs";

const s = (x, n) => String(x).padStart(n);
const SETTING = [
  ["w=1 / local / rotating", { w: 1, direction: "rotating" }],
  ["w=1 / local / sticky", { w: 1, direction: "sticky" }],
  ["w=1 / local / rotating + token", { w: 1, direction: "rotating", token: true }],
  ["w=1 / local / primary", { w: 1, direction: "primary" }],
  ["w=majority / majority / primary", { w: MAJORITY, read: "majority", direction: "primary" }],
  ["w=majority / majority / rotating", { w: MAJORITY, read: "majority", direction: "rotating" }],
];
console.log(`loan shard member distances ${DELAY.loan.join("/")}, fine shard's ` +
  `${DELAY.fine.join("/")} turns.\n${TURNS} turns, one loan record every ${GAP} turns (${OPS} ops); ` +
  `the fine record is sent one turn\nafter its own loan record's ack.\n`);
console.log("setting (write concern / read concern / direction) | read-your- | monotonic | causal | stale  | ack  | wait");
console.log("                                                    |writes miss |  violation |  break | reads  | turns | (turns)");
console.log("-----------------------------------------------------|------------|------------|--------|--------|-------|--------");
for (const [name, choice] of SETTING) {
  const r = run(choice);
  console.log(`${name.padEnd(52)} | ${s(`${r.rywMiss}/${OPS}`, 10)} | ${s(r.monoViol, 10)} | ` +
    `${s(`${r.causalBreak}/${TURNS}`, 6)} | ${s(`${r.stale}/${TURNS}`, 6)} | ${s(r.ack, 5)} | ${s(r.wait, 6)}`);
}
const b = run({ w: 1, direction: "rotating", token: true });
console.log(`\nwait with the token on: ${b.writeWait} turns across ${OPS} read-backs, ` +
  `${b.readWait} turns in the reading session\n(${b.waited}/${TURNS} reads waited).`);

// Independent of the run: the fine being visible while its loan is not can only happen if the
// loan member's distance exceeds the fine member's distance by more than the gap between the two
// writes (ack turns + 1).
for (const w of [1, MAJORITY]) {
  const gap = ackTurns("loan", w) + 1;
  const n = SECONDARY.flatMap((lm) => SECONDARY.map((fm) =>
    DELAY.loan[lm] > DELAY.fine[fm] + gap)).filter(Boolean).length;
  console.log(`independent of the run: the gap between the two writes at w=${w === 1 ? "1" : "majority"} ` +
    `is ${gap} turns;\n${n} of ${SECONDARY.length ** 2} member pairs break causality regardless of direction.`);
}
const farthest = Math.max(...DELAY.loan), mc = ackTurns("loan", MAJORITY);
console.log(`independent of the run: the largest member distance is ${farthest} turns, the majority ack turns ${mc};` +
  `\nbecause ${farthest} < ${mc}, the value a majority read returns is independent of the member read from.`);
```

```
loan shard member distances 0/3/5/6/8, fine shard's 0/1/2/3/4 turns.
108 turns, one loan record every 8 turns (12 ops); the fine record is sent one turn
after its own loan record's ack.

setting (write concern / read concern / direction) | read-your- | monotonic | causal | stale  | ack  | wait
                                                    |writes miss |  violation |  break | reads  | turns | (turns)
-----------------------------------------------------|------------|------------|--------|--------|-------|--------
w=1 / local / rotating                               |      12/12 |         24 | 24/108 | 60/108 |     0 |      0
w=1 / local / sticky                                 |      12/12 |          0 | 12/108 | 36/108 |     0 |      0
w=1 / local / rotating + token                       |       0/12 |          0 |  0/108 | 47/108 |     0 |    156
w=1 / local / primary                                |       0/12 |          0 |  0/108 |  0/108 |     0 |      0
w=majority / majority / primary                      |       0/12 |          0 |  0/108 |  0/108 |    10 |      0
w=majority / majority / rotating                     |       0/12 |          0 |  0/108 |  0/108 |    10 |      0

wait with the token on: 96 turns across 12 read-backs, 60 turns in the reading session
(24/108 reads waited).
independent of the run: the gap between the two writes at w=1 is 1 turns;
12 of 16 member pairs break causality regardless of direction.
independent of the run: the gap between the two writes at w=majority is 11 turns;
0 of 16 member pairs break causality regardless of direction.
independent of the run: the largest member distance is 8 turns, the majority ack turns 10;
because 8 < 10, the value a majority read returns is independent of the member read from.
```

## Which Setting Preserves Session Guarantees

In row one, the loan desk cannot read its own writes at all: all 12 of 12 acknowledged writes were
missing from the read-back at the turn of acknowledgement. This does not depend on the run: under
`w=1`, ack turns is 0, while the nearest secondary is 3 turns behind, so a read-back to a secondary
never finds the record. On the loan screen, the member's loan list appears empty right after the
book was handed over.

The same row has 24 monotonic violations: routing is rotating, so every fourth turn the read jumps
from the nearest secondary to the farthest, which has not yet applied the write, and the loan
count on screen goes backward.

Row two applies the Caching, Queues and Asynchronous Processing course's sticky routing to reads:
the session binds to one secondary. Monotonic violations drop from 24 to 0, since a member's
sequence number never goes backward; stale reads drop from 60 to 36. The one unchanged column is
read-your-writes, still 12/12 — stickiness binds the client to a secondary, and that secondary is
still behind the primary.

Row four gives both session guarantees for free: 0/12, 0 violations, 0 stale reads, 0 turns
waited — pulling the read direction to the primary is the cheapest path to them. This setting's
cost does not show here; the previous lesson measured it as 3 unanswered requests during the
outage window and 3 reads that saw data later rolled back.

## The Causal Link Breaking

The causal-break column shows where session guarantees fall short. Under rotating, 24 of 108
reads saw the fine while the loan record it cited was not visible; under sticky that falls to 12
but does not zero out, even though the same row shows 0 monotonic violations. This divergence is
the lesson's main result: sticky routing preserves a member's own internal ordering, not the link
between two shards' separate clusters. The fine shard's nearest secondary is 1 turn behind, the
loan shard's is 3, and the fine write is sent only 1 turn after its loan's acknowledgement; the
two-turn gap produces a break on every transaction.

Row three shows the setting that closes the link. The session carries the furthest point it has
seen in a session token; a read is not answered from a member lagging behind that point — it
waits until the member catches up. All three violation columns drop to 0. The cost is in the wait
column: 156 turns, 96 across 12 read-backs and 60 in the reading session, with 24 of 108 reads
waiting.

The same row's stale-reads column stays at 47/108, and this is not a flaw but the definition:
causal consistency says the answer will not go backward, not that it will be current.

The final two run-independent lines give the break's structure. A break happens when the loan
member's distance exceeds the fine member's distance by more than the gap between the writes.
Under `w=1` this gap is 1 turn, and 12 of 16 member pairs break causality regardless of direction.
Under `w=majority` the gap widens to 11 turns and the count falls to 0 — raising the
acknowledgement level does not lower the probability of a break, it removes it structurally.

## Translating the Setting Into a Model Name

| Setting | Model given | Measured counterpart |
|---|---|---|
| `w=1`, `local`, rotating secondary | eventual consistency | 12/12 miss, 24 violations, 24/108 breaks |
| `w=1`, `local`, sticky secondary | eventual consistency and monotonic reads | 0 violations, breaks still at 12/108 |
| `w=1`, `local`, session token | causal consistency | all three at 0, 156 turns waited, 47/108 stale |
| `w=majority`, `majority` | strong consistency | every column 0, 10 turns of ack per write |

The last two rows give the same numbers: a majority read returns the same value from the primary
or a rotating secondary. The reason is independent of the run — the largest member distance is 8
turns, the majority ack turns is 10, and 8 < 10 means no member can lag behind the commit point.
A member 12 turns away in a distant site would break this inequality, making a majority read
depend on which member it was read from.

That rows four and five cannot be told apart is a limit of the table, not the settings. While the
cluster is healthy, `primary` + `local` gives all of strong consistency's zeros for free. The
previous lesson measured the difference: when the primary failed, `w=1` produced 3 lost writes,
`w=majority` produced 0 — the 10 turns of ack per write is the price of that difference.

## Summary

- A store does not select a consistency model, it selects a setting: the write concern, read
  concern, and read direction triple corresponds to a model name, and whether it earns that name
  can be counted.
- Under `w=1`, every setting reading from a secondary produced read-your-writes failures at
  12/12; this does not depend on the run, since ack turns is 0 while the nearest secondary is 3
  turns behind.
- Sticky routing brought monotonic violations from 24 to 0 but causal breaks only from 24 to 12:
  a session guarantee preserves ordering inside one member, not the link between two shards'
  clusters.
- Causal consistency was achieved with a session token; all three violation columns dropped to 0,
  its cost was 156 turns waited, and stale reads stayed at 47/108.
- Pulling the acknowledgement level to the majority widened the gap between the two writes from 1
  turn to 11 turns and brought breaking member pairs from 12 to 0; a structural change, not a
  change in probability.
- On a healthy cluster, `primary` + `local` gave the same zeros as strong consistency for free;
  the difference shows only in the outage window, and its price is 10 turns per write.

## Next Step

The measurements so far assumed the cluster was whole: members could see each other, and only
delay changed. When the link breaks, the same settings mean something different: `w=majority`
stops writes on the side that cannot assemble a majority, while a `local` read keeps answering.
This trade-off was built and measured in the Introduction to System Design course under the names
CAP and PACELC. In the context of a store, though, it is misstated: a store is said to be "CP" or
"AP," as if that were a product property. The next lesson corrects this: the same cluster runs
the same split under more than one setting, and the label is counted as a property of the
setting, not the store. The 10 turns of ack measured here are paid even on a healthy cluster —
that is the bill outside any split.
