You can only
guarantee two.
In 2000 a computer scientist proved that every distributed system makes a silent tradeoff. Consistency. Availability. Partition tolerance. Pick any two. This page is why that choice is unavoidable, and what every system you use quietly chose without telling you.
The three promises
His name was Eric Brewer. In 2000 he stood up at a conference and told the entire tech industry something they did not want to hear: you cannot have all three. Almost nobody believed him. Two years later, Gilbert and Lynch proved it formally.
Every database, app, and blockchain built since then has been forced to live with the consequences. This is not an engineering limitation. This is not a hardware problem. This is mathematics. And once it clicks, you will never look at a loading spinner the same way again.
Every distributed system, every app, database, and blockchain that runs across multiple machines, makes three promises to its users. The CAP theorem says you can fully keep two of them. Never all three. Here is what they mean.
The tradeoff, expressed as types
In a real system the choice is made in the architecture, before a single line of business logic. Here it is as a plain enum: a key-value read that either refuses (CP) or answers with possibly-stale data (AP).
// The CAP tradeoff expressed as types.
// In a real system you choose your guarantees
// before you write a single line of logic.
enum CapChoice {
// Strong consistency: may reject requests
// during a partition to avoid stale data.
ConsistencyOverAvailability,
// Always available: may return stale data
// during a partition to stay responsive.
AvailabilityOverConsistency,
}
// A key-value read that respects the choice.
fn get_value(
choice: &CapChoice,
local_value: u64,
can_reach_primary: bool,
primary_value: u64,
) -> Option<u64> {
match choice {
// CP: refuse if we cannot verify the latest value.
CapChoice::ConsistencyOverAvailability => {
if can_reach_primary {
Some(primary_value)
} else {
None // refuse rather than risk stale data
}
}
// AP: return what we have, even if stale.
CapChoice::AvailabilityOverConsistency => {
Some(local_value) // always respond
}
}
}#include <stdint.h>
typedef enum {
CAP_CONSISTENCY, // refuse requests if uncertain
CAP_AVAILABILITY // always respond, may be stale
} CapChoice;
// Returns -1 if CP and a partition is detected.
int64_t get_value(
CapChoice choice,
int64_t local_value,
int can_reach_primary,
int64_t primary_value
) {
if (choice == CAP_CONSISTENCY) {
// CP: only answer if we can verify.
return can_reach_primary ? primary_value : -1;
} else {
// AP: always answer, even if stale.
return local_value;
}
}Try it: cut the network yourself
This is the whole theorem in one widget. Two data centres holding the same $500 balance. Cut the network between them, choose your guarantee, and try to withdraw money from both sides at once. Watch what each choice costs you.
What every system actually chose
Every system you use every day has already made this choice. Silently. In the architecture. Before you signed up. Here is what they chose and why.
The choices, side by side
| system | choice | sacrifices | why |
|---|---|---|---|
| Your Bank | CP | Availability | Money requires truth |
| Google Docs | AP | Consistency | Collaboration > sync |
| AP | Consistency | Messages > order | |
| Netflix | AP | Consistency | Uptime > accuracy |
| Bitcoin | CP | Availability | Trust requires proof |
| HBase | CP | Availability | Analytics > uptime |
| DynamoDB | AP | Consistency | Scale > accuracy |
| Cassandra | AP | Consistency | Always available |
CP and AP reads, in code
The difference between CP and AP is not a config flag; it is the shape of the read path. A CP read waits for a quorum and refuses if it cannot get one. An AP read answers immediately from local state and never blocks.
use std::time::{Duration, Instant};
// A CP database read. Returns None if it cannot
// confirm the latest state across a quorum.
fn cp_read(
node: &Node,
key: &str,
quorum_timeout: Duration,
) -> Option<String> {
let responses = node.broadcast_read(key);
let deadline = Instant::now() + quorum_timeout;
let mut confirmations = 0;
for response in responses {
if Instant::now() > deadline {
// Timeout: refuse rather than risk stale data.
// This is the CP tradeoff in code.
return None;
}
if response.is_ok() {
confirmations += 1;
}
}
if confirmations >= node.quorum_size() {
Some(node.local_value(key))
} else {
None // CP: refuse if quorum not reached
}
}
// An AP database read. Always returns something,
// even if it might be out of date.
fn ap_read(node: &Node, key: &str) -> String {
node.local_value(key)
.unwrap_or_else(|| "default".to_string())
}#include <stdbool.h>
#include <time.h>
// CP read: blocks until quorum or timeout.
// Returns NULL if it cannot confirm consistency.
char* cp_read(Node* node, const char* key, int timeout_ms) {
int confirmations = 0;
clock_t start = clock();
while (confirmations < node->quorum_size) {
int elapsed = (clock() - start) * 1000 / CLOCKS_PER_SEC;
if (elapsed > timeout_ms) {
return NULL; // CP: refuse on timeout
}
if (poll_node(node, key)) {
confirmations++;
}
}
return node->local_value; // confirmed consistent
}
// AP read: always returns immediately.
// May be stale, will always respond.
char* ap_read(Node* node, const char* key) {
return node->local_value ? node->local_value : "default";
}return None / return NULL on timeout. That single line is a system declaring itself CP: it would rather give you nothing than give you something wrong. An AP system has no such line; it always has an answer ready.Beyond CAP: PACELC and real-world nuance
CAP tells you what happens when the network breaks. But networks break rarely. What about the other 99.9% of the time? PACELC answers that.
PACELC extends CAP with one insight: even when there is no partition, you still face a tradeoff.
- If Partition (P): choose Availability (A) or Consistency (C). This is plain CAP.
- Else (E), in normal operation: choose Latency (L) or Consistency (C).
network partition?
├── YES → Availability vs Consistency (the CAP tradeoff)
└── NO → Latency vs Consistency (the PACELC tradeoff)
The second tradeoff is the one you live with most of the time. Fast response or guaranteed correctness, on every single request, every single second. A system that double-checks every read with a quorum is correct but slow; a system that answers from the nearest replica is fast but can be briefly wrong.
| system | partition | normal | meaning |
|---|---|---|---|
| DynamoDB | PA | EL | Fast always, eventually consistent |
| Cassandra | PA | EL | Available, tunable consistency |
| HBase | PC | EC | Consistent, pay the latency cost |
| MySQL | PC | EC | ACID, slower writes |
| Bitcoin | PC | EC | Truth costs time |
Consensus in code: the heart of every CP system
How does a CP system actually decide a value is safe to commit? It collects votes from its nodes and commits only when a majority (a quorum) agrees. Below is a simplified version of the check at the core of Raft, the algorithm behind etcd and CockroachDB.
// Simplified Raft-style consensus check.
// Used by etcd, CockroachDB, and similar CP systems.
#[derive(Debug, PartialEq)]
enum ConsensusResult {
Committed(String), // majority agreed
Pending, // still gathering votes
Rejected, // could not reach quorum
}
fn check_consensus(
votes: &[Option<String>],
quorum: usize,
) -> ConsensusResult {
let total = votes.len();
let agreements: Vec<&String> = votes.iter().flatten().collect();
if agreements.len() >= quorum {
// Majority agreed on a value: this is how
// CP systems commit a write.
ConsensusResult::Committed(agreements[0].clone())
} else if total - agreements.len() > total - quorum {
// Too many nodes unreachable to ever reach quorum.
// CP: reject rather than risk inconsistency.
ConsensusResult::Rejected
} else {
ConsensusResult::Pending
}
}
fn main() {
// 5 nodes, need 3 for quorum.
let votes = vec![
Some("value_42".to_string()),
Some("value_42".to_string()),
Some("value_42".to_string()),
None, // node unreachable
None, // node unreachable
];
match check_consensus(&votes, 3) {
ConsensusResult::Committed(v) => println!("Committed: {}", v),
ConsensusResult::Rejected => println!("Rejected: partition detected"),
ConsensusResult::Pending => println!("Waiting for quorum..."),
}
// Output: Committed: value_42
}#include <stdio.h>
#include <string.h>
#include <stddef.h>
typedef enum { COMMITTED, PENDING, REJECTED } ConsensusResult;
ConsensusResult check_consensus(
const char** votes, // NULL = unreachable node
size_t total,
size_t quorum,
char* out_value,
size_t out_size
) {
size_t agreements = 0;
const char* agreed_value = NULL;
for (size_t i = 0; i < total; i++) {
if (votes[i] != NULL) {
if (!agreed_value) agreed_value = votes[i];
if (strcmp(votes[i], agreed_value) == 0) agreements++;
}
}
if (agreements >= quorum) {
if (out_value && agreed_value)
strncpy(out_value, agreed_value, out_size);
return COMMITTED;
}
size_t unreachable = total - agreements;
if (unreachable > total - quorum) return REJECTED;
return PENDING;
}
int main(void) {
const char* votes[] = { "value_42", "value_42", "value_42", NULL, NULL };
char result[64];
ConsensusResult r = check_consensus(votes, 5, 3, result, sizeof(result));
if (r == COMMITTED) printf("Committed: %s\n", result);
else if (r == REJECTED) printf("Rejected: partition detected\n");
else printf("Pending: waiting for quorum\n");
return 0;
}Bitcoin and the CAP theorem
Bitcoin is the most famous CP system ever built. Consistency above everything. Every node must agree on the same ledger. No exceptions, no stale reads, no temporary inconsistency that reconciles later.
The double-spend problem is just the CAP theorem applied to a financial ledger. If two nodes accept the same Bitcoin in two different transactions simultaneously, the ledger is inconsistent and someone gets money they should not have. That is the exact failure the AP path in the widget above produced.
Satoshi solved it with Proof of Work. Instead of a central authority deciding which transaction is valid, every node independently runs the same algorithm and the longest valid chain wins. Not because someone decided, but because every node applies the same rule. This is consensus without coordination: consistency without a coordinator, the CAP theorem solved using computation as the arbiter.
// Bitcoin's CAP choice, in pseudocode
if cannot_reach_consensus() {
halt_new_blocks(); // go offline
// never serve stale state
// consistency is non-negotiable
// availability is the sacrifice
}
This is why you wait for confirmations. This is why Bitcoin is slow. This is why it has never been successfully double-spent in over fifteen years. Every second of waiting is the price of consistency in a trustless world.
Where the CAP theorem appears in BitRoot
CAP is not an isolated result; it touches almost every layer of the stack below it. The shortest path from each: