PACELC Theorem: Latency vs Consistency Trade-offs
Explore the PACELC theorem extending CAP theorem with latency-consistency trade-offs. Learn when systems choose low latency over strong consistency and vice versa.
PACELC describes a trade-off that CAP ignores: even when the network is healthy, you still choose between latency and consistency. Synchronous replication (strong consistency) adds latency proportional to the number of replicas—as much as 90ms for a 3-node geo-distributed cluster. Asynchronous replication responds immediately but lets reads return stale data. DynamoDB and Cassandra are PA/EC systems: they prioritize low latency and accept eventual consistency by default. MongoDB and HBase are PC/EC: they prioritize strong consistency and accept higher write latency. The practical impact is real: a user in London hitting US-East synchronously sees 200ms writes; asynchronously it is under 100ms. Choose PA/EC for social feeds, IoT dashboards, gaming leaderboards. Choose PC/EC for financial transactions, inventory management, booking systems.
PACELC Theorem: Latency and Consistency Trade-offs
Pronunciation: PACELC is pronounced /ˈpækəlsi/ (pack-el-see).
The CAP theorem tells us that during a network partition, we must choose between consistency and availability. But what happens when there is no partition? PACELC answers this by describing another trade-off: latency versus consistency.
If you have read the CAP theorem guide on this blog, you already know about partition-time trade-offs. PACELC extends that thinking to normal operation, when the network is healthy but you still need to make architectural choices.
Introduction
| Scenario | Recommendation |
|---|---|
| Real-time gaming, IoT dashboards | Choose low latency (PA/EC systems) |
| Financial transactions, inventory | Choose strong consistency (PC/SC systems) |
| Geo-distributed read-heavy workloads | Eventual consistency acceptable |
| User-generated content (social feeds) | Read-your-writes with eventual consistency |
| Systems requiring both | Use tunable consistency (Cassandra, DynamoDB) |
When to Use Low Latency (PA/EC)
These scenarios share a common thread: latency costs more than staleness. When a user is staring at a screen, every millisecond adds up. But a few hundred milliseconds of data lag rarely hurts the business.
Social media feeds, real-time dashboards, and gaming leaderboards fall into this bucket. Users notice lag more than occasional staleness. A news feed that is 30 seconds behind does not break the experience. One that takes 3 seconds to load breaks it constantly.
High-throughput ingestion is the other major category. IoT sensor data, log aggregation, and metrics collection often generate orders of magnitude more writes than reads. Forcing synchronous replication across all nodes for every sensor reading would collapse write throughput. Eventual delivery is fine here since the data is only being read historically anyway.
Read-heavy workloads are worth flagging separately. When 95% of your operations are reads, optimizing reads for latency makes sense even if writes become slightly more inconsistent. CDNs, product catalogs, and search indices all fit here. A stale product price might surprise a customer at checkout. Selling inventory you do not have is a different, more serious problem.
Geo-distributed user bases are the last major driver. When your users are spread across continents and your primary is in one region, round-trip time to that primary is a hard floor on latency. If the primary is in US-East, a user in Singapore pays 200ms before any query processing. Async replication to a local replica cuts that to single digits.
When Not to Use Low Latency Priority
Some applications simply cannot tolerate the staleness that eventual consistency allows. For these systems, serving wrong data costs more than waiting for a response.
Financial transactions are the clearest case. Banking transfers, payment processing, and stock trading all need to know the exact current state when they execute. If you read a balance of $1,000 and initiate a $900 transfer, eventual consistency means that balance could have changed on another node before your write propagates. The result is an overdraft that should not have been possible. Synchronous replication prevents this by making sure the read and write operate on the same state.
Inventory management carries the same risk differently. E-commerce order processing, seat booking, and resource allocation all depend on accurate available quantities. An eventually consistent system might confirm a sale for the last item while another node confirms the same sale at almost the same time. One customer gets the item, the other gets a backorder notification and lost goodwill.
Systems with regulatory requirements need strict ordering that eventual consistency cannot provide. Audit logs, compliance records, and legal documents often require events to be recorded in the exact sequence they occurred. If two datacenters accept the same transaction with different timestamps during a partition, figuring out which happened first becomes impossible without a single authoritative source of truth.
Collaborative applications like document editing and shared workspaces break in subtle ways when users see conflicting versions. If User A and User B both edit a shared document and their changes resolve to an older version than either saw, the experience feels broken even if no data was technically lost. The gap between what users saw and what persisted destroys trust in the system.
In all these cases, the application cannot detect and compensate for inconsistency at the business logic level. When wrong data means direct monetary loss, regulatory trouble, or eroded user trust, strong consistency is not optional. You pay the latency cost because the alternative is worse.
Core Concepts
CAP focuses exclusively on partition scenarios. During a partition, you choose between returning errors (consistency) or returning potentially stale data (availability). Partitions tend to be infrequent though. The more persistent trade-off surfaces during regular operation: how quickly can the system respond versus how consistent is that response?
PACELC says:
If there is no partition, the system still faces a trade-off between latency and consistency.
The acronym breaks down as:
- Partition - Network partition occurs
- Availability or Consistency - Choose one during partition
- Error or Latency - When no partition, choose between low latency or strong consistency
- Consistency - Strong consistency has a latency cost
Formal Mathematical Definition
PACELC can be stated formally as:
Given: A distributed system with replication factor N and no network partition
Trade-off:
L_latency = f(C_consistency)Where:
L_latency= system response timeC_consistency= consistency level (from eventual to strong)
Strong consistency requires synchronous replication, which adds latency. Asynchronous replication responds immediately after the local write. So if strong consistency is required: L_latency >= L_sync. If eventual consistency is acceptable: L_latency <= L_local + L_propagation.
The latency difference between synchronous (strong consistency) and asynchronous (eventual consistency) replication is:
Latency Delta = L_sync - L_async
= (N * L_network) - L_local
≈ N * L_network (when L_local << L_network)
For a 3-replica system with 30ms inter-region latency:
- Synchronous write: ~90ms (3 x 30ms to contact all replicas)
- Asynchronous write: ~1ms (local write acknowledgment)
graph TD
A[System Operation] --> B{Partition?}
B -->|Yes| C[CP or AP Choice]
C --> D[Consistency]
C --> E[Availability]
B -->|No| F{Latency Priority?}
F --> G[Low Latency]
F --> H[Strong Consistency]
G --> I[Eventual Consistency]
H --> J[Synchronous Replication]
The Consistency Spectrum
The latency-consistency trade-off manifests differently across different database systems and deployment scenarios. This section explores the practical spectrum of consistency models, from strong to eventual, and how different systems navigate the PACELC trade-off in production.
The Latency-Consistency Spectrum
Strong consistency requires synchronization. When you write data, you must wait for that write to propagate to all relevant nodes before confirming the operation to the client. This synchronization takes time, and time means latency.
Eventual consistency lets the system respond immediately after the local write, propagating changes to other nodes in the background. The response comes back fast, but subsequent reads might return stale data for a while.
Consider a simple example:
// Strong consistency write - high latency
async function writeConsistent(key, value) {
await replicas.sync(key, value); // Wait for all replicas
return { success: true };
}
// Eventual consistency write - low latency
async function writeEventual(key, value) {
localCache.set(key, value); // Write locally, respond immediately
replicas.syncAsync(key, value); // Propagate in background
return { success: true };
}
The latency difference can be substantial. A strongly consistent write might take 50-200ms in a geo-distributed system. An eventually consistent write might complete in under 5ms.
PACELC Database Classification
Just as CAP classifies databases as CP or AP, PACELC classifies them along the latency-consistency axis. This classification helps you understand which systems prioritize low latency over strong consistency and vice versa.
| Database | PACELC Classification | What it means |
|---|---|---|
| DynamoDB | PA/EC | Prioritizes low latency, accepts eventual consistency |
| Cassandra | PA/EC | Same as DynamoDB, tunable per query |
| MongoDB | PC/EC | Strong consistency, willing to accept higher latency |
| HBase | PC/EC | CP approach, synchronized writes |
| etcd | PC/SC | Strong consistency, moderate latency |
| Redis | PA/EL | Low latency, eventual consistency by default |
Systems that accept higher latency can provide stronger consistency guarantees. Systems that prioritize low latency may serve stale data.
How PA/EC Systems Handle Conflicts
PA/EC systems like DynamoDB and Cassandra use eventual consistency, which means conflicts are inevitable and must be resolved somehow. Understanding the conflict resolution strategies helps you choose the right system for your use case.
Why conflicts are guaranteed in PA/EC: When you choose Availability over Consistency during a partition, both sides of the partition keep accepting writes. When the partition heals, you have two divergent histories that must be merged. There is no algorithm that can “discover” which side is correct — they are both correct from their own perspective. The system has to pick a rule for resolving the conflict, and the rule will sometimes lose information.
What this means in practice:
- The resolution strategy is part of your data model. A counter that uses LWW will silently drop increments during partitions. A counter that uses a CRDT will not. Picking the system is picking the merge rule.
- Application code is the last line of defence. When the database’s automatic resolution is wrong for your domain (a financial double-spend, an inventory double-allocation), the application has to detect and compensate. There is no way to push this fully into the storage layer.
- Read-repair and anti-entropy are not optional. After a partition heals, the system needs active processes to detect and resolve divergence. Without them, replicas can stay divergent indefinitely under load.
The four strategies in production use today are covered in detail in the next sub-section. They differ in how much information they preserve, how much they cost in storage and compute, and how much logic they push into the application layer.
Conflict Resolution in PA/EC Systems
Last-Write-Wins (LWW)
The simplest strategy: the most recent write wins based on timestamp or vector clock. DynamoDB supports this via UpdateExpression with conditional writes.
// DynamoDB conditional write - last-write-wins
await dynamodb.update({
TableName: "users",
Key: { userId },
UpdateExpression: "SET #name = :name, #updatedAt = :ts",
ConditionExpression: "#updatedAt < :ts",
ExpressionAttributeNames: { "#name": "name", "#updatedAt": "updatedAt" },
ExpressionAttributeValues: { ":name": "Alice", ":ts": Date.now() },
});
Vector Clocks
Cassandra uses vector clocks (or client-supplied timestamps) to track causality. When conflicts occur, the system can determine which write supersedes another based on the partial ordering.
Vector Clock Example:
Write A: {node1: 1}
Write B: {node1: 1, node2: 1} // B happened after A
Conflict: {node1: 1} vs {node1: 1, node2: 1} → B wins
Conflict-Free Replicated Data Types (CRDTs)
For certain data structures, CRDTs provide automatic conflict resolution. G-Counters, PN-Counters, LWW-Registers, and OR-Sets all have mathematically proven convergence properties.
// CRDT-style counter increment (simplified)
class PNCounter {
constructor() {
this.positive = {}; // increments
this.negative = {}; // decrements
}
increment(nodeId) {
this.positive[nodeId] = (this.positive[nodeId] || 0) + 1;
}
decrement(nodeId) {
this.negative[nodeId] = (this.negative[nodeId] || 0) + 1;
}
value() {
return (
Object.values(this.positive).reduce((a, b) => a + b, 0) -
Object.values(this.negative).reduce((a, b) => a + b, 0)
);
}
}
Read Repair vs Anti-Entropy
| Mechanism | When It Runs | How It Works |
|---|---|---|
| Read Repair | During reads | Conflicts detected at read time, repairs propagate to outliers |
| Anti-Entropy Repair | Background (Cassandra repair) | Merkle tree comparison finds and fixes divergence |
| Hinted Handoff | Writer detects down node | Write stored locally, replayed when node recovers |
DynamoDB uses read repair automatically — when you read and find stale data, the system asynchronously repairs it. Cassandra’s repair command (nodetool repair) performs anti-entropy repair using Merkle trees.
When to Use Which Strategy
- Financial balances: None of these — use strong consistency (PC/SC)
- Shopping cart contents: CRDTs or version vectors for automatic merge
- User profile updates: Last-write-wins acceptable for non-critical data
- Leaderboard scores: LWW sufficient, occasional staleness acceptable
Consistency Tuning Patterns
Middle Ground Consistency Models
Beyond strong and eventual consistency, there are several middle ground models. The Consistency Models post covers read-your-writes, monotonic reads, and other guarantees that sit between the two extremes.
These models matter because PACELC is not actually a binary choice. The real world offers a spectrum. A system can provide strong consistency for some operations and eventual consistency for others, depending on what the use case requires.
Tunable Consistency Patterns
One of the most powerful features of modern distributed databases is the ability to tune consistency per-query rather than system-wide. This flexibility lets you optimize different operations for different requirements.
Consistency Level Patterns by Operation
Read Patterns
| Consistency Level | Use When | Latency | Staleness Risk |
|---|---|---|---|
ONE | Acceptable staleness, need speed | Lowest | High |
QUORUM | Balance of speed and freshness | Medium | Low |
ALL | Must read latest written | High | None |
LOCAL_QUORUM | Geo-distributed, local DC speed | Medium | Medium |
// DynamoDB read with configurable consistency
async function readWithConsistency(table, key, consistency = "eventual") {
const params = {
TableName: table,
Key: key,
ConsistentRead: consistency === "strong",
};
if (consistency === "local") {
// Use Local Secondary Index with local quorum
return await dynamodb.query({
...params,
IndexName: "LSI-index",
ConsistentRead: false,
});
}
return await dynamodb.get(params);
}
Write Patterns
| Consistency Level | Durability | Latency | Use Case |
|---|---|---|---|
ONE | Low | Lowest | High-volume logs, metrics |
QUORUM | Medium | Medium | User-generated content |
ALL | High | Highest | Financial transactions |
ASYNC | Variable | Instant | Non-critical updates |
// Cassandra write with configurable consistency
async function writeWithConsistency(cassandra, query, consistency = "ONE") {
const levels = {
ONE: cassandra.types.consistencies.one,
QUORUM: cassandra.types.consistencies.quorum,
ALL: cassandra.types.consistencies.all,
LOCAL_QUORUM: cassandra.types.consistencies.localQuorum,
};
return await cassandra.execute(query, [], {
consistency: levels[consistency],
});
}
Adaptive Consistency
Some systems support adaptive consistency based on operational conditions:
DynamoDB Streams + DAX
// Adaptive consistency based on data age sensitivity
async function adaptiveRead(table, key) {
const now = Date.now();
const dataAge = now - key.lastUpdated;
// For recently written items, use strong consistency
// For older items, eventual consistency is fine
const consistency = dataAge < 5000 ? "strong" : "eventual";
return await dynamodb.get({
TableName: table,
Key: key,
ConsistentRead: consistency === "strong",
});
}
Latency-Aware Routing
// Route to nearest replica for reads, configurable consistency
async function latencyAwareRead(key, options = {}) {
const replicas = await serviceDiscovery.getReplicas(key);
const sorted = replicas.sort((a, b) => a.latencyMs - b.latencyMs);
// For quorum reads, contact minimum required replicas closest to us
const required =
options.consistency === "quorum" ? Math.ceil(replicas.length / 2) + 1 : 1;
return await Promise.race(
sorted.slice(0, required).map((r) => readFromReplica(r, key)),
);
}
Per-Operation Consistency Matrix
| Operation Type | Recommended Consistency | Why |
|---|---|---|
| User login/auth | Strong (QUORUM/ALL) | Session data must be consistent |
| Read user profile | Eventual (ONE) | Stale profile is acceptable |
| Add to cart | Strong (QUORUM) | Prevent overselling |
| View cart | Eventual (ONE) | Cart staleness tolerated |
| Process payment | Strong (ALL) | Cannot double-charge |
| Update inventory | Strong (QUORUM) | Prevent overselling |
| Display product | Eventual (ONE) | Brief staleness fine |
| Write review | Eventual (ONE) | Review order not critical |
| Read reviews | Eventual (ONE) | Aggregate over time |
Consistency in Multi-Region Deployments
Geo-distributed systems face a fundamental tension: strong consistency across regions requires synchronous cross-region communication, which adds 100-200ms latency per hop.
Solutions:
-
Single-leader replication: All writes go to primary region, reads can be local. Strong consistency within primary, eventual across regions.
-
Multi-leader with async: Writes accepted locally, replicated async. Conflict resolution required.
-
Leave-one-region-out: Accept that one region will be unavailable during partition rather than allowing split-brain.
// DynamoDB Global Tables - multi-region replication
const globalTable = new DynamoDB.GlobalTables({
replicas: [
{ Region: "us-east-1" },
{ Region: "eu-west-1" },
{ Region: "ap-southeast-1" },
],
});
// Writes automatically replicate to all regions
// Conflicts resolved via last-writer-wins (configurable)
await globalTable.putItem({
Item: { userId: "123", email: "alice@example.com" },
});
Regional Replication and Latency
Geo-replication is often overlooked in PACELC discussions. When you replicate data across regions, the physical distance between nodes adds baseline latency. This affects whether you can afford strong consistency.
If your primary users are in Europe and your database replicas are in Asia, synchronous replication across that distance adds 100-200ms to every write. Users notice this. Asynchronous replication keeps writes fast but introduces the possibility of data loss if the primary fails before syncing.
The Availability Patterns post covers how to structure redundancy and failover to minimize both downtime and latency spikes.
Worked Example: User in London Hitting US-East
Consider a user in London using an application with its primary database in US-East (Virginia). The round-trip time (RTT) between London and US-East is approximately 80-100ms.
Total Write Latency Breakdown (Synchronous Replication):
| Component | Time | Notes |
|---|---|---|
| London → US-East network | 85ms | Physical distance ~5,500 km |
| Load balancer + TLS | 5ms | Connection setup and routing |
| Database write + fsync | 15ms | Local disk write |
| Consensus (Raft/Paxos) | 10ms | Leader acknowledgment |
| US-East → London network | 85ms | Response return |
| Total synchronous write | ~200ms | Unacceptable for user-facing ops |
Total Write Latency Breakdown (Asynchronous Replication):
| Component | Time | Notes |
|---|---|---|
| London → US-East network | 85ms | Write to primary |
| Database write + local ack | 10ms | Immediate acknowledgment |
| Total async write | ~95ms | ~50% faster |
| Replication to replicas | Background | 50-500ms depending on load |
Latency Budget Decision:
If your target write latency is < 100ms for a good user experience:
- Synchronous replication to US-East from London is not viable (200ms)
- Asynchronous replication achieves ~95ms, meeting the budget
- Or: Place a replica in London (EU-West) for synchronous local writes, async replication to US-East
Trade-off Analysis:
| Approach | Write Latency | Durability | Conflict Risk |
|---|---|---|---|
| Sync to US-East only | ~200ms | Highest | None |
| Async to US-East | ~95ms | Moderate | Data loss on primary failure |
| Sync to EU, async to US | ~20ms local, ~95ms remote | High local, moderate remote | Potential divergence |
Leader Election and Failover Patterns
When the primary node fails in a distributed database, proper leader election prevents split-brain scenarios where multiple nodes believe they are the primary.
Consensus-Based Election (Raft/Paxos)
Systems like etcd and CockroachDB use consensus algorithms to elect leaders. A follower times out waiting for a heartbeat and requests votes from other nodes. The candidate with majority support wins.
sequenceDiagram
participant F1 as Follower 1
participant F2 as Follower 2
participant C as Candidate
participant L as Leader
Note over L: Leader times out, starts election
L->>F1: Timeout, voting for myself
L->>F2: Timeout, voting for myself
F1->>C: Vote granted
F2->>C: Vote granted
C->>F1: I won election, I am leader now
C->>L: Step down
C->>F2: I won election, I am leader now
Lease-Based Failover
Cassandra uses a lease mechanism where the coordinator requests a lease from replica nodes. If the lease holder fails, other nodes wait for the lease to expire before taking over.
// Cassandra lease acquisition (simplified)
async function acquireLease(key, nodeId) {
const leaseTimeout = 30000; // 30 second lease
try {
await cassandra.execute(
`UPDATE lease_table USING TTL ${leaseTimeout / 1000} SET owner = ? WHERE key = ?`,
[nodeId, key],
);
return true; // We hold the lease
} catch (e) {
return false; // Another node holds the lease
}
}
Split-Brain Prevention
Split-brain occurs when network partition causes multiple nodes to believe they are the primary, leading to divergent data states. Prevention strategies:
| Strategy | How It Works | Trade-off |
|---|---|---|
| Majority quorum | Writes require N/2+1 nodes | Unavailable if minority partition |
| Fencing tokens | Leader receives token, must present it | 3-phase commit complexity |
| Dead node timeout | Wait for heartbeat timeout before failover | Slower failover detection |
| Epoch numbers | Strictly increasing epoch with each leader | Prevents late writes to old leader |
Fencing Token Pattern
// Fencing token prevents split-brain writes
class FencedWrite {
constructor() {
this.epoch = 0;
this.token = 0;
}
async write(key, value) {
// Get current leader with epoch
const leader = await consensus.getLeader();
this.epoch = leader.epoch;
// Write with fencing token
const result = await storage.write(key, value, {
fencingToken: ++this.token,
epoch: this.epoch,
});
// If write fails due to stale token, retry with new leader
if (result.staleToken) {
throw new Error("Write rejected - new leader elected");
}
return result;
}
}
Automatic vs Manual Failover
| Failover Type | Use Case | Risk |
|---|---|---|
| Automatic | Fast recovery, unattended systems | Potential for false positives, cascading failures |
| Manual | Controlled recovery, operator judgment | Slower, requires human intervention |
| Hybrid | Automatic for common cases, manual for regional failures | Best of both worlds |
sequenceDiagram
participant Client as London User
participant LB as EU Load Balancer
participant Primary as EU Primary
participant Replica as US-East Replica
Client->>LB: Write request
LB->>Primary: Route to EU Primary
Primary->>Primary: Local sync write
Primary-->>Client: Ack (~20ms)
Note over Primary,Replica: Async replication (background)
Client->>LB: Write request (if US-East only)
LB->>Primary: Route to US-East Primary
Primary->>Primary: Sync write (85ms RTT + 15ms)
Primary-->>Client: Ack (~200ms)
Quorum vs Synchronous Replication Latency Comparison
| Aspect | Synchronous Replication | Quorum (R+W>N) | Eventual (W=1) |
|---|---|---|---|
| Write Latency | Highest (all replicas) | Medium (quorum size) | Lowest (local only) |
| Read Latency | Lowest (any replica) | Medium (quorum) | Lowest (any replica) |
| Durability | Highest (all replicas ack) | Medium (quorum ack) | Lowest (single ack) |
| Availability | Lowest (partition blocks) | Medium | Highest |
| Consistency | Strong (linearizable) | Configurable (strong to eventual) | Eventual |
| Fault Tolerance | N-1 replicas can fail | N/2 replicas can fail | N-1 replicas can fail |
| Network Dependency | Critical (all must respond) | Quorum must respond | Only primary must respond |
| Typical Use Case | Financial transactions | Balanced consistency | Caches, logs |
Latency Calculation Examples
Synchronous (ALL replicas):
Latency = 2 * RTT + N * write_time
Example: 3 nodes, 30ms RTT, 5ms write = 2*30 + 3*5 = 75ms
Quorum (R=2, W=2, N=3):
Latency = 2 * RTT + max(write_time_primary, write_time_quorum)
Example: 3 nodes, 30ms RTT = ~60ms + overhead
Eventual (W=1):
Latency = RTT + write_time_local
Example: 30ms RTT + 5ms = ~35ms
When to Use Each Approach
| Scenario | Recommended Approach | Reason |
|---|---|---|
| Financial transactions | Synchronous or Quorum (ALL/QUORUM) | Durability critical |
| User-generated content | Quorum (LOCAL_QUORUM) | Balance latency/durability |
| Real-time dashboards | Eventual (ONE) | Lowest latency |
| IoT sensor data | Eventual (ONE) | High volume, some loss acceptable |
| Session state | Quorum (QUORUM) | Moderate consistency needed |
PACELC vs CAP: When Each Theorem Applies
CAP and PACELC are not competing theories — they describe different trade-offs at different times. Understanding when each applies is crucial for system design. This section provides a framework for applying each theorem correctly in different scenarios. — they describe different trade-offs at different times. Understanding when each applies is crucial for system design.
| Scenario | CAP Applies | PACELC Applies |
|---|---|---|
| Network partition occurring | Yes - choose C or A | No - partition already happened |
| Network healthy, normal operation | No - both C and A achievable | Yes - latency vs consistency trade-off |
| Planning a new system | Both apply sequentially | Primary during normal operation |
| Debugging an outage | Check CAP classification first | Check latency budgets second |
Decision Framework
graph TD
A[System Design Decision] --> B{Network Partition?}
B -->|Yes| C[CAP applies]
C --> D{Choose Consistency or Availability}
D --> E[CP: Strong consistency]
D --> F[AP: High availability]
B -->|No| G{PACELC applies}
G --> H{Latency Priority?}
H -->|Yes| I[PA/EC: Eventual consistency]
H -->|No| J[PC/SC: Strong consistency]
Key Distinction
CAP answers: “What happens when something goes wrong?” (partition tolerance)
PACELC answers: “What happens when everything goes right?” (normal operation latency)
Most systems spend 99%+ of their time in normal operation with no partitions. PACELC captures the trade-offs that matter most of the time, while CAP captures the trade-offs that matter during the rare but critical partition events.
Both theorems are necessary for complete distributed systems reasoning.
Trade-off Analysis
| Design Decision | Low Latency (PA/EC) | Strong Consistency (PC/SC) | Trade-off Consideration |
|---|---|---|---|
| Replication Strategy | Asynchronous | Synchronous | Sync adds N×L_network latency |
| Write Path Latency | ~1-5ms local ack | ~50-200ms quorum ack | 10-100x latency difference |
| Durability | Moderate (single ack) | Highest (all replicas ack) | Risk window for data loss |
| Conflict Resolution | LWW, Vector Clocks, CRDTs | Single leader, consensus | Eventual systems need conflict handling |
| Partition Behavior | Continue serving reads/writes | Halt writes to maintain consistency | AP stays available; CP stays consistent |
| Typical Latency Budget | <10ms write target | <100ms write acceptable | Depends on geographic distribution |
| Failure Window | Replication lag during recovery | Unavailable during partition | Different availability profiles |
| Use Case Alignment | Social feeds, IoT, gaming | Financial, inventory, bookings | Match consistency to business requirements |
Quick Decision Matrix
| Scenario | Recommended Profile | Consistency Level |
|---|---|---|
| User-facing interactive (<100ms SLA) | PA/EC | Eventual (ONE/LOCAL_QUORUM) |
| Financial transactions | PC/SC | Strong (QUORUM/ALL) |
| Geo-distributed read-heavy | PA/EC | Eventual (ONE) |
| Audit-critical operations | PC/SC | Strong (QUORUM/ALL) |
| High-volume logging/metrics | PA/EC | Eventual (ONE) |
| Shopping cart contents | PA/EC or PC/SC | Depends on overselling tolerance |
Practical Implications
When Low Latency Matters More
Some applications need fast responses more than perfect consistency:
Real-time gaming leaderboards - A few milliseconds of staleness in a player’s score does not break the game. Players notice lag, not occasional score discrepancies.
Social media feeds - Users expect content to load instantly. If their feed is 30 seconds stale, nobody notices or cares. If it takes 3 seconds to load, users complain.
IoT sensor dashboards - Historical accuracy matters, but real-time visualization needs speed. Slight staleness does not affect the physical systems being monitored.
When Strong Consistency Matters More
Some applications cannot tolerate staleness:
Financial transactions - Transferring money requires knowing the exact current balance. Eventual consistency here means people can spend money they do not have.
Inventory management - Overselling products because of stale inventory counts costs money and reputation.
Booking systems - Reservations must not double-book. This requires a single source of truth at the time of booking.
Capacity Estimation
Latency Budget Worksheet
For a 50ms target read latency in a geo-distributed system:
| Component | Latency Contribution | Notes |
|---|---|---|
| Network (client to LB) | 5-10ms | Depends on geographic distance |
| Load balancer processing | 1-2ms | Minimal if stateless |
| Database query execution | 10-20ms | Varies by query complexity |
| Replication lag (eventual) | 0-50ms | Async can be significant |
| Network (DB to client) | 5-10ms | Return path |
| Total | 21-92ms | Exceeds budget if replication is slow |
Consistency Level Latency Comparison (Cassandra)
| Consistency Level | Expected Latency | Quorum Size |
|---|---|---|
| ONE | 2-5ms | 1 node |
| QUORUM | 15-30ms | N/2 + 1 nodes |
| ALL | 30-100ms | All nodes |
| LOCAL_QUORUM | 10-20ms | Local DC only |
| EACH_QUORUM | 50-150ms | All DCs, highest latency |
Common Pitfalls / Anti-Patterns
Pitfall 1: Assuming Eventual Consistency is Always Safe
Problem: Teams default to eventual consistency everywhere because it is fast, then discover that some operations actually require strong consistency.
Why this happens: Eventual consistency is the path of least resistance. It writes fast, reads fast, and demos show it performing beautifully. The trouble is that demo data never includes the financial transfer or inventory decrement that exposes the gap.
The danger hides until it cannot. You can write successfully, get a confirmation back, and still lose the data if the primary fails before replication completes. Your application thought the write was durable. It was not.
Teams get caught in concrete ways:
- Checkout flow reads cart total with eventual consistency and charges the wrong amount because inventory was decremented elsewhere
- User profile updates disappear during a brief network hiccup, leaving users confused
- Session tokens stored with eventual consistency let a user appear logged in on one node but not another
Map every write to a consistency requirement before choosing consistency levels. Does this write need to be durable immediately, or can it tolerate a few hundred milliseconds of exposure? Profile your read paths to separate those that can tolerate staleness from those that cannot. When in doubt, start stronger and dial back. It is easier to accept lower latency for specific operations than to retrofit consistency into a system that assumed none.
Pitfall 2: Mixing Consistency Levels Without Testing
Problem: Using different consistency levels for reads and writes in the same code path without testing the interaction.
Why this is harder than it looks: The interaction between a strongly consistent write and an eventually consistent read trips up most teams. You write with QUORUM, confirming that 2 of 3 replicas accepted the value. You then read with ONE and might hit the replica that has not yet received the update. The write succeeded. The read returned stale data. The user just watched their data vanish.
This gets worse when consistency levels vary by operation. A user updates their profile with a strong write, then immediately views their profile with an eventual read. Depending on which replica the read hits, they might see the old value. Multiply this across hundreds of operations in a session and the system feels broken even when every individual operation is correct.
The kicker is that it works fine in testing. Your staging environment has low latency and no network hiccups. Deploy across regions with variable latency and the inconsistency window opens immediately.
Concrete failure modes:
- User writes a post, sees it disappear, writes it again, now has two copies
- User changes a password, gets logged out, logs back in with the old password still accepted
- Cart items vanish after checkout but the charge goes through
Test your consistency guarantees under failure conditions. Inject network partitions with chaos engineering and verify your application handles them correctly. Pay special attention to the boundary between operations using different consistency levels. Add audit logging in staging so you can trace which consistency level served each request.
Pitfall 3: Ignoring Replication Topology
Problem: Choosing QUORUM consistency without understanding that nodes may be in different datacenters with 50ms+ latency between them.
The topology trap: When you configure QUORUM in a 3-node cluster where each node lives in a different datacenter, you are not getting a balanced consistency level. You are getting a system that waits for 2 of 3 datacenters to respond. If US-East to EU-West is 85ms RTT and US-East to AP-Southeast is 170ms RTT, your QUORUM write latency is bounded by the slowest response. That is not a middle ground. That is a slow write that spans the globe.
QUORUM sounds like a consistency property. It is not. It is a counting property. It does not know or care where your nodes are. In a geo-distributed cluster, counting responses and caring about geography are different problems.
Teams discover this in staging with nodes on the same switch. They deploy to production where nodes are in Virginia, Dublin, and Singapore. Suddenly every write is slow. They blame the database. The database is working correctly. It is doing exactly what QUORUM says: waiting for a majority of nodes.
LOCAL_QUORUM exists because Cassandra needed to separate consistency from geography. It requires a quorum within the local datacenter only, ignoring cross-region latency. Most applications want this: fast local writes with consistency among local replicas, accepting that cross-region replication happens asynchronously.
Map your cluster topology before choosing consistency levels. If your nodes span datacenters, default to LOCAL_QUORUM for most operations. Reserve EACH_QUORUM or ALL for critical writes that must survive datacenter failure, and accept that those writes will be slow. For read-heavy workloads, LOCAL_ONE contacts only the nearest replica for the fastest possible reads.
Pitfall 4: Assuming Clocks are Synchronized
Problem: Using timestamp-based conflict resolution assumes all nodes have synchronized clocks. Clock skew between nodes can make this completely unreliable.
Why clock skew is everywhere: NTP synchronization looks good on paper. All servers sync to a time source, clocks agree, problem solved. In practice, NTP has edge cases that break timestamp ordering. A busy server might step its clock backward when it resyncs. A VM might be paused by the hypervisor, freezing its clock while the rest of the system moves forward. Network delay between the time server and the client introduces jitter that NTP cannot fully compensate for. GPS-based time sources have their own failure modes.
The result is that two servers can genuinely disagree about which event happened first, even with NTP running correctly. Server A writes at timestamp 1000 and Server B writes at timestamp 1001, but Server B’s clock was running fast and the actual physical time of Server A’s write was later. A last-write-wins system using these timestamps will silently discard Server A’s write.
This is not theoretical. The 2015 Docker leap second bug caused clocks to repeat a second, breaking timestamp-based ordering in distributed systems across the internet. VM migration and live snapshot/restore cause clock inconsistencies that are harder to detect, often only surfacing as data loss.
Vector clocks solve this by tracking causality instead of wall-clock time. A Lamport clock increments whenever an event happens. If event A caused event B, then A’s logical timestamp is less than B’s. Vector clocks extend this by having each node track its own counter, so you can determine not just ordering but which node was responsible for which version.
Cassandra’s client-supplied timestamps are a simplified version of this approach. The client generates a timestamp that all nodes accept as authoritative, bypassing server clock issues entirely. DynamoDB uses server-side timestamps by default but supports conditional writes, which let you implement compare-and-swap without relying on clock agreement.
Use logical timestamps (Lamport clocks or vector clocks) for conflict resolution, not wall-clock time. If you must use timestamps, hybrid logical clocks combine physical time with a logical counter to handle clock skew gracefully. Most distributed databases do this by default. If you are building conflict resolution at the application layer, you need to handle it yourself.
Production Failure Scenarios
| Failure Scenario | Impact | Mitigation |
|---|---|---|
| Primary region goes down | All writes fail if using PC/EC with sync replication | Use async replication to secondary with manual failover; accept data loss window |
| Replication lag spike | Reads return stale data for extended period | Monitor replication lag; alert on thresholds; tune consistency level per query |
| Split-brain during failover | Both primaries accept writes, creating divergent data | Use consensus-based leader election (Raft/Paxos); always write to quorum |
| Cassandra repair running | Temporary spike in read latency (anti-entropy repair) | Schedule repairs during low-traffic windows; use incremental repair |
| Network latency spike | Requests timeout even without partition | Implement timeout with retry using exponential backoff; circuit breakers |
| Clock skew across regions | Timestamp-based conflict resolution breaks | Use logical clocks (Lamport) instead of wall-clock for ordering |
Dynamo-Style vs Traditional Database Trade-offs
The PACELC theorem manifests differently depending on whether a database follows the Dynamo tradition (AP-first, tunable) or the traditional approach (CP-first, strong guarantees).
| Aspect | Dynamo-Style (PA/EC) | Traditional (PC/SC) |
|---|---|---|
| Design Priority | Low latency, high availability | Strong consistency, data correctness |
| Conflict Resolution | LWW, vector clocks, CRDTs | Single leader, synchronous replication |
| Partition Handling | Continue serving reads/writes | Halt writes to maintain consistency |
| Latency Profile | ~1-5ms async writes | ~50-200ms sync writes |
| Use Case Fit | Social feeds, product catalogs, IoT | Financial transactions, inventory, bookings |
| Schema Flexibility | Often key-value or wide-column | Rich schema with joins |
| Query Model | Primary key focused | Complex queries, secondary indexes |
| Typical Examples | DynamoDB, Cassandra, Riak | MongoDB, HBase, etcd, CockroachDB |
When to Choose Each Approach
Choose Dynamo-style (PA/EC) when:
- Your users are distributed across geographies and latency matters more than absolute consistency
- Your application tolerates brief staleness (social feeds, analytics, IoT)
- You need tunable consistency to optimize different operations
- Your workload is read-heavy with occasional writes
Choose Traditional (PC/SC) when:
- Data correctness is non-negotiable (financial, medical, inventory)
- You need complex queries and relational integrity
- Your users are in a single region and can tolerate higher latency
- Regulatory requirements demand audit trails and ordering guarantees
Hybrid Architectures
Pure PA/EC or PC/SC systems are uncommon in production. Most real deployments mix approaches across layers, treating the latency-consistency trade-off as a per-operation decision rather than a system-wide commitment.
The primary database layer usually runs PC/SC for operations that modify authoritative state. Payment processing, inventory updates, and user authentication all need a single source of truth that all nodes agree on. These operations accept higher latency because correctness is non-negotiable. A customer will wait 200ms for a transfer to complete; they will not accept discovering their payment vanished after a failover.
The cache or CDN layer almost always runs PA/EC. Caches serve reads at sub-millisecond latency by accepting that the cached value might be slightly stale. The cache is never the authoritative record, it is a performance optimization. If the cache returns yesterday’s exchange rate for a currency conversion, the business impact is minimal. If it returns a stale price for a product, the worst case is a mildly surprised customer. This asymmetry between cache and primary makes PA/EC appropriate for all caching layers.
Event sourcing patterns use the distinction explicitly. The event log itself is PC/SC. It is the immutable record of what happened, and it must be consistent. But derived views, read models, and projection tables that serve queries can run PA/EC. When a new event is appended to the log, it propagates asynchronously to the read models. Queries against the read models are fast and eventually consistent. If the read model falls behind, the business keeps operating. The authoritative record is safe in the event log.
Read replicas show the same principle. A primary node accepts writes with synchronous replication to one or two replicas, achieving strong consistency for the write path. Additional replicas receive changes asynchronously and serve reads with eventual consistency. This is the classic “read-scale-out” pattern that DynamoDB, Cassandra, and MongoDB all support. The write path pays the latency cost for strong consistency. The read path serves from the nearest replica at local latency.
This layered approach is why DynamoDB Global Tables and Cassandra with tunable consistency have become popular. Rather than committing to one PACELC profile at the system level, the same database can operate as PC/SC for some operations and PA/EC for others. A DynamoDB table with ConsistentRead=true on a write operation behaves like PC/SC. The same table with ConsistentRead=false on a read behaves like PA/EC. The consistency decision happens per-request, not per-system.
When evaluating databases, look at consistency flexibility, not just the default profile. A database that defaults to eventual consistency but supports strong consistency per query gives you more room to maneuver than one that defaults to strong consistency with no way out.
Interview Questions
Expected answer points:
- PACELC states: L_latency = f(C_consistency) — latency is a function of the consistency level chosen
- Synchronous replication latency: L_sync = N × L_network — you wait for all N replicas to acknowledge
- Asynchronous replication latency: L_async = L_local — respond immediately after local write
- For a 3-replica system with 30ms inter-node latency: sync write = ~90ms, async write = ~1-5ms
- The latency delta grows linearly with replication factor and network distance
Expected answer points:
- Raft uses a leader election mechanism with timeouts and voting
- Followers timeout waiting for heartbeat from leader and become candidates
- Candidate requests votes from other nodes; majority wins election
- Only one node can achieve majority — prevents multiple leaders
- Split-brain occurs when multiple nodes accept writes without coordination; Raft prevents this through majority election
- Term numbers in Raft also prevent stale writes — a node with an older term cannot become leader
Expected answer points:
- Vector clocks track the partial ordering of events across distributed nodes
- Each node maintains its own counter that increments with every write
- When nodes exchange state, they compare vector clocks to determine causality
- Write A supersedes Write B if A's vector clock dominates B's (all components >= and at least one >)
- Conflicting writes can be detected and resolved — either automatic merge (CRDTs) or conflict detection
- Cassandra uses client-supplied timestamps as a simpler alternative to full vector clocks
Expected answer points:
- ONE: Contact 1 replica — lowest latency, highest staleness risk, suitable for high-volume non-critical data
- QUORUM: Contact N/2+1 replicas — balanced consistency and latency, good default for user-generated content
- ALL: Contact all replicas — strongest consistency, highest latency, partition-intolerant
- LOCAL_QUORUM: Contact quorum within local datacenter only — ideal for geo-distributed deployments
- EACH_QUORUM: Contact quorum in every datacenter — highest latency, used for critical writes that must survive DC failure
- Choice depends on your durability requirements vs latency tolerance
Expected answer points:
- Hinted handoff is a failure recovery mechanism for when a replica node is temporarily unavailable
- When the coordinator detects a down node, it stores the write locally with a hint about the intended recipient
- Once the failed node recovers, the coordinator replays the stored writes to the missing replica
- This ensures the write eventually reaches all replicas even during temporary failures
- Handoff has a TTL — if the node is down too long, hints expire and anti-entropy repair handles reconciliation
- Hinted handoff reduces the window of inconsistency after failure recovery
Expected answer points:
- Read-heavy workloads benefit more from eventual consistency — many more reads than writes, staleness acceptable
- Use QUORUM reads with ONE writes for read-heavy PA/EC workloads — fast reads, slower but durable writes
- Write-heavy workloads require careful consistency level choice — consider QUORUM or LOCAL_QUORUM writes
- For write-heavy with strong consistency needs (financial transactions), PC/SC is appropriate despite latency cost
- Global write-heavy workloads face cross-region latency — consider regional sharding with local synchronous writes
- Tunable consistency lets you optimize reads and writes independently based on access patterns
Expected answer points:
- Tunable consistency is the practical implementation of PACELC's latency-consistency spectrum
- Cassandra allows consistency level per query: ONE, QUORUM, ALL, LOCAL_QUORUM, EACH_QUORUM
- DynamoDB's ConsistentRead parameter toggles between eventual (~15-30ms) and strong (~30-50ms) reads
- You can run PA/EC workloads (low-latency, eventual consistency) in the same database as PC/SC (strong consistency)
- Example: Cassandra with W=ONE, R=QUORUM is PA/EC; W=ALL, R=ALL is PC/SC
- This flexibility is why many NoSQL databases support tunable consistency — one database fits multiple PACELC profiles
Expected answer points:
- Merkle trees are hash trees where each leaf node is the hash of a data range, and parent nodes hash their children
- Cassandra's nodetool repair compares Merkle trees between replicas to find divergent ranges
- Only ranges with different hashes need synchronization — no full data transfer required
- Merkle trees detect both missing updates and corrupted data efficiently
- Tree size is proportional to replica size divided by leaf granularity — trade-off between memory and detection precision
- Anti-entropy repair runs as a background process and handles accumulated divergence that read repair cannot catch
Expected answer points:
- Fencing tokens prevent a stale leader from writing after failover — the old leader might still think it's valid
- When a new leader takes over, it receives an incremented epoch number or token
- Every write must present the current fencing token; the storage system rejects writes with stale tokens
- Even if network partition causes the old leader to send writes, they are rejected due to outdated token
- This is a 3-phase pattern: detect failover → increment token → reject stale writes
- Fencing tokens alone are not sufficient — combine with majority quorum for complete split-brain prevention
Expected answer points:
- Inject network partitions between datacenters using tools like Chaos Monkey or Toxiproxy
- During partition, verify CP systems reject writes while AP systems accept stale writes
- Measure actual latency under different consistency levels — verify your assumptions match reality
- Kill replica nodes and verify read repair and anti-entropy repair work as expected
- Test failover timing — verify fencing tokens prevent split-brain and leader election completes correctly
- Always test in staging first; design rollback procedures; monitor your consistency violation metrics during experiments
Further Reading
- CAP Theorem — Understanding partitions, consistency, and availability trade-offs
- Consistency Models — Read-your-writes, monotonic reads, and causal consistency
- Availability Patterns — Redundancy, failover, and high availability design
- Dynamo: Amazon’s Key-Value Store — Origin of eventual consistency design
- Lessons from Distributed Systems — Practical distributed systems wisdom
- Jepsen Consistency Testing — Formal testing of distributed system guarantees
Quick Recap Checklist
- PACELC extends CAP theorem by describing latency trade-offs even when NO partition occurs
- PACELC states: “if there is a partition (P), the system must choose between Availability (A) and Consistency (C). Else (E), even without partitions, the system must choose between Latency (L) and Consistency (C)”
- PACELC categorises databases into five classes: EL=EA, EL=EC, EL=LC, EC=LC, LA=EA
- EL=EA systems (e.g., Cassandra, DynamoDB) choose low latency and availability, sacrifice consistency during partitions
- EL=EC systems (e.g., HBase) choose low latency and consistency, sacrifice availability during partitions
- EL=LC systems (e.g., Riak) choose low latency and consistency, sacrifice availability during partitions
- EC=LC systems (e.g., VoltDB) choose consistency and low latency, sacrifice availability always
- LA=EA systems (e.g., Most SQL primary-backup DBs) choose low latency and availability, sacrifice consistency always
- Cassandra’s configurable consistency levels let you choose your PACELC trade-off per query
- Understanding PACELC helps you predict database behaviour before failures happen, not just during them
Conclusion
PACELC complements CAP by highlighting the latency-consistency trade-off that exists even when the network behaves. Here’s what I keep coming back to:
- Without partitions, latency and consistency still conflict - Synchronization enables consistency but costs time.
- Most systems prefer low latency - DynamoDB, Cassandra, and similar systems default to eventual consistency for a reason.
- Choose based on your use case - Financial systems often need strong consistency. Social feeds usually do not.
- Consider the spectrum - The Consistency Models post covers the middle ground between strong and eventual.
CAP and PACELC together give you a framework for thinking through distributed system design. Neither theorem tells you what to choose, but both help you understand the consequences of your choices.
Real-world Failure Scenarios
Scenario 1: Riak’s Eventual Consistency Surprise
What happened: A financial services company built a trading platform on Riak KV, an eventual-consistency database. During a period of high market volatility, several traders placed orders that appeared to execute successfully but were silently dropped during network re-partitioning.
Root cause: Riak prioritises availability (EL=EA in the PACELC model). The “successful” write responses were returned during a window where the specific key’s primary and fallback nodes were temporarily unreachable. When the partition healed, the values were reconciled to their pre-write state due to vector clock resolution.
Impact: Approximately $2.3 million in trades needed manual reconciliation. The company’s trading platform had to halt operations for 3 hours while auditors verified which transactions were valid.
Lesson learned: Eventual consistency windows can extend unpredictably under network partitions. Financial systems requiring immediate consistency must choose EC systems explicitly, not assume “eventual” resolves quickly enough for business transactions.
Scenario 2: Cassandra’s Latency Trade-offs Under Load
What happened: A large e-commerce company running Apache Cassandra experienced “knife-edge” performance collapse during Black Friday traffic. System latency, which had been stable at p99 < 50ms, spiked to timeouts (>5000ms) within a 15-minute window.
Root cause: Cassandra’s EL=LA (Latency-Availability) model means it will sacrifice latency for availability under load. As nodes became overloaded, hinted handoff and read repair operations consumed increasing CPU and I/O, creating a positive feedback loop of latency increase.
Impact: Shopping cart checkouts failed for approximately 12% of users during peak traffic. Revenue loss estimated at $1.8M during the degraded period.
Lesson learned: Cassandra’s availability-first design means it will “stay up” by accepting ever-higher latencies. Applications requiring predictable low-latency responses must implement client-side timeouts and fail-over to read-from-replicas, not rely on the database to maintain latency SLAs.
Scenario 3: HBase’s Consistency Trade-offs in Production
What happened: LinkedIn’s initial deployment of Apache HBase experienced data inconsistency bugs during routine cluster maintenance. Region splits and compactions occasionally left regions in an inconsistent state where different replica hosts returned different values for the same key.
Root cause: HBase is EC (Consistency-first) in its PACELC profile. However, the implementation uses ZooKeeper for coordination, and during the ZooKeeper leadership election window, region assignment metadata became temporarily stale, causing some client requests to route to replica nodes serving stale data.
Impact: User profile updates were occasionally lost during the inconsistency window. Approximately 0.3% of profile changes during maintenance windows were silently reverted to previous values within 24 hours.
Lesson learned: Even EC systems can exhibit temporary inconsistency during administrative operations. Implement application-level checksums and audit trails for critical data, especially around known maintenance windows.
Category
Tags
Related Posts
Gossip Protocol: Scalable State Propagation
Learn how gossip protocols enable scalable state sharing in distributed systems. Covers epidemic broadcast, anti-entropy, SWIM failure detection, and real-world applications like Cassandra and Consul.
Consistency Models in Distributed Systems: Complete Guide
Learn about strong, weak, eventual, and causal consistency models. Understand read-your-writes, monotonic reads, and picking the right model for your system.
CAP Theorem: Consistency vs Availability Trade-offs
Learn the fundamental trade-off between Consistency, Availability, and Partition tolerance in distributed systems with practical examples.