Measure the Dekker pattern and the price of r + w ≥ d
Simulate a modelBreak it on purposeNeeds Python
Problem
Sequential consistency means all clients see operations in one order consistent with each client's own program order. Between two regions a message takes d to arrive, and that sets a floor: a read's latency r plus a write's latency w must be at least d, or a client can act on information the other region has not received. The Dekker test shows it. A writes x = 1 then reads y; B writes y = 1 then reads x. In any single order, at least one of them must see the other's 1. With local reads and local writes, both can read 0.
Simulate two "regions" as two threads or two processes with a message delay d between them (for example time.sleep(0.08) per message). Each holds a copy of x and y, both 0. Run the Dekker test: client A writes x = 1 then reads y; client B writes y = 1 then reads x. First use local reads and local writes: count how often both reads return 0 (it should be nearly every run). Then make writes wait for an acknowledgement from the other copy and count again (it should drop to 0). Record the latency of read and write in each variant and check that r + w is at least d in every variant that never shows the forbidden outcome.
The script also tries a write that sleeps d (plus 5 ms) instead of waiting for the acknowledgement.
The question. How often do both clients read 0 with local operations, with a write that sleeps d, and with a write that waits for the other copy's acknowledgement, and what is r + w in each?
Predict
Expected: with fully local operations, (0, 0) appears almost always. A version that sleeps D inside the write before returning removes it.
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
measure-the-dekker-pattern-and-the.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Measure the Dekker pattern and the latency floor. Two "regions" with a one-way message delay d. Client A writes x = 1 then reads y; client B writes y = 1 then reads x. With local reads and writes, both can read 0, which no single copy could produce; making the write wait for the other copy removes it, and the latency it costs is at least d. """ import argparse, json, threading, time p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--delay", type=float, default=0.08, help="d: one-way message delay between the regions, seconds.") p.add_argument("--runs", type=int, default=100, help="Runs per variant.") p.add_argument("--margin", type=float, default=0.005, help="Seconds a waiting write adds to d, so it does not race the message arriving at the same instant.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() D = args.delay def trial(mode): rep = {"A": {"x": 0, "y": 0}, "B": {"x": 0, "y": 0}} acked = {"A": threading.Event(), "B": threading.Event()} def send(to, frm, k, v): # asynchronous replication with an ack back after d def deliver(): rep[to][k] = v threading.Timer(D, acked[frm].set).start() threading.Timer(D, deliver).start() out, lat = {}, {} def client(me, other, mine, theirs): t0 = time.perf_counter() rep[me][mine] = 1; send(other, me, mine, 1) if mode == "sync write": acked[me].wait() # the write returns once the other copy has it (2d round trip) if mode == "write waits d": time.sleep(D + args.margin) # the write returns after one delay t1 = time.perf_counter() out[me] = rep[me][theirs] lat[me] = (t1 - t0, time.perf_counter() - t1) a = threading.Thread(target=client, args=("A", "B", "x", "y")) b = threading.Thread(target=client, args=("B", "A", "y", "x")) a.start(); b.start(); a.join(); b.join() time.sleep(2.2 * D) # let in-flight messages land before the next trial return (out["A"], out["B"]), lat rows = [] for mode in ("local", "write waits d", "sync write"): both_zero, w, r = 0, [], [] for _ in range(args.runs): res, lat = trial(mode) both_zero += res == (0, 0) for wl, rl in lat.values(): w.append(wl); r.append(rl) rows.append({"variant": mode, "both_zero": both_zero, "both_zero_pct": both_zero / args.runs * 100, "write_ms": sum(w) / len(w) * 1000, "read_ms": sum(r) / len(r) * 1000, "r_plus_w_ms": (sum(w) + sum(r)) / len(w) * 1000}) x = rows[-1] print(f"{mode:<14} (0,0) in {both_zero:>3} of {args.runs} runs write {x['write_ms']:6.1f} ms read {x['read_ms']:5.2f} ms " f"r+w {x['r_plus_w_ms']:6.1f} ms (d = {D * 1000:.0f} ms)") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"variants": rows, "summary": [{"d_ms": D * 1000}]}}, 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 measure-the-dekker-pattern-and-the.py uv run measure-the-dekker-pattern-and-the.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --delay0.08 | d: one-way message delay between the regions, seconds. |
| --runs100 | Runs per variant. |
| --margin0.005 | Seconds a waiting write adds to d, so it does not race the message arriving at the same instant. |
Our run
The Dekker test, 100 runs each, d = 80 ms
| Write | Runs with (0, 0) | w (ms) | r (ms) | r + w (ms) |
|---|---|---|---|---|
| local | 100 | 0.198 | 0.002 | 0.199 |
| write waits d | 5 | 91.7 | 0.003 | 91.7 |
| sync write | 0 | 174 | 0.003 | 174 |
local (0,0) in 100 of 100 runs write 0.2 ms read 0.00 ms r+w 0.2 ms (d = 80 ms)
write waits d (0,0) in 5 of 100 runs write 91.7 ms read 0.00 ms r+w 91.7 ms (d = 80 ms)
sync write (0,0) in 0 of 100 runs write 173.9 ms read 0.00 ms r+w 173.9 ms (d = 80 ms)What we found
With local reads and writes, both clients read 0 in 100 of 100 runs, with r + w of 0.199 ms, as expected. Waiting for the other copy's acknowledgement removed it completely (0 runs), at r + w = 174 ms: a full round trip, comfortably above d.
Sleeping d inside the write did not quite remove it: 5 runs still showed (0, 0), even with r + w = 91.7 ms. A sleep is not an acknowledgement. Under load, a message can take longer than d to be applied, and a write that only waits for the expected delay returns before the other copy has the value. r + w ≥ d is necessary, not sufficient: the write has to know the other side has it.
Read it
r + w ≥ d: local reads and local writes together are impossible for sequential consistency; the network delay is a conserved total. A design chooses where to pay it. Reads in Singapore can be local only if writes pay the cross-region delay, or the other way round.
The failed variant is the general lesson about timing: a protocol that assumes a delay bound is correct only while the bound holds. Waiting for an acknowledgement replaces an assumption about time with a fact about order, and that is the version that survives a slow network.