Skip to content
Software Atlas

CAP & PACELC

What “consistent” and “available” really mean when data lives on several machines, why CAP only bites during a network split, and why the trade-off you pay every day is latency against consistency.

⏱ ~45 min read13 sections7 interactive widgetsGo · JavaScript · SQL
FoundationsPractitionerStaff-level
Foundations§01

The 30-second version

In one sentenceWhen copies of the same data can't reach each other, every copy has to choose: refuse to answer (stay correct, but unavailable) or answer from what it has (stay up, but maybe wrong). And even when they can reach each other, keeping them in step costs time on every request.

The first half of that sentence is the CAP theorem. The second half is PACELC, and it's the half you'll feel every day.

Try it. The simulator below runs exactly this story. CP mode refuses when unsure; AP mode always answers.

Partition simulator: two bank branches
One account, $100, a copy at each branch. Cut the line, have customers use both branches, then repair it.
In CP mode Branch A is the head office and keeps the one true ledger. Branch B must phone A before every answer.
Branch Ahead office
$100
no activity yet
☎ line up
Branch Bmust ask A
$100
no activity yet
cash paid out: $0customers kept waiting: 0 ms✓ the branches agree
Try it: cut the line, then press − $60 at both branches. Do it once in CP mode and once in AP mode, and repair the line each time.

What you just saw

  • In CP mode, Branch B refused everything while the line was cut, even a harmless balance check. Branch A carried on. When the line came back there was nothing to fix: only one branch had changed the account.
  • In AP mode, both branches stayed open. Two $60 withdrawals were each fine on their own copy, but together they broke the rule “balance never goes below $0”. Repairing the line can't put the cash back.
  • Deposits merged fine in AP mode. +$50 here and +$50 there add up to the same total in any order. Some kinds of data survive “always answer” perfectly well; §09 is about telling them apart.

The part people forget

Go back to CP mode with the line up and press a few buttons at Branch B. Every transaction there needs a phone call to A before B can answer: watch the ☎ B→A→B +70 ms badge and the “customers kept waiting” counter grow. Nothing is broken, and B is still slower than it would be on its own. In AP mode the same buttons answer at once (+0 ms) and phone the other branch afterwards. Between real data centers that “phone call” is a network round trip of anywhere from 1 ms to over 200 ms, paid on every request that needs an up-to-date answer.

That is the everyday trade-off: latency versus consistency. Partitions are rare; round trips happen on every request.

Where this topic goes

  • §02–§03: the minimum distributed-systems background, then what CAP actually says (and the popular “pick 2 of 3” version gets wrong).
  • §04–§05: what “consistent” means precisely, and the ladder of weaker promises below it.
  • §06–§08: PACELC, real systems, and choosing consistency per request in Go and JavaScript code.
  • §09–§11: designing with it, what partitions look like in production, and pitfalls.
Foundations§02

Distributed systems in five minutes

In one sentenceMachines in a distributed system only know about each other through messages, and messages can be lost or late. So no machine ever knows for sure what another one is doing right now.

The words

  • A node is one machine (or one running process) in the system.
  • A distributed system is several nodes working together over a network to look like one service.
  • A replica is one copy of the same data, kept on a different node. You keep replicas so that losing one machine doesn't lose the data, and so that users far away can read from a copy near them.

In a group chat, every phone has a copy of the conversation: each phone is a replica of the chat history. When you send a message, it goes out over the network and the other phones update their copies when it arrives. Until then, their copies are behind yours.

Nodes only talk through messages

A node can't look inside another node's memory. To find out anything (“what's the balance?”, “are you alive?”), it sends a message and waits for a reply. Three things can happen to that message:

  1. It arrives quickly. (The normal case, almost always.)
  2. It arrives late: the network is congested, or the other node is busy.
  3. It never arrives: a cable, switch or firewall dropped it, or the other node crashed.

You can't tell slow from dead

Here is the fact that everything in this topic grows from. Below are three very different failures. Look at them from the client's side only:

Three different failures, one identical view from the client
Request lost
clientserver✖withdraw $60timeout!
The network dropped it. The server never knew.
Server crashed
clientserver✖withdraw $60timeout!
It may or may not have done the withdrawal first.
Server slow / reply lost
clientserverfrozenwithdraw $60timeout!
It did the withdrawal. The answer arrives late, or never.
Time runs downward. In all three, the client sends a request and hears nothing before its timeout. It cannot tell which story happened, so it can't know whether the withdrawal took place.

All the client knows is “I asked and nobody answered”. So it uses a timeout: “if I hear nothing in 2 seconds, I'll treat the other side as unreachable.” But a timeout is a guess. The other node might be fine and about to reply. It might even have done the work already.

A network partition

A network partition is when the network splits the nodes into groups that can't reach each other, even though every node is still running. Each group can still talk among itself and to some users. Each group may think the other group has died.

Partitions don't need a dramatic cable cut. A misconfigured firewall rule, a failing network switch, or a node so overloaded that it answers nothing in time all look the same from the outside. (§10 shows the real-life causes.)

Foundations§03

CAP, stated properly

In one sentenceIf the network splits, a replicated system can't be both always-answering and always-up-to-date. It has to give up one of them until the network heals.

Eric Brewer proposed this in a talk in 2000; Seth Gilbert and Nancy Lynch proved it in 2002. The name comes from three properties. Each one means something narrower in the theorem than the same word means in everyday engineering talk, so read these carefully.

The three letters

C — Consistency. The system behaves as if there were one single copy of the data. Once a write is confirmed, every read anywhere returns it (or something newer). The precise name is linearizability, and §04 is all about it. Note: this isn't the C in ACID, which is about a database's rules being kept inside a transaction. Same letter, different idea.

A — Availability. Every request that reaches a node that hasn't crashed gets a real answer, not an error. “Sorry, try later” doesn't count. This is much stricter than the everyday meaning of availability (“up 99.9% of the time”): it says every working node must answer every request.

P — Partition tolerance. The system keeps following its rules even when the network loses messages between nodes. In other words: partitions can happen, and the system has a plan for them.

The proof, in four lines

Take the simplest possible system: two replicas, A and B, each holding x = 0.

  1. The network between A and B splits. No messages get through.
  2. A client writes x = 1 to A. If A wants to be available, it must accept and confirm the write. It can't tell B.
  3. Another client reads x from B. If B wants to be available, it must answer. All it has is x = 0.
  4. The read returned 0 after a confirmed write of 1. That's not consistent. The only way to stay consistent was for A or B to refuse, which isn't available.

That's the whole proof. Nothing clever is required, which is a good sign that the result is fundamental: it comes straight from §02's facts.

What CAP actually says: one decision, made only while the network is split
1. The network splits. Node A and node B are both running, but messages between them are lost. A request arrives at B for data that A may have changed.
B can't check with A. It must pick one:
Refuse (or wait) → CP
Return an error or time out. Every answer that is given is up to date.
Gives up: Availability on B's side.
Answer from its own copy → AP
Reply with what B has. It may be stale, and writes on both sides may conflict.
Gives up: Consistency (linearizability) until the network heals.
When the network is healthy there's no forced choice: a system can be both consistent and available. CAP only describes the partition case. Which is why “pick 2 of 3” is misleading.

Why “pick 2 of 3” is misleading

CAP is often drawn as a triangle with the slogan “pick any two”. That suggests three equal options: CA, CP and AP. It's wrong in three ways.

  • P isn't optional. Between machines connected by a network, partitions will happen. “Not P” would mean “we've decided the network never fails”, which isn't a decision you get to make. So the real choice is only between C and A.
  • The choice only exists during a partition. On a normal day, when messages get through, a system can be perfectly consistent and answer every request. CAP says nothing at all about that case. (§06 shows that the normal case has a different trade-off.)
  • It's not one switch for the whole system. A system can make different choices for different data or different operations: refuse withdrawals but keep answering balance checks with a “might be a minute old” label. Real systems also sit on a scale between the two extremes rather than at one end.

A better statement, close to how Brewer himself restated it in 2012: when a partition happens, each operation on each piece of data must either wait or risk being wrong.

What CAP doesn't say

  • It doesn't say anything about speed. A system that takes 30 seconds to answer still counts as “available” in the theorem. Users wouldn't agree. (§10 comes back to this.)
  • It doesn't say a CP system is often down. It's only unavailable on the side of a split that can't confirm, and only during the split.
  • It doesn't say an AP system is always wrong. Most of the time its copies agree; it just can't promise that.
  • It doesn't promise you'll get even one of the two. Plenty of real systems are neither: a database with a slow async replica that also loses data on failover gives up C and can still be unavailable. CAP is an upper limit, not a guarantee.
Practitioner§04

What “consistent” means: linearizability

In one sentenceA system is linearizable if it behaves as though there's one copy of the data and every operation happens at one instant somewhere between “sent” and “answered”. Consequence: once anyone has seen a new value, nobody can see the old one again.

“Every read returns the latest write” sounds simple until you ask: latest according to whom? On different machines, operations overlap in time, and a request isn't instant: it's sent, travels, gets processed, and the answer travels back. Linearizability is the precise answer to “latest”.

Operations take time

To keep things small, consistency is usually explained on a register: one named value, x, that you can write and read. Think of one row in a database or one key in a cache.

Draw each operation as a bar from the moment the client sent it to the moment it got the answer. Somewhere inside that bar, the system actually did the work, but the client can't see exactly when. That instant is the operation's linearization point.

The rule for linearizability:

  1. You must be able to put one dot inside every bar.
  2. If you then read the dots left to right, every read must return the value of the latest write before its dot (or the starting value, if there's none).

If at least one way of placing the dots works, the history is linearizable. If no placement works, it isn't.

Try it

The checker below searches every possible ordering for you. Start with Overlapping write: Bob saw the new value, and Carol, who started after Bob had finished, saw the old one. Click the reads to change their values and watch the dots appear when a valid placement exists.

Linearizability checker
Each bar is one operation, from when the client sent it to when it got the answer. Click a read (dashed) to change the value it returned.
Alice's write takes a while. Bob reads during it; Carol reads later, but still during it. Click Bob's and Carol's reads to change what they got. x starts at 0.
t=0t=1t=2t=3t=4t=5t=6t=7t=8t=9t=10AliceBobCarolwrite x=1read x → 1read x → 0
No way to put one dot inside every bar so that each read returns the latest write before its dot.
✗
Linearizable (one copy, real time)
Bob already saw x=1, and Carol's read started after Bob's finished. Once anyone has seen the new value, nobody may see the old one again. Change Carol's read to 1, or Bob's to 0, and it becomes linearizable.

Three lessons from those histories:

  • Overlapping operations may go either way. While Alice's write is in progress, a read may return 0 or 1. Both are fine.
  • But the system can't change its mind. Once Bob has seen 1, the write has “happened”. Carol's later read must also see 1. This is the property that makes linearizable systems feel like one copy.
  • Finished means finished. In Stale after the write finished, the write was confirmed before the read began. In real time it came first, so a linearizable read must return 1. Distance, replicas and caches are no excuse.
  • Concurrent writes may land in either order, but once chosen it's fixed. In Two writers, then reads, Alice's and Bob's writes overlap, so the final value may be 1 or 2. But Carol reads after both have finished, so her two reads must agree: 1 then 1, or 2 then 2. Reading 1 and then 2, with no write in between, is not allowed.

Why it's expensive

To promise “no read will ever see the old value after anyone saw the new one”, a replica can't just answer from its own copy. It has to make sure it isn't behind. In practice that means one of:

  • Sending every read and write through a single leader, and having that leader check with a majority (a quorum) that it's still the leader before answering, because a leader cut off by a partition might have been replaced without knowing it.
  • Running a consensus protocol such as Raft so that a majority agrees on the order of every write. The Consensus & Raft topic covers how.

Either way, each operation waits for at least one round trip to other nodes. During a partition, the side without a majority can't do that, so it must refuse. Linearizability is the C in CAP.

Practitioner§05

The consistency ladder

In one sentenceBelow linearizability there's a ladder of weaker promises. Each step down lets through one more kind of surprise, and in exchange needs less waiting and survives more failures.

A consistency model is the contract between a data store and your code: “here's what reads can return after writes”. The stronger the contract, the less your code has to worry about. The weaker it is, the faster and more available the store can be. Here are the rungs you'll meet, strongest first:

The consistency ladder
↑ easier to reason about · ↓ faster, stays up in more failures
Linearizable
Behaves like one copy. Once anyone sees a new value, everyone does.
Allows: Nothing surprising.
e.g. etcd, ZooKeeper, Spanner, a single Postgres primary
Sequential
Everyone sees the same order of operations, and each client's own order is kept. Real time between clients is not.
Allows: Bob reads an old value after Alice was told “saved”.
e.g. Rare as a product promise; ZooKeeper reads come close
Causal
If B depends on A, everyone sees A before B. Unrelated writes may show up in different orders.
Allows: Two people see two unrelated comments in opposite orders.
e.g. MongoDB causal sessions, some geo-replicated research systems
Read-your-writes, monotonic reads
Per client only: you see your own writes, and your view never goes backwards.
Allows: You see a reply before the post it answers.
e.g. What you build on top of replicas (Replication §06–§07)
Eventual
If writes stop, all copies end up the same. That's all.
Allows: You save, reload, and your change is gone (for a while).
e.g. Async replicas, DNS, Cassandra at ONE, DynamoDB default reads
Each rung allows everything the rungs above it allow, plus at least one more surprise. (Session guarantees and causal are not a perfect line in every textbook; this is the usual teaching order.)

Each rung, by the surprise it allows

The checker below has one history per rung boundary. For each, it tells you which models allow it. Go through them in order; each story is something real users have reported as a bug.

Which models allow this history?
Each bar is one operation, from when the client sent it to when it got the answer. Click a read (dashed) to change the value it returned.
Alice's write has completed (she got “OK”) before Bob even starts reading. x starts at 0.
t=0t=1t=2t=3t=4t=5t=6t=7t=8t=9t=10AliceBobwrite x=1read x → 0
No way to put one dot inside every bar so that each read returns the latest write before its dot.
✗
Linearizable (one copy, real time)
The write finished at t=3, the read started at t=4.5, so in real time the write came first. A linearizable read must return 1.
✓
Sequential (one order, each client's own order kept)
Sequential consistency ignores real time between different clients: “Bob's read, then Alice's write” is a valid single order. One order that explains every read: Bob: read x→0 → Alice: write x=1
✓
Causal (causes before effects)
Every client saw each cause before its effects. Writes that didn't depend on each other may appear in different orders to different clients.
✓
Read-your-writes + monotonic reads (per-client promises)
Each client sees its own writes, and nobody's reads go backwards in time.
✓
Eventual (copies converge… eventually)
Allowed. Eventual consistency only promises that copies agree once writes stop; any value that was written may be returned meanwhile.

Stale after the write finished (linearizable ✗, sequential ✓). Sequential consistency promises that everyone sees the same order of operations and that each client's own operations stay in the order it did them. It does not promise that the order matches the clock on the wall between different clients. So Bob may be “behind” Alice: the store can pretend his read happened before her write. ZooKeeper's ordinary reads behave roughly like this: every server applies the writes in the same order, but the server you read from may be a little behind.

Two writes, two orders (sequential ✗, causal ✓). Alice and Bob write at the same moment, independently. Carol sees 1 then 2; Dan sees 2 then 1. There's no single order everyone agrees on, so this isn't sequential. But neither write depended on the other, so causal consistency allows it. Think of two unrelated comments on a post showing up in different orders for different readers. Usually harmless.

Reply before the post (causal ✗, session ✓). Bob read Alice's post and then replied: his reply was caused by the post. This cause-and-effect relationship is called causality. Causal consistency promises that anyone who sees an effect also sees its cause. Carol sees the reply but not the post, an answer to a question she can't see. Read-your-writes and monotonic reads (below) don't forbid this: they only constrain what Carol sees of her own writes and reads, and Carol never saw the post herself. (A third session guarantee, writes-follow-reads, would forbid it; all four session guarantees together add up to causal.)

My write vanished (session ✗, eventual ✓). Alice saves, reloads, and her change is gone. Session guarantees are promises to each client about its own view: read-your-writes (you always see your own changes) and monotonic reads (your view never goes backwards). This history breaks the first. “Session guarantees” is the family name; the ladder and the checker show these two because they're the ones you'll use most. Replication §06 shows how replica lag causes exactly this, and §07 how to fix it in code; this topic won't repeat that.

Eventual allows all of the above. Eventual consistency only promises that if writes stop, all copies end up the same. About any individual read, it promises nothing.

What each rung costs

RungWhat it needsDuring a partition
LinearizableEvery operation confirmed with a leader or a majority, in real timeOnly the majority side can serve
SequentialOne agreed order of writes (still a leader or consensus), reads may lagWrites need the majority side
CausalTrack what each write depended on (e.g. with version vectors); hold back writes whose causes haven't arrivedBoth sides can keep working
SessionRemember, per client, what it wrote or saw; route its reads accordinglyBoth sides, if each client sticks to its side
EventualNothing during writes; a way to merge copies laterBoth sides keep working

The big break is between sequential and causal. Causal is roughly the strongest model that can stay available on both sides of a partition; everything above it needs coordination that a partition blocks. (The precise result, by Mahajan, Alvisi and Dahlin in 2011, is about a slightly stronger variant called real-time causal consistency.) That's why research and newer databases put so much effort into causal consistency: it's as much as you can have without giving up availability.

Practitioner§06

PACELC: the trade-off you pay every day

In one sentencePartition? Choose Availability or Consistency. Else (a normal day)? Choose Latency or Consistency. The Else half is the one you pay on every request.

CAP only talks about the rare moment when the network is split. Daniel Abadi pointed out in 2010 that this misses the trade-off engineers actually argue about. Even with a perfect network, a replica can only promise an up-to-date answer by checking with other replicas first, and checking takes a round trip. That's PACELC, read as “if P then A or C, else L or C”.

Worked example: one write, three choices

Your database has three replicas in three cloud regions: the leader in US East (Virginia), followers in Europe (Ireland) and Asia (Singapore). Your app server sits next to the leader. Rough round-trip times between regions:

From → toRound trip (approx.)
Inside one region~1 ms
US East ↔ Europe~70 ms
US East ↔ Asia~215 ms

The leader writes to its own disk in about 1 ms. Then it can reply at three different moments:

Leader replies after…Write takesWhat you get
its own disk only (async)≈ 1 msFast. Followers catch up later; reads from them may be stale, and if Virginia burns down, the last writes are gone.
one follower confirmed (majority: 2 of 3)≈ 1 + 70 = 71 msEvery confirmed write is in two regions. A majority read always sees it.
both followers confirmed (all 3)≈ 1 + 215 = 216 msEvery copy is current, but the slowest region sets the pace, and one region down blocks all writes.

Now a request that saves five things one after another: 5 ms, 355 ms, or over a second. Nothing is broken. The network is perfect. You're paying for consistency with time.

One write, three choices (to scale)
async
majority
→ Europe → back, 70 ms
all
→ Asia → back, 215 ms
Async waits for nothing remote. A majority write waits for the nearest follower. Writing to all waits for the farthest one.

Reads have the same choice

A user in Europe can read from the replica in Ireland (~1 ms, but maybe a little behind), or from the leader in Virginia (~70 ms, always current). It's the same trade-off again, per read.

Try the combinations yourself. Things to try:

  1. User in Europe, writes leader only, reads nearest: reads take ~1 ms and may be stale; writes still pay the round trip to the leader in Virginia (~71 ms).
  2. Same user, reads from the leader: reads are fresh, and pay the trip to Virginia.
  3. Writes leader + 1 follower, reads nearest 2. Here W = 2 (copies that confirm each write) and R = 2 (copies each read asks). R + W = 4 is more than the 3 copies, so every read asks at least one copy that has the latest write (Replication §10 explains this quorum rule). Reads see the latest acknowledged write (with caveats, §11) without having to go specifically to the leader. For a Europe user the nearest two include Virginia anyway; switch the user to Asia and the nearest two are Singapore and Ireland, so a fresh read waits ~165 ms instead of the 215 ms trip to Virginia.
  4. Move the leader to Asia and watch every write from Europe get slower.
PACELC calculator
Three replicas: US East, Europe, Asia. Pick where the user is, where the leader is, and how much waiting each write and read does.
1 write
71 ms
3 writes in a row
213 ms
1 read
1 ms
Write = user→leader round trip (70 ms) + ~1 ms disk. Round-trip times are rough public-cloud figures.
Else (normal day)E → L
Reads may be stale: a replica can be ~108 ms or more behind the leader (seconds under load).
Writes return before any other region has them: fast, but a lost leader region loses them.
Partition (a region cut off)P
The leader's side keeps writing. Clients that can't reach the leader can't write; if the leader's region dies, acknowledged writes not yet copied are lost.
Reads keep working everywhere, but cut-off replicas serve older and older data.

How systems get labeled

A PACELC label has two halves: what the system does during a partition, and what it does otherwise.

  • PA/EL: answers during partitions, and doesn't wait for other replicas normally. Fast and always up, eventually consistent. (Dynamo-style stores at their defaults.)
  • PC/EC: refuses during partitions, and waits for other replicas normally. Slower, but behaves like one copy. (Consensus-based systems.)
  • PC/EL and PA/EC exist too, and are usually a sign of configuration choices; §07 has examples.

Most real systems let you choose per request, which is why §08 is about code.

Practitioner§07

Real systems, honestly classified

In one sentenceA database isn't “CP” or “AP”. A configuration of a database, for a particular operation, is. Most popular systems can be set up either way.

Here's where common systems land with typical settings. Read the caveats column; it matters more than the label.

System (typical setup)PACELCCaveat
Google Spanner, CockroachDB (distributed SQL)PC/ECEvery write waits for a majority of replicas. Stale “follower reads” are available on request, and those are EL.
etcd, ZooKeeper, Consul (coordination services)PC/ECConsensus-based. Some read modes (e.g. etcd's serializable reads, ZooKeeper reads without sync) skip the check and may be stale.
Cassandra, ScyllaDB at ONEPA/ELAt QUORUM it becomes more PC/EC. LOCAL_QUORUM is PC/EC only inside one data center; across data centers it's still PA/EL (see below). Either way conflicts use last-write-wins; only lightweight transactions are linearizable.
DynamoDB, default readsPA/ELConsistentRead: true makes single-region reads strong (EC). Global tables replicate asynchronously between regions by default.
Postgres primary + async replicas, reading from replicasPC/ELWrites need the primary (PC). Replica reads are fast and may be stale (EL), and keep working in a partition (so reads alone are PA).
Postgres with a synchronous standbyPC/ECCommits wait for the standby; if it's unreachable, commits wait forever (Replication §05).
MongoDB replica setdependsControlled by write concern (how many members must confirm a write) and read concern (how confirmed the data you read must be). Since 5.0 the default write concern is majority; reads default to the primary but with readConcern: "local", which can return a write that only the old primary had; if that primary fails before copying it, the new primary never had it, and the write is discarded (rolled back) when the old one rejoins. Ask for "majority" or "linearizable" when you need it.

Abadi's 2010 blog post and 2012 paper put Dynamo, Cassandra and Riak in PA/EL; BigTable, HBase and fully ACID distributed databases in PC/EC; MongoDB (with the defaults of that time) in PA/EC; and Yahoo's PNUTS in PC/EL. Several of those defaults have changed since, which is exactly the point: the label belongs to a configuration, not a product.

LOCAL_QUORUM across two data centers

LOCAL_QUORUM means “a majority of the replicas in my data center”. That's great for latency, but a write in one data center and a read in another can miss each other completely:

LOCAL_QUORUM across two data centers
Each data center (DC) holds 3 replicas of x, 6 in total. Alice writes x = 1 in DC 1; a moment later Bob reads x in DC 2.
DC 1 (US East)Alice writes here
replica 1
x=1
W
replica 2
x=1
W
replica 3
x=0
DC 2 (Europe)Bob reads here
replica 4
x=0
R
replica 5
x=0
R
replica 6
x=0
W = confirmed Alice's write (2 of 3 in DC 1)R = asked by Bob's read (2 of 3 in DC 2)each request waits ~1 ms
No overlap: the read asked two DC 2 replicas, and the write hasn't reached DC 2 yet (it's copied there in the background). Bob reads x = 0, a stale value, even though Alice was told “saved”.

So LOCAL_QUORUM everywhere is PC/EC within a data center and PA/EL between them: during a split between data centers both sides keep serving (PA), and on a normal day nobody waits for the other ocean (EL). If a read in DC 2 must see a write made in DC 1, use QUORUM (or EACH_QUORUM for writes) and pay the cross-ocean round trip. One more trap: some drivers can be configured with a downgrading retry that silently turns a failed QUORUM into ONE (one of the Cassandra cards in the classifier below).

Now you try. For each setup, decide what it does during a partition (A or C) and on a normal day (L or C):

Classify the system
For each setup: during a Partition, does it choose A or C? Else (normal day), Latency or Consistency?
Cassandra, reads and writes at consistency level ONE
Postgres primary + async replica; the app reads from the replica
Google Spanner (default reads and writes)
Cassandra at QUORUM, but the driver retries at ONE when a quorum can't be reached
DynamoDB with default (eventually consistent) reads
etcd (or ZooKeeper) storing who the current leader is
Cassandra at QUORUM, a single data center with 3 replicas
Answered 0 / 7

Three questions to ask about any system

  1. Which operations? Writes, reads, and transactions often have different behavior. “Reads are PA, writes are PC” is a common honest answer.
  2. Which settings? Consistency levels, write/read concerns, synchronous_commit, driver retry policies. Look at what your code sends, not the default in a blog post.
  3. Has anyone tested it? Claims like “strongly consistent” have often failed independent testing with injected partitions (§10). If there's a public Jepsen report for the version you run, read it.
Practitioner§08

Choosing consistency per request, in code

In one sentenceYou rarely pick one consistency level for a whole system. You pick it per write and per read: a page-view counter doesn't deserve the round trip a payment does.

Most data stores expose the PACELC dial directly. The general name is tunable consistency. Here's the same idea in four systems, each with the cost of turning it up.

Postgres: how many copies before “committed”?

With a synchronous standby configured (Replication §05 sets one up), Postgres's synchronous_commit setting decides what COMMIT waits for. The useful part: you can change it for one transaction.

write.go
// write saves one row. level decides how many copies must have it before COMMIT returns.
func write(ctx context.Context, db *pgxpool.Pool, level, note string) error {
	return pgx.BeginFunc(ctx, db, func(tx pgx.Tx) error {
		// true = only for this transaction (same as SET LOCAL)
		_, err := tx.Exec(ctx, "SELECT set_config('synchronous_commit', $1, true)", level)
		if err != nil {
			return err
		}
		_, err = tx.Exec(ctx, "INSERT INTO events(note) VALUES ($1)", note)
		return err
	})
}

// ctx, cancel := context.WithTimeout(ctx, 2*time.Second) // ← give up after 2 s
// defer cancel()
// write(ctx, db, "local", "page_view")  ← own disk only: fast
// write(ctx, db, "on", "payment 42")    ← waits for the standby too

Measured on the test machine, with primary and standby on the same computer (so the “network” costs almost nothing), 200 commits each:

LevelGo (pgx)Node (pg)
local~0.6 ms per commit~1.1 ms
on~1.5–1.8 ms~2.5 ms

Even with zero distance, waiting for a second copy more than doubled commit time. Across regions, add a full round trip (~70 ms between US East and Europe) to every on commit.

Then we stopped the standby. local commits carried on as if nothing had happened. on commits hung until the Go context deadline of 2 s, and failed with timeout: context deadline exceeded. That's CAP in one setting: local chose availability, on chose consistency. (And careful: the timed-out row was still committed on the primary. Replication §05 explains why that's dangerous.)

Redis: WAIT for replicas

Redis replicates asynchronously. The WAIT numreplicas timeout command blocks until that many replicas have received this connection's earlier writes, or the timeout passes, and returns how many did.

wait.go
conn := rdb.Conn() // one dedicated connection: WAIT only counts writes made on it
defer conn.Close()

if err := conn.Set(ctx, "balance", 30, 0).Err(); err != nil {
	return err
}
acked, err := conn.Wait(ctx, 1, 100*time.Millisecond).Result() // ← 1 replica, at most 100 ms
if err != nil {
	return err
}
if acked < 1 {
	log.Println("only on the primary so far") // the SET is NOT undone
}

Run against a local primary and replica: with the replica up, WAIT returned 1 in well under a millisecond. With the replica stopped, it waited the full 100 ms and returned 0, and the value stayed written on the primary. WAIT tells you how far a write got; it doesn't make Redis strongly consistent, and the Redis docs say so. A failover can still lose a write that WAIT reported as copied, if the newly promoted replica wasn't one of those that got it.

Cassandra: a consistency level on every query

In Cassandra each query carries its own consistency level, deciding how many of the N replicas must answer. These are the N, W, R numbers from Replication §10.

cassandra.go
// feed: any one replica will do. Fast, maybe a little stale.
func feed(ctx context.Context, session *gocql.Session, uid string) ([]string, error) {
	iter := session.Query(`SELECT post_id FROM feed WHERE user_id = ?`, uid).
		Consistency(gocql.One).WithContext(ctx).Iter()
	var ids []string
	var id string
	for iter.Scan(&id) { // ← one row per loop; false when rows run out or on error
		ids = append(ids, id)
	}
	if err := iter.Close(); err != nil { // ← always Close: it frees the iterator and reports errors
		return nil, err
	}
	return ids, nil
}

// balance: wait for 2 of the 3 replicas in this data center.
func balance(ctx context.Context, session *gocql.Session, acct string) (int64, error) {
	var cents int64
	err := session.Query(`SELECT cents FROM balances WHERE account = ?`, acct).
		Consistency(gocql.LocalQuorum).WithContext(ctx).Scan(&cents)
	if err != nil {
		return 0, err // ← CP: an error instead of a guess
	}
	return cents, nil
}

The LOCAL_QUORUM read only guarantees fresh data if writes to balances also use LOCAL_QUORUM (2 + 2 > 3) in the same data center. Across data centers the two quorums don't overlap (the diagram in §07); use QUORUM/EACH_QUORUM there. A read at QUORUM after a write at ONE can miss it. For “only if nobody else did first” logic, such as claiming a username, plain consistency levels aren't enough: use a lightweight transaction (INSERT … IF NOT EXISTS), which runs Paxos and is linearizable.

DynamoDB: one flag per read

DynamoDB reads are eventually consistent by default. Set ConsistentRead and the read reflects every write that succeeded before it.

dynamo.go
out, err := db.GetItem(ctx, &dynamodb.GetItemInput{
	TableName:      aws.String("accounts"),
	Key:            map[string]types.AttributeValue{"id": &types.AttributeValueMemberS{Value: id}},
	ConsistentRead: aws.Bool(true), // ← strong read: costs 2× an eventual one
})

The cost is spelled out in the AWS docs, and it's PACELC on a price list: a strongly consistent read uses twice the read capacity of an eventually consistent one, can have higher latency, isn't supported on global secondary indexes, and “might not be available if there is a network delay or outage”, in which case DynamoDB may return a server error (HTTP 500). Strong reads give up availability; that's the C-or-A choice, in the docs.

(The DynamoDB and Cassandra snippets were compiled against the real Go modules and loaded in Node 22, but not run against a live cluster. The Postgres and Redis ones were run as shown.)

The dial, side by side

SystemWeaker / fasterStronger / slowerSet per
Postgres + standbysynchronous_commit = localon, remote_applytransaction
Postgres readsreplicaprimary (Replication §07)query
Redisplain writewrite + WAIT n tcommand
CassandraONELOCAL_QUORUM, QUORUM, ALL; SERIAL for LWTquery
DynamoDBdefault readConsistentRead: trueread
MongoDBw: 1, readConcern: "local"w: "majority", readConcern: "majority" / "linearizable"operation
Practitioner§09

Designing around it: which data needs what

In one sentenceAsk of each piece of data: what goes wrong if two copies disagree for a few seconds? If the answer is “someone briefly sees an old number”, go eventual. If it's “we sold the last seat twice”, that operation needs a single decision-maker.

Back in §01, two kinds of operation behaved very differently in AP mode. Deposits merged cleanly. Withdrawals broke the rule “balance never below $0”. That difference is the key to designing with CAP.

Rules that need one decision-maker

An invariant is a rule that must always be true, like “balance ≥ 0” or “each username belongs to one person”. Some invariants can only be checked by looking at all recent writes at once:

  • Balances and stock that can't go below zero. Two replicas that each see “1 seat left” will each sell it.
  • Uniqueness. Usernames, email addresses, order numbers. Two replicas that each see “alice is free” will give it to two people.
  • Locks and leadership. “Only one worker processes this job”, “only one node is the leader”. Two sides of a partition that each grant the lock have no lock at all. (Use a coordination service, with a fencing token on everything the lock-holder touches; see Consensus & Raft.)

These operations need linearizability: a leader, a consensus system, or a lightweight transaction. During a partition, they should refuse on the side that can't confirm. An error message is cheaper than an oversold flight.

Data that's fine eventually

  • Counters people glance at: likes, view counts, follower counts. Off by a few for a few seconds, nobody notices.
  • Feeds, timelines, recommendations, search results. They were never exact anyway.
  • Presence and activity: “online”, “last seen 5 minutes ago”, typing indicators.
  • Your own analytics and logs, as long as they converge.

These can be AP: served from the nearest replica, written locally, merged later.

Make operations merge on their own

The deposit trick generalizes. Operations whose order doesn't matter are called commutative: +50 then +20 equals +20 then +50. If every update is commutative, replicas can accept them independently and merge later without conflicts.

  • Store “+1 like from replica 2” instead of “likes = 18”. Each replica counts its own increments; the total is the sum. This is the simplest CRDT, and Replication §09 introduces the others (sets, text).
  • Store events (“added item”, “removed item”) instead of overwriting state, then compute the state from the events.

Compare with “set balance = 40”: two different overwrites can't both win, so one gets thrown away, usually by last write wins.

Patterns for the hard cases

Home region. Give each record one owner. Alice's account lives in Europe; all writes to it go there, even from Asia. Writes are linearizable per record, and most users are near their home. This home region pattern is the most common way to run in several regions without conflicts.

Escrow (split the budget). A shop has 100 units of stock in two regions. Give each region 50 to sell on its own, with no coordination. Only when a region runs low does it ask the other for more. Each region can approve sales locally, even during a partition, and the total can never go negative. The idea is old (database researchers described “escrow transactions” in 1986) and shows up today in inventory, quota and budget systems.

Accept, then apologize. Sometimes availability is worth occasionally breaking the rule. Amazon's shopping cart famously accepted every “add to cart” and merged conflicting carts later; an airline may overbook and compensate. If the business can repair a rare mistake (refund, voucher, apology email), you can choose AP and handle the conflict as a business process. Sagas are the tool for undoing work across services with compensating steps.

Mix within one request. A checkout page can read product descriptions and reviews from the nearest replica, and run the payment and stock decrement against the leader. Consistency is chosen per operation, not per app.

✕ Consistency as an engineering default

“We use database X, so everything is eventually consistent.” Months later, a support ticket reveals that two customers both bought the last ticket.

✓ Consistency as a product decision

For each operation, ask the product owner: which is worse here, a short error or slower page, or a wrong answer we fix later? Write the answer next to the code that picks the consistency level.

Staff-level§10

Staff-level: partitions in real life

In one sentenceIn production, most “partitions” aren't cut cables. They're slow things: a paused process, an overloaded node, a link that drops some packets one way. Your timeouts decide when slow counts as partitioned.

What partitions actually look like

Network partitions do happen inside a single data center, more often than most people assume: a 2014 survey of public incident reports by Peter Bailis and Kyle Kingsbury (“The Network Is Reliable”) collected dozens, from switch failures to misconfigured firewalls. But in day-to-day operation, the partition you'll actually meet is usually one of these:

  • A paused process. A stop-the-world pause for garbage collection, a virtual machine being moved to another host, or a container being starved of CPU. The node is frozen for seconds; then it wakes up and carries on as if no time had passed.
  • An overloaded node. It's alive, but its queues are so long that heartbeats and requests time out. To everyone else, it's unreachable.
  • An asymmetric partition. A can send to B, but B's replies to A are lost. A thinks B is dead; B thinks everything is fine.
  • A partial partition. A can reach B and C, but B and C can't reach each other. Each of them gets a different picture of who is alive.
  • Gray failure. The node passes its health checks but fails 30% of real requests. It's “up” on the dashboard and “down” for users.

The paused leader

The paused process is the most dangerous, because the paused node doesn't know it was gone:

A leader pauses for longer than its lease
node A
leader
GC pause: frozen
writes!
A's lease
valid
node B
elected leader
Node A's lease (its right to act as leader) expired during the pause. B was elected. A wakes up mid-request and writes as if it's still the leader.

A checked “am I still leader?” just before the pause, and the answer was yes. Then it froze. By the time it resumed, B had been elected, and now two nodes act as leader: split brain. No amount of checking in A's own code can close this gap, because the pause can land between the check and the write. The fix lives on the receiving side: every write carries a fencing token (a number that goes up with each new leader), and storage rejects tokens older than the newest it has seen. Replication §08 shows fencing; Consensus covers leases.

Test it: inject faults, check the history

Consistency bugs only appear under faults, so you have to cause the faults. The approach popularized by Jepsen:

  1. Run several clients doing reads and writes, and record each operation's start time, end time and result (exactly the bars in §04).
  2. Meanwhile, inject faults: cut the network between nodes, pause processes, skew clocks, kill and restart nodes.
  3. Afterwards, check the recorded history against the model the system claims: “is there a valid linearization?”

Tools for each step: iptables rules or tc netem on Linux to drop or delay packets, Toxiproxy as a fault-injecting proxy between your app and its dependencies, and checkers such as Porcupine (a Go library) or Knossos and Elle (Jepsen's own). Even without a full Jepsen setup, a test that freezes your leader process with kill -STOP (exactly like a long GC pause) for 30 seconds and then resumes it with kill -CONT will teach you a lot about your failover.

Multi-region architectures through PACELC

ArchitecturePACELCPaysTypical users
One leader region, read replicas elsewherePC/EL for replica readsFar users pay a round trip on every write; replica reads may be staleMost apps; the simplest option
Home region per user/tenantPC/EC per recordUsers away from home pay the trip; moving a user's home is a migrationBanks, SaaS with data-residency rules
Consensus across 3+ regions (Spanner, CockroachDB)PC/ECEvery write waits for the nearest majority: tens of msGlobal ledgers, inventory
Multi-active, merge later (DynamoDB global tables, Cassandra multi-DC)PA/ELConflicts are resolved by last write wins or by your codeCarts, profiles, sessions, feeds

A useful trick for the consensus option: place replicas so that a majority is close together. With replicas in Virginia, Ohio and Ireland, a majority (Virginia + Ohio) can commit in ~10–15 ms, while Ireland still holds a copy if a whole US region fails.

Harvest and yield: “available” isn't all or nothing

CAP's A is binary: answer or don't. Real services can degrade. Armando Fox and Eric Brewer named the two dials harvest and yield:

  • Yield: the share of requests that get an answer.
  • Harvest: how complete each answer is.

A search engine with 10 shards and one unreachable can fail every query (yield drops) or answer from 9 shards (harvest drops to 90%, yield stays 100%). For search, feeds and dashboards, a 90% answer beats an error. For a bank balance, it doesn't.

Latency is the real availability

CAP counts an answer that arrives after 30 seconds as “available”. Users don't: after a couple of seconds they've given up, and your load balancer has probably retried elsewhere. In practice:

  • Your timeout is your partition detector. Every timeout in your code is the line where “slow” becomes “treat it as gone”. Set it too short and every tail-latency spike looks like a partition and triggers failovers; too long and users wait for a node that isn't coming back.
  • Most incidents are slowdowns, not clean splits. A dependency that gets 10× slower causes queues, timeouts and retries everywhere. That's a latency problem, and PACELC's “E” side, not the P side, is where you'll spend your design effort.
  • Retries after a timeout can double-apply writes. The first attempt may have succeeded (§02's “reply lost” case). Make writes idempotent; the Idempotency topic shows how.
Practitioner§11

Pitfalls

1

“Our database is CA”

A system that runs on more than one machine doesn't get to opt out of partitions (§03). “CA” usually means one of two things: it's really a single node, or nobody has decided what happens when the network splits, which means it will do something unplanned. Ask the vendor: “during a partition, does the minority side return errors or stale data?”

2

“X is CP” or “Y is AP”, without the settings

The same database is PA/EL with one configuration and PC/EC with another (§07). Cassandra at ONE and at LOCAL_QUORUM behave very differently, and LOCAL_QUORUM behaves differently again across data centers; so does MongoDB with different read and write concerns. Write down the consistency level each operation actually uses, and review it like any other code.

3

Assuming a quorum means linearizable

R + W > N makes read and write sets overlap, but with last-write-wins conflicts, sloppy quorums and partially failed writes, reads can still go backwards (Replication §10). If you need compare-and-set behaviour, use the store's linearizable feature (Cassandra lightweight transactions, a consensus-backed store), not a bigger quorum.

4

A fallback that silently weakens the guarantee

“If the QUORUM read fails, retry at ONE.” “If the primary is down, read from the replica.” Each sounds like resilience. Each quietly turns a CP operation into an AP one, exactly when the partition makes the difference matter (a downgrading retry, one of the Cassandra cards in §07's classifier). If you fall back, make it explicit: label the answer as possibly stale, or refuse for operations that can't tolerate it.

5

Uniqueness or locks on an eventually consistent store

“Check if the username exists, then insert it” on an AP store lets two replicas both say “free” (§09). Same for a lock stored in a cache with async replicas: during failover, two clients can hold it. Use a unique constraint on a single leader, a lightweight transaction, or a consensus-backed lock with a fencing token.

6

Synchronous cross-region writes in a loop

Ten writes, one after another, each waiting ~70 ms for another region, is 700 ms before the page can respond (§06). Batch them into one transaction, make them asynchronous where the data allows, or move the user's data to their home region.

7

Treating a timeout as “it didn't happen”

A timeout means “I don't know” (§02). The write may have committed. Retrying without an idempotency key can charge a card twice. Retrying a sync commit that timed out in Postgres can insert a row twice, because the first one committed locally.

8

Trusting wall clocks to pick a winner

Last-write-wins compares timestamps from different machines, and those machines' clocks disagree (clock skew). A write that happened later can lose to one with a clock running fast, and the losing write vanishes without an error. See Clocks for safer ways to order events.

Practitioner§12

Check yourself

Predict the answer before you click. Each explanation points back to the section that covers it.

QUESTION 1 / 8
A “CA” database?

A vendor says their distributed database is “CA: consistent and available, so we don't need partition tolerance.” What's the best reading of that claim?

QUESTION 2 / 8
Is this linearizable?

Alice's write x=1 gets “OK” at 10:00:00.100. Bob starts a read at 10:00:00.300 and gets x=0.

QUESTION 3 / 8
PACELC, normal day

Your app in Europe writes to a leader in US East (RTT ≈ 70 ms) and waits for a follower in Asia (US East–Asia RTT ≈ 215 ms) before replying. Roughly how long does each write take?

QUESTION 4 / 8
Which data needs linearizability?

You're designing a social app. Which of these most needs a linearizable (CP) store?

QUESTION 5 / 8
Quorum math

Cassandra keeps N = 3 copies. You write at ONE and read at QUORUM (2). Is every read guaranteed to see the latest successful write?

QUESTION 6 / 8
The 30-second GC pause

Node A is the leader. Its process freezes for 30 s in a garbage-collection pause; heartbeats stop; the cluster elects B. A wakes up and continues handling the request it was in the middle of. What is this, in CAP terms?

QUESTION 7 / 8
Pick the cheaper fix

Users complain that after editing their profile, the page sometimes shows the old version. Reads go to async replicas. Which is the smallest fix?

QUESTION 8 / 8
Harvest vs yield

A search service has 10 shards. One is unreachable. It can fail the whole query, or answer from the 9 shards it can reach. Answering from 9 shards keeps ___ at 100% and lowers ___ to 90%.

Answered 0 / 8
Foundations§13

Cheat sheet

✂️
CAP, correctlyDuring a partition, each operation either refuses/waits (CP) or answers from its own copy (AP). No partition, no forced choice.
🔺
Not “pick 2 of 3”P isn't optional between machines. “CA” means single node or no plan.
⏱️
C = linearizableOne dot inside every operation's bar; reads return the latest write before their dot. Once seen, never unseen.
🪜
The ladderLinearizable → sequential → causal → read-your-writes / monotonic → eventual. Causal is about the strongest that stays available.
🐢
PACELCElse: every consistent write or read pays a round trip. ~1 ms in a region, ~70 ms US↔Europe, ~215 ms US↔Asia.
🎛️
Choose per operationBalances, uniqueness, locks: strong. Likes, feeds, presence: eventual. Write the reason next to the code.
🧮
Merge, don't overwriteCommutative updates (counters, events) survive AP. Overwrites need a winner, and LWW loses data.
🧊
Partitions are mostly slownessGC pauses, overload, one-way links. Timeouts decide when slow counts as gone. Fence old leaders.
You needUse
Only one winner (username, last seat, lock)Single leader, consensus store, or INSERT … IF NOT EXISTS (LWT); fencing tokens on the lock
Users see their own changesRead-your-writes: leader reads after a write, or an LSN token (Replication §07)
A confirmed write survives losing a regionSync / majority replication across regions; accept the round trip
Fast reads everywhereNearest replica, eventual; label staleness where it matters
Fresh reads without the leaderR + W > N (e.g. LOCAL_QUORUM both ways, in the same data center; across DCs use QUORUM)
Writes in every region during a splitMulti-active + CRDTs
Linearizable per record, mostly-local latencyHome region per record
SystemWeaker / fasterStronger / slower
Postgressynchronous_commit = local, replica readson / remote_apply, primary reads
Redisplain writeWAIT n timeout (still not strongly consistent)
CassandraONELOCAL_QUORUM, QUORUM; SERIAL (LWT)
DynamoDBdefault readConsistentRead: true (2× cost)
MongoDBw: 1, readConcern: "local"w: "majority", readConcern: "majority" / "linearizable"
Finished reading?Mark it complete to track your progress on a roadmap. Saved in this browser only.
Up next in Distributed SystemsConsensus & Raft
Read →