CAP Theorem, PACELC, and Distributed System Tradeoffs
A field guide to the tradeoffs every distributed system makes — during a network partition, and even when nothing is broken. What you give up, when, and why the "right" answer depends entirely on what a stale read actually costs you.
1. CAP Theorem
Every distributed system can guarantee at most 2 of 3 properties:
| Property | Definition |
|---|---|
| Consistency (C) | Every read receives the most recent write or an error |
| Availability (A) | Every request receives a response (not an error), though it may be stale |
| Partition Tolerance (P) | The system continues operating when network messages are dropped/delayed |
Why P is non-negotiable: Networks fail. Any distributed system deployed across nodes will experience partitions. You cannot sacrifice P — you can only choose between C and A when a partition occurs.
So the real tradeoff is:
- CP: When partitioned, reject requests to guarantee consistency (return error or block)
- AP: When partitioned, serve potentially stale data to guarantee availability
CAP says you pick 2 of 3 properties. Why, in practice, do engineers really only choose between C and A?
2. Network Partition: CP vs AP Behavior
graph TD
classDef primary fill:#2980b9,stroke:#1f618d,color:#fff
classDef replica fill:#7f8c8d,stroke:#616a6b,color:#fff
classDef cut fill:#e74c3c,stroke:#c0392b,color:#fff
classDef cpResult fill:#c0392b,stroke:#922b21,color:#fff
classDef apResult fill:#e67e22,stroke:#ba6018,color:#fff
CLIENT["Client"] -->|"write x=5"| N1["Node 1 — Primary<br/>holds x=5"]:::primary
N1 -->|"replicate x=5"| N2["Node 2 — Replica<br/>in sync, holds x=5"]:::replica
N1 -.->|"NETWORK PARTITION<br/>replication link down"| N3["Node 3 — Replica<br/>cut off, still holds x=0"]:::cut
subgraph CP["CP system — during the partition"]
N3 -->|"read request"| CPR["Refuses to answer<br/>returns ERROR — can't guarantee freshness"]:::cpResult
end
subgraph AP["AP system — during the partition"]
N3 -->|"read request"| APR["Answers anyway<br/>returns x=0, stale but available"]:::apResult
end
CP (ZooKeeper, etcd): Node 3 refuses to serve reads — it may be out of sync. Client gets an error.
AP (Cassandra, DynamoDB): Node 3 serves stale data. Client gets a response, possibly wrong.
Node 3 is cut off from the primary and knows it might be stale. Rather than guess, it refuses the request — the client gets an error, or the request blocks until quorum is restored. Examples: ZooKeeper, etcd, Kafka when min.insync.replicas can't be met.
The system is betting that a wrong answer is worse than no answer. Once the partition heals, the isolated node simply catches up via normal replication — it never accepted any writes while cut off, so there's nothing to reconcile.
Node 3 keeps serving — it returns x=0, stale but a real response. Examples: Cassandra, DynamoDB.
The system is betting that a possibly-wrong answer beats none. Once the partition heals, the diverged replicas must reconcile — read repair, anti-entropy, hinted handoff, or a vector-clock/version-vector merge (see Section 9) — because Node 3 may have accepted writes the primary never saw.
During the partition, what does the cut-off node (Node 3) actually return in an AP system versus a CP system?
x=0, stale but a real response, because being available matters more than being right. CP: Node 3 refuses to answer at all and returns an error, because it can't guarantee its data is fresh and won't risk serving (or accepting) something wrong.3. Real System Classifications
| System | CAP Class | Consistency | Availability | Notes |
|---|---|---|---|---|
| PostgreSQL | CP | Strong (single node) / configurable (replication) | Sacrifices A on partition | Synchronous replication blocks writes if replica unreachable |
| Redis | CP (default) | Strong on primary | Primary down = unavailable | Sentinel/Cluster still prioritize consistency; async replication means brief CP |
| Cassandra | AP | Eventual (tunable) | Always responds | Tunable via quorum; default eventual; AP at heart |
| DynamoDB | AP (default) / CP (optional) | Eventual by default | High availability across AZs | Strong consistency available per-request at higher latency |
| ZooKeeper | CP | Linearizable | Leader election required; partitioned nodes go unavailable | Used for coordination — correctness over uptime |
| etcd | CP | Linearizable (Raft) | Quorum required; minority partition = unavailable | Powers Kubernetes control plane |
| Kafka | CP | Strong within partition (ISR) | Unavailable if leader + ISR lost | min.insync.replicas governs the tradeoff |
| MongoDB | CP (default) | Strong on primary (w:majority) | Secondary reads = eventual | Read preference + write concern are tunable |
According to the classification table, is Kafka CP or AP, and what setting governs that tradeoff?
min.insync.replicas is the knob: it sets how many replicas (including the leader) must be in the ISR for a write to be accepted at all.Why Consensus Gives You CP
The table above lists etcd's consistency as "Linearizable (Raft)" — but why does a Raft- or Paxos-based consensus protocol earn you the C in CP, rather than just happening to be strong most of the time? The mechanism is quorum writes. Every write has to be replicated to and acknowledged by a majority of nodes before the leader commits it and answers the client — that's exactly the same W+R>N overlap guarantee from the quorum math in Section 8, with W set to a majority and every read served by (or forwarded to) the current leader.
That single rule is what produces both halves of CP simultaneously. Consistency falls out because any future leader must also win a majority vote, and a majority-vote quorum always overlaps with any majority-write quorum in at least one node — so a new leader can never be elected without seeing every previously committed write. Availability loss on a minority partition isn't a bug or a side effect — it's the same rule enforced from the other direction: a minority of nodes can never assemble a quorum for a write or an election, so it cannot make progress, full stop, not "make progress slowly." That inability to progress is the unavailability CAP describes.
The actual leader-election state machine, term numbers, and log-replication protocol that implement this — plus a live, interactive Raft election simulator you can click through node failures on — live in databases/replication.md § 9, Consensus Algorithms. This section only covers the CAP-level "why"; that one covers the "how."
A minority partition in a Raft/Paxos cluster can't get writes acknowledged. Is that because it's slow to reach quorum, or because it's structurally impossible?
4. Consistency Models Spectrum
From strongest to weakest:
graph LR
classDef strong fill:#c0392b,stroke:#922b21,color:#fff
classDef high fill:#e67e22,stroke:#ba6018,color:#fff
classDef medium fill:#f1c40f,stroke:#b7950b,color:#000
classDef weak fill:#27ae60,stroke:#1e8449,color:#fff
LIN["Linearizable<br/>strongest — real-time global order"]:::strong --> SEQ["Sequential<br/>one shared order, no wall-clock guarantee"]:::high --> CAU["Causal<br/>only causally-related ops ordered"]:::medium --> EVT["Eventual<br/>weakest — converges given enough time"]:::weak
Strongest model. All operations appear instantaneous; reads always reflect the latest write globally.
Example: etcd reads. After a leader writes, any subsequent read anywhere returns that value.
Cost: High latency — requires cross-node coordination.
All nodes see operations in the same order, but not necessarily in real time.
Example: A multi-player game where all clients see moves in the same sequence, but with some lag.
Cost: Weaker than linearizability; no wall-clock guarantee, only a shared ordering.
Operations that are causally related (A happens before B) are seen in that order by all nodes. Concurrent operations may be seen in different orders on different nodes.
Example: "Reply to a post" must appear after the original post. MongoDB with causal sessions.
Cost: Lower than sequential; only tracks causally linked operations, everything else is unconstrained.
Weakest model. If no new updates, all replicas will converge to the same value — eventually.
Example: DynamoDB default reads, Cassandra default, DNS propagation, S3 read-after-write on different regions.
Cost: Reads may be stale; conflicts possible.
What actually distinguishes causal consistency from sequential consistency?
5. PACELC
CAP only covers behavior during partitions. PACELC extends it:
If Partition → choose A or C Else (normal operation) → choose Latency or Consistency
Even without failures, replicating synchronously costs latency.
| System | P→A or C | E→L or C | Notes |
|---|---|---|---|
| DynamoDB | PA | EL | Eventual reads are faster; strong reads cost 2x latency |
| Cassandra | PA | EL | Quorum reads add latency vs ONE reads |
| ZooKeeper | PC | EC | Always consistent; latency accepted |
| etcd | PC | EC | Raft consensus on every write; consistency first |
| MongoDB | PC/PA | EC/EL | Depends on write concern (w:majority = PC/EC) |
| PostgreSQL | PC | EC | Synchronous standby = consistent but higher write latency |
| Spanner | PC | EC | TrueTime-based global linearizability; latency is a known cost |
| Riak | PA | EL | Designed for AP; vector clocks for conflict resolution |
graph TD
classDef question fill:#34495e,stroke:#212f3c,color:#fff
classDef cpath fill:#c0392b,stroke:#922b21,color:#fff
classDef apath fill:#e67e22,stroke:#ba6018,color:#fff
START{"Is the system<br/>currently partitioned?"}:::question
START -->|"Yes"| PICKAC{"Pick: Availability<br/>or Consistency?"}:::question
START -->|"No — normal operation"| PICKLC{"Pick: Latency<br/>or Consistency?"}:::question
PICKAC -->|"Choose A"| PA["PA — serve stale data,<br/>stay available (Cassandra, DynamoDB)"]:::apath
PICKAC -->|"Choose C"| PC["PC — refuse requests,<br/>stay correct (ZooKeeper, etcd)"]:::cpath
PICKLC -->|"Choose L"| EL["EL — respond fast,<br/>replicate asynchronously (Cassandra, DynamoDB)"]:::apath
PICKLC -->|"Choose C"| EC["EC — wait for sync replication,<br/>pay the latency (ZooKeeper, etcd, Spanner)"]:::cpath
Does the PACELC latency-vs-consistency tradeoff only kick in during a network partition?
6. Consistency vs Availability Tradeoffs in Practice
DynamoDB: AP saves latency
Eventually consistent read: ~1ms — reads from any replica
Strongly consistent read: ~2ms — reads only from leader
At 100k req/s: eventually consistent = 2x throughput capacity
DynamoDB's default eventual consistency means your shopping cart may show a stale item count — acceptable. A banking balance cannot use this.
ZooKeeper: CP costs availability
graph TD
classDef healthy fill:#27ae60,stroke:#1e8449,color:#fff
classDef degraded fill:#f39c12,stroke:#ba6018,color:#fff
classDef down fill:#e74c3c,stroke:#c0392b,color:#fff
A["All 3 nodes healthy<br/>quorum = 3/3<br/>leader serves reads + writes ✓"]:::healthy -->|"1 node fails"| B["1 node down<br/>quorum intact = 2/3<br/>still fully operational ✓"]:::degraded
B -->|"2nd node fails"| C["2 nodes down<br/>quorum lost = 1/3<br/>ALL reads + writes REFUSED ✗"]:::down
B -->|"failed node recovers"| A
C -->|"a node recovers"| B
ZooKeeper refuses to serve rather than risk inconsistency. This is correct for distributed locking and leader election — a stale lock is worse than no lock.
In a 3-node ZooKeeper cluster, what happens the moment a second node goes down?
7. Tunable Consistency
DynamoDB
# Eventual consistent read (default, faster)
table.get_item(Key={"id": "123"})
# Strongly consistent read (latest data, higher latency)
table.get_item(Key={"id": "123"}, ConsistentRead=True)
Cassandra Quorum
-- Strong consistency (W+R > N)
INSERT INTO orders ... USING CONSISTENCY QUORUM; -- W=2 of 3
SELECT * FROM orders WHERE ... CONSISTENCY QUORUM; -- R=2 of 3
-- High availability, eventual
INSERT INTO orders ... USING CONSISTENCY ONE; -- W=1 of 3
SELECT * FROM orders WHERE ... CONSISTENCY ONE; -- R=1 of 3
MongoDB Read/Write Concern
// Strong: wait for majority replica acknowledgment
db.collection.insertOne(doc, { writeConcern: { w: "majority" } })
// Read from primary only (latest)
db.collection.find({}).readPreference("primary")
// Read from secondary (may be stale, lower latency)
db.collection.find({}).readPreference("secondaryPreferred")
8. Quorum Math
For a cluster of N nodes with W write acknowledgments and R read replicas consulted:
Strong consistency requires: W + R > N
This guarantees at least one node in the read set saw the latest write.
| N | W | R | W+R | Guarantee |
|---|---|---|---|---|
| 3 | 2 | 2 | 4 > 3 | Strong — overlap guaranteed |
| 3 | 1 | 1 | 2 < 3 | Eventual — reads may miss latest write |
| 3 | 3 | 1 | 4 > 3 | Strong — but writes are slow (all 3 must ack) |
| 3 | 1 | 3 | 4 > 3 | Strong — but reads hit all nodes |
| 5 | 3 | 3 | 6 > 5 | Strong — tolerates 2 node failures |
| 5 | 2 | 2 | 4 < 5 | Eventual |
| 5 | 1 | 5 | 6 > 5 | Strong — reads scan everything, very slow |
graph TD
classDef write fill:#e67e22,stroke:#ba6018,color:#fff
classDef read fill:#3498db,stroke:#2471a3,color:#fff
classDef overlap fill:#27ae60,stroke:#1e8449,color:#fff
classDef idle fill:#7f8c8d,stroke:#616a6b,color:#fff
subgraph CLUSTER["3-node cluster — W=2 write acks, R=2 read replicas (W+R=4, N=3: overlap guaranteed)"]
N1["Node 1<br/>acked the write, has latest value"]:::overlap
N2["Node 2<br/>acked the write, has latest value"]:::write
N3["Node 3<br/>never received the write, stale"]:::idle
end
WSET["Write set (W=2)<br/>Node 1 + Node 2"]:::write
RSET["Read set (R=2)<br/>Node 1 + Node 3"]:::read
N1 --> WSET
N2 --> WSET
N1 --> RSET
N3 --> RSET
WSET -.->|"shared member: Node 1 —<br/>the read set always includes<br/>at least one node from the write set"| RSET
Availability tradeoff: Higher W = slower writes (more nodes must respond). Higher R = slower reads. Tune based on whether reads or writes are on the hot path.
Fault tolerance: With W+R>N and N=3, W=2, R=2 → you can lose 1 node and still have quorum. With N=5, W=3, R=3 → tolerate 2 node failures.
For N=3 with W=1, R=1, is this configuration strongly consistent?
Try It Yourself: Live Quorum Overlap
The table above states W+R>N as a static arithmetic rule. But the guarantee it describes is really about specific nodes, not just counts — a write quorum and a read quorum only actually overlap if the particular nodes chosen for each happen to share a member. Toggle real nodes into a write set and a read set below and see whether they actually intersect, for sizes both above and at-or-below the W+R>N threshold.
Try this: set N=5. Pick W = {0, 1} and R = {2, 3} — sizes 2 and 2, W+R=4, which is not greater than N=5, and these two picks share no node: no overlap. Now, keeping the same sizes, try W = {0, 1} and R = {0, 2} instead — still W+R=4 ≤ N=5, but this particular pair does share node 0. That's the point: below the W+R>N threshold, overlap is possible but not guaranteed — whether any given pair overlaps depends entirely on which specific nodes got picked, which is luck unless a protocol enforces it. Now expand the read set to {2, 3, 4} (R=3) — still W+R=5, not greater than N=5, and you can still find non-overlapping picks. Finally grow the read set to 4 nodes (W+R=6 > N=5) and try several completely different node choices for both sets: every single one overlaps, no matter which specific nodes you pick — with only 5 nodes total, 2 write nodes and 4 read nodes can't help but share one. That's pigeonhole, not luck, and it's exactly the guarantee a real quorum protocol enforces on every request by construction (see "Why Consensus Gives You CP" above), rather than leaving it to chance the way this toy does.
9. Vector Clocks and Version Vectors
Distributed systems need to detect concurrent writes — changes made on different nodes with no causal ordering.
Vector Clock
Each node maintains a counter per node. On every event:
- Increment own counter
- Merge (take max) on receive
[A:0, B:0, C:0] — one counter per node, all zeroed.
x=5. Its clock becomes [A:1, B:0, C:0].
x=9. Its clock becomes [A:0, B:1, C:0] — a different write, from a different starting point, at roughly the same time.
[A:1, B:0, C:0]. B sends its update: C sees [A:0, B:1, C:0]. Neither vector clock is a superset of the other.
C cannot determine which write happened first → conflict detected. System must reconcile (last-write-wins, merge, or surface to application).
Try It: Live Vector Clocks
The stepper above walks through one scripted sequence. Below, drive three real vector clocks yourself — write on any node, replicate in either direction between any pair, and watch the status line classify the two clocks it just compared as concurrent or causally ordered. Reproduce the stepper's exact scenario (Write @ A, Write @ B, Replicate A→C, Replicate B→C — watch the last step report "concurrent"), then try other orderings: what happens if you replicate A→C before B ever writes?
Conflict Resolution Strategies
| Strategy | Used By | Behavior |
|---|---|---|
| Last Write Wins (LWW) | Cassandra, DynamoDB (default) | Timestamp decides; data loss possible |
| Multi-value (siblings) | Riak | Return all conflicting values; app resolves |
| Application merge | Shopping cart (Amazon Dynamo paper) | Union of items — never lose an addition |
| Operational transform | Google Docs | CRDTs for text merging |
Version Vectors vs Vector Clocks
- Vector clocks: track causality per event
- Version vectors: track causality per replica/object (more common in databases)
DynamoDB uses version vectors internally. Cassandra uses timestamps (LWW), not vector clocks — simpler but can lose data on concurrent writes.
Nodes A and B each write concurrently, then both replicate to Node C. Can C tell which write happened first just by comparing the vector clocks?
[A:1, B:0, C:0] and [A:0, B:1, C:0] — neither is a superset of the other, so they're causally concurrent, not ordered. C detects a conflict and must reconcile it with a strategy (last-write-wins, multi-value siblings, application merge, CRDTs), it cannot derive a "first" write from the clocks alone.10. Practical Guidance: CP vs AP by Use Case
| Use Case | Choose | Reason |
|---|---|---|
| User authentication / session tokens | CP | Stale session = security hole. Revoked token must be invalid immediately. Use Redis with strong consistency or a CP store. |
| Shopping cart | AP | Losing an item add is worse than a brief stale count. Use eventual consistency with conflict resolution (merge/union). Amazon's Dynamo paper was designed for this. |
| Inventory count | CP | Overselling is a business problem. Need exact counts. Use transactions (Postgres, DynamoDB transactions, or Cassandra LWT). |
| Social media feed | AP | A tweet appearing 200ms late is fine. Blocking all reads for consistency kills UX. Cassandra/DynamoDB eventual reads are correct here. |
| Payment processing | CP | Money cannot be double-spent or lost. Requires linearizability + transactions. Use Postgres, Spanner, or DynamoDB with transactions + idempotency keys. |
Decision Framework
graph TD
classDef question fill:#34495e,stroke:#212f3c,color:#fff
classDef cp fill:#c0392b,stroke:#922b21,color:#fff
classDef ap fill:#e67e22,stroke:#ba6018,color:#fff
subgraph H1["Heuristic 1 — blast radius of a stale read"]
Q1{"Is stale data dangerous?<br/>security, money, inventory"}:::question
Q1 -->|"Yes"| CP1["CP — accept availability<br/>degradation on partition"]:::cp
Q1 -->|"No"| AP1["AP — accept stale reads<br/>for higher availability"]:::ap
end
subgraph H2["Heuristic 2 — can the caller just retry?"]
Q2{"Is the operation idempotent<br/>and safely retryable?"}:::question
Q2 -->|"Yes"| AP2["AP is safer —<br/>client retries on a stale read"]:::ap
Q2 -->|"No"| CP2["CP required —<br/>a retry could double-charge/deduct"]:::cp
end
subgraph H3["Heuristic 3 — latency vs. accuracy"]
Q3{"Is low latency more important<br/>than perfect accuracy?"}:::question
Q3 -->|"Yes"| AP3["AP + eventual consistency —<br/>feeds, analytics, caches"]:::ap
Q3 -->|"No"| CP3["CP —<br/>financial records, auth tokens, locks"]:::cp
end
Why is a shopping cart a good fit for AP while inventory count is not, even though both are "commerce" data?
Summary
CAP: Network partition is inevitable → choose CP or AP
PACELC: Even without partition → Latency (AP) vs Consistency (CP)
CP systems: ZooKeeper, etcd, PostgreSQL (sync), Kafka (ISR)
AP systems: Cassandra, DynamoDB (default), Riak, Couchbase
Quorum: W + R > N → strong consistency
Vector clocks: detect concurrent writes, enable conflict resolution
Rule of thumb:
Money / Auth / Locks → CP
Feeds / Carts / Caches → AP