root.system / 0x12 / pacelc

CAP told you what breaks.
PACELC tells you what you choose every second.

The CAP theorem only applies during a disaster. Partitions happen maybe twelve times a year. PACELC covers the other 525,948 minutes: the tradeoff that never stops, even when everything is working perfectly.

CAP has a secret it doesnt advertise.

It only matters during a disaster.

A network partition, the cut wire from page seventeen, happens rarely. Maybe a dozen times a year, if your system is busy. The rest of the time everything is healthy, every machine can reach every other machine, and CAP has nothing to say.

But your database is still making a tradeoff in those healthy moments. On every single request. Right now.

Because even with a perfect network, keeping two copies of your data in perfect agreement takes time. You can wait for every replica to confirm before you answer, slow but correct. Or you can answer from the nearest copy at once, fast but maybe slightly stale.

Latency, or consistency. That choice never stops.

That is PACELC. If there is a Partition, choose A or C, the old CAP question. Else, in normal life, choose L or C. The else clause is where your system actually lives, 525,948 minutes a year.

CAP told you what breaks when the network fails. PACELC tells you what you are trading away when it doesnt.

Lets read the whole sentence.

Beginner// level 01

The two tradeoffs

You learned the CAP theorem. Consistency, availability, partition tolerance: pick two. And you felt smart. Then someone asked: but what about when the network isn't broken? CAP had nothing to say, because CAP only describes disasters, and partitions are rare.

But every single request that hits your system when everything is working fine still faces a choice: speed or correctness.

In 2012 a computer scientist named Daniel Abadi looked at the CAP theorem and said: Brewer was right, but he only told half the story. So he extended it. If there is a Partition, choose between Availability and Consistency. Else, when everything is normal, choose between Latency and Consistency. Four letters: PA/EL or PC/EC. The most honest description of every database ever built. Once you see it, you find it everywhere: in your own code, in every system you have ever used, every day of your career.

CAP describes what happens when your network breaks. PACELC extends it with a second question: what happens the rest of the time? Even during perfect network conditions, every distributed system still chooses between speed and correctness on every single request.

The PACELC decision tree

network partition detected?
├── YES → CAP applies
│         choose: Availability (A)  or  Consistency (C)

└── NO  → Else (E) applies
          choose: Latency (L)  "answer fast, maybe stale"
                or Consistency (C)  "answer correct, takes longer"

The four labels

PA
Available during partition
When the network splits, keep answering. Stay online even if the two halves drift apart. The CAP A.
PC
Consistent during partition
When the network splits, refuse rather than diverge. Go offline rather than serve a value you cannot verify. The CAP C.
EL
Low latency in normal ops
No partition, healthy network: answer as fast as possible from the nearest replica, even if it is slightly behind.
EC
Consistent in normal ops
No partition, healthy network: still verify with the primary or a quorum before answering. Correct, but slower.

The balance query, two ways

EL · low latency
Fast, possibly stale
Answer immediately. Return the nearest server's value, maybe 1 millisecond. But that value might be 50 milliseconds out of date.
EC · consistent
Slow, always correct
Check with the primary first. Verify the latest value. Return the confirmed answer, 50 to 100 milliseconds. Slow, but always right.
// the else clause
Networks rarely partition. But every request chooses EL or EC. Every time, without exception. The "else" clause is the one you live inside every second your system is healthy, which is almost always.

The choice, expressed as types

Like the CAP page, the choice is structural. A read either returns a possibly-stale local value (EL) or pays to verify with the primary (EC).

Rust• • •
// PACELC expressed as a type system.
// Every read operation implicitly makes this choice.
#[derive(Debug)]
enum PacelcRead {
    // EL: answer fast, tolerate staleness
    LowLatency { max_staleness_ms: u64 },
    // EC: answer correct, pay the latency cost
    StrongConsistency { require_quorum: bool },
}

fn read_balance(
    strategy: &PacelcRead,
    local_cache: u64,
    local_age_ms: u64,
    fetch_from_primary: impl Fn() -> u64,
) -> u64 {
    match strategy {
        // EL: return local value if fresh enough
        PacelcRead::LowLatency { max_staleness_ms } => {
            if local_age_ms <= *max_staleness_ms {
                local_cache // fast, possibly stale
            } else {
                fetch_from_primary() // cache expired
            }
        }
        // EC: always fetch from primary
        PacelcRead::StrongConsistency { .. } => {
            fetch_from_primary() // slow, always correct
        }
    }
}

fn main() {
    let el = PacelcRead::LowLatency { max_staleness_ms: 100 };
    let ec = PacelcRead::StrongConsistency { require_quorum: true };

    // EL: returns cache if < 100ms old. Fast. Instagram does this.
    let _ = read_balance(&el, 500, 50, || 500);

    // EC: always goes to primary. Correct. Banks do this.
    let _ = read_balance(&ec, 500, 50, || 500);
}
C• • •
#include <stdint.h>
#include <stdbool.h>

typedef enum {
    PACELC_EL, // low latency, tolerate staleness
    PACELC_EC  // strong consistency, pay latency
} PacelcStrategy;

typedef struct {
    uint64_t value;
    uint64_t age_ms;
} CachedValue;

typedef uint64_t (*FetchPrimary)(void);

uint64_t read_balance(
    PacelcStrategy strategy,
    CachedValue cache,
    uint64_t max_staleness_ms,
    FetchPrimary fetch
) {
    switch (strategy) {
        case PACELC_EL:
            // EL: use cache if fresh enough
            if (cache.age_ms <= max_staleness_ms) {
                return cache.value; // fast, maybe stale
            }
            return fetch(); // cache expired, go remote
        case PACELC_EC:
            // EC: always go to primary
            return fetch(); // slow, always correct
        default:
            return fetch();
    }
}

Feel it: send a read under each strategy

Same setup as the CAP visualiser, no partition this time. A primary and a nearby replica. Update the primary so the replica falls behind, then send reads under EL and EC and watch the latency-versus-correctness tradeoff happen in milliseconds.

// feel the tradeoff
Primary · source of truthONLINE
$1000
always current
Local replica · nearest serverSYNCED
$1000
matches primary
routes to local replica
request log
send a request to begin.

update the primary so the replica falls behind, then send reads under each strategy. EL answers from the nearest replica in 1ms (possibly stale); EC verifies with the primary in 52ms (always correct). networks rarely partition, but every request still chooses EL or EC.

Intermediate// level 02

What every system actually chose

Every database, every distributed system, every blockchain has a PACELC label: four letters that describe its entire design philosophy. Here is what the most important ones chose, and why.

PA/EL · DynamoDB
Sacrifices correctness
Amazon's flagship database. During a partition it stays available; in normal ops it optimises for speed, returning the nearest server's value instantly even if slightly behind. You have felt this: Amazon showing an item in stock when it just sold out. The system prioritised your experience over perfect inventory accuracy.
PC/EC · HBase
Sacrifices speed
Apache's big-data database. During a partition it stays consistent; in normal ops it pays the latency cost. Every read goes to the primary, every write is confirmed before returning. You have felt this: a bank transfer that takes a few seconds. The system is verifying with every relevant server before telling you yes.
PA/EL* · Cassandra
Sacrifice depends on the query
The most flexible choice. Default: optimise for speed. Tunable per query: ONE reads from one node (fastest), QUORUM from a majority (balanced), ALL from every node (slowest, correct). You have felt this: Instagram loading your feed instantly while your follower count updates slowly. Same database, different consistency levels per operation.
PC/EC · MySQL
Sacrifices scale
The classic relational database. Every write confirmed before response, every read consistent. Slower than distributed alternatives, but correct, always. You have felt this: a form submission that takes a noticeable moment to confirm. The database is making sure your data is durably stored before telling you it worked.
PC/EC ∞ · Bitcoin
Sacrifices speed, always
The most extreme consistency any system has ever chosen. During a partition it halts rather than diverge; in normal ops, ten-minute confirmations. Bitcoin made the latency a feature: waiting is the proof of consistency. You have felt this: waiting ten minutes for a confirmation. That is not a bug. That is the price of absolute truth in a network with no central authority.
PA/EL ⚡ · Solana
Sacrifices fault tolerance under load
The opposite of Bitcoin. Optimises latency as aggressively as Bitcoin optimises consistency. Proof of History is a cryptographic clock that lets nodes agree on ordering without waiting for each other. 400ms confirmations versus Bitcoin's 10 minutes, 1500 times faster. You have felt this: Solana outages that Bitcoin has never had. Push EL to the physical limit and consistency becomes fragile under stress.
PC/EC → · Ethereum
Sacrifices speed, intentionally
Started closer to availability; Proof of Stake moved it toward consistency, with deterministic finality after two epochs (about 12 minutes). Moving up the PACELC spectrum deliberately, because DeFi requires it: a financial contract that can be reversed is not a financial contract. You have felt this: ETH transfers that take longer than Solana but feel more final. The system is choosing certainty over speed.

The labels, side by side

systempartitionnormalspeedcorrectness
DynamoDBPAEL★★★★★★★★
HBasePCEC★★★★★★★★
CassandraPA*EL*★★★★★★★★
MySQLPCEC★★★★★★★★
BitcoinPCEC★★★★★
SolanaPAEL★★★★★★★★
EthereumPCEC★★★★★★★★

Tunable consistency, like Cassandra

The most flexible systems let you pick the PACELC tradeoff per query. ONE is EL (fast, may be stale), ALL is EC (slow, refuses if nodes disagree), QUORUM sits in between. Same database, different point on the spectrum for every read.

Rust• • •
use std::collections::HashMap;

#[derive(Debug, Clone, Copy)]
enum ConsistencyLevel {
    One,    // EL: fastest, least consistent
    Quorum, // balanced: majority must agree
    All,    // EC: slowest, most consistent
}

struct DistributedKV {
    nodes: Vec<HashMap<String, u64>>,
}

impl DistributedKV {
    fn read(&self, key: &str, level: ConsistencyLevel) -> Option<u64> {
        let responses: Vec<Option<u64>> = self
            .nodes
            .iter()
            .map(|node| node.get(key).copied())
            .collect();

        match level {
            // EL: return first available response. Fast, may be stale.
            ConsistencyLevel::One => responses.iter().flatten().next().copied(),

            // Balanced: a majority must agree.
            ConsistencyLevel::Quorum => {
                let quorum = self.nodes.len() / 2 + 1;
                let mut counts: HashMap<u64, usize> = HashMap::new();
                for val in responses.iter().flatten() {
                    *counts.entry(*val).or_insert(0) += 1;
                }
                counts
                    .into_iter()
                    .find(|(_, count)| *count >= quorum)
                    .map(|(val, _)| val)
            }

            // EC: ALL nodes must agree. Slowest, most consistent.
            ConsistencyLevel::All => {
                let values: Vec<u64> = responses.iter().flatten().copied().collect();
                if values.len() == self.nodes.len()
                    && values.windows(2).all(|w| w[0] == w[1])
                {
                    values.first().copied()
                } else {
                    None // nodes disagree: refuse
                }
            }
        }
    }
}

fn main() {
    let db = DistributedKV {
        nodes: vec![
            [("balance".to_string(), 500)].into(),
            [("balance".to_string(), 500)].into(),
            [("balance".to_string(), 495)].into(), // stale
        ],
    };

    println!("{:?}", db.read("balance", ConsistencyLevel::One));    // Some(500)
    println!("{:?}", db.read("balance", ConsistencyLevel::Quorum)); // Some(500)
    println!("{:?}", db.read("balance", ConsistencyLevel::All));    // None
}
C• • •
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

typedef enum {
    CONSISTENCY_ONE,    // EL: fastest
    CONSISTENCY_QUORUM, // balanced
    CONSISTENCY_ALL     // EC: most consistent
} ConsistencyLevel;

// Returns -1 if consistency cannot be guaranteed.
int64_t distributed_read(
    const int64_t *node_values, // -1 = unreachable
    size_t node_count,
    ConsistencyLevel level
) {
    size_t quorum = node_count / 2 + 1;
    size_t reachable = 0, agreements = 0;
    int64_t first_value = -1;

    for (size_t i = 0; i < node_count; i++) {
        if (node_values[i] < 0) continue;
        reachable++;
        if (first_value < 0) first_value = node_values[i];
        if (node_values[i] == first_value) agreements++;
    }

    switch (level) {
        case CONSISTENCY_ONE:
            return first_value; // EL: first wins
        case CONSISTENCY_QUORUM:
            return (agreements >= quorum) ? first_value : -1;
        case CONSISTENCY_ALL:
            return (reachable == node_count && agreements == node_count)
                   ? first_value : -1;
    }
    return -1;
}

int main(void) {
    int64_t nodes[] = {500, 500, 495}; // two agree, one stale
    printf("ONE:    %lld\n", distributed_read(nodes, 3, CONSISTENCY_ONE));    // 500
    printf("QUORUM: %lld\n", distributed_read(nodes, 3, CONSISTENCY_QUORUM)); // 500
    printf("ALL:    %lld\n", distributed_read(nodes, 3, CONSISTENCY_ALL));    // -1
    return 0;
}
// one database, many tradeoffs
The ALL path returning None / -1 when nodes disagree is EC in its purest form: it would rather refuse than hand back a value it cannot fully verify. The ONE path never refuses. Same code, same data, opposite ends of the PACELC spectrum, chosen at call time.
Advanced// level 03

PACELC in your own code

Most developers think PACELC is a database concern. It isn't. You make PACELC decisions in every codebase you touch, every day, without knowing it.

You have been doing this all along

PA/EL
Cache an API response
You cache an API response for 60 seconds and return it to the next 1000 users without re-fetching. Fast, possibly stale.
PC/EC
Loading spinner before render
You show a loading spinner while fetching the latest data before rendering the page. Correct, slower.
PA/EL
Stale-while-revalidate
You show a stale value immediately and refresh it in the background. Fast perceived load, eventually consistent.
PC/EC
Wait for the confirmation email
You make the user wait for a confirmation email before letting them proceed. Correct, higher friction.
PA/EL
Offline-first app
Your app works offline using a local database that syncs when reconnected. Available always, consistent eventually.
the realisation
You already knew this
You have been making PACELC decisions every day of your career. You just did not have a name for it. Now you do.

Stale-while-revalidate vs always-fresh

The two most common PACELC patterns in application code, side by side. el_read returns instantly and refreshes in the background; ec_read blocks until the data is fresh. The difference is one spawn versus one blocking call.

Rust• • •
use std::time::{Duration, Instant};
use std::sync::{Arc, Mutex};

#[derive(Clone)]
struct Cache<T: Clone> {
    value: T,
    fetched_at: Instant,
    ttl: Duration,
}

// PA/EL: stale-while-revalidate.
// Returns immediately, refreshes in the background.
fn el_read<T: Clone + Send + 'static>(
    cache: Arc<Mutex<Cache<T>>>,
    fetch: impl Fn() -> T + Send + 'static,
) -> T {
    let cached = cache.lock().unwrap();
    if cached.fetched_at.elapsed() > cached.ttl {
        // Stale: trigger a background refresh, but do not wait.
        let cache_clone = Arc::clone(&cache);
        std::thread::spawn(move || {
            let fresh = fetch();
            let mut c = cache_clone.lock().unwrap();
            c.value = fresh;
            c.fetched_at = Instant::now();
        });
    }
    cached.value.clone() // EL: fast, possibly stale
}

// PC/EC: always consistent.
// Blocks until fresh data is fetched.
fn ec_read<T: Clone>(
    cache: Arc<Mutex<Cache<T>>>,
    fetch: impl Fn() -> T,
) -> T {
    let mut cached = cache.lock().unwrap();
    if cached.fetched_at.elapsed() > cached.ttl {
        cached.value = fetch(); // EC: slow, always correct
        cached.fetched_at = Instant::now();
    }
    cached.value.clone()
}
C• • •
#include <time.h>
#include <stdbool.h>
#include <stdint.h>
#include <pthread.h>

typedef struct {
    uint64_t        value;
    time_t          fetched_at;
    int             ttl_seconds;
    pthread_mutex_t lock;
} Cache;

typedef uint64_t (*FetchFn)(void);

// PA/EL: return immediately, refresh in the background.
uint64_t el_read(Cache *cache, FetchFn fetch) {
    pthread_mutex_lock(&cache->lock);
    uint64_t result = cache->value;
    bool stale = difftime(time(NULL), cache->fetched_at) > cache->ttl_seconds;
    pthread_mutex_unlock(&cache->lock);

    if (stale) {
        // Simplified: real code hands this to a thread pool.
        uint64_t fresh = fetch();
        pthread_mutex_lock(&cache->lock);
        cache->value = fresh;
        cache->fetched_at = time(NULL);
        pthread_mutex_unlock(&cache->lock);
    }
    return result; // EL: return before the refresh completes
}

// PC/EC: block until fresh.
uint64_t ec_read(Cache *cache, FetchFn fetch) {
    pthread_mutex_lock(&cache->lock);
    if (difftime(time(NULL), cache->fetched_at) > cache->ttl_seconds) {
        cache->value = fetch(); // EC: block and fetch fresh data
        cache->fetched_at = time(NULL);
    }
    uint64_t result = cache->value;
    pthread_mutex_unlock(&cache->lock);
    return result; // EC: always current
}

PACELC across the blockchain ecosystem

Every blockchain is a PACELC opinion. Read the four letters and you understand the entire design.

  • Bitcoin (PC/EC): consistency above everything. Every transaction waits for global consensus across ten thousand nodes, ten minutes per block. The latency is the proof. No shortcut, no exception, no stale reads.
  • Ethereum (PC/EC, strengthening): started with longer finality windows; Proof of Stake introduced deterministic finality after two epochs (about twelve minutes). Moving deliberately toward stronger consistency because DeFi contracts cannot tolerate reversal.
  • Solana (PA/EL): Proof of History provides a cryptographic clock, so nodes agree on ordering without waiting for each other. 400ms confirmations, 1500x faster than Bitcoin. The tradeoff: outages Bitcoin has never had. Optimise EL to the physical limit and partition tolerance weakens under load.
  • Sui and Aptos (hybrid): simple transfers go PA/EL, skipping global consensus to settle in under a second; complex transactions go PC/EC, requiring global agreement and taking longer but always correct. The most nuanced PACELC answer yet: a different tradeoff per operation type.

// PACELC as a design tool
// Ask this about every system you build:
//
// IF partition:
//   choose A (stay online, risk divergence)
//   or     C (go offline, guarantee truth)
//
// ELSE normal operation:
//   choose L (answer fast, risk staleness)
//   or     C (answer correct, pay latency)
//
// Your answer defines your architecture.
// Everything else follows from it.

PACELC across ScrapyBytes

The same ideas surface all over ScrapyBytes. Here is where this page connects to the rest of the curriculum, and how to follow each thread.

CAP Theorem

PACELC is CAP plus the part CAP ignored. CAP only describes the partition; PACELC adds the else case: latency versus consistency when the network is healthy. Read the cap-theorem page first.

scrapybytes.vercel.app/cap-theorem
Distributed Systems

Every distributed system makes the PACELC choice on every request, usually without telling you. The distributed-systems page is the setting; this page is the dial.

scrapybytes.vercel.app/distributed-systems
Networking

The latency in PACELC is network latency: the speed of light and the round trips from the networking page. Geography is why consistency costs time.

scrapybytes.vercel.app/networking
Blockchain

Bitcoin is PC/EC: consistency during partitions and consistency in normal operation, paid for in latency. The blockchain page is PACELC taken to the strict extreme.

scrapybytes.vercel.app/blockchain
Nodes

The tradeoff is between nodes that must agree and the time their messages take to travel. The nodes page is who sits on each side of the round trip.

scrapybytes.vercel.app/nodes
Big O Notation

Latency is a constant factor, not a Big O class, yet it dominates real distributed performance. The big-o page measures growth; this page is a reminder that constants still decide who wins.

scrapybytes.vercel.app/big-o
Memory

Your cache is PACELC in miniature: L1 is fast and possibly stale (EL), RAM is slower but consistent (EC). The memory page makes this tradeoff billions of times a second.

scrapybytes.vercel.app/memory
Pointers

A pointer to a value on another machine is a remote reference: follow the local cache (EL) or go to the source (EC). The pointers page is where every RPC becomes a PACELC choice.

scrapybytes.vercel.app/pointers
Arrays

A sharded array reads from the nearest shard (EL) or verifies against the primary (EC). The arrays page is the structure this tradeoff runs on once it is distributed.

scrapybytes.vercel.app/arrays
Hashing

Blockchains pay the EC cost up front with hashing: a block hash is slow to produce, instant to verify. The hashing page is the PC/EC tradeoff optimised.

scrapybytes.vercel.app/hashing
next up / 0x13
The system that chose PC/EC above everything: how Bitcoin actually works.
blockchain