S070 · CAP & PACELC — the Actual Trade-Offs
The three-letter theorem everyone quotes wrong, and the five-letter one that fills in the gap. Consistency, Availability, Partition-tolerance — and what happens the 99.9% of the time your network is fine.
🎯 Explain CAP and PACELC accurately (not the interview cliché), classify real databases against them, and pick a consistency model given a workload.
Why this session exists
"CAP theorem" is the most cargo-culted phrase in distributed systems. Almost every explanation you'll hear online — "you can only have two of three!" — is wrong. This session unpacks what Brewer actually proved, what modern practitioners (Abadi's PACELC) added, and how to use both to reason about real systems like DynamoDB, Cosmos, MongoDB, Cassandra, and Spanner.
- State CAP precisely: during a partition, you must choose between Consistency and Availability.
- Explain PACELC: even when NOT partitioned, systems trade Latency for Consistency.
- Classify DynamoDB, Cosmos, Cassandra, MongoDB, Spanner, Postgres against CAP + PACELC.
- Pick a consistency level (linearizable / bounded staleness / session / eventual) for a given workload.
Prerequisites
- S042 · Transactions & ACID — you know what atomicity + isolation mean.
- S062 · Load balancers — health checks + failover mental model.
- S063 · Caching — TTL + staleness trade-offs at small scale.
(a) Intuition · 5 min
You have $1000. You visit ATM-A and withdraw $600. Simultaneously, your partner visits ATM-B and withdraws $600. Both ATMs used to sync via a landline. This morning, the line is down.
Two choices for the bank: (1) refuse both withdrawals — consistent but unavailable — or (2) let both go through and reconcile later, resulting in a $200 overdraft — available but temporarily inconsistent.
There is no third option. The line is down; you cannot both stay accurate AND keep serving customers.
That's CAP. When your distributed system suffers a Partition (network failure between nodes), you must choose: keep serving requests with possibly-stale data (AP), or reject requests until the partition heals (CP). You cannot have both.
The trap is quoting "you get 2 of 3". You don't get to choose Partition-tolerance — networks WILL partition. The real choice is "when it happens, which do I preserve?"
CAP, precisely — Brewer + Gilbert-Lynch
- Consistency — every read sees the most recent write or an error (linearizability, roughly).
- Availability — every request receives a (non-error) response, even if some nodes are down.
- Partition-tolerance — the system keeps working even when arbitrary messages between nodes are dropped.
- The theorem: in the presence of a network Partition, you must sacrifice EITHER Consistency OR Availability. There's no CA option in a real distributed system because P is not optional in the real internet.
The gap CAP leaves — what happens when there's NO partition?
A quick history so the ideas make sense
- 2000Brewer proposes CAP at PODCEric Brewer's keynote conjecture: consistency, availability, partition-tolerance — pick two.
- 2002Gilbert & Lynch formalize itMIT researchers prove CAP as a theorem, with precise definitions.
- 2007Dynamo paper · Amazon‘Highly available key-value store’ — the archetype of AP. Explicitly trades consistency for uptime.
- 2012Abadi publishes PACELC‘CAP is incomplete — you also trade Latency for Consistency in normal operation.’ The theorem practitioners actually use.
- 2013Spanner paper · Google‘Externally-consistent distributed transactions at global scale’ — a CP system with GPS + atomic clocks (TrueTime). Existence proof that CP-with-good-latency is possible if you're willing to buy the hardware.
- 2017Cosmos DB launches with 5 consistency levelsThe industry accepts consistency is a SPECTRUM, not a switch. Cosmos exposes it directly.
(b) Visual walkthrough · 15 min
CAP — the classic diagram (and its trap)
The trap: the "CA" corner is where beginners land. In a single-node database (Postgres on one box), there's no partition and CA is trivially satisfied — but it's not distributed. Once you go multi-node, P is a given and you're always choosing CP or AP.
PACELC — the fuller picture
Reading the four combinations
The consistency spectrum (Jepsen's chart, simplified)
Strongest · single-machine feel
- As if all operations happened one-by-one in real time order
- The gold standard: Spanner, etcd, ZooKeeper, CockroachDB
- Costs latency (2-3 network round-trips minimum for writes)
- Fit: leader election, distributed locks, financial ledgers
‘Stale but no more than K seconds’
- You'll see writes at most K seconds old (or K operations behind)
- Cosmos DB's ‘bounded staleness’ level
- Fit: dashboards, product catalogues where a bit of lag is fine
Read-your-own-writes
- You see YOUR writes immediately; others may lag
- Cosmos DB default; ‘causal consistency’ is a close cousin
- Fit: user profiles, shopping carts — user must see their edits
‘Someday, all reads will agree’
- Different clients see different values for a while
- Dynamo, Cassandra default; S3 for many years
- Cheapest, fastest, hardest to reason about
- Fit: view counters, non-critical feeds, backups
A classification cheat sheet
PC/EC in the partition domain (primary unreachable = writes rejected). Replicas serve stale reads for latency = PC/EL if you enable that.
PC/EC — writes need majority ack, reads can go to secondary with staleness allowance.
AP by default (‘eventually consistent reads’). Also supports strongly-consistent reads = PC when you opt in per-request.
AP; tunable consistency per-request (QUORUM, ONE, ALL). Classic PA/EL.
PC/EC — global linearizability via consensus (Paxos/Raft) + tightly-synchronized clocks. You pay the latency for the guarantee.
Explicit 5 levels. Session (default) = PA/EL for most reads with per-session read-your-write.
"CAP says pick two of Consistency, Availability, Partition tolerance. My datacenter network is reliable, so I'll take CA."
Partitions are not a choice — they are a property of the network, and any network can partition. So P is not optional, and CAP reduces to a single decision made during a partition: refuse requests to preserve consistency, or serve them and accept divergence. A "CA system" is just a system that has not yet had a partition, and will behave badly when it does.
The myth is sticky because the theorem is stated as three symmetric letters, which invites reading it as a menu. It also matches lived experience: within one rack the network really is reliable most of the time, so CA feels like a defensible engineering judgement. It fails because partitions include far more than cable failures — a saturated NIC, a long GC pause, an overloaded switch, or a misapplied firewall rule are all indistinguishable from a partition to the nodes involved. Partition tolerance is not about surviving cut cables; it is about behaving correctly when a node cannot tell whether its peer is dead or merely unreachable.
Ask the only CAP question that has an operational answer:
A partition splits your cluster. A write arrives at the
minority side. What happens?
(a) rejected / times out -> you chose C
(b) accepted, reconciled later -> you chose A
(c) nobody knows -> you have not chosen,
the default has chosen
for youMost systems are (c) until an incident answers it. Find the answer by reading the configuration, not the marketing page.
Why is the CAP tradeoff genuinely unavoidable? Surely a sufficiently clever protocol could do better.
- 1Consider two nodes holding a replica of the same value, with a network link between them, and a partition severing that link.forced by · this is the minimal case; if it is impossible here it is impossible in any larger system
- 2A write arrives at node A. Node A cannot reach node B, and cannot distinguish "B is down" from "B is alive but unreachable and possibly taking its own writes".forced by · a timeout is the only evidence available, and it is produced identically by both situations
- 3If A accepts the write, then a client reading from B sees the old value. The two replicas now disagree, so the system is not consistent in the linearizable sense.forced by · consistency requires all readers to observe a single agreed order of operations
- 4If A refuses the write until it can reach B, then A is unavailable for that request for the duration of the partition, which is unbounded.forced by · availability requires every non-failing node to answer, and A is not failing — it is choosing to refuse
- 5There is no third option, because A must either produce a response or not, and it has no information beyond what it can observe locally.forced by · a node cannot act on information it cannot obtain, and the partition is precisely what prevents obtaining it
Therefore the tradeoff is a consequence of information, not of engineering skill. No protocol escapes it because no protocol can give A knowledge of B while the link is down.
And note what this predicts: since partitions are rare, a theorem about behaviour during partitions says nothing about the other 99.9% of the time — and yet systems clearly differ in normal operation too. That gap is exactly what PACELC fills: if Partitioned, choose Availability or Consistency; Else, choose Latency or Consistency. The second half is the one you feel every day, because synchronous replication to a distant replica costs a round trip on every single write whether or not anything has failed. CAP describes your worst day; PACELC describes your average one.
Every distributed data system has two separate conversations. During a partition: do we refuse writes or accept divergence? During normal operation: do we wait for replicas to acknowledge, or answer immediately and replicate afterwards?
The second conversation happens on every request and determines your latency. The first happens rarely and determines whether an incident is a brief outage or a data reconciliation project.
- Consistency in CAP means linearizability — every read sees the most recent write, as if there were one copy. It is not the C in ACID, which is about invariants within a transaction. The two words are unrelated and conflating them causes real confusion in design discussions.
- Quorum systems choose C by construction: a majority must agree, so the minority side of a partition cannot make progress. This is why a three-node cluster survives one failure and a two-node cluster survives none — two nodes have no majority when split.
- Availability in CAP is stricter than uptime. It means every non-failing node answers every request. A system that stays up but returns errors from the minority side has chosen C, regardless of what its availability dashboard reports.
- The choice can be per-operation rather than per-system. Reading a product description and transferring money have completely different tolerances, and modern datastores expose per-request consistency levels precisely so you can make the decision where it belongs.
Fire this model when you see: a vendor claiming to beat CAP · a two-node cluster described as highly available · writes accepted on both sides of a partition · a request latency floor that matches a cross-region round trip · someone choosing a database on consistency claims without asking about the else branch.
Your service spans two regions. Does a write wait for the remote region to acknowledge before returning success?
Match the guarantee to the frequency of the failure. Zone failures are far more common than region failures, so pay for synchronous replication where it buys the most, and accept asynchronous across regions with an explicit, measured and alerted replication lag.
The decision that must never be left implicit is what happens at failover with unreplicated writes. If the answer is "we lose them silently", say so in writing and get agreement, because the alternative — discovering it during a failover while the business asks where the last four minutes of orders went — is how a technical event becomes an organisational one. And measure replication lag continuously: it is the direct, numerical statement of how much data you are currently prepared to lose.
(c) Hands-on · 25 min
We'll build a tiny Python simulator that demonstrates CAP mechanically: a two-node replicated store where you can toggle a partition, choose CP or AP behaviour, and watch reads/writes behave accordingly. No servers, just enough code to make the theorem tangible.
"""s070_cap_sim.py — a 100-line simulator to make CAP concrete.
Two nodes, one key. A client writes to Node A; Node A tries to replicate to Node B.
You control:
* partition on/off (drop messages between A and B)
* mode: 'CP' (reject writes if replication fails) or 'AP' (accept and diverge)
Run: python s070_cap_sim.py
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional
import time
@dataclass
class Node:
name: str
store: dict[str, str] = field(default_factory=dict)
@dataclass
class Cluster:
a: Node
b: Node
partitioned: bool = False
mode: str = "CP" # "CP" or "AP"
replication_delay_ms: int = 10
# ---- write path ----
def write(self, key: str, value: str) -> tuple[bool, str]:
"""Client always writes to node A. A tries to replicate to B."""
self.a.store[key] = value
if self.partitioned:
if self.mode == "CP":
# Consistency: undo local write, tell client we failed
del self.a.store[key]
return False, "REJECTED (partition, CP: won't accept without replica ack)"
else: # AP
# Availability: accept, replicas will diverge
return True, "ACCEPTED on A (partition, AP: will reconcile later)"
# Normal path: replicate to B
time.sleep(self.replication_delay_ms / 1000)
self.b.store[key] = value
return True, "ACCEPTED (replicated to B)"
# ---- read path ----
def read(self, key: str, from_node: str = "a") -> tuple[Optional[str], str]:
node = self.a if from_node == "a" else self.b
val = node.store.get(key)
return val, f"read from {node.name.upper()}"
def print_state(c: Cluster) -> None:
print(f" A={c.a.store} B={c.b.store} partition={c.partitioned} mode={c.mode}")
def main() -> None:
c = Cluster(a=Node("a"), b=Node("b"))
print("\n=== SCENARIO 1: normal write, replication OK ===")
ok, msg = c.write("balance", "1000")
print(f"write balance=1000 → {ok} · {msg}")
print_state(c)
print("\n=== SCENARIO 2: partition + CP mode ===")
c.partitioned = True
c.mode = "CP"
ok, msg = c.write("balance", "1200")
print(f"write balance=1200 → {ok} · {msg}")
print_state(c)
print(" Read from A:", c.read("balance", "a"))
print(" Read from B:", c.read("balance", "b"))
print(" Correct: system refused inconsistent write; both nodes still show old value.")
print("\n=== SCENARIO 3: partition + AP mode (concurrent divergent writes) ===")
c.mode = "AP"
ok, msg = c.write("balance", "800") # A accepts
print(f"client-1 writes balance=800 on A → {ok} · {msg}")
# Meanwhile a client hits B directly (simulate)
c.b.store["balance"] = "1500"
print("client-2 writes balance=1500 directly on B (available side)")
print_state(c)
print(" Read from A:", c.read("balance", "a"))
print(" Read from B:", c.read("balance", "b"))
print(" DIVERGENCE — same key, two values. Standard AP behaviour during partition.")
print("\n=== SCENARIO 4: partition heals, we need conflict resolution ===")
c.partitioned = False
# Simplest strategy: last-write-wins by an imaginary timestamp
print(" Applying LWW reconciliation (choose one)...")
winner = c.b.store["balance"] # pretend B's write was later
c.a.store["balance"] = winner
print_state(c)
print(" Note: LWW SILENTLY LOSES client-1's write. This is the ‘lost update’ pitfall of AP.")
print(" Real systems use vector clocks / CRDTs / application-level merge to do better.")
print("\n=== SCENARIO 5: PACELC — even without partition, replica reads may be stale ===")
c.a.store["balance"] = "2000"
print("write balance=2000 to A (no partition)")
print(" IMMEDIATE read from A:", c.read("balance", "a"))
print(" IMMEDIATE read from B (before replication):", c.read("balance", "b"),
"← STALE — replication is async")
time.sleep(c.replication_delay_ms / 1000)
c.b.store["balance"] = "2000" # replication catches up
print(" After replication delay, read from B:", c.read("balance", "b"))
if __name__ == "__main__":
main()What each block does
Anatomy of the simulator
Extend the simulator so read(key, strong=True) ignores the from_node param and always reads from A:
def read(self, key, from_node="a", strong=False):
node = self.a if strong else (self.a if from_node == "a" else self.b)
return node.store.get(key), f"read from {node.name.upper()} (strong={strong})"Now retry scenario 5 with c.read("balance", "b", strong=True). Always returns the correct value — because strong reads go to A. Cost: you can't spread read load across replicas for the queries you marked strong. This is the exact trade DynamoDB makes with its ConsistentRead=true flag: 2× the RCU cost for strong reads.
(d) Production reality · 15 min
A metadata-service partition caused DynamoDB's internal metadata cache to become inconsistent. Because DynamoDB is AP, individual reads didn't error — they returned varying answers depending on which replica served them. The metadata inconsistency then cascaded to routing, and half of DynamoDB in us-east-1 went unavailable for hours.
Every AWS service that depended on DynamoDB (many) started failing.
w:1 (acknowledge on primary only, no replicas required). In a partition + primary failure, unacknowledged writes could be lost. Jepsen (Kyle Kingsbury) demonstrated this repeatedly in 2013-2015 posts that reshaped how the industry talks about consistency.w:majority. Users had to opt into the risky default. This is a case study in how a small default change can move a system from ‘AP with quiet lost writes’ to ‘CP with acknowledged durability’.Common conceptual failure modes
- ‘We're CA because we run in one datacentre’ — you're not distributed. Add a second node and you'll rediscover P immediately.
- ‘We chose CP so we're safe’ — CP means you drop availability during partitions. Your SLO needs to reflect this; ‘99.999% CP’ is possible only if partitions are extremely rare.
- ‘Eventual is fine, users won't notice’ — until user X sees ‘likes: 42’ on their profile and refreshes to ‘likes: 41’. Now they're confused. Session consistency (‘you see your own writes’) is the pragmatic default.
- ‘Multi-master is like multi-primary Cassandra, right?’ — no. Cassandra with QUORUM writes is a specific consistency model. ‘Multi-master’ RDBMS often means active-active MySQL, which has different (weaker) guarantees. Vocabulary matters.
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
- What's the CAP trap most people fall into? (‘pick 2 of 3’ — P is not optional)
- What does PACELC add that CAP alone misses?
- Give two real databases and their CAP + PACELC classification.
What comes next
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.