Skip to content
About
Main content

R + W > N is not recency

Simulate a modelNeeds Python

Problem

In a system with N , a write waits for W acknowledgements and a read asks R replicas. If R + W > N, every 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 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

With R = 2 and a partially applied write, how often does a second read return an older value than the first?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    r-w-n-is-not-recency.py51 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat 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

Two back-to-back reads after a failed write
ReadsSecond read olderFailed write seen
R = 221.8%88.9%
R = 2 with read repair0%89.2%
R = ALL0%100%
Reference runApple M2 · 8 cores · 8 GB
Output · uv run r-w-n-is-not-recency.py
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%
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

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. 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 and repair timing, which change how long the inconsistency lasts but not that it can happen.