CAP Theorem and Database Trade-offs
CAP theorem explained for practitioners: CP vs AP trade-offs, PACELC, tunable consistency per database, and how to avoid stale reads and split-brain.
Introduction
The CAP theorem states that a distributed data store can guarantee at most two of three properties during a network partition: Consistency, Availability, and Partition Tolerance. Since partitions are inevitable in any system that spans more than one machine, you’re really choosing between CP (Consistency + Partition Tolerance) and AP (Availability + Partition Tolerance) behavior when the network fails — not picking two letters off a menu for all time.
This guide covers what each property actually guarantees, how the PACELC extension fills in the gaps CAP leaves, how real databases let you tune the trade-off per query, and a decision framework you can apply per operation rather than per product.
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, what the literature calls linearizability.
Client writes X=10 to Node A
Client reads X from Node B → must get 10 (or an error)
Examples: PostgreSQL on a single primary, MongoDB with majority write concern + linearizable read concern, etcd, ZooKeeper.
Availability (A)
Every request receives a non-error response, without the guarantee that it contains the most recent write.
Client writes X=10 to Node A (partitioned from Node B)
Client reads X from Node B → gets a stale value (e.g., X=5)
Examples: Cassandra, DynamoDB, Riak, Couchbase.
Partition Tolerance (P)
The system keeps operating despite network partitions: periods when nodes can’t exchange messages.
Node A and Node B can't talk to each other
System still responds to requests on both sides
Reality check: partition tolerance isn’t optional in a distributed system. Networks drop packets, switches fail, availability zones get isolated. You don’t choose P — you choose what to sacrifice when P is exercised: C (AP) or A (CP).
CP vs AP in Practice
The choice only bites during a partition. The question that drives it: when two nodes can’t talk, is it worse to reject the operation or to return possibly stale data?
CP Systems (Choose Consistency)
| When to Choose | Examples |
|---|---|
| Financial transactions | Bank account balances, stock trades |
| Inventory management | E-commerce stock counts |
| Configuration stores | Service discovery, feature flags |
| Leader election | Distributed locks, cluster coordination |
Trade-off: during a partition, the system may refuse writes (sacrificing availability) to avoid divergent state. A PostgreSQL primary that loses its synchronous standbys can be configured to block commits until one comes back.
AP Systems (Choose Availability)
| When to Choose | Examples |
|---|---|
| Social media feeds | Timeline, activity feed |
| Analytics and metrics | Time-series data, click tracking |
| Session stores | User session caching |
| Content delivery | CDN caches, read replicas |
Trade-off: during a partition, both sides accept writes, creating divergent state that must be reconciled later — last-write-wins, CRDTs, vector clocks, or manual conflict resolution.
What the Theorem Doesn’t Say
CAP is quoted constantly and read carefully far less often. Four clarifications prevent most wrong turns:
- “Pick two” isn’t a permanent product label. The trade-off only applies while a partition exists. Outside a partition, a system can be consistent and available — that’s why PACELC exists.
- CAP consistency is linearizability, not the C in ACID. ACID consistency means the database enforces invariants (constraints, foreign keys). CAP consistency means every read sees the latest committed write. They’re unrelated properties that share a name.
- The choice is per operation, not per product. Cassandra isn’t “an AP database”; it’s a database that defaults to AP behavior and lets you request CP behavior with
QUORUM/ALL. Same data, different guarantees per query. - Availability in CAP is total, not “five nines”. The theorem means every request to a non-failed node gets a response. A 99.999% uptime SLA is a different concept — a CP system can still have excellent availability in the SLA sense.
PACELC: Extending CAP
CAP only describes behavior during a partition. PACELC (Abadi, 2012) adds the second question: else, when the system runs normally, do you optimize for latency or consistency?
| System Type | If Partition | Else |
|---|---|---|
| PA/EL | Available | Latency-optimized (eventual consistency) |
| PA/EC | Available | Consistency-optimized |
| PC/EL | Consistent | Latency-optimized |
| PC/EC | Consistent | Consistency-optimized |
Real mappings:
- DynamoDB — PA/EL: stays available through partitions, defaults to eventually consistent reads when healthy (strongly consistent reads are opt-in and cost twice the read units).
- Cassandra — PA/EL by default, but per-query consistency levels let a single cluster behave as PA/EC or even PC/EC for specific operations.
- etcd / ZooKeeper — PC/EC: linearizable by default; a minority-side node refuses to serve during a partition.
- MongoDB replica set — PC/EC in the default configuration (
majoritywrites, primary reads); moving reads to secondaries trades consistency for latency, drifting toward PC/EL.
Consistency Models
Consistency isn’t binary; it’s a spectrum. The useful models, strongest to weakest:
| Model | Guarantee | Example |
|---|---|---|
| Linearizable (strong) | All reads see the latest committed write | PostgreSQL primary, etcd, ZooKeeper |
| Sequential | All nodes see operations in the same order (not necessarily latest) | ZooKeeper default reads |
| Causal | Reads respect cause-effect ordering | COPS, MongoDB causal consistency sessions |
| Read-your-writes / session | Within a session, reads see your own writes | Cosmos DB session level, DynamoDB consistent reads |
| Bounded staleness | Reads lag at most a defined window | Cosmos DB bounded staleness |
| Eventual | Replicas converge given no new writes | Cassandra default, S3, DNS |
# Cassandra: tune the trade-off per query
from cassandra import ConsistencyLevel
# Strong-ish for this read: wait for a replica majority
session.execute(
"SELECT * FROM users WHERE id = %s",
(user_id,),
consistency_level=ConsistencyLevel.QUORUM,
)
# Fast, may be stale: fine for counters and analytics
session.execute(
"SELECT count(*) FROM events",
consistency_level=ConsistencyLevel.ONE,
)
The practical rule of thumb: W + R > N gives you consistency for that operation (W write replicas, R read replicas, N replication factor). With N=3, QUORUM + QUORUM overlaps. ONE + ONE is the fast, sloppy end.
Tuning Consistency in Real Databases
The CAP choice is usually made per query, not per cluster. How each system exposes the knob:
MongoDB — writeConcern controls durability/acknowledgment; readConcern controls what a query may see:
// CP-leaning: majority-committed, linearizable read
db.accounts.find({ _id: id })
.readConcern("linearizable")
.writeConcern("majority", { wtimeout: 5000 });
// AP-leaning: read from a secondary, accept staleness
db.events.find({})
.readConcern("local")
.readPreference("secondaryPreferred");
DynamoDB — eventual by default; ConsistentRead=true roughly doubles cost and is still only per-partition strong.
Cosmos DB — the most granular public menu: strong, bounded staleness, session, consistent prefix, eventual. Session is the default and covers most apps.
etcd / ZooKeeper — linearizable reads by default; etcd has serializable reads that skip the consensus round for stale-but-fast results.
PostgreSQL read replicas — async replication means replica reads are eventually consistent. synchronous_commit and hot-standby feedback narrow the window but never close it; for read-your-writes, pin the session to the primary or track replay LSN.
Network Partitions in Practice
A partition isn’t always a cut cable. In production it looks like:
- AZ or region isolation — a cloud availability zone loses connectivity; nodes on each side keep serving their local clients.
- Asymmetric partitions — A can reach B but B can’t reach A. The nastiest kind: health checks disagree, and each side believes the other is down (classic split-brain setup).
- Long GC pauses or CPU starvation — a JVM stop-the-world pause or a saturated CPU makes a node miss heartbeats; the cluster treats it as partitioned even though packets flow fine.
- Micro-partitions — flapping links that drop for seconds, recover, drop again. Each flap can trigger a leader election and a burst of rejected writes in CP systems.
Every distributed system detects partitions the same crude way: a timeout. There’s no way to distinguish “node dead” from “network dead” — that ambiguity is exactly why the CAP trade-off can’t be escaped. Tune heartbeat intervals and lease times to your real network, and alert on election storms, not just hard failures.
Decision Checklist
For each operation (not each system), walk through these questions:
- If this read is 5 minutes stale, does anything break? If not, AP/eventual is the right default.
- If this write is rejected during a partition, what’s the user-facing failure? A failed payment is worse than a delayed notification.
- If two partition sides write the same key, can you reconcile? Counters and sets reconcile well (CRDTs); financial balances don’t.
- Does the operation need read-your-writes? Session consistency is usually enough; you don’t need global linearizability for a user to see their own update.
- What’s the actual cost of coordination? A quorum read in Cassandra or a synchronous commit in PostgreSQL adds real milliseconds per operation; measure it before choosing strong.
Real-World Examples
E-Commerce Checkout
| Operation | Required Consistency | System Choice |
|---|---|---|
| Check stock | Strong (don’t oversell) | CP — query primary node |
| Add to cart | Session | AP — cache with session affinity |
| View recommendations | Eventual | AP — read from cache |
| Process payment | Strong | CP — ACID transaction |
Social Media Feed
| Operation | Required Consistency | System Choice |
|---|---|---|
| Post a tweet | Eventual | AP — accept write, propagate async |
| View feed | Eventual | AP — cached, may be seconds stale |
| Like a post | Eventual | AP — increment counter, reconcile later |
| Delete account | Strong | CP — ensure all replicas delete |
The pattern in both tables: critical paths get CP treatment, the 90% of operations around them stay AP. Systems that force everything through one consistency level pay for it in latency or outages.
When CAP Reasoning Doesn’t Apply
Not every component deserves a CP/AP analysis:
- Single-node deployments. No network partition possible, so a single-node PostgreSQL is trivially CA. The moment you add a standby for failover, partition behavior becomes a real question.
- Ephemeral caches. If a cache miss or stale hit costs nothing (a recomputation, a re-render), don’t spend consistency budget on it.
- Client-side state. LocalStorage, in-app caches and offline-first queues follow sync semantics, not CAP semantics — conflicts are resolved at merge time, not read time.
- Analytics replicas. A reporting replica that’s minutes behind doesn’t need CAP thinking; it needs a lag SLA.
Detailed Scenario: Database Choice for a FinTech App
A payments platform split into microservices, where each service picks its own point on the spectrum:
System: FinTech payment app (microservices)
Services: Accounts, Transfers, Notifications, Analytics
Requirements matrix per service:
| Service | Consistency | Availability | Latency | Choice |
|---------|-------------|--------------|---------|--------|
| Accounts (balances) | Strong (CP) | 99.99% | < 10ms | PostgreSQL (primary + sync replica) |
| Transfers | Strong (CP) | 99.99% | < 50ms | PostgreSQL + saga pattern |
| Notifications | Eventual (AP) | 99.9% | < 500ms | Cassandra (write-heavy) |
| Analytics | Eventual (AP) | 99.9% | < 5s | ClickHouse (columnar OLAP) |
| Session cache | Eventual (AP) | 99.95% | < 2ms | Redis (in-memory) |
| Feature flags | Strong (CP) | 99.9% | < 50ms | etcd (Raft consensus) |
PostgreSQL config for the Accounts service:
- Primary: 1 instance (writes)
- Sync replicas: 2 (reads + failover)
- Synchronous commit: ON (wait for at least 1 replica)
- Failover: Patroni with etcd for consensus
postgresql.conf:
synchronous_commit = on
synchronous_standby_names = "FIRST 1 (replica1, replica2)"
wal_level = replica
max_wal_senders = 10
Cassandra config for Notifications:
- 5 nodes across 3 datacenters
- Replication factor: 3 per datacenter
- Consistency level: LOCAL_QUORUM for writes
- Compaction: Size-tiered (STCS) for notification data
CREATE KEYSPACE notifications WITH replication = {
"class": "NetworkTopologyStrategy",
"dc1": 3, "dc2": 3, "dc3": 3
};
CREATE TABLE notifications (
user_id UUID,
notification_id TIMEUUID,
type TEXT,
payload JSON,
read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP,
PRIMARY KEY (user_id, notification_id)
) WITH CLUSTERING ORDER BY (notification_id DESC);
Network partition handling:
- Accounts (CP): If the primary loses contact with replicas,
it rejects writes. Users can't transfer money
but balances stay consistent.
- Notifications (AP): If a datacenter is isolated,
notifications are accepted on both sides.
Hinted handoff resolves consistency on reconnect.
Monitoring:
- PostgreSQL replication lag: < 100ms (alert if > 500ms)
- Cassandra repair: run weekly for anti-entropy
- etcd leader changes: alert if > 1 per hour
- Failed transfers due to partition: real-time dashboard
Lessons learned:
- Not every service needs the same database
- CP for money, AP for everything else
- The cost of strong consistency is latency and reduced availability
- Watching replication lag catches partition damage early
Troubleshooting
Failures you’ll actually see when the CAP trade-off is misconfigured:
- Stale reads after failover: the new primary was elected before the old one’s last writes replicated. Tighten
synchronous_commit/majoritywrite concern, or fence the old primary with STONITH-style isolation. - Split-brain writes: both partition sides accept writes for the same key; on heal you get divergent values. Mitigate with quorum reads/writes, or accept it and reconcile with CRDTs or last-write-wins where losing a write is harmless.
- Quorum lost, cluster read-only: a CP system (etcd, MongoDB) loses its majority and rejects writes: the CP trade-off working as designed. Restore members fast; never force a minority to serve writes.
- Replication lag grows steadily: watch
pg_stat_replication, MongoDBreplSetGetStatus, or Cassandranodetool. Split large writes, check disk I/O on replicas, and verify the network between AZs. - Hinted handoff backlog after a healed partition (Cassandra): writes piled up as hints while a node was down; repair and monitor
hinted_handoffmetrics before the window expires (default 3h).
What Works
- Don’t default to strong consistency everywhere — it costs latency and availability
- Identify consistency requirements per operation — not all data needs the same guarantees
- Use saga patterns for distributed transactions — don’t force ACID across services
- Design for idempotency — eventual consistency means retries, and retries mean duplicates
- Monitor replication lag — lag is the distance between “written” and “visible everywhere”
- Track partition events as incidents — when a CP store rejects writes during a partition, that’s a production incident with a runbook, not a surprise
Common Mistakes
- Treating all data as if it needs strong consistency — most application data is fine with eventual
- Believing “distributed” means “more consistent” — replication adds failure modes, not guarantees
- Using a CP database for an AP workload (or vice versa) — match the tool to the requirement; see NoSQL selection
- Ignoring replication lag in read-after-write scenarios — users may not see their own writes immediately; pin reads to primary or use session consistency
- Assuming the consistency level is fixed per product — Cassandra, MongoDB, Cosmos DB and DynamoDB all let you choose per query; the default is a starting point, not the answer
- Testing the happy path only — kill a node or drop a network route in staging before trusting your CP/AP choice in production
Further Reading
- Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services — Gilbert & Lynch’s formal proof (SIGACT News, 2002)
- Problems with CAP, and Yahoo’s little known NoSQL system — Daniel Abadi introducing the PACELC extension
- Cassandra tunable consistency — official docs on
ONE,QUORUM,LOCAL_QUORUM - Cosmos DB consistency levels — the five-level menu from strong to eventual
- Jepsen analyses — real partition-fault testing of distributed databases, the best place to see CAP trade-offs break in practice
- Companion lab — 3-node etcd cluster with a partition script plus the CP/AP config snippets from this guide
Key Takeaways
- Partitions are inevitable, so the real choice is CP vs AP, made per operation rather than per product.
- Outside a partition, consistency and availability coexist; PACELC captures what the system trades then (latency vs consistency).
- Tunable consistency (
ONE/QUORUM/ALL, read/write concerns, Cosmos DB levels) means one cluster can serve both CP and AP workloads. - Money paths get CP; feeds, sessions and analytics stay AP. Verify under an induced partition in staging before you trust it in production.
Frequently Asked Questions
Is it possible to have all three CAP properties?
No. The theorem is a proof: during a network partition you must choose between consistency and availability. No distributed system guarantees all three at once — you can only pick which guarantee to give up while the partition lasts.
Does CAP mean I can't have consistency and availability at all?
No. When there's no partition, you can have both, which is the whole point of the PACELC extension. Many systems are CA in normal operation and reveal their CP or AP behavior only during failures.
How do I choose between CP and AP?
Ask: "What hurts more — a failed write or stale data?" If failed writes are unacceptable (payments, inventory, config), choose CP. If stale data is acceptable (feeds, analytics, sessions), choose AP. Most real systems mix both: CP on the critical path, AP everywhere else.
What's the difference between CAP and PACELC?
CAP covers only partition time. PACELC adds the normal-operation trade-off: when there's no partition (else), do you optimize for latency or consistency? DynamoDB is PA/EL (available under partition, latency-optimized otherwise); etcd is PC/EC.
What is tunable consistency?
Systems like Cassandra, MongoDB and Cosmos DB let you set the consistency level per operation — ONE (fast, eventual), QUORUM (majority, consistent), ALL (maximum consistency, lowest availability). You choose the trade-off per query, not per cluster.
Is a single-node database affected by CAP?
No partitions means no CAP trade-off: a single-node PostgreSQL is trivially consistent and available. CAP questions start the moment you add a replica — for failover, scaling reads, or geographic distribution.
Related Resources
NoSQL Database Selection — MongoDB, DynamoDB, Cassandra
A practical guide to choosing the right NoSQL database. Compare document, key-value, wide-column, and graph stores with selection criteria and migration tips.
GuideComplete Guide to Database Sharding
Master database sharding. Covers range-based, hash-based, and directory-based partitioning strategies, consistent hashing, shard key selection, cross-shard queries, resharding, Vitess, Citus, and when to shard vs scale vertically with practical examples.
GuideRead Replicas: Scale Reads Without Changing Your App
Production guide to database read replicas: streaming replication setup, read/write routing (app, ORM and proxy-based), replication lag handling, monitoring, failover and tuning for PostgreSQL, MySQL and cloud-managed replicas.
RecipeMicroservices Communication Patterns
Choose between synchronous and asynchronous communication patterns for resilient microservices architectures.
RecipeRetry Backoff: Budgets, Deadlines, and Library Configs
Implement resilient retry strategies with exponential backoff, jitter, and circuit breaker integration for transient failure recovery.
RecipeDatabase Replication
Set up and manage database replication for high availability, read scaling, and disaster recovery with primary-replica architectures.