R + W > N is not recency
Simulate a modelNeeds Python
Problem
In a quorum system with N replicas, a write waits for W acknowledgements and a read asks R replicas. If R + W > N, every read set overlaps every write set, which is often described as strong consistency. It is less than that. A write that reaches only some replicas before failing (fewer than W acknowledgements, so the client is told it failed) still sits on those replicas. A read that happens to include one of them returns the new value; the next read, from a different pair, returns the old one. Reads can go backwards, and a write reported as failed can appear later.
Use a local 3-node Cassandra cluster (or a toy simulation of N = 3). Write with W = 2, read with R = 2, and make one write reach only one replica by pausing the other two with docker pause. Read twice in a row with random coordinator choice and count how often the second read returns an older value than the first (the monotonic-reads violation). Then repeat with read repair enabled, and with a client that always reads at ALL.
The script is the toy simulation the lab allows: one replica holds the failed write's version 2, the other two hold version 1.
The question. With a partially applied write, how often does a second read return an older value than the first, and do read repair or reading at ALL stop it?
Predict
Expected: with a partially applied write, back-to-back reads can go backwards; the failed write (acknowledged by fewer than W nodes) can still appear later.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
r-w-n-is-not-recency.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """R + W > N is not recency. A toy N = 3 replicated register (the chapter's simulation option). A write with W = 2 reaches only one replica before the others are paused, so it fails; reads with R = 2 and a random coordinator then go back and forth. Repeated with read repair, and with reads at ALL. """ import argparse, json, random p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--trials", type=int, default=10_000, help="Pairs of back-to-back reads per variant.") p.add_argument("--seed", type=int, default=0, help="Random seed.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() random.seed(args.seed) def setup(): # version 1 is on all three replicas; the write of version 2 reached only replica 0, then 1 and 2 were paused: # fewer than W = 2 acks, so the client was told the write failed. Replica 0 keeps version 2. return [2, 1, 1] def read(reps, r, repair): chosen = random.sample(range(3), r) newest = max(reps[i] for i in chosen) if repair: for i in chosen: reps[i] = newest # read repair: bring the stale replicas up to date return newest rows = [] for label, r, repair in (("R = 2", 2, False), ("R = 2 with read repair", 2, True), ("R = ALL", 3, False)): backwards, failed_seen = 0, 0 for _ in range(args.trials): reps = setup() a = read(reps, r, repair); b = read(reps, r, repair) backwards += b < a failed_seen += 2 in (a, b) rows.append({"variant": label, "backwards_pct": backwards / args.trials * 100, "failed_write_seen_pct": failed_seen / args.trials * 100}) print(f"{label:<24} second read older than the first: {backwards / args.trials:6.1%} " f"the failed write seen: {failed_seen / args.trials:6.1%}") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"variants": rows}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run r-w-n-is-not-recency.py uv run r-w-n-is-not-recency.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --trials10,000 | Pairs of back-to-back reads per variant. |
| --seed0 | Random seed. |
Our run
Two back-to-back reads after a failed write
| Reads | Second read older | Failed write seen |
|---|---|---|
| R = 2 | 21.8% | 88.9% |
| R = 2 with read repair | 0% | 89.2% |
| R = ALL | 0% | 100% |
R = 2 second read older than the first: 21.8% the failed write seen: 88.9%
R = 2 with read repair second read older than the first: 0.0% the failed write seen: 89.2%
R = ALL second read older than the first: 0.0% the failed write seen: 100.0%What we found
With R = 2, the second read returned an older value than the first in 21.8% of pairs, and the write the client was told had failed was seen in 88.9%. Read repair stopped the backwards reads (0%) but spread the failed write further: once a read found it, it was copied to the other replicas. Reading at ALL never went backwards and always saw the failed write (100%).
Read it
Record both observations.
R + W > N is an intersection argument about successful operations; it says nothing about what failed writes leave behind or about the order of two reads. Monotonic reads need a session token or a sticky replica; "a failed write never appears" needs the write to be atomic across replicas, which a quorum write is not. This lab is a toy simulation of the mechanism; a real Cassandra cluster adds hinted handoff and repair timing, which change how long the inconsistency lasts but not that it can happen.