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.
You build a database. You run it on two machines instead of one, so that if a server dies the other keeps going.
Smart. Safe. Until the wire between them is cut.
Now you have two machines that cannot talk, and a customer standing in front of one of them asking for their balance.
You have exactly two choices, and both of them are bad.
Answer with the number you have, and risk it being wrong, the other machine may hold newer data you cant see.
Or refuse to answer until the wire is fixed, and leave the customer staring at a spinner.
Wrong, or unavailable. Pick one. There is no third door.
In 2000 a computer scientist named Eric Brewer proved this is not a bug you can engineer away. Its a law. Every distributed system youve ever used makes this exact tradeoff the moment the network splits.
Consistency. Availability. Partition tolerance. You can promise two. Never three.
On page sixteen you learned that no single machine knows everything. This is the price of that.
Lets meet the three promises.
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.
CAP Theorem across ScrapyBytes
The same ideas surface all over ScrapyBytes. Here is where this page connects to the rest of the curriculum, and how to follow each thread.
CAP is the headline theorem of distributed systems. The distributed-systems page is the world; this page is the law that world obeys.
scrapybytes.vercel.app/distributed-systems →PACELCPACELC starts where CAP stops. CAP covers the partition; PACELC adds the latency-versus-consistency choice you make even when the network is fine. The pacelc page is the sequel.
scrapybytes.vercel.app/pacelc →NetworkingCAP only bites because networks partition. Packets get lost, links go down. The networking page is why the P in CAP is not optional.
scrapybytes.vercel.app/networking →BlockchainBitcoin answers CAP by choosing consistency and paying for it with ten-minute waits. The blockchain page is CAP turned into a design decision.
scrapybytes.vercel.app/blockchain →NodesCAP is about agreement among nodes that cannot all reach each other. The nodes page is who is trying to agree.
scrapybytes.vercel.app/nodes →HashingDistributed databases use consistent hashing to spread data across nodes, and CAP decides what happens to that data during a partition. The hashing page places the keys; this page is what breaks when the network does.
scrapybytes.vercel.app/hashing →BinaryEvery value a distributed system stores is ultimately binary, so disagreement is two servers holding different bits at the same logical address. The binary page is what the conflict is made of.
scrapybytes.vercel.app/binary →MemoryCAP is a memory problem at heart: two machines, two copies, one truth, impossible to keep aligned when the link between them fails. The memory page is the copies it splits.
scrapybytes.vercel.app/memory →Linked ListA blockchain is a linked list whose pointers are hashes, so tampering with any node is detectable by every other. The linked-list page is consistency made structural.
scrapybytes.vercel.app/linked-list →ArraysDistributed arrays are called shards, and CAP decides whether they must all agree before responding or each can answer alone. The arrays page is the structure being split.
scrapybytes.vercel.app/arrays →