CAP Made Countable: a Partition Costs You 12 Refusals or 3 Conflicts
Two regions, one inter-region link, the same workload replayed twice. Cut the link and the CP (single-leader) deployment keeps every replica consistent but the follower side refuses 12 of 20 requests; the AP (multi-leader) deployment serves all of them but 3 keys diverge and must be reconciled on heal. You don't “pick 2 of 3” — the partition happens either way, and you only choose availability XOR consistency while it lasts.
Concept
You do not "pick 2 of 3." Two regions, R1 and R2, each a client and a replica, joined by one inter-region link. The link goes down — a partition, an event imposed on you, not a setting you toggled. Replay the same fixed workload against two deployments of the same two regions: (a) CP — single-leader, the leader in R1; and (b) AP — multi-leader, both regions write locally. While partitioned, the CP deployment keeps every replica consistent but R2 (the non-leader side) must refuse every write and linearizable read — 12 of 20 requests turned away, zero divergence. The AP deployment serves every request at both regions — 0 refusals — but the two states drift apart, and on heal 3 keys conflict and need reconciliation. Same partition, same workload; you chose availability XOR consistency, and each choice cost something countable.
Provenance
Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 10 — Consistency and Consensus, §"The Cost of Linearizability" / §"The CAP theorem" / §"The Unhelpful CAP Theorem". The claim under test, paraphrased:
When a network partition occurs, a system must choose between linearizability and availability — it cannot have both while the partition lasts.
The book is emphatic that CAP is not "pick two of Consistency, Availability, Partition tolerance." Partitions are a fault you cannot opt out of; the theorem only forces a choice — consistent OR available — while a partition is happening. Here you build the two regions, cut the link, and measure both costs.
Prerequisites
The surprise is that "partition tolerance" is not a thing you choose — the partition chooses you, and then you pay one of two measurable bills. Each line notes where it shows up.
- Linearizability — the strong-consistency model where every read returns the most recent write, as if there were a single copy. A follower cut off from the leader cannot promise this, which is why R2 must refuse linearizable reads in the CP run.
- The CAP theorem's actual statement — under a network Partition you get Consistency or Availability, not both; it says nothing about a partition-free system. It shows up as the two deployments giving up opposite things during the same outage.
- Single-leader vs multi-leader replication — one writable copy (all writes funnel to the leader) versus many (every region writes locally). This is the CP/AP switch: single-leader ⇒ CP, multi-leader ⇒ AP.
- "CA" is not a real operating point — you can't choose to not tolerate partitions; a partition still happens, and a "CA" system just fails when it does. There is no third column to pick, only C-or-A once the partition is here.
Where to learn the prerequisites
Linearizability & the CAP theorem (#1, #2, #4): DDIA §"Linearizability", §"The Cost of Linearizability", §"The CAP theorem", and §"The Unhelpful CAP Theorem"; Gilbert & Lynch, "Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services" (2002) for the formal proof; Brewer, "CAP Twelve Years Later" (2012) for why "2 of 3" misleads. Single- vs multi-leader (#3): DDIA Ch. 6 §"Leaders and Followers" and §"Multi-Leader Replication". If only one idea carries the result, make it this: the partition is not a choice. Everything else follows — because you cannot decline the partition, the only decision left is which of consistency or availability to surrender while it lasts.
Environment these numbers came from
Deterministic, so the counts reproduce exactly. Captured on macOS 26.5.2 (Darwin 25.5.0), arm64, Python 3.15.0a8, standard library only — no Docker, no deps. No real network: the two regions, the link, and the partition are simulated in one process, so the result is fixed every run. Fixed workload (23 requests: 3 at R1, 20 at R2), RNG seeded (Random(1010)) so R2's request interleaving reproduces byte-for-byte.
Mental model
A partition is an event imposed on you, not an option. Once the link is down, the same two regions behave differently depending only on how writes are routed.
- CP — refuse service on the wrong side to stay consistent. The leader is in R1. R2 is a follower; with the leader unreachable it cannot accept writes (no one to order them) and cannot serve linearizable reads (it can't confirm it holds the latest). So it refuses them. It may still answer stale reads from its own frozen copy. Consistency is preserved — only the leader ever wrote, so nothing diverges — at the cost of availability on R2.
- AP — serve everywhere and reconcile later. Both regions are leaders; each answers every request from its local replica. Nothing is refused. But each side writes without the other seeing it, so their states diverge; on heal, any key written on both sides with different values is a conflict to reconcile.
Both deployments face the identical partition and replay the identical workload. The whole thing is one script, code/cap_partition_tradeoff.py.
Setup
cd study/ddia/ch10/code
python3 cap_partition_tradeoff.py
Do not read the output yet — make each prediction first.
Step 1 · the partition and the workload
Predict. The link between R1 and R2 is cut, and 23 requests arrive during the outage: 3 at R1, 20 at R2. Is "partition tolerance" something either deployment gets to decline — can a well-designed system just avoid this situation?
Can a well-designed system decline the partition?
======================================================================
CAP, made countable -- two regions, one link, the SAME workload twice
======================================================================
R1 = {client, replica} R2 = {client, replica} link: DOWN (partition)
workload: 23 requests during the partition (3 at R1, 20 at R2)
The partition is NOT a setting we chose -- the link is cut. Both
deployments below face the identical outage; they differ only in what
they give up while it lasts.
The partition is not on the menu. It has already happened to both deployments. The only thing left to decide is what to give up while it lasts.
Step 2 · CP: count the refusals
Predict. Single-leader, leader in R1. Of R2's 20 requests during the partition, how many must be refused? (R2's stream is 7 writes, 5 linearizable reads, and 8 stale-tolerant reads.) And how many keys will have diverged when the link heals?
How many of R2's 20 requests are refused, and how many keys diverge?
----------------------------------------------------------------------
(a) CP -- single-leader, leader in R1 => choose CONSISTENCY
----------------------------------------------------------------------
R2 is a follower cut off from the leader. It CANNOT serve writes or
linearizable reads, so it refuses them; it still serves stale reads.
R2 requests refused (writes + linearizable reads): 12 of 20
R2 requests served (stale-tolerant reads only): 8 of 20
example stale read at R2: read('c') -> ('v0', v0) (leader has moved on; R2 is behind)
keys diverged (need reconciliation on heal): 0
COST: 12 refused requests. availability sacrificed on the minority side; consistency intact.
12 of 20 refused, 0 diverged. The 7 writes and 5 linearizable reads all need the leader, which R2 can't reach, so they're turned away; only the 8 stale reads are served — and those come back behind (c still reads v0 while the leader has moved on). Consistency is perfect precisely because R2 refused to write.
Step 3 · AP: count the conflicts
Predict. Same partition, same 23 requests, but now multi-leader — both regions write locally. How many requests are refused this time? And after the link heals, how many keys will have diverged and need reconciliation? (R1 wrote x, y, z; R2 wrote x, y, z, w, a, b, c.)
How many requests are refused, and how many keys diverge?
----------------------------------------------------------------------
(b) AP -- multi-leader, both regions accept writes => choose AVAILABILITY
----------------------------------------------------------------------
Every request is served locally at whichever region issued it.
Nothing is refused -- but the two replicas drift apart.
requests refused: 0 of 23
requests served: 23 of 23
keys diverged (need reconciliation on heal): 3 -> ['x', 'y', 'z']
key 'x': R1 has ('r1', v1) R2 has ('r2', v1) <- conflict
key 'y': R1 has ('r1', v1) R2 has ('r2', v1) <- conflict
key 'z': R1 has ('r1', v1) R2 has ('r2', v1) <- conflict
COST: 3 conflicting keys to reconcile. consistency sacrificed; availability intact.
0 refused, 3 diverged. Full availability — every request answered — but x, y, and z were each written on both sides during the partition, so both hold version 1 with different values (r1 vs r2). The count of conflicts is exactly the keys the two regions touched in common. w, a, b, c were written only at R2, so they don't conflict.
Step 4 · the takeaway
Predict. Line the two costs up. Was there any deployment that paid neither cost — no refusals and no divergence — during the partition?
Did any deployment pay neither cost?
======================================================================
TAKEAWAY
======================================================================
CP: refused = 12 diverged keys = 0
AP: refused = 0 diverged keys = 3
The partition happened to BOTH deployments -- it was never optional.
You did not 'pick 2 of 3'. You chose, WHILE partitioned, between
refusing service (CP) and diverging (AP), and each cost is countable.
No free deployment exists. CP paid 12 refusals to keep 0 divergence; AP paid 3 conflicts to keep 0 refusals. One of the two bills always comes due while the link is down — that is the whole content of CAP.
What you should see
- Step 1: the partition is imposed on both deployments — 23 requests arrive during an outage neither chose.
- Step 2 (CP): 12 of 20 R2 requests refused (7 writes + 5 linearizable reads); 8 stale reads served (one returns
v0, behind the leader); 0 keys diverged.
- Step 3 (AP): 0 of 23 refused; 3 keys (
x, y, z) diverged and need reconciliation.
- Step 4: CP = {refused 12, diverged 0}; AP = {refused 0, diverged 3}. No deployment paid neither cost.
Why
From first principles: while the link is down, R2 is a node that has lost contact with the rest of the system. Ask it to honor a write or a linearizable read and it is trapped between two things it cannot both do — (i) answer with the latest value, which it may not have, since a newer write could exist on the far side of the partition it cannot see; and (ii) answer at all. It can guarantee at most one. If it insists on (i), correctness, it must sometimes refuse to answer — that is CP, and the refusals are the 12 requests R2 turned away. If it insists on (ii), always answering, it must sometimes answer with a value that another region has concurrently overwritten — that is AP, and the divergence is the 3 keys both sides wrote. There is no third door, because the node genuinely does not know what happened on the other side of the cut. Gilbert & Lynch's proof is exactly this impossibility formalized; the script just makes each horn of it countable.
Notice the two costs are duals of the same fact. CP refuses R2's writes, so only the leader ever mutates state, so on heal there is nothing to reconcile — 0 divergence is bought with 12 refusals. AP accepts every write, so nothing is refused, so both sides advance x, y, z independently — 0 refusals is bought with 3 conflicts. Move the cost from one column and it reappears in the other; you never make it disappear, because the partition — the thing that severed R2 from the truth — was never yours to remove.
The boundary — bring the link UP and you get both
CAP forces the choice only during a partition. With the link healthy, R2 reaches the leader on every request: the CP deployment serves all 20 of R2's requests linearizably (0 refusals) and keeps 0 divergence — consistency and availability at once. The trade-off is not a permanent property of the system; it is a property of the failure mode. And "available" here has a specific technical meaning: every non-failing node returns a non-error response. It does not mean "fast," and it does not promise the node that is partitioned away stays up — only that nodes still running keep answering.
Go deeper
- PACELC — the else-case. CAP only speaks about the partition. PACELC adds: Else (no partition), you still trade Latency against Consistency. Make R2's linearizable reads in the healthy case pay a round-trip to the leader and measure the added latency; that is the "EL vs EC" knob CAP is silent about, and it's the trade you actually pay 99.9% of the time.
- Reconcile the AP conflicts. Add a merge step on heal — last-write-wins by region-stamped timestamp, then a version-vector sibling set — and watch LWW silently drop one region's write while the version vector preserves both as conflicts for the app to resolve (the Ch. 6 CRDT/sibling machinery).
- Shrink the minority. Make R2 a 1-node minority of a 5-node CP cluster whose other 4 nodes are on R1's side. Predict which side keeps serving writes (the majority that can still form a quorum) and confirm the minority is the side that must refuse — CP availability follows the quorum, not the geography.
Sources: DDIA 2e, Ch. 10, §"The Cost of Linearizability", §"The CAP theorem", §"The Unhelpful CAP Theorem" · Gilbert & Lynch, "Brewer's Conjecture..." (2002) · Brewer, "CAP Twelve Years Later" (2012).