StackPractices
intermediate By Mathias Paulenko

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?

flowchart diagram: Network partition

CP Systems (Choose Consistency)

When to ChooseExamples
Financial transactionsBank account balances, stock trades
Inventory managementE-commerce stock counts
Configuration storesService discovery, feature flags
Leader electionDistributed 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 ChooseExamples
Social media feedsTimeline, activity feed
Analytics and metricsTime-series data, click tracking
Session storesUser session caching
Content deliveryCDN 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:

  1. “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.
  2. 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.
  3. 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.
  4. 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 TypeIf PartitionElse
PA/ELAvailableLatency-optimized (eventual consistency)
PA/ECAvailableConsistency-optimized
PC/ELConsistentLatency-optimized
PC/ECConsistentConsistency-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 (majority writes, 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:

ModelGuaranteeExample
Linearizable (strong)All reads see the latest committed writePostgreSQL primary, etcd, ZooKeeper
SequentialAll nodes see operations in the same order (not necessarily latest)ZooKeeper default reads
CausalReads respect cause-effect orderingCOPS, MongoDB causal consistency sessions
Read-your-writes / sessionWithin a session, reads see your own writesCosmos DB session level, DynamoDB consistent reads
Bounded stalenessReads lag at most a defined windowCosmos DB bounded staleness
EventualReplicas converge given no new writesCassandra 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:

  1. If this read is 5 minutes stale, does anything break? If not, AP/eventual is the right default.
  2. If this write is rejected during a partition, what’s the user-facing failure? A failed payment is worse than a delayed notification.
  3. If two partition sides write the same key, can you reconcile? Counters and sets reconcile well (CRDTs); financial balances don’t.
  4. 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.
  5. 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

OperationRequired ConsistencySystem Choice
Check stockStrong (don’t oversell)CP — query primary node
Add to cartSessionAP — cache with session affinity
View recommendationsEventualAP — read from cache
Process paymentStrongCP — ACID transaction

Social Media Feed

OperationRequired ConsistencySystem Choice
Post a tweetEventualAP — accept write, propagate async
View feedEventualAP — cached, may be seconds stale
Like a postEventualAP — increment counter, reconcile later
Delete accountStrongCP — 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 / majority write 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, MongoDB replSetGetStatus, or Cassandra nodetool. 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_handoff metrics 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

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.