System design · 4 of 5

Distributed consensus and reliability

CAP and PACELC, Raft, idempotency, circuit breakers, rate limiting, backoff with jitter, heartbeats and failover.

Updated 2026-09-08
On this page

A distributed system is one in which a machine you have never heard of can fail and stop your work. The network loses packets, clocks drift, processes pause, and every message may arrive twice or never. This article is about the handful of ideas that let systems agree, retry, back off and fail over without making things worse.

The map

Read this first when short on time. Every branch is a section below.

Figure 1. The whole article on one page. Every branch is a section below; fold what you know, open what you do not.

CAP: what a partition forces you to choose

The CAP theorem says that when the network splits a system into groups that cannot talk to each other, each group must either refuse to answer (stay consistent) or answer with what it has (stay available). It is not a menu of three from which you pick two; partitions are not optional, so the only choice is what to do during one.

flowchart TB
  subgraph Normal["Normal operation"]
    direction LR
    A1[node A] <--> B1[node B]
  end
  subgraph Split["Network partition"]
    direction LR
    A2[node A] x--x B2[node B]
  end
  Split --> CP["CP choice: the minority side refuses writes, maybe reads, until the partition heals"]
  Split --> AP["AP choice: both sides keep answering, data diverges, reconcile later"]
  Normal ~~~ Split
Figure 2. During a partition, refusing is the consistent choice and answering is the available one.
Consistency (C)
Linearizability: every read returns the most recent write, as if there were one copy. Not the C in ACID.
Availability (A)
Every request to a working node gets a non-error response, even if it is stale.
Partition tolerance (P)
The system keeps operating when messages between nodes are lost. Any real network needs this.
ChoiceDuring a partitionSystems that lean this wayReach for it when
CPNodes that cannot reach a majority stop serving writes (and often reads) so no two histories divergeetcd, ZooKeeper, Consul, HBase, Spanner, MongoDB with majority writesConfiguration, locks, leader election, money, anything where a wrong answer is worse than no answer
APEvery node keeps accepting reads and writes; conflicts are resolved when the partition healsCassandra, DynamoDB (eventually consistent reads), Riak, CouchDB, DNSShopping carts, likes, presence, feeds, anything where a stale answer beats an error

Three misreadings to avoid. A single-node database is not "CA": it is not distributed, so CAP does not apply. Most systems are not one letter or the other but tunable per operation: DynamoDB offers strongly consistent reads at double the cost, Cassandra lets each query choose its consistency level. And "available" in CAP is a yes-or-no property under partition, not the 99.99 percent you put in an SLO.

In the wild
  • ZooKeeper and etcd choose CP: a minority partition becomes read-only or unavailable, which is exactly what you want from the thing holding your cluster's locks.
  • Amazon's shopping cart was the motivating example for Dynamo: it is better to accept an add-to-cart on a partitioned node and merge carts later than to show an error.
  • Google Spanner is CP and still delivers more than five nines of availability, because Google's private network makes partitions very rare; CAP describes what happens during a partition, not how often one happens.

PACELC: the trade-off when nothing is broken

PACELC extends CAP with the question that matters every day, not just during outages: when there is no partition, do you pay latency for consistency, or accept stale reads for speed? A replicated write is either acknowledged after every copy has it (slow, consistent) or after one has it (fast, may lag).

flowchart LR
  P{Partition?} -- yes --> PA[Availability]
  P -- yes --> PC[Consistency]
  P -- no, the normal case --> EL[Low latency: answer from the nearest copy]
  P -- no, the normal case --> EC[Consistency: wait for the copies to agree]
Figure 3. "If Partition, then Availability or Consistency; Else, Latency or Consistency."
SystemPartitionElseWhy
DynamoDB, Cassandra, RiakALDynamo lineage: answer fast from any replica, reconcile later
MongoDB (default)ACReads go to the primary by default; on partition a new primary is elected and old writes may roll back
Spanner, CockroachDB, etcd, ZooKeeperCCConsensus on every write; latency is the price of never being wrong
Yahoo PNUTSCLRare: gives up consistency in normal operation for latency but not under partition
Cosmos DBTunableTunableFive named consistency levels chosen per request
In the wild
  • Cassandra's consistency levels are PACELC as a dial: ONE is EL, QUORUM is closer to EC, and the application picks per query.
  • Spanner's commit latency is measured in tens of milliseconds even inside one region because every commit waits for a Paxos majority; Google considers that a fair price for global serializability.
  • DynamoDB global tables are PA/EL across regions: writes are accepted locally and replicated within about a second, with last-writer-wins on conflict.

Consensus: getting machines to agree

Consensus is how several machines agree on one value, or on one ordered log of values, even when some of them crash or messages are lost. It is the foundation under leader election, distributed locks, configuration stores and replicated state machines. Raft is the algorithm most systems use today; Paxos is its older, harder-to-explain ancestor that solves the same problem.

Raft in plain words

  1. One leader at a time. Time is divided into numbered terms. In each term, at most one node is leader; the others are followers. All writes go through the leader.
  2. Election. A follower that hears nothing from the leader for a randomised timeout (150 to 300 ms is typical) becomes a candidate, increments the term and asks for votes. A majority of votes makes it leader. Random timeouts are what stop two candidates from splitting the vote forever.
  3. Log replication. The leader appends each write to its log and sends it to the followers. Once a majority has written the entry, it is committed: applied to the state machine and acknowledged to the client.
  4. Safety. A node can only win an election if its log is at least as up to date as the majority's, so a committed entry is never lost, and any two logs that agree on an entry agree on everything before it.
sequenceDiagram
  autonumber
  participant C as Client
  participant L as Leader
  participant F1 as Follower 1
  participant F2 as Follower 2
  C->>L: SET x = 5
  L->>L: append entry (term 3, index 41)
  L->>F1: AppendEntries(41)
  L->>F2: AppendEntries(41)
  F1-->>L: ok
  Note over L: 2 of 3 have it: majority, entry 41 is committed
  L->>L: apply to state machine
  L-->>C: ok
  F2-->>L: ok (late, does not matter)
  Note over L,F2: next heartbeat tells followers that 41 is committed
Figure 4. One Raft write. The leader waits for a majority, not for everyone, which is why a slow follower does not slow the cluster.
stateDiagram-v2
  [*] --> Follower
  Follower --> Candidate: election timeout, no heartbeat
  Candidate --> Leader: majority of votes
  Candidate --> Follower: another leader seen, higher term
  Candidate --> Candidate: split vote, new random timeout
  Leader --> Follower: discovers a higher term
Figure 5. Raft node states. Every node starts as a follower and only a majority can make a leader.

Why a majority

Any two majorities of the same set overlap in at least one node. That single fact is what carries knowledge across elections: the new leader's majority must include someone who saw the old leader's committed entries. It also sets the arithmetic: a cluster of 2F + 1 nodes survives F failures, so 3 nodes tolerate 1 and 5 tolerate 2. Even numbers add cost without adding tolerance, which is why clusters are 3 or 5.

Quorums without a leader

Leaderless stores use a related overlap trick without full consensus. With N replicas, a write waits for W acknowledgements and a read asks R replicas; if W + R is greater than N, every read overlaps every write and sees the latest version. Cassandra's QUORUM is W = R = ⌈(N + 1) / 2⌉. This gives read-your-writes but not the ordering guarantees of a consensus log, because two concurrent writes can still race; version vectors or last-writer-wins settle those.

In the wild
  • etcd is Raft, and Kubernetes stores all cluster state in it; every kubectl apply ends as a Raft commit.
  • Kafka replaced ZooKeeper with KRaft, its own Raft implementation, for cluster metadata; ZooKeeper itself runs ZAB, a close cousin.
  • CockroachDB and TiKV run one Raft group per data range, so consensus scales out with the data instead of bottlenecking on one group.
  • Spanner uses Paxos groups per shard; Consul uses Raft for its catalog and locks.
Watch out

Consensus costs at least one round trip to a majority per write, which across regions is tens of milliseconds. Use it for the things that must never disagree (membership, leaders, locks, metadata) and keep bulk data on replication that does not need it. Also: consensus tolerates crashes, not lies; that is a different, far more expensive problem (Byzantine fault tolerance).

Replicated state machine
Every node applies the same log in the same order, so every node ends in the same state. Consensus is about agreeing on the log.
Term
Raft's logical clock. Every message carries the sender's term; a higher term always wins.
Quorum
The minimum number of nodes that must agree for an operation to count. Usually a majority.

Idempotency: safe to do twice

An operation is idempotent when performing it several times has the same effect as performing it once. In a distributed system you never know whether a timed-out request was processed, and brokers deliver at least once, so every write path that matters must be safe to repeat.

sequenceDiagram
  autonumber
  participant C as Client
  participant S as Payment service
  participant D as Store
  C->>S: POST /charge, Idempotency-Key: 7f3a, $40
  S->>D: is 7f3a known? no
  S->>D: charge $40, save (7f3a, result)
  S--xC: response lost in the network
  C->>S: retry: POST /charge, Idempotency-Key: 7f3a, $40
  S->>D: is 7f3a known? yes
  S-->>C: same response as before, nothing charged again
Figure 6. The key turns "charge the card" into "make sure this charge exists", which is safe to repeat.

Ways to get there

suspend fun charge(req: ChargeRequest, key: String): ChargeResult {
    idempotency.get(key)?.let { return it }               // seen before: replay the stored result
    return db.transaction {
        // the unique index on idempotency_keys(key) makes the check-and-insert atomic:
        // a concurrent duplicate fails here and is answered from the store on its retry
        idempotencyKeys.insert(key, status = "in_progress")
        val result = payments.charge(req)
        outbox.insert(PaymentCharged(result.id))            // same transaction as the change
        idempotencyKeys.complete(key, result)
        result
    }
}
In the wild
  • Stripe's Idempotency-Key header is the reference design: keys are kept for 24 hours and a repeat with different parameters is rejected.
  • AWS APIs use client tokens (for example when launching EC2 instances) so a retried call creates one instance, not two.
  • Kafka's idempotent producer assigns each producer an id and sequence numbers, so a retried batch is written once even if the first attempt's acknowledgement was lost.
  • SQS FIFO queues deduplicate on a message id for five minutes; Debezium's outbox connector is a ready-made relay for the outbox pattern.
Watch out

Idempotency keys must be scoped to what they protect: a key per user, per operation. Reusing one key for two different requests either blocks the second or, worse, returns the first's result for it. And "check then act" without atomicity is the classic bug: two retries arriving together both see "not known" and both charge.

Circuit breakers, timeouts and bulkheads

A circuit breaker watches calls to a dependency and, once enough of them fail, stops making calls for a while so that the caller fails fast and the dependency gets room to recover. Timeouts and bulkheads are its two companions: a timeout bounds how long any call may wait, and a bulkhead bounds how many callers can wait on one dependency at once.

stateDiagram-v2
  [*] --> Closed
  Closed --> Open: failure rate over threshold in the sliding window
  Open --> HalfOpen: wait duration elapsed
  HalfOpen --> Closed: trial calls succeed
  HalfOpen --> Open: a trial call fails
  note right of Closed: calls flow, outcomes are counted
  note right of Open: calls fail immediately, fallback runs
  note right of HalfOpen: a few probe calls are let through
Figure 7. The three states. Half-open is what lets the breaker recover without a human resetting it.
In the wild
  • Netflix Hystrix popularised the pattern; it is retired, and Resilience4j is its successor on the JVM, with Polly playing the same role in .NET.
  • Envoy and Istio implement breakers at the mesh layer as outlier detection: a backend that returns too many 5xx responses is ejected for a while, with no application code.
  • AWS SDKs ship with per-client timeouts, retry policies and, in adaptive mode, client-side rate limiting.
Watch out

One breaker per dependency, never one global breaker; otherwise a failing analytics service opens the breaker for the database too. And do not confuse a breaker with a health check: the breaker reacts to your own calls failing, which is the only signal that matters for your callers.

Throttling: rate limiting and load shedding

Throttling caps how many requests a client, a tenant or the whole system may make per unit of time, and rejects the rest with a clear signal. It protects the system from abuse, from accidental floods and from its own popularity, and it makes capacity a number you can reason about.

flowchart LR
  R[refill: 100 tokens per second] --> B[("bucket<br/>capacity 500")]
  Q[request arrives] --> T{token available?}
  B --- T
  T -- yes: take one --> A[allow]
  T -- no --> D["reject: 429, Retry-After"]
Figure 8. Token bucket. The refill rate is the sustained limit; the capacity is how big a burst is allowed.
AlgorithmHow it worksBurstsNotes
Token bucketTokens drip in at a fixed rate up to a capacity; each request takes oneAllowed, up to the capacityThe most common; two numbers, cheap to implement, matches how people think about limits
Leaky bucketRequests queue in a bucket that drains at a fixed rate; overflow is rejectedSmoothed into a steady streamShapes traffic rather than just capping it; adds queueing delay
Fixed windowA counter per minute (or hour) resets at the boundaryUp to double the limit at a window edgeSimplest; the boundary burst is the flaw
Sliding window logStore each request's timestamp, count those within the last windowExactPrecise but memory per request
Sliding window counterWeighted blend of the previous and current fixed windowsApproximate but no edge burstThe usual compromise for distributed limiters

Where and how

In the wild
  • Amazon API Gateway usage plans are token buckets: a steady rate plus a burst per API key.
  • Stripe runs token-bucket request limiters per user plus a separate concurrency limiter, and has written about how the concurrency limiter catches slow-endpoint incidents that a pure rate limit misses.
  • GitHub's API allows 5,000 requests per hour per token and returns the remaining budget in headers on every response.
  • Cloudflare rate limiting works at the edge across all data centers, and Envoy can call a global rate limit service so every proxy shares one budget.

Backoff and jitter: retrying without a stampede

Retrying a failed request is the right instinct and the wrong reflex. If every client retries at the same fixed delay, a brief outage turns into a synchronised wave that hits the recovering service at exactly the same instant. Exponential backoff spreads retries over time; jitter spreads them across clients.

Fixed backoff: every client retries at 1 s, 2 s, 4 s 1 s: all at once 2 s: again 4 s: again Full jitter: each client waits a random time between 0 and the backoff 0 s 8 s same twelve retries, spread out; the service sees a trickle instead of three waves
Figure 9. Backoff alone still synchronises clients that failed together. Jitter is what breaks the pattern.
suspend fun <T> withRetry(maxAttempts: Int = 5, base: Duration = 100.milliseconds, cap: Duration = 20.seconds,
                          retriable: (Throwable) -> Boolean, block: suspend () -> T): T {
    var attempt = 0
    while (true) {
        try {
            return block()
        } catch (e: Throwable) {
            attempt++
            if (attempt >= maxAttempts || !retriable(e)) throw e
            val backoff = minOf(cap, base * (1L shl attempt))          // exponential, capped
            delay(Random.nextLong(0, backoff.inWholeMilliseconds))     // full jitter
        }
    }
}
In the wild
  • AWS SDKs default to three attempts with exponential backoff and jitter, and the newer adaptive mode adds a client-side token bucket that slows a client down when it keeps getting throttled.
  • Google's SRE practice is retry budgets per client (about 10 percent) plus a per-request retry cap of three, precisely to prevent cascades.
  • gRPC retry policies are declared in service config with backoff multipliers and a list of retriable status codes.
  • Kubernetes controllers requeue failed reconciliations with exponential backoff, which is why a broken object does not pin a controller at 100 percent CPU.

Heartbeats, leases and failure detection

A heartbeat is a periodic "I am alive" message, and failure detection is the art of deciding that its absence means death rather than delay. Nothing in a distributed system can tell the two apart with certainty, so every detector is a timeout, and every timeout is a bet.

sequenceDiagram
  autonumber
  participant L as Leader (old)
  participant S as Lock service
  participant N as Leader (new)
  participant D as Storage
  L->>S: renew lease (every 5 s)
  S-->>L: lease valid until t+15, token 33
  Note over L: long GC pause, no renewals
  S->>S: lease expired at t+15
  N->>S: acquire lease
  S-->>N: granted, token 34
  N->>D: write with token 34
  Note over L: pause ends, still believes it is leader
  L->>D: write with token 33
  D--xL: rejected: token 33 is older than 34
Figure 10. A lease with a fencing token. The paused old leader's write is refused, which is the only thing that makes the failover safe.

The pieces

Split brain

Two nodes both believing they are leader is split brain, and it is the failure mode every mechanism above exists to prevent. It happens when a detector declares a live node dead: the node keeps working, a replacement is started, and both write. Prevention is layered: elect through a majority so two leaders cannot both hold a quorum; expire leadership with leases so a partitioned leader steps down on its own; and fence at the storage so a stale leader's writes are refused even if it never notices.

In the wild
  • Kubernetes nodes renew a lease every 10 seconds and are marked NotReady after 40 seconds of silence; pods are evicted only minutes later, a deliberately slow chain to avoid stampedes.
  • ZooKeeper sessions with ephemeral nodes are the original lease: when the session times out, the node's locks and leadership vanish with it.
  • Kafka consumers heartbeat to the group coordinator; miss session.timeout.ms and the group rebalances partitions to the survivors.
  • Amazon RDS Multi-AZ monitors the primary and promotes the standby, repointing DNS, typically within a minute or two; Aurora completes the same failover in about 30 seconds.
Watch out

A distributed lock without a fencing token is a hope, not a lock. The holder can be paused (GC, VM migration, a slow disk) for longer than the lease, resume, and act on a lock it no longer holds. Redlock-style locks over Redis have exactly this gap; using a consensus store with fencing tokens, or making the protected operation idempotent, closes it.

Lease
Ownership with an expiry. Must be renewed; expires on its own if the holder goes silent.
Fencing token
A monotonically increasing number attached to each lease grant, checked by the resource being protected.
Phi accrual
A failure detector that outputs a suspicion level from the history of heartbeat intervals instead of a fixed yes or no.

Recap

  • CAP: partitions happen, so the real choice is what to do during one. CP systems refuse on the minority side; AP systems keep answering and reconcile. Most stores are tunable per operation.
  • PACELC adds the everyday trade: without a partition, pay latency for consistency or accept stale reads for speed. Dynamo-style stores are PA/EL; consensus stores are PC/EC.
  • Raft: numbered terms, one leader, randomised election timeouts, entries committed once a majority has them. Majorities overlap, so committed entries survive elections; 2F + 1 nodes tolerate F failures.
  • Quorums (W + R greater than N) give overlap without a leader but not ordering; consensus is for metadata, locks and leaders, not bulk data.
  • Idempotency: at-least-once delivery and retried timeouts make it mandatory. Use idempotency keys with atomic check-and-insert, conditional writes, consumer dedupe, and the outbox pattern for atomic "write plus publish".
  • Circuit breakers go closed, open, half-open; pair them with timeouts that propagate deadlines and bulkheads that isolate each dependency. Retry only behind a breaker.
  • Throttle with token buckets (rate plus burst) at the edge, share state in Redis when limits must be exact, answer with 429 and Retry-After, and shed low-value load first when the whole system is over capacity.
  • Retries need exponential backoff, full jitter, a retry budget, a list of retriable errors, and one layer that owns them.
  • Failure detection is a timeout and therefore a bet. Leases make leadership expire; majorities prevent two leaders; fencing tokens make a stale leader harmless. Without fencing, a distributed lock is not a lock.

Questions

Try answering each one out loud before opening it. Lead with the one-line answer, then the reasoning, then one detail from a real system.

Explain the CAP theorem in plain words. What do people get wrong about it?

When a network partition splits a distributed system, each side must either refuse to answer to stay consistent or answer with possibly stale data to stay available; you cannot have both during the partition.

  • It is not "pick two of three": partitions are not optional, so the choice is only between C and A while one lasts.
  • C means linearizability, not the C in ACID; A means every working node answers, not a percentage uptime.
  • Most real systems are tunable per operation rather than being purely CP or AP.

ZooKeeper going read-only on the minority side is CP; a Cassandra node accepting writes while cut off is AP.

Is a single PostgreSQL instance CP or AP? What about with replication?

A single instance is neither; CAP applies to distributed systems and one node has no partitions to survive. With synchronous replication and majority-based failover it behaves CP; with asynchronous read replicas, reads become AP-style because replicas answer with stale data during a partition.

  • Synchronous replication refuses to commit when the standby is unreachable, which is the CP behaviour.
  • Asynchronous replicas keep serving whatever they last received.
  • Failover without fencing can produce two leaders, which is neither C nor safe.

The honest answer is per path: the write path with a synchronous standby is CP, the replica read path is AP.

What does PACELC add to CAP, and why does it matter more day to day?

PACELC says that even without a partition you must choose between low latency and consistency, because a consistent replicated write waits for replicas to agree; partitions are rare, so this trade-off is the one you live with every second.

  • Dynamo-style stores are PA/EL: available under partition, fast otherwise.
  • Consensus stores such as Spanner and etcd are PC/EC: consistent in both cases, paying latency.
  • Cassandra's consistency levels expose the dial per query.

Spanner's tens-of-milliseconds commit latency is the E-C cost made visible.

How does Raft elect a leader and replicate a log? Why does it need a majority?

Followers that stop hearing heartbeats become candidates after a random timeout and ask for votes; a majority makes a leader for that term, which then appends client writes to its log, replicates them, and commits an entry once a majority has stored it.

  • Random timeouts prevent split votes from repeating forever.
  • A candidate needs a log at least as up to date as the majority's, so committed entries are never lost.
  • Any two majorities overlap, which carries knowledge of committed entries into the next term.

2F + 1 nodes tolerate F failures: three nodes survive one, five survive two, and even numbers add cost without tolerance.

Quorum reads and writes: what does W + R > N guarantee, and is it consensus?

It guarantees that every read overlaps every acknowledged write on at least one replica, so a read sees the latest acknowledged version; it is not consensus, because concurrent writes can still conflict and there is no agreed ordering of operations.

  • N replicas, W acknowledgements per write, R replicas queried per read.
  • Conflicts are settled by version vectors or last-writer-wins, not by a leader.
  • Consensus gives a single ordered log; quorums give overlap.

Cassandra's QUORUM level is the everyday example; etcd is what you use when you need the ordered log.

What is idempotency, and how would you make a payment API idempotent?

An idempotent operation has the same effect whether performed once or many times; for payments, the client sends a unique idempotency key with each attempt and the server atomically records the key with the result and replays that result on any repeat.

  • The check-and-record must be atomic, through a unique constraint or conditional insert, or two simultaneous retries both charge.
  • Keys expire after a day; a repeat with different parameters is an error.
  • Conditional writes and consumer-side dedupe cover the paths that are not HTTP requests.

Stripe's Idempotency-Key header is the design most teams copy.

What problem does the outbox pattern solve?

It lets a service update its database and publish an event atomically without a distributed transaction: the event is written to an outbox table in the same local transaction, and a relay publishes it to the broker afterwards.

  • Dual writes (database then broker) fail halfway and leave the two out of sync.
  • The relay publishes at least once, so consumers deduplicate on the event id.
  • Change data capture tools can act as the relay by reading the outbox table's changes.

Together with idempotent consumers this yields effectively-once processing across services.

Describe a circuit breaker's states and how you would configure one.

Closed lets calls through and counts failures; open fails fast for a set time once the failure rate crosses a threshold; half-open lets a few trial calls through and closes on success or reopens on failure.

  • Configure a sliding window, a failure threshold, a minimum call count, an open duration, and a slow-call threshold.
  • Provide a fallback for the open state: cached, default or degraded response.
  • One breaker per dependency, never a global one.

Resilience4j on the JVM and Envoy's outlier detection at the mesh layer are the two common implementations.

Why does every network call need a timeout, and how do you choose one?

Without a timeout a hung dependency holds the caller's thread or connection forever and exhausts its pool; choose timeouts from the dependency's latency distribution (around its p99 plus margin) and make the deadline travel with the request so downstream calls never outlive their caller.

  • Separate connect and read timeouts; connect should be short.
  • Propagated deadlines (gRPC, Go context) prevent wasted work after the client has given up.
  • Combine with bulkheads so a slow dependency exhausts only its own pool.

A timeout that is longer than the caller's own timeout is the same as none.

What is the bulkhead pattern?

Isolating resources per dependency, usually a bounded thread pool or semaphore each, so that one slow or failing dependency can consume only its own share and cannot take the whole service down.

  • Named after ship compartments that contain flooding.
  • Pairs with timeouts and circuit breakers: the bulkhead limits blast radius while the breaker stops the bleeding.
  • Also applies at the fleet level: separate pools of servers for separate traffic classes.

A payment provider hanging should exhaust its ten permits, not the two hundred threads serving the catalogue.

Token bucket or leaky bucket, and where would you enforce rate limits?

Token bucket allows bursts up to a capacity while enforcing a sustained rate, and is the usual choice for API limits; leaky bucket smooths traffic into a constant stream at the cost of queueing delay; enforce limits at the edge so rejected requests never reach the services.

  • Fixed windows allow double bursts at boundaries; sliding window counters fix that cheaply.
  • Exact distributed limits live in Redis with an atomic script; approximate local limits are fine for protection.
  • Return 429 with Retry-After and remaining-quota headers.

API Gateway usage plans, Stripe's per-user limiters and GitHub's 5,000 per hour are all token buckets.

Why is exponential backoff not enough, and what else do retries need?

Backoff spreads one client's retries over time but clients that failed together still retry together, so you add jitter to spread them across clients, a retry budget to cap total retries, and rules about which errors and which operations may be retried at all.

  • Full jitter: wait a random time between zero and the backoff; AWS found it recovers fastest with the fewest calls.
  • Budget: retries at most 10 percent of traffic, then fail fast.
  • Retry timeouts and 503s, never 400s; retry only idempotent operations or ones carrying an idempotency key; retry at one layer only.

Three layers each retrying three times turns one failure into 27 requests.

How does failure detection work, and why is it always a guess?

Nodes send heartbeats and a detector declares a node failed after a silence longer than a timeout; it is a guess because a slow network, a paused process and a dead machine all look identical from outside.

  • Short timeouts detect fast but produce false positives on GC pauses; long ones are safe but slow.
  • Adaptive detectors such as phi accrual score suspicion from the heartbeat history.
  • Gossip spreads membership without a central monitor.

Kubernetes waits 40 seconds before marking a node NotReady and minutes more before evicting pods, precisely because detection is uncertain.

What is split brain, and how do you prevent it?

Split brain is two nodes both acting as leader after a false failure detection or partition; prevent it by electing through a majority, expiring leadership with leases, and fencing writes with monotonically increasing tokens so a stale leader's writes are refused.

  • Majorities cannot both be held, so two leaders cannot both hold a quorum.
  • A lease makes a partitioned leader step down on its own when it cannot renew.
  • Fencing protects the resource even if the old leader never notices it lost.

The classic failure is a leader paused by garbage collection that resumes after a replacement was elected and keeps writing.

How would you build a distributed lock, and what are the pitfalls?

Grant the lock as a lease from a consensus store such as etcd or ZooKeeper, require the holder to renew it, and attach a fencing token that the protected resource checks; the pitfall is trusting a lock whose holder may be paused past its expiry.

  • A lock without expiry deadlocks when the holder dies.
  • A lock with expiry but no fencing lets a paused holder act after losing it.
  • Locks over a single Redis node, or Redlock over several, still lack fencing; make the protected operation idempotent if you must use them.

ZooKeeper's ephemeral sequential nodes give a lease and a monotonic token in one primitive, which is why it became the standard lock service.

A dependency starts returning errors during a traffic spike. Walk through how a well-built service behaves.

Timeouts bound each call, the bulkhead limits how many callers wait, retries with jittered backoff stay inside a budget, the circuit breaker opens and serves a fallback, and rate limiting or load shedding at the edge protects the rest of the system while the dependency recovers.

  • Fail fast rather than queue: a fast error beats a slow one.
  • Degrade: serve the page without the failing feature.
  • Recover gently: the half-open state probes before restoring full traffic.

Every one of these is a mechanism from this article, and their absence is how a single slow service becomes a full outage.