Every distributed database's marketing page eventually mentions "consistency," "availability," or "partition tolerance" — and usually implies it has all three. It can't. The CAP theorem states that a distributed system can guarantee at most two of the three simultaneously, and in practice, one of those three isn't optional, which reduces the real choice to just one axis.
That one axis is still only part of the story. Most of the time there's no partition at all, and the trade-off you actually feel day to day is between consistency and latency. This post covers both.
The three properties
- Consistency (C): every read receives the most recent write, or an error. All nodes see the same data at the same time — there's no such thing as "node A already has the update, node B doesn't yet."
- Availability (A): every request to a non-failing node receives a response (not an error), though not necessarily the most recent data.
- Partition tolerance (P): the system keeps operating even when network failures split it into groups of nodes that can't communicate with each other.
The "C" in CAP is not the "C" in ACID. ACID consistency means a transaction moves the database from one valid state to another — constraints hold. CAP consistency is about replicas agreeing: formally, linearizability, meaning the system behaves as if there were a single copy of the data and every operation took effect at one instant between its start and end. Mixing the two up is the most common source of confused CAP arguments.
Why P isn't actually optional
Network partitions — a switch failing, a link dropping, a data center losing connectivity — will happen in any real distributed system; it's not a design choice you get to opt out of, it's a physical certainty over a long enough timeline. That leaves only two real options once a partition occurs, which is why CAP is usually phrased not as "pick 2 of 3" but as "when a partition happens, pick C or A":
- Choose consistency (CP): during the partition, nodes that can't confirm they have the latest data refuse to serve requests (or return an error) rather than risk serving something stale or conflicting. The system sacrifices availability to protect correctness.
- Choose availability (AP): every node keeps answering requests during the partition, even if it can't confirm it has the latest write — meaning different nodes may return different, temporarily inconsistent answers. The system sacrifices strict consistency to stay responsive.
Outside of a partition, a well-designed system can be both consistent and available — the trade-off only bites during the partition itself. CAP is a statement about what you're forced to give up under failure, not a permanent handicap.
A partition, step by step
Three replicas of a bank balance, A, B and C, with C cut off from
the other two by a failed link. A client connected to C asks to withdraw
₹500.
- CP behaviour:
Cknows it can't reach a majority. It can't be sureAandBhaven't already processed a withdrawal, so it refuses: "service unavailable, try again". Clients that can reachAorBcarry on normally, because those two still form a majority. - AP behaviour:
Caccepts the withdrawal and records it locally. Meanwhile a client onAwithdraws ₹500 too. When the link heals, the replicas discover two withdrawals from a balance that only covered one. Now the system needs a conflict rule — and for money, no automatic rule is good.
The example shows why CP isn't "worse": for a minority side of a partition, an error is the honest answer.
PACELC: the trade-off when nothing is broken
CAP only describes behaviour during a partition. The PACELC formulation adds the other half: if there's a Partition, choose Availability or Consistency; else, choose Latency or Consistency.
The "else" half is the one you live with every day. Keeping replicas strongly consistent means a write isn't acknowledged until several replicas — possibly in different zones or regions — confirm it. That costs round trips. Serving reads from the nearest replica without checking the others is fast, but may be stale.
| system | during a partition | otherwise |
|---|---|---|
| Postgres with synchronous replica | PC — writes block without the replica | EC — every write waits for it |
| Postgres with async replicas | PA for reads from replicas | EL — fast, replicas may lag |
| Cassandra / DynamoDB (default) | PA | EL |
| Cassandra at QUORUM | PC for those operations | EC |
| etcd / ZooKeeper / Spanner | PC | EC |
PACELC explains why "we use an AP database" usually means "we wanted low latency", not "we planned for partitions".
What real databases choose
| System | Typical choice | Behaviour during a partition |
|---|---|---|
| Traditional RDBMS (single primary) | CP-leaning | Writes fail or block if the primary is unreachable |
| etcd, ZooKeeper, Consul | CP | Refuses writes without quorum — correctness over uptime, by design (they’re used for coordination) |
| Cassandra, DynamoDB | AP (tunable) | Every reachable node answers; conflicts resolved later |
| MongoDB (default) | CP-leaning | Reads/writes route through the primary; secondaries can lag |
Many modern databases don't hardcode the choice — they expose it as a
tunable per-operation setting. Cassandra's consistency level (ONE,
QUORUM, ALL) and DynamoDB's choice between eventually-consistent and
strongly-consistent reads let you pick the trade-off per query instead
of the database imposing one globally.
Quorums: tuning the dial
With N replicas, a write waits for W acknowledgements and a read asks R replicas and takes the newest answer. If R + W > N, every read set overlaps every write set in at least one replica, so a read sees the latest acknowledged write.
| N, W, R | R + W > N? | behaviour |
|---|---|---|
| 3, 2, 2 | yes | the usual QUORUM setting: survives one node down for both reads and writes |
| 3, 3, 1 | yes | fast reads, but any one node down blocks writes |
| 3, 1, 3 | yes | fast writes, but any one node down blocks reads |
| 3, 1, 1 | no | fastest and most available; reads can be stale |
Quorum overlap is necessary but not sufficient for linearizability — with concurrent writes and failed writes partially applied, a quorum system can still return surprising results without extra machinery like read repair on every read. The quorum reads and writes post goes into those edge cases.
Eventual consistency: AP's actual promise
Choosing availability doesn't mean giving up on consistency forever — it means deferring it. Eventual consistency is the guarantee that if no new writes occur, all replicas will eventually converge to the same value — "eventually," not "immediately." During the gap, different nodes can legitimately disagree.
// Simplified: an AP-style key-value store during a partition
// Node A (reachable by client 1):
nodeA.set("cart:42", { items: 3 }); // accepted immediately
// Node B (reachable by client 2, partitioned from A):
nodeB.get("cart:42"); // might still return the OLD value, or nothing
// Once the partition heals, replication catches up and both
// nodes converge — "eventually" consistent.
The system remains available (both nodes answered) but was not consistent (they answered differently) during the partition — exactly the AP trade being made explicit.
How replicas actually converge
"Eventually" is implemented by a few concrete mechanisms, usually all at once:
- Hinted handoff: when a replica is down, another node accepts the write on its behalf with a note — a hint — and forwards it when the replica comes back.
- Read repair: when a read asks several replicas and they disagree, the coordinator sends the newest value to the stale ones.
- Anti-entropy: a background process compares replicas and fixes differences. Comparing every row would be too expensive, so replicas exchange Merkle trees — hashes of ranges of data — and only drill into ranges whose hashes differ.
- Gossip: nodes share membership and state with random peers, so information spreads through the cluster in logarithmic time — see gossip protocols.
The spectrum between strong and eventual
"Strong" and "eventual" are two ends of a range. Several useful guarantees sit in between, and they're often what an application really needs:
| model | guarantee | typical use |
|---|---|---|
| Linearizable | acts like one copy; reads see the latest write | locks, leader election, balances |
| Sequential | everyone sees operations in the same order | replicated logs |
| Causal | if A caused B, everyone sees A before B | comments and replies, chat |
| Read-your-writes | you always see your own updates | profile edits, posting |
| Monotonic reads | you never see time go backwards | paging through a feed |
| Eventual | replicas converge if writes stop | counters, caches, catalogues |
Many "eventual consistency bugs" are really missing session guarantees. A user who edits their profile and sees the old version after refresh has hit a read-your-writes problem, which can be fixed without making the whole system strongly consistent — see read-your-writes and replication lag.
Resolving conflicts once the partition heals
Eventually-consistent systems need a rule for what happens when two replicas that diverged during a partition need to merge back into one value. Common approaches:
- Last-write-wins (LWW): attach a timestamp to every write, keep the newest. Simple, but can silently discard a legitimate concurrent write if clocks aren't perfectly synchronized.
- Vector clocks: track causality per replica so the system can detect "these two writes were genuinely concurrent, not one following the other" and surface the conflict instead of guessing — this is how Amazon's original Dynamo paper handled shopping cart merges. See Lamport and vector clocks.
- CRDTs (Conflict-free Replicated Data Types): data structures specifically designed so that merging two divergent replicas always produces a well-defined, consistent result with no manual conflict resolution — a counter that only increments, or a set that only grows, can always be merged unambiguously regardless of the order updates arrive in.
A grow-only counter shows how simple a CRDT can be. Each replica counts its own increments; the value is the sum; merging takes the per-replica maximum:
// state: { [replicaId]: count }
const increment = (s, id) => ({ ...s, [id]: (s[id] ?? 0) + 1 });
const value = (s) => Object.values(s).reduce((a, b) => a + b, 0);
const merge = (a, b) => {
const out = { ...a };
for (const [id, n] of Object.entries(b)) out[id] = Math.max(out[id] ?? 0, n);
return out;
};
Merge is commutative, associative and idempotent, so replicas can exchange state in any order, any number of times, and still agree.
Choosing CP or AP for your own system
The right choice depends on what an inconsistency actually costs versus what an outage costs:
- Banking, inventory counts, anything where overselling or double spending is the failure mode → CP. A rejected request is recoverable; a wrong balance is not.
- Social media feeds, product catalogs, shopping carts, view/like counters → AP. A slightly stale feed or an eventually-corrected cart is a far better user experience than an error page during a network blip.
And it doesn't have to be one choice per system. An e-commerce site can keep the catalogue and recommendations AP, and the stock count and payment ledger CP — each piece of data gets the guarantee its failure mode needs.
Common misreadings
- "CA systems exist." Only if the network never partitions, which means a single machine. A single-node database is "CA" in the trivial sense that there's nothing to partition — and it's unavailable whenever that node is.
- "AP means inconsistent." It means inconsistent during partitions and replication lag, which may be milliseconds. Most of the time AP systems return the latest data.
- "CP means unavailable." Only the minority side of a partition is unavailable. A CP cluster with a majority intact keeps serving.
- "Pick a database by its CAP letter." Pick by the guarantees each operation needs. Most real databases let you choose per query.
Wrap-up
CAP theorem isn't a checklist of features to pursue — it's a statement about an unavoidable trade-off during a network partition, which will happen. Partition tolerance isn't optional in any real distributed system, which leaves one genuine decision: refuse to answer when you can't guarantee freshness (CP), or answer anyway and let replicas reconcile afterward (AP, backed by eventual consistency and a conflict-resolution strategy like LWW, vector clocks, or CRDTs). PACELC adds the everyday version — consistency costs latency even when nothing is broken. Neither choice is universally right; it depends on whether a stale answer or a failed request is the cheaper mistake for that piece of data.