Skip to content
SeeItFlow
System Design · 11 chapters · 36 incidents · ~89 min read

Distributed Key-Value Store Failures: Quorum Loss, Recovery & Hot Keys

Thirty-six hypothetical failure scenarios for a fictional durable key-value store, from torn log entries and full disks to stale leaders, tombstone resurrection, hot keys and region loss. Each is walked through as trigger, timeline, symptom, root cause, detection, containment, recovery, prevention, remaining guarantee and tradeoff, with five worked timelines.

Mangesh MandlikLast updated: How lessons are made
Overview

The Failure Map: Where the Promises Break

A key-value store fails in the places where it makes promises: when it says a write is durable, when it says one owner holds a key, when it says two of three replicas agree, and when it says a deleted value is gone. Thirty-six incidents, each walked from trigger through recovery, show what the reference design does at each of those promises and what it cannot do.

Every incident here is a hypothetical scenario for the fictional Meridian Profiles platform (tenants Atlas, Birch and Cedar; replica group members A, B and C; regions EU and US). None describes a real outage at a real company, and every number is a teaching assumption or derived arithmetic, not a measurement. The design being stressed is the one in Complete Design: partitioned, leader-based replication with a replicated commit log per partition, majority commit, single-key linearizable access in the home region, and asynchronous remote replicas. A leaderless alternative appears twice, labeled as such, because its failures are instructive and different.

Each incident carries the same ten elements, in a table at its end: trigger, timeline, visible symptom, root cause, detection, containment, recovery, prevention, what still holds, and the cost of the fix. The prose before the table does the real explaining and is arranged around whatever makes that failure interesting, whether that is arithmetic, an ordering of events or an argument about who is allowed to decide. Where the answer is not what you would guess (a 412 that means *you* won; a repair that restores deleted data; a backup that cannot be decrypted), the prose says so up front.

What the system is doing while it fails: five postures

The requirements for the reference design say that every failure should end in one of a small set of postures, never in false success. Available means requests succeed with the stated guarantee. Stale means requests succeed but a labeled read mode returns older data. Read-only or rejecting means the store refuses writes (503 UNAVAILABLE, 429 when throttled, WRONG_EPOCH for stale routing) because it cannot commit them safely. Unavailable means it cannot serve the operation at all, as when no majority exists. Unsafe appears only when an invariant was missing, and the incident says which one. A timeout is not a posture; it is an unknown outcome.

Vocabulary the incidents lean on

The incidents use a handful of terms freely, so here they are once, in the sense this page uses them. A write-ahead log (WAL) is an append-only file where a change is recorded before it is applied; after a crash the store replays it. `fsync` is the operating-system call that forces buffered file bytes onto the storage device, and a record counts as durable on one node only after it returns. A memtable is the in-memory sorted buffer of recent writes; it is flushed into an immutable sorted file called an SSTable, and compaction merges those files in the background. A partition is a slice of the keyspace; its replicas (here A, B and C) form a group with one leader that orders writes and followers that copy them, and a learner is a replica still catching up that does not yet vote. A quorum is a majority of the group (2 of 3), and an entry is committed once a quorum holds it durably.

Leadership is numbered: a term increases on every election, and messages from an older term are refused. Ownership of a partition is numbered separately by the map epoch. The replication protocol described is Raft-style, meaning the leader-and-majority design published as the Raft consensus algorithm, though the incidents do not depend on any particular implementation. Linearizable means every operation appears to take effect once, at a single instant between its start and its reply, so a read after an acknowledged write never sees older data. An idempotency key is a client-chosen identifier that lets the store recognize a retry and return the first outcome instead of applying twice. A tombstone is a versioned delete marker that stays until every replica has seen it. RPO (recovery point objective) is how much recent data a failover may lose, and RTO (recovery time objective) is how long recovery takes. Leaderless terms (siblings, read repair, hinted handoff, last-write-wins) are defined where the two alternative-design incidents use them.

Swipe to pan
Failure decision matrix for RF=3Failure decision matrix (RF=3, majority = 2)Which operations stay safe? Stale follower reads are labeled and may return old dataScenarioQuorum?Linearizable readWriteStale readAll 3 healthyyes (3/3)safesafestale ok1 follower downyes (2/3)safesafestale okLeader down, before electionyes, no leaderunavailableunavailablestale onlyNew leader elected (2 up)yes (2/3)safesafestale ok2 of 3 replicas downNO (1/3)unavailablerejectedstale onlyLeader isolated aloneNO on its sidemust step downrejectedstale onlyRF=3 survives ONE failure for writes, not any two.RF=5 (majority 3) survives two, at higher write cost. Stale reads trade freshness for availability.
For a partition's replicas: with a quorum available, reads and writes are safe; without it, the reference design rejects writes and linearizable reads instead of guessing.

Failure map: all 36 incidents

IDIncident (hypothetical)AreaPosture of the reference designInvariants
F01Process crash before the WAL appendHost & storageRejecting (retryable); no false successI1, I6
F02Crash after the append but before the flushHost & storageSafe if acks wait for flush; lossy only under an unsafe configI1
F03Crash after commit, before the responseHost & storageAvailable; outcome unknown to the clientI1, I6, I11
F04Torn or corrupt WAL entryHost & storageAvailable after truncation to the last valid recordI1
F05Disk full on the leaderHost & storageRejecting writes (503); reads continueI1, I12
F06Disk full on a followerHost & storageAvailable with reduced fault tolerance (2 of 3)I12
F07SSTable checksum corruptionHost & storageAvailable if another replica is healthy; the corrupt copy errors, never liesI1
F08Compaction backlog and the P99 spikeHost & storageAvailable but slow; at the extreme, writes are throttledI8
F09Snapshot corruption and repair from another replicaHost & storageAvailable (two healthy replicas); the rebuilding replica is excludedI1, I8
F10A hot key saturates one partition leaderPartitioningAvailable but overloaded for the leader's partition; neighbors payI8, I12
F11A hot tenant saturates its neighborsMulti-tenancyAvailable; fairness violated until per-tenant limits engageI7, I8
F12New node added with unsafe modulo remappingPartitioningUnavailable/incorrect for moved keys until revertedI2
F13Rebalance interrupted halfway throughPartitioningAvailable if the move is a resumable state machine; unsafe if the fence is open-endedI2, I8
F14Stale partition-map epoch sends writes to the old ownerPartitioningRejecting with a redirect; correct after one extra round tripI2
F15Split or migration race loses an update when fencing is absentPartitioningUnsafe without fencing; safe with fence-then-flipI2, I3
F16A key replicated into the wrong namespacePartitioningAvailable; confidentiality and integrity broken for the affected keysI7
F17Leader crashes before quorum commitReplicationUnavailable for a brief election; committed data intactI3
F18Leader crashes after commit, new leader must honor itReplicationAvailable after election; reads wait for the new leader's first commitI3
F19A minority network partition attempts to accept writesReplicationMinority side rejects writes (503); majority side availableI4
F20A stale leader fails to be fencedReplicationSafe for writes via terms; reads depend on the lease or read-index ruleI3, I4
F21A follower serves a stale read during lagReplicationAvailable; reads stale (labeled) for follower-stale modeI3
F22Concurrent updates and the lost-update trapReplicationAvailable; correct only if the compare-and-set is part of the replicated writeI11
F23Leaderless divergent siblings and conflict resolutionAlternative designAvailable (AP-style); concurrent writes diverge until reconciledI11
F24The quorum-overlap claim fails under sloppy replacementsAlternative designAvailable (AP-style); read-after-write not guaranteed despite R+W>NI11
F25A delayed tombstone resurrects an older valueData lifecycleAvailable; deleted data returns until reconciledI5, I10
F26Clock skew alters last-write-wins and TTL resultsData lifecycleAvailable; wrong winners and early or late expiryI5, I11
F27Failed restore reintroduces deleted secretsOps & securityAvailable; semantic safety broken until deletes are replayedI5, I10
F28Replica repair overwhelmed by foreground trafficOps & securityAvailable with degraded latency; repair slower than plannedI8
F29Control-plane or map-service outage: warm versus cold clientsOps & securityAvailable for warm clients; cold starts fail; no rebalancingI2, I12
F30Cross-region replication lag after an abrupt disasterGeoEU unavailable; US available and stale (v10) if promotedI9
F31Incorrect regional failover creates split-brainGeoUnsafe: two regions accept writes for the same keysI9
F32Region-residency policy blocks an otherwise convenient failoverGeoUnavailable for EU-resident tenants; available for othersI9, I7
F33A multi-tenant scan bypasses authorization via a caller-supplied tenant IDOps & securityAvailable; confidentiality broken for the affected endpointI7
F34Key-size abuse or oversized writes exhaust memory and diskOps & securityRejecting oversize requests; available otherwiseI12
F35Backup encryption key inaccessible during restoreOps & securityAvailable while healthy; no recovery path until the key is regainedI1, I12
F36Regional write-read mismatch and a read-your-writes violationGeoAvailable; remote reads stale (eventual) unless routed homeI9, I3
All 36 incidents by area. Invariants I1–I12 are the module's numbered design invariants, listed in the callout below.
F01, F02, F03, F04, F17, F18

Crash and Durability Failures

A write that reaches the leader can die in at least five different places before the client hears anything, and each place leaves a different truth on disk. Six incidents walk those places: the process dying before it logs, the log that was written but never flushed, the commit nobody was told about, the half-written record, and the two leader crashes that straddle quorum.

Two ideas keep these incidents apart. Durable means the record survives the failure being discussed, which for a single node is a completed fsync and for a replicated partition is a majority of replicas holding it durably. Acknowledged means the client was told so. A correct store only ever acknowledges after durable; it can never promise the reverse, because the reply can be lost after the commit. That asymmetry is why a timeout is always an *unknown outcome*, and why most of the fixes below are some form of retry with an idempotency key.

F01 — Process crash before the WAL append

Meridian's leader for partition P7 receives Put(atlas/user42/settings) for evt-024. The request has been parsed and validated, and the process is killed by the kernel's out-of-memory handler before it calls the append routine. Nothing was written, nothing was applied, nothing was acknowledged.

From the client's side this looks identical to far worse outcomes. It sent a request and the connection reset or timed out; it has no way to tell whether the server died before the log append, after it, or after the commit. The safe contract is the same in all three cases: treat it as unknown, reread or retry with the *same* idempotency key. Here the retry finds no record of the key and applies the write once, which is exactly right. The store's job is to make sure this cheap case and the expensive ones (F03, F18) are handled by one client rule rather than three.

FacetF01 at a glance
Reference system during the incidentAvailable after restart or failover; nothing to repair, nothing lost, because nothing was promised.
TriggerThe server process dies between request validation and the first WAL append.
Timelinet0 request arrives; t0+2 ms process killed; t0+1 s supervisor restarts it, or the group elects a new leader within an election timeout; client retries.
Visible symptomConnection reset or timeout on one request; a short burst of 503/UNAVAILABLE from the partition while it has no leader.
Root causeA process boundary sits between 'request received' and 'record durable'; the client was never told the write happened.
DetectionProcess-exit and restart counters, OOM-kill events, leader-election count per partition, client timeout rate by partition.
ContainmentNothing to contain beyond keeping retries bounded with jitter so recovery is not met with a storm.
RecoveryClient retries with the same idempotency key; the new leader sees no record and applies it as new.
PreventionMemory limits and admission control so the process is not killed under load; idempotency keys on every mutating call.
What still holdsNo acknowledged write is lost (I1) and no write applies twice (I6).
Cost of the fixIdempotency keys must be stored and retained for a documented window, and clients carry a retry loop.
F01: the ten elements, in order, plus the posture of the reference design.

F02 — Crash after the append but before the flush

This one depends entirely on a single line of the write path. The leader has appended the record to the log file, so the bytes sit in the operating system's page cache, and the machine loses power before the kernel writes them to the device. After restart the file ends one record earlier than the process believed. If the store acknowledged *after* fsync returned, nothing was promised for this record and the client retry (F01 logic) repairs it. If someone had configured the log for speed, acknowledging after write() and flushing on a timer, then evt-024 was acknowledged and has vanished.

Group commit is why the safe configuration is affordable: records arriving in a 1 ms window share one flush, so a node applying 6,250 writes/s flushes about 6.25 records at a time (assumed figures). The other trap is below the filesystem. A drive or controller with a volatile write cache that acknowledges flushes it has not performed can lose a record the store believed was durable; the guarantee is only as honest as the storage stack, so the operations answer is a hardware and filesystem checklist plus a power-pull test, not a hopeful reading of the config.

FacetF02 at a glance
Reference system during the incidentAvailable once restarted. Under the safe setting no acknowledged write is lost; under an async-flush setting the system is knowingly lossy for the last flush interval.
TriggerPower loss or kernel panic after write() has returned but before the log was flushed to stable storage.
Timelinet0 append; t0+0.4 ms record sits in the page cache; t0+0.6 ms power fails; t0+90 s node restarts and replays a log that ends before the record.
Visible symptomUnacked write: client timeout and a clean retry. Acked-then-lost (unsafe config only): the record is absent and a later Get returns v7 after the client saw success for v8.
Root causeDurability was not established before the acknowledgement; page cache is volatile, and volatile device caches can break an honest flush.
DetectionA restart-time gap between the last acknowledged sequence number and the last replayed one; fault-injection tests that cut power mid-load; a config audit for the flush mode.
ContainmentIf acked-then-lost writes are possible, freeze writes on affected nodes and compare acknowledged request IDs from client logs with replayed sequence numbers.
RecoveryIn a replicated partition, other replicas that flushed the entry still hold it, and the leader catches the restarted node up from the log; on a single copy the record is gone and clients must replay from their own records.
PreventionFlush before acknowledging, group commit for throughput, hardware that honours flush, and a periodic power-pull test.
What still holdsWith flush-before-ack on a majority, an acknowledged write survives any single node's power loss.
Cost of the fixAcknowledgement latency includes a flush; group commit trades a millisecond of waiting for a flush that is shared.
F02: the ten elements, in order, plus the posture of the reference design.

F03 — Crash after commit, before the response

Leader A appends evt-024, B acknowledges, the entry commits at v8, and A dies before its reply leaves the socket. The write is durable and applied. The client sees a timeout. Everything now depends on what the client does next, and there are three branches that differ sharply.

Swipe to pan
Ambiguous timeout and idempotent retryA timeout means unknown, so retries need an idempotency keyPut evt-024 with idempotencyKey = idem-9f; the response is lost on the way backClientLeaderDedup record1 Put idem-9f2 commit v8, record idem-9f → v83 200 OK v8✕timeout: success or failure? unknown4 retry Put idem-9f (same payload)found idem-9f → v8, do not re-apply5 200 OK, v8 (replayed result)What the retry achievesThe write applied once, not twice.The caller gets the original result,including the committed version.Records expire after a retentionwindow; say how long.Key reuse with new dataSame key, different payload is aclient bug. Answer 409 Conflict;never replay the old result for adifferent request.Alternative: reconcileConditional writes can read back:Get shows v8 with my value, so mywrite applied. Without a key or acondition, blind retries may duplicate.
A timeout hides whether the write committed; the same idempotency key turns a retry into a lookup instead of a second apply.

If it retries a plain Put carrying the same idempotency key, the new leader looks the key up in the replicated dedup table, finds the committed result and returns v8 again; nothing is applied twice. If it retries without a key, an overwrite Put of the same value is harmless but an increment or an append would apply twice. If it retries a CAS with expectedVersion=7, the answer is 412, because the record is already at v8. That 412 is the cruel one: it looks like a lost race, but the winner was the client itself. A client that gets 412 after an unknown outcome must read the record and compare it with what it tried to write before concluding anything.

FacetF03 at a glance
Reference system during the incidentAvailable once a leader exists; the committed write is intact and may be returned from the dedup table.
TriggerThe leader crashes or loses its connection after the commit but before the client receives 200 with committedVersion.
Timelinet0 commit at majority; t0+1 ms A dies; t0+500 ms client times out; t0+800 ms retry hits the new leader.
Visible symptomA timeout, then either a repeated 200 with v8 (idempotency key), a double-applied side effect (no key), or a 412 on a CAS retry.
Root causeCommit and response are separate events; the network cannot tell the client which of them occurred.
DetectionTimeout-then-success rates by endpoint, repeat idempotency-key hits, 412s whose current value equals the attempted value.
ContainmentNone needed in the store; if clients are retrying hard, apply load shedding so the retry wave is bounded.
RecoverySame-key retry; for CAS, reread and compare to the intended value before treating 412 as a lost race.
PreventionRequire idempotency keys on non-idempotent mutations, store them in the replicated state machine, and document the retention window.
What still holdsThe write applies at most once for any key still inside its retention window.
Cost of the fixDedup state costs memory and replication traffic; after the window expires the guarantee lapses, so the window must outlast the client retry horizon.
F03: the ten elements, in order, plus the posture of the reference design.

F04 — Torn or corrupt WAL entry

A power cut lands in the middle of a 4 KiB append and the log ends with half a record. On restart, replay reads records one at a time, each carrying a length, a sequence number and a checksum. The last record's checksum fails, and the engine stops there, truncates the file to the previous good record and carries on. This is a tail tear, and it is safe: a record is acknowledged only after it was flushed whole, so a half-written one was never promised to anyone.

The same checksum failure in the *middle* of the log is a different animal. Records after it may be perfectly valid, but you cannot tell whether the corrupted one was acknowledged, and skipping it silently would apply later records on top of a gap. The rule is stop, do not skip: mark the replica as damaged, take it out of the voting set until it is rebuilt, and repair it from a healthy peer. In a replicated group that is routine (F09). On a single copy, the honest answer is that the engine cannot continue without an operator's decision.

FacetF04 at a glance
Reference system during the incidentAvailable if two intact replicas remain; a single damaged replica is excluded and rebuilt.
TriggerPower loss during an append, or media corruption of an earlier log segment.
Timelinet0 append starts; t0+0.2 ms power fails; restart replays 1,204,311 records and finds record 1,204,312 with a bad checksum at the tail; truncate and continue.
Visible symptomTail tear: a replay log line and nothing user-visible. Mid-log corruption: the replica refuses to start and drops out of the quorum.
Root causeAppends are not atomic at the device level; checksums detect the break, but only sequence numbers reveal gaps.
DetectionReplay checksum-failure counters split by tail versus middle, replicas excluded from voting, segment scrub results.
ContainmentDo not hand-edit the log. Quarantine the damaged replica and check the group still has a majority.
RecoveryTail tear: automatic truncation. Mid-log: wipe the replica's state and rebuild from a snapshot plus the leader's log.
PreventionChecksums and sequence numbers per record, periodic log scrubbing, storage with end-to-end integrity where available.
What still holdsCommitted entries remain on the other replicas; a corrupt copy never gets to vote with bad data.
Cost of the fixChecksums cost a little CPU per record and scrubbing costs background I/O.
F04: the ten elements, in order, plus the posture of the reference design.

F17 — Leader crashes before quorum commit

Leader A appends log entry 42 for evt-024 at term 4 and sends it to B and C. A crashes before it hears back from either. What happened to the entry depends on how far it travelled. If B received it, B holds entry 42 and C may not. Nobody counted a majority, so the entry is uncommitted: it may survive or vanish, and either outcome is allowed because the client was never told.

The election decides which. A new leader must have a log at least as up to date as a majority of voters. If B, which holds entry 42, wins, it will carry the entry forward and commit it once it commits something in its own term (a no-op entry is the usual trigger), so a write the client saw time out may appear moments later. If C, which lacks it, wins, entry 42 is overwritten when B syncs. A client that polls after a timeout can see the write as absent and then present; that is legal, and it is the reason the reference design calls the post-timeout state unknown rather than failed.

FacetF17 at a glance
Reference system during the incidentWrites pause until a new leader is elected, then resume; the minority-side copy of an uncommitted entry may be applied or discarded.
TriggerThe leader dies after replicating to fewer than a majority of replicas.
Timelinet0 A appends 42 at term 4; t0+3 ms sends; t0+5 ms A dies; election timeout (the Raft paper suggests values like 150–300 ms) elapses; B or C wins term 5; entry 42 commits or is discarded.
Visible symptomA timeout for the client, a 503 burst for the partition, and sometimes a successful Get shortly after the timeout.
Root causeA log entry is not committed until a majority holds it; a leader's local append proves nothing.
DetectionElection count and duration, term bumps, the gap between last-log-index and commit-index at failover.
ContainmentNone; keep clients from retrying without idempotency keys while the election runs.
RecoveryClients retry with the same key; the new leader dedups if entry 42 was kept.
PreventionRandomized election timeouts to avoid split votes; idempotency keys.
What still holdsAnything acknowledged was committed and is never lost; anything unacknowledged may or may not be present.
Cost of the fixShort unavailability on every leader loss, plus the client burden of treating timeouts as unknown.
F17: the ten elements, in order, plus the posture of the reference design.

F18 — Leader crashes after commit, new leader must honor it

Here the commit was real: A and B hold entry 42, and A dies before replying. C, still behind at entry 41, cannot become leader, because B would deny it a vote for having a shorter log. Only B (or A on return) can win, and whoever wins holds every committed entry. That rule, that an elected leader contains everything already committed, is the heart of why majority commit survives leader changes.

The sharper edge is the *read* that follows. Once B is leader, it may not know the true commit index until it has committed something in its own term. A linearizable read that B answered from its local state too early could omit the entry that A had already acknowledged to some other client. So the new leader does two things first: commits a no-op in its term, then serves linearizable reads only after confirming it still holds leadership (a read-index round to a majority, or a valid lease). During those few milliseconds, linearizable reads wait or get 503; follower-stale reads, if the caller chose them, stay available and are marked as stale.

FacetF18 at a glance
Reference system during the incidentLinearizable reads and writes pause briefly during election; the acknowledged write is preserved.
TriggerA leader fails after a majority held the entry but before the client was answered, then B takes over.
Timelinet0 commit at index 42; t0+2 ms A dies; t0+250 ms B wins term 5; t0+256 ms B commits a no-op at 43; reads resume at index ≥ 42.
Visible symptomSome clients see a timeout then 200 on retry; reads against the group briefly return 503 or wait.
Root causeThe old leader's commit knowledge was in its memory; the new one must rebuild it safely.
DetectionTime from leader loss to first successful linearizable read; reads rejected with 'not ready'.
ContainmentDo not widen read modes to ‘serve something’ unless the API contract labels the answer stale.
RecoveryResume normal service; the no-op commit marks recovery complete.
PreventionVote rules that favor up-to-date logs; no-op on election; read-index or bounded leases.
What still holdsEvery acknowledged write is visible to every later linearizable read.
Cost of the fixA small, bounded pause on every failover.
F18: the ten elements, in order, plus the posture of the reference design.
F05, F06, F07, F08

Disk and Storage-Engine Failures

Disks fail in boring ways: they fill up, they flip bits, and they fall behind on housekeeping. The incidents here are the quiet ones, where nothing crashes and the service degrades because the storage engine ran out of room, trust or time.

A log-structured engine makes these failures interact. Compaction needs free space to make free space; a slow follower forces the leader to keep log it would otherwise discard; and a checksum mismatch is only useful if the store refuses to return the corrupt bytes. The planning numbers in this module keep volumes at or below 60% full (an assumed target) precisely because the engine's own housekeeping needs the headroom.

F05 — Disk full on the leader

Atlas runs a bulk import, and the volume holding partition P7's leader reaches 100%. The next WAL append fails with an out-of-space error. The store has one honest option: refuse the write. Acknowledging into memory and hoping would be a silent durability failure, so the leader returns 503 and, depending on policy, steps down so that a follower with free space can lead.

The trap is that the remedies need space too. Compaction rewrites SSTables into new files before deleting the old ones, so it cannot run on a full disk; a delete writes a tombstone, which is more data; and snapshots, used to catch up followers, need scratch space. A volume that is 100% full is therefore close to wedged. The practical defense is thresholds well before the wall: alert at an assumed 70%, throttle ingest at an assumed 80%, and keep a reserved emergency file or reserved blocks that only the recovery path may use. Hand-deleting WAL segments to make room is the one move that converts a capacity incident into data loss.

FacetF05 at a glance
Reference system during the incidentReads served; writes for the affected partitions rejected; no false success.
TriggerDisk usage on the leader's volume reaches the hard limit during an import.
Timelinet0 usage 78%; t0+40 min 94%; t0+55 min 100%; appends fail; leadership moves or writes return 503 until space is freed.
Visible symptom503/UNAVAILABLE for writes to the partitions on that volume; compaction stalls; latency of reads rises as files pile up.
Root causeIngest outpaced both compaction and capacity planning; the engine has no space for its own cleanup.
DetectionDisk-free percentage per volume and its growth slope, compaction debt, append error counts.
ContainmentThrottle the importing tenant (429), shift leadership to a replica with room, free the reserved headroom for compaction.
RecoveryMove some partition replicas to other nodes, add capacity, then resume writes and let compaction catch up.
PreventionPlan for volumes to sit at or below the assumed 60% target, alert at 70% used and throttle ingest at 80%, add per-tenant ingest quotas, headroom reserved for compaction and snapshots.
What still holdsNo write is acknowledged without durable space; reads stay correct.
Cost of the fixIdle headroom: the 60% target means paying for disk the steady state does not use.
F05: the ten elements, in order, plus the posture of the reference design.

F06 — Disk full on a follower

Follower C's volume fills, perhaps because it also hosts a different partition's large replica. C can no longer append, so it falls behind and eventually stops acknowledging. A and B, a majority of three, keep committing; clients notice nothing. That is the dangerous part. The partition is now one failure from losing write availability, and the dashboards that show only client error rates stay green.

A secondary effect makes it worse. The leader keeps the log tail that C has not confirmed so that C can catch up from it; with C stalled, the leader's log retention grows, and now the leader's disk is under pressure for a failure that is not its own. Policy has to pick a point where the leader stops holding the log for C and instead plans a snapshot-based rebuild once C has space, accepting that C will be restored from a snapshot rather than a log tail.

FacetF06 at a glance
Reference system during the incidentAvailable and correct, but only a single further replica failure from rejecting writes.
TriggerThe follower's volume fills while the leader's does not.
Timelinet0 C at 100%; t0+2 min C stops acking; A and B continue; t0+30 min leader's retained log has grown by the gap; under-replicated alert fires.
Visible symptomNo client errors; under-replicated-partition count rises; leader log retention grows; commit latency may creep if C was the faster follower.
Root causePlacement put a growing partition next to a full volume; majority commit hid a dead third replica.
DetectionUnder-replicated partitions, follower lag in entries and seconds, disk-free on followers.
ContainmentFree space on C or evict other replicas from it; cap log retention for the lagging follower.
RecoveryRestore C via snapshot when it has space; verify it rejoined the voting set.
PreventionAlert on under-replicated partitions as a page, not a ticket; balance placement by disk headroom, not just count.
What still holdsCommitted data is held by two replicas; the partition tolerates no more failures while C is out.
Cost of the fixAlerting on invisible degradation produces pages for incidents that clients never felt.
F06: the ten elements, in order, plus the posture of the reference design.

F07 — SSTable checksum corruption

A read for birch/user17/settings reaches a data block in an SSTable whose stored checksum no longer matches its content, a bit flip on the medium that happened weeks ago. Without checksums the engine would hand back whatever the bytes decode to, possibly a plausible but wrong value. With them, the read fails for that key on that replica, the error is logged and the request is retried on another replica.

The deeper risk is compaction. If the engine merges the corrupt table into a new one without verifying, it produces a fresh file with a valid checksum over corrupt data, laundering the damage. So reads and compaction both verify block checksums, and a background scrub reads cold files before they are needed. When a bad block is found, the replica marks the table suspect, and the repair path fetches the affected key range from a replica at the same commit point, rewrites it, and records the repair. If the corrupt copy is the leader's, linearizable reads for those keys return 503 or move with leadership until repaired.

FacetF07 at a glance
Reference system during the incidentAvailable through replicas; the damaged replica returns errors for the affected range rather than wrong values.
TriggerSilent media corruption of an SSTable block, discovered by a read, compaction or a scrub.
Timelinet0 read touches block 91 of table 000214; checksum mismatch; the replica is marked suspect; the read is retried on B; t0+20 min repair streams the range and rewrites the table.
Visible symptomA handful of retried reads and a corruption counter; if all copies were damaged, errors for that key range.
Root causeStorage returned bytes that differ from what was written, and only an end-to-end checksum can notice.
DetectionBlock checksum failure counters, scrub results per volume, repairs per day.
ContainmentTake the table out of read service; do not compact it until repaired.
RecoveryStream the key range from a healthy replica at the same commit point and rebuild the table; replace the device if failures cluster.
PreventionVerify checksums on read and compaction, scrub cold data, spread replicas across failure domains so one bad batch of disks cannot hit all three.
What still holdsCorrupt data is never returned as valid; repair needs at least one healthy replica.
Cost of the fixScrubbing and verification cost I/O and CPU.
F07: the ten elements, in order, plus the posture of the reference design.

F08 — Compaction backlog and the P99 spike

Compaction is the engine's debt payment. Writes land in memtables, flush into small immutable SSTables, and compaction merges them in the background to keep lookups cheap. When the write rate stays above what compaction can absorb, the tables pile up. Reads now probe more files, so tail latency climbs first, while the average hides it. Past a configured threshold the engine slows or stalls writes to let compaction catch up, which is when a P99 incident becomes a write-rejection incident.

Swipe to pan
Compaction amplification, disk headroom and P99 latencyCompaction: one logical write, many physical writesWrite amplification of 10× is an assumed illustration; measure the real factor for your workloadClient write1 KiB putWAL appendfsync, 1 copyMemtable flushnew SSTableCompactionrewrites it againRewritten …at each levelPeak arithmetic (assumed inputs)204.8 MB/s client × RF 3 × 10 = 6,144 MB/s ≈ 6.1 GB/s of cluster disk writes6,144 MB/s ÷ 96 nodes = 64 MB/s of sustained disk writes per nodeAverage load: 40.96 × 3 × 10 = 1,228.8 MB/s cluster-wide = 12.8 MB/s per nodeVolume fill: compaction needs temporary roomdata + indexes (target ≤ 60% full)temp spacedanger zone60% lineHow falling behind couples to P99 latencyCompaction debt growsMore SSTables per keyReads probe more filesDisk and I/O saturateP99 rises, writes stallControlsCap compaction I/O, keep disk below 60%,alert on debt before latency, add nodes.Trade-offCompacting harder cuts read cost but stealsI/O from foreground traffic; tune, do not max.
Every write is rewritten by the log, the flush and each compaction pass; when compaction falls behind, tables pile up, reads probe more files and tail latency rises before errors do.

Bloom filters keep the reads tolerable for a while. At 10 bits per key and seven hash functions the false-positive rate is about 0.82% (classic arithmetic, not a vendor figure), so a lookup for an absent key across 5 tables expects about 0.041 needless disk reads, and across 40 tables about 0.33. Present keys still pay for every table that might hold a newer version. That is read amplification, and it is why the metric to watch is not CPU but pending compaction bytes and table count per level.

FacetF08 at a glance
Reference system during the incidentAvailable with elevated read latency; writes to the affected partitions may be throttled by the engine.
TriggerA sustained write burst, above the compaction budget, perhaps while repair also streams data.
Timelinet0 write rate 3× normal; t0+10 min table count per partition 5 → 40; t0+15 min read P99 climbs; t0+25 min engine throttles writes.
Visible symptomRising P99 and P99.9 for reads, then write latency, then 503/429 from throttling; compaction pending bytes rising.
Root causeForeground write volume exceeded the rate at which background merging reduces read and space amplification.
DetectionPending compaction bytes, tables per partition, write-stall counters, read P99 per partition and per tenant.
ContainmentThrottle the heavy writer, pause repair or rebalance traffic, grant compaction more I/O budget within limits.
RecoveryLet compaction drain; verify table counts and P99 return to baseline before restoring the throttle.
PreventionCapacity for write amplification (the page's assumed 10× planning factor), per-tenant ingest quotas, compaction rate limits that keep foreground P99 visible.
What still holdsNo data is lost or reordered; reads remain correct, only slower.
Cost of the fixThrottling a writer is a visible policy cost; extra compaction I/O budget slows foreground reads.
F08: the ten elements, in order, plus the posture of the reference design.
F09, F27, F28, F35

Repair, Backup and Restore Failures

Repair and restore are the paths you use after something else went wrong, which makes them the paths most likely to be untested, throttle-less and badly timed. Four incidents show what happens when recovery itself is the hazard: a corrupt snapshot, a restore that undoes deletes, a rebuild that starves foreground traffic, and a backup nobody can decrypt.

F09 — Snapshot corruption and repair from another replica

Follower C is far enough behind that the leader no longer has the log entries it needs, so it sends a snapshot: the state machine at a known log index, streamed in chunks. C writes the chunks to disk and, at the end, verifies a whole-snapshot hash. The hash does not match; a chunk was damaged on disk or in transit. C refuses to install it and stays a non-voting learner.

The correct response is almost boring: discard, and ask again, preferably from a different source. Replica B holds the same committed state, so the control plane retargets the transfer to B and the leader is spared another heavy read. The incident turns nasty only if the system installs first and verifies later, or if the same damaged snapshot file is the only copy. Snapshots carry their last-included index and term so the replica resumes the log from exactly the right point, and per-chunk checksums let a retry resend only the broken chunk.

FacetF09 at a glance
Reference system during the incidentAvailable; C is rebuilt without becoming a voter until its state is verified.
TriggerA snapshot transfer to a lagging or replaced replica is corrupted in transit or on disk.
Timelinet0 snapshot streaming; t0+9 min final hash mismatch; t0+10 min C rejects it and requests a new source; t0+25 min second transfer from B verifies; C catches up on the log tail.
Visible symptomA snapshot-failed event and a longer under-replicated period; no client-visible effect.
Root causeBytes changed between the source state and the installed state; installation without verification would turn that into permanent divergence.
DetectionSnapshot verification failures, rebuild duration, under-replicated partition age.
ContainmentKeep the target as a learner; pick a different source if failures repeat.
RecoveryReinstall from a healthy replica at the same or later commit index, then replay the log.
PreventionHash every chunk and the whole snapshot, record the last-included index and term, install only after verification.
What still holdsCommitted data stays correct; a replica that fails verification never votes.
Cost of the fixVerification adds a read pass over the snapshot and delays catch-up.
F09: the ten elements, in order, plus the posture of the reference design.

F27 — Failed restore reintroduces deleted secrets

On Monday a Birch administrator deletes birch/secrets/api-token. On Tuesday a bad migration corrupts a tenant's records and operators restore the partition from Sunday's backup. The backup holds the token, so the restore puts it back, live, with an old version number, and nothing in the store objects: from its point of view this is just a recovered record. The tombstone that recorded the delete was written after the backup.

Swipe to pan
Point-in-time restore: snapshot plus WAL replay to a chosen indexBackup + WAL archive: restore to a chosen pointIllustrative log indexes for one partition; the idea is snapshot consistency point + replayindex 9001500archived WAL replayedsnapshot @ 1000consistency pointrestore target 1341bad bulk delete @ 1342not replayedRestore procedurePick target indexLoad snapshotReplay WAL 1001…1341Verify checksumsFence & switch overWhat makes it consistentThe snapshot records the log index it covers,so replay starts at exactly 1001: no gap, nodouble apply. Taken per partition from areplica, then replay brings it to one point.What goes wrongNo WAL archive: restore only to snapshot time.Untested restores: the first try is the incident.A restored partition must be fenced from oldleaders and match other partitions' cut point.Replicas are not backupsReplication copies the bad delete to every follower within moments. Backups are the onlydefense against logical errors, so keep them separate, encrypted, and restore-tested.
A snapshot restore alone resurrects old state; replaying the archived log to a chosen point also replays the deletes.

Backup restore is a time machine for *everything*, including things people wanted gone. The defences are procedural and structural. Restore into a quarantine copy, then replay the archived write-ahead log up to the desired point in time so the delete is applied (point-in-time recovery). Keep a deletion ledger outside the restored data (tenant, key, deleted-at) and re-apply it before the restored partition rejoins. For secrets, delete the encryption key for the record, so restored ciphertext is useless. And remember that physical removal from backups is bounded by retention: with an assumed 35-day backup policy, a deleted value can persist in old backups for up to 35 days, which must be stated rather than hidden.

FacetF27 at a glance
Reference system during the incidentAvailable; restored data is wrong in a security-relevant way until the delete is re-applied.
TriggerAn older backup is restored over newer state; deletes between the backup and the failure are not in it.
TimelineSun 02:00 backup taken; Mon 10:00 delete; Tue 09:00 restore from Sunday's backup; Tue 09:40 the token authenticates again.
Visible symptomA deleted credential works again; audit logs show a Get of a key with a delete event on record.
Root causeA backup captures state, not intent; deletes after its timestamp are not part of the restored data.
DetectionPost-restore reconciliation of the deletion ledger against restored keys; audit queries for keys with a ledger delete after the backup's timestamp.
ContainmentQuarantine the restored partition before it serves, or block the specific secret namespaces.
RecoveryReplay WAL archive to the incident-minus-epsilon time; re-apply ledger deletes; rotate any secret that was live again.
PreventionPoint-in-time recovery, a deletion ledger, per-tenant key encryption so deletion can mean key destruction, restore drills that include deletes.
What still holdsAfter reconciliation, the logical state matches the pre-incident state; the old value may persist in old backups until retention expires.
Cost of the fixKeeping a WAL archive and a ledger costs storage, and restores take longer because replay is part of the procedure.
F27: the ten elements, in order, plus the posture of the reference design.

F28 — Replica repair overwhelmed by foreground traffic

Node 41 dies and its 32 partition replicas, about 64 GB of replicated data (the worksheet's assumed per-node figure), must be rebuilt elsewhere. The rebuild streams from the surviving replicas, which are also serving the day's peak. At the planning peak each node already runs at about 73% of an assumed node budget, so adding a rebuild stream on the same nodes pushes them past it. Foreground P99 rises just as the cluster is most exposed, running 2-of-3 on 32 partitions.

Both extremes are wrong. Unthrottled repair finishes quickly and takes down foreground latency; heavily throttled repair keeps latency flat and extends the window in which a second failure would cost quorum. The arithmetic is simple enough to decide on: at an assumed 50 MB/s cap, 64 GB takes about 21 minutes, at 200 MB/s about 5. The operations choice is a cap that foreground P99 can tolerate, spread across many source nodes rather than concentrated on two, raised overnight and lowered at peak, and visible on the same dashboard as under-replicated partitions.

FacetF28 at a glance
Reference system during the incidentAvailable; latency elevated; fault tolerance reduced on 32 partitions until the rebuild completes.
TriggerA node is lost and replaced during peak traffic; rebuild streams compete with foreground I/O.
Timelinet0 node 41 lost; t0+2 min replacement scheduled; t0+5 min streams begin; t0+8 min source nodes at 85% of assumed budget, read P99 up; throttle lowers the cap; rebuild extends to ~40 minutes.
Visible symptomUnder-replicated partitions peaking at 32; read and write P99 up on source nodes; compaction debt on targets.
Root causeRepair and foreground work share the same disks and network, and no bandwidth policy separated them.
DetectionRebuild throughput, per-node I/O utilization, under-replicated partition age, foreground P99 on source nodes.
ContainmentLower repair rate if P99 breaches the objective; avoid concentrating sources on one node.
RecoveryResume at a higher rate off-peak; confirm replicas are caught up and rejoined.
PreventionBandwidth caps with priority classes, spreading rebuilds across sources, headroom planned for one node loss, rebuild-time budgets in the SLO.
What still holdsCommitted data remains correct; only the margin for a second failure and latency suffer.
Cost of the fixA slower rebuild extends the exposure window; a faster one costs foreground latency.
F28: the ten elements, in order, plus the posture of the reference design.

F35 — Backup encryption key inaccessible during restore

The nightly backups are encrypted with a key held in the organization's key-management service, which is the right design: a stolen backup is useless without the key. During an incident, a restore begins and fails at the first decrypt. The key's access policy was tightened last month and the restore role was not in it; or the key was scheduled for deletion; or the key service is itself in the affected region. Whatever the cause, the backups exist and cannot be read, which in practice means there are no backups.

This failure is invisible until the day it matters, because backup jobs write encrypted files successfully and a *restore* is the only operation that proves the key is usable. So the control is a restore drill that exercises the production decryption path on a schedule, in addition to checksums of the backups themselves. Other controls: separate duties so that deleting a key needs a second approver and a waiting period; keep old key versions after rotation for the full retention window; and hold an escrowed recovery path with documented access that works when the primary key service is unavailable.

FacetF35 at a glance
Reference system during the incidentNormal service continues; disaster-recovery capability is silently absent until tested.
TriggerA key policy change, scheduled deletion or key-service outage blocks decryption of backups.
Timelinet0 key policy tightened; weeks pass with green backup jobs; tN restore starts; tN+1 min decrypt fails; tN+30 min escalation to key administrators.
Visible symptomRestore job fails with an access-denied or key-not-found error, while all backup jobs show success.
Root causeBackups were verified for existence and size but never for decryptability by the restore role.
DetectionScheduled restore drills with decrypt verification, alerts on key policy changes and deletion schedules, key-version inventory.
ContainmentCancel any pending key deletion; restore the role's access; recover the key from escrow if necessary.
RecoveryDecrypt and restore after access is regained; re-verify the full chain with a drill.
PreventionRegular restore drills including decryption, two-person approval and waiting periods for key deletion, key versions retained through retention, escrow with tested access.
What still holdsBackup contents are confidential and intact; availability of the restore path depends on key custody.
Cost of the fixDrills and escrow take operator time; split custody slows emergency access.
F35: the ten elements, in order, plus the posture of the reference design.
F12, F13, F14, F15, F16, F29

Routing and Ownership Failures

Every key must resolve to exactly one owner at a time, and every incident here is a way that sentence stops being true: the mapping changes the wrong way, a move is interrupted, a client acts on yesterday's map, a key is built without its tenant, or the service that publishes the map goes dark.

The mechanism that keeps ownership honest is small. Keys hash to a fixed number of logical partitions (1,024 in the planning worksheet); a partition map says which replica group owns each partition and carries an epoch that increases on every ownership change; requests carry the epoch the client believes in; and the owner rejects a request whose epoch is stale. Most of what follows is what happens when one of those four pieces is missing.

F12 — New node added with unsafe modulo remapping

The cluster has three nodes and the router assigns a key to hash(key) mod 3. Capacity is short, so an operator adds a fourth node and changes the router to mod 4. For a uniformly distributed hash a key stays on its node only if its residue under 3 equals its residue under 4, which holds for 3 of every 12 residues. A quarter of the keys stay and 75% move, which with 2.048 TB of logical data means about 1.536 TB must migrate (4.608 TB counting three copies).

The damage comes before the migration finishes. The router now sends Get(atlas/user42/settings) to a node that has never seen it. That node answers 404, which is a *false* not-found for a record that exists. Writes land on the new owner too, so for the moved keys there are now two divergent copies. The recovery is to revert the router to the old mapping while the copies are still unmerged and to reconcile any writes made in between by version. The durable fix is not a cleverer modulus but a different shape: hash keys to fixed virtual partitions, and move whole partitions between nodes. Adding a fourth node to three then moves only about 25%, all of it onto the new node, 0.512 TB logical, and moves them with the epoch protocol of F14 and F15.

FacetF12 at a glance
Reference system during the incidentWrongly routed reads return 404 and writes diverge for about 75% of keys until the mapping is reverted.
TriggerThe routing function's denominator changes while data still sits where the old function put it.
Timelinet0 router flipped to mod 4; t0+10 s 404 rate jumps; t0+3 min support tickets for ‘missing settings’; t0+6 min rollback; t0+1 h reconcile writes made on new owners.
Visible symptomA step change in 404s and cache misses across tenants after the config deploy; writes succeed on nodes that did not own the key before.
Root causePlacement was a function of node count, so changing the count changed the owner of most keys.
Detection404 rate versus baseline, correlation with the router config change, per-node key-count shifts.
ContainmentRoll the router back; freeze membership changes.
RecoveryReconcile any divergent writes by version; replay moves through the safe cutover procedure.
PreventionVirtual partitions with a versioned partition map; no placement function that depends on the node count.
What still holdsAfter revert, data placed by the old mapping is intact; only writes during the window need reconciliation.
Cost of the fixA partition map is another piece of metadata to protect, version and distribute.
F12: the ten elements, in order, plus the posture of the reference design.

F13 — Rebalance interrupted halfway through

Partition P7 is being moved from node 2 to the new node 4. The copy has finished, the controller has fenced the source (it stopped accepting writes), and then the controller process crashes before it publishes the new epoch. The partition now has an owner that will not accept writes and a destination that is not yet the owner. If nothing intervenes, clients receive 503 for P7 indefinitely.

Two design choices decide how bad this is. First, the migration must be a recorded, resumable state machine (snapshot, catch-up, verify, fence, flip, retire) stored with the control plane, so a restarted controller reads 'P7 is fenced, destination verified' and finishes the flip rather than starting over. Second, the fence must carry a deadline: if the flip is not committed in, say, a few seconds, the source un-fences itself under its old epoch and the move is abandoned. A fence without a timeout turns a controller crash into an outage; a flip without a fence turns it into F15. One more detail keeps the timeout itself safe: abandoning the move and committing the flip must be mutually exclusive transitions recorded in the control plane's replicated state, so a slow controller cannot flip ownership after the source has already un-fenced and resumed writes. The partially copied data on node 4 is simply unused garbage until the flip, and is cleaned up if the move is canceled.

FacetF13 at a glance
Reference system during the incidentWrites to one partition pause for the fence period; the rest of the cluster is unaffected.
TriggerThe migration controller crashes between fencing the source and publishing the new epoch.
Timelinet0 copy verified; t0+100 ms fence; t0+250 ms controller dies; t0+5 s fence deadline; source resumes writing at its old epoch, or the restarted controller completes the flip.
Visible symptom503 for P7 writes for a bounded interval; a migration shown as ‘stuck in fence’ on the dashboard.
Root causeA multi-step ownership change was not atomic and had no timeout on its unsafe intermediate state.
DetectionMigration state age by phase, fenced-partition count, writes rejected by the fence.
ContainmentLet the fence timeout expire, or complete the flip if the destination is verified; pause further migrations.
RecoveryRestart the controller from the persisted state; resume or roll back, then delete partial copies.
PreventionPersisted migration state machine, fence deadline, throttled concurrent moves.
What still holdsOne owner per partition at all times; no acknowledged write is lost.
Cost of the fixA short fence is a tiny write-unavailability window on every move.
F13: the ten elements, in order, plus the posture of the reference design.

F14 — Stale partition-map epoch sends writes to the old owner

A client library caches the partition map at epoch 17. During a rebalance, P7 moves and the map advances to epoch 18. The client sends Put(atlas/user42/settings) for evt-024 to the old owner with epoch=17. The old owner knows it is no longer the owner of P7 at epoch 18, so it refuses: a retryable WRONG_EPOCH (a 421-style redirect) carrying the new owner and epoch. The client refreshes its map and resends the same request with the same idempotency key.

What the epoch buys you is clear if you imagine its absence. Without the check, the old owner, still holding a copy of P7, accepts the write, acknowledges it, and the write lands on a replica group that the rest of the cluster has stopped consulting. It is lost the moment the old replica is retired. The cost of the right design is one extra hop for stale clients, paid only after moves, and a small bit of discipline in the client: refresh on WRONG_EPOCH, do not hammer the old owner, and bound the redirect loop so that a map-service outage does not become a spin (F29).

FacetF14 at a glance
Reference system during the incidentStale clients are redirected, not served; no write lands on a non-owner.
TriggerA client acts on a cached map epoch that predates an ownership change.
Timelinet0 map epoch 18 published; t0+40 ms client sends epoch 17; t0+41 ms WRONG_EPOCH with new owner; t0+90 ms resend succeeds at epoch 18.
Visible symptomA burst of WRONG_EPOCH responses after each rebalance, decaying as caches refresh; slightly higher latency for affected clients.
Root causeRouting information is replicated by caching, so some readers are always a little behind the truth.
DetectionWRONG_EPOCH rate and its decay time after a move, clients stuck on old epochs, redirect-loop counters.
ContainmentNone needed unless the rate does not decay; check the map distribution path.
RecoveryClients refresh and retry with the same idempotency key.
PreventionEpoch on every request, owner-side epoch checks, bounded retry on redirect.
What still holdsNo write is accepted by a non-owner.
Cost of the fixExtra latency for stale callers and mandatory client map-refresh logic.
F14: the ten elements, in order, plus the posture of the reference design.

F15 — Split or migration race loses an update when fencing is absent

Take the migration from F13 and remove the fence. The destination takes a snapshot at log index 9,000 and begins applying later entries. The source is still the owner and accepts a write, Put(atlas/user42/settings) as index 9,001, and acknowledges it. The controller then flips ownership, having decided the destination is 'close enough'. The destination has entries through 9,000 only. The acknowledged write at 9,001 lives on a source that is about to be retired, and reads at the new owner return the older value. The lost update was acknowledged, durable and then orphaned.

The ordering that prevents it is strict: fence the source so it accepts no new writes, let the destination apply through the source's *final* index, confirm the destination's applied index equals the source's last index, only then commit the epoch change. In this example the fence lasts until the destination reports index 9,001 applied; the write in flight at the fence is either included in that index or rejected with a retryable error. An invariant worth testing directly: run a continuous writer with a checker during rebalances in a staging cluster and assert that every acknowledged write is readable afterwards.

FacetF15 at a glance
Reference system during the incidentSilent data loss if the cutover has no fence; none if the cutover follows fence, catch up to the final index, flip.
TriggerOwnership flips while the source can still accept writes that the destination has not applied.
Timelinet0 snapshot at 9,000; t0+300 ms write 9,001 acked at source; t0+500 ms flip; t0+501 ms reads at destination return the pre-9,001 value.
Visible symptomA later Get returns an older version than one acknowledged earlier; version regressions on one partition after a rebalance.
Root causeTwo nodes were authoritative for an interval; the handoff did not carry the last acknowledged entries.
DetectionA continuous correctness checker, version-regression alerts, source and destination last-index comparison at cutover.
ContainmentStop migrations; identify moved partitions in the affected window.
RecoveryRecover missing entries from the retired source's log while it still exists, then re-apply in order.
PreventionFence, catch up to the final index, then flip; keep the old copy for a rollback window.
What still holdsWith the correct protocol every acknowledged write exists on the new owner before it serves.
Cost of the fixA brief write pause at cutover and a retention period for old copies.
F15: the ten elements, in order, plus the posture of the reference design.

F16 — A key replicated into the wrong namespace

A new code path in the settings service builds the key user42/settings instead of atlas/user42/settings; the tenant prefix was dropped. The store accepts it: a key is just bytes, and the hash puts it wherever it lands. Birch has its own user42 and the same code path now writes user42/settings for both. The two tenants' records overwrite each other, and a read for Birch's user returns Atlas's preferences.

No storage fault produced this; the store did exactly what it was asked. The failure is that tenant identity was an argument the caller could forget. The reference design applies the tenant namespace on the server side, derived from the verified credential, so that a request cannot name a key outside its tenant, and rejects keys that do not begin with the tenant's prefix. Detection comes from the same place: a canary tenant that tries to read keys it does not own should always get 404, and a periodic scan checks that every stored key begins with a known tenant prefix. Recovering the overwritten records means finding them in the log or in backups by the key and version history, and restoring each to its tenant.

FacetF16 at a glance
Reference system during the incidentAvailable and wrong: reads and writes succeed but are shared across tenants.
TriggerA client change omits the tenant prefix when composing keys.
Timelinet0 deploy; t0+4 min first overwrite; t0+9 min Birch reports Atlas's settings; t0+12 min deploy rolled back; t0+2 h affected keys restored from the log.
Visible symptomUsers see another tenant's settings; versions jump unexpectedly; keys appear without a tenant prefix.
Root causeIsolation depended on client-composed keys rather than the server enforcing namespace from identity.
DetectionA scan for keys without a valid tenant prefix, cross-tenant canary reads, anomaly checks on version jumps.
ContainmentRoll back the client; block writes to keys without tenant prefix at the store.
RecoveryFind the affected key range by prefix and version history; restore from the WAL archive or backup; notify affected tenants.
PreventionServer-side tenant namespace from verified credentials, key-shape validation, canary tests in CI.
What still holdsAfter enforcement, no tenant can address another's keyspace; previously overwritten values are recoverable only as far as history was retained.
Cost of the fixServer-side namespacing makes the API a little less flexible and tenant IDs mandatory in every layer.
F16: the ten elements, in order, plus the posture of the reference design.

F29 — Control-plane or map-service outage: warm versus cold clients

The partition map is published by a small control-plane service. Raft groups elect leaders by themselves, and the data plane does not need the control plane to serve reads and writes. So when the control plane goes dark, the first thing the on-call sees is that most traffic keeps working. The clients that were already running hold a cached map and keep sending to the right owners. The trouble sits at the edges: a freshly deployed pod has no cached map and cannot bootstrap; a partition that moves, which now cannot happen anyway, would leave warm clients with a map that is never refreshed.

Practical hardening is cheap. Every storage node can serve the current map it has, so a cold client needs only a list of seed nodes, not the control plane. Caches are kept past their usual refresh interval during a control-plane outage rather than being dropped. Rebalancing, splits and node adds are frozen by policy while the control plane is down, since they need a coordinator, and automated failover of whole regions (F31) must not run without it. The outage is then boring: warm service, no topology changes, a degraded deploy path.

FacetF29 at a glance
Reference system during the incidentWarm clients serve normally; cold clients fail to bootstrap; topology changes are frozen.
TriggerThe control-plane service holding the partition map becomes unreachable.
Timelinet0 control plane down; t0+2 min warm clients unaffected; t0+10 min a rolling client deploy fails to start new pods; t0+30 min recovery.
Visible symptomPod start failures on map fetch, a stale-map-age gauge climbing, no change in data-plane error rates.
Root causeA metadata dependency sat on the bootstrap path of clients, and nobody had designed a degraded mode for it.
DetectionMap age at clients, bootstrap failure rate, control-plane health, migrations paused.
ContainmentFreeze rebalances and deploys of new clients; extend cache lifetimes.
RecoveryRestore the control plane; resume migrations after verifying map consistency.
PreventionSeed-node bootstrap, long-lived map caches, a replicated control plane, no hard dependency on it for steady-state requests.
What still holdsOwnership is unchanged while it is down, so single-owner semantics hold.
Cost of the fixMore client logic and a map-serving path on storage nodes.
F29: the ten elements, in order, plus the posture of the reference design.
F10, F11, F33, F34

Hot Spots, Noisy Tenants and Abuse

Averages hide the failures in a partitioned store. A cluster that is comfortable on average can be on fire at one leader, one tenant or one oversized request, and in each case the fix is to put a limit or a fan-out in front of the thing that is concentrated.

F10 — A hot key saturates one partition leader

Meridian's tenants all read one record, atlas/feature=checkout_policy, on every page load. Use a simplified 100-partition illustration (distinct from the 1,024-partition fleet plan): an average of 200,000 reads/s spread over 100 partitions is 2,000/s per partition, and the dashboard looks calm. This one key takes 60,000 reads/s, 30× the average partition and 30% of fleet average read traffic, all landing on whichever replicas serve it. If linearizable reads must go through the leader and one replica handles an assumed 25,000 point reads/s, that leader is at 240% of its budget and queues, and every other key on the partition inherits the queue.

Swipe to pan
Hot partition histogram: one key cannot be spread by hashingOne hot key defeats uniform hashingSimplified 100-partition illustration (the fleet plan uses 1,024); 20 of 100 bars drawn60k0reads/savg 2,000/s60,000/s (30×)partition index (hash of key picks one; a single key never splits)Why hashing can't helpThe hot key is one key, so itlives on one partition group.Fleet is 6% busy at peak, yetthis group is the bottleneck.Mitigations by operationReads (spread out)Need ≥ 3 replicas at 25,000/s:60,000 ÷ 25,000 = 2.43 × 25,000 = 75,000 → 80% busyFollower reads (labeled stale),near cache with TTL (optional),request coalescing.Writes (not spread)One key = one leader = oneserial order of writes.Salting or fan-out does not fixa single key's write rate.Key splitting only works forread-mostly data you can shard.Operate itDetect with per-key / per-partition heat metrics, notnode averages.Rate-limit the tenant, shedload, and tell the owner tocache or redesign the key.
The average hides the hot key: 60,000 reads/s on one key is 30 times the 2,000/s average partition.

Fan-out is available for reads and not for writes. Serving the key from three replicas with follower-stale reads, explicitly labeled and bounded, gives 3 × 25,000 = 75,000 reads/s of capacity, so 60,000 runs at 80%, workable but thin. A short-TTL client or edge cache, explicitly optional and with the staleness it implies, removes most of the traffic entirely; request coalescing collapses simultaneous identical reads to one. What none of this does is fix writes: all updates to the one key still serialize through one leader, and salting the key into many copies is only legitimate for read-only data. The lesson for capacity planning is that per-key peak, not partition average, sets the limit.

FacetF10 at a glance
Reference system during the incidentAvailable; the hot partition's leader is saturated and its other keys see higher latency.
TriggerA single key receives 60,000 reads/s, above any one replica's assumed 25,000/s point-read budget.
Timelinet0 launch of a new dashboard; t0+30 s read P99 on partition P23 climbs; t0+2 min queues build and 503 appears for other keys on P23; t0+10 min follower reads and a TTL cache engaged.
Visible symptomOne partition's CPU and P99 spike while fleet averages stay low; top-key report shows one key at 30× average.
Root causeLoad concentrates on a key, and linearizable access to one key is served by one leader.
DetectionPer-key and per-partition request rates, top-N key sampling, leader CPU and queue depth.
ContainmentEnable stale-labeled follower reads for that key if the contract allows, add a short TTL cache, and shed or throttle abusers with 429.
RecoveryKeep the cache; confirm the leader is below budget and queues drained.
PreventionHot-key detection, documented read modes for config-like data, cache and coalescing guidance for clients.
What still holdsLinearizable reads stay correct for callers that choose them; stale-labeled reads are bounded and visibly stale.
Cost of the fixReads become stale by the TTL or follower lag, a semantic change the caller must accept.
F10: the ten elements, in order, plus the posture of the reference design.

F11 — A hot tenant saturates its neighbors

Atlas starts a bulk import of tenant configuration, 15,000 writes/s sustained, while Birch and Cedar run ordinary traffic. The three tenants share leaders and compaction budgets. Atlas's burst fills the write queue of partitions it shares, commit latency for everyone rises, and Cedar's single settings update now takes 600 ms instead of 20. Nothing is broken; one tenant simply consumed the shared resource.

The response has two layers. At admission, per-tenant quotas return 429 to the tenant that exceeds its share, with a retry hint, before the write ever reaches the log. Inside the store, weighted fair queues keep one tenant's backlog from monopolising a partition's commit path, and compaction and repair budgets are accounted per tenant where the engine allows it. For a tenant that is permanently large, placement helps: move Atlas's partitions to dedicated nodes so its spikes cannot pass through Birch's leaders. The 429 versus 503 distinction matters here: 429 tells the client it exceeded *its* quota, while 503 says the service cannot take the request, and clients should react to them differently. See rate limiting and bulkheads.

FacetF11 at a glance
Reference system during the incidentAvailable for all tenants; the noisy tenant is throttled, others recover.
TriggerOne tenant's sustained write burst consumes shared commit and compaction capacity.
Timelinet0 import starts; t0+20 s shared partitions' commit queue grows; t0+45 s Cedar latency 30× normal; t0+1 min per-tenant limit enforces 429 on Atlas; t0+3 min latency recovers.
Visible symptomElevated P99 for Birch and Cedar correlated with Atlas's write rate; Atlas receives 429.
Root causeIsolation was by logical namespace only; the physical resources underneath were shared and not metered per tenant.
DetectionPer-tenant request rate, queue time and P99 by tenant, 429 rates by tenant.
ContainmentThrottle the noisy tenant first; avoid global shedding that penalises quiet tenants.
RecoveryRaise Atlas's quota deliberately or move its partitions; verify neighbors' P99 returned to baseline.
PreventionPer-tenant quotas at admission, weighted fair queuing, dedicated placement for very large tenants, accounting of compaction and repair per tenant.
What still holdsCorrectness is unaffected; fairness is an enforced policy, not a given.
Cost of the fixQuotas may throttle legitimate bursts; dedicated placement costs more hardware.
F11: the ten elements, in order, plus the posture of the reference design.

F33 — A multi-tenant scan bypasses authorization via a caller-supplied tenant ID

Scan by prefix is a product feature only on range-partitioned data, and Meridian's scan endpoint takes a tenant field in the request body: Scan(tenant=birch, prefix=user). The authorization check verifies the caller's token is valid, and then trusts the body to say whose data to read. An Atlas credential sends tenant=birch and receives Birch's records.

The bug is one of identity source. A tenant must come from the *verified credential*, never from a field the caller controls, and then be applied at every layer: the scan's start and end keys are clamped to the tenant's prefix, an empty or wildcard prefix is rejected, and storage, logs and admin operations all work in that namespace. Detection is an audit comparison: the tenant in the token against the tenant touched by the operation, alerted on any mismatch. Containment is to disable or restrict the endpoint immediately, then use the access logs to scope what was read. A fix that merely validates the body against the token still leaves the unnecessary field in the API; remove it.

FacetF33 at a glance
Reference system during the incidentAvailable; the scan endpoint is exploitable until fixed.
TriggerA client supplies another tenant's ID in the scan request and the server uses it.
Timelinet0 first cross-tenant scan; t0+hours (say 6 h) detection by audit comparison; detection+15 min endpoint disabled; following days exposure analysis from audit logs.
Visible symptomAudit records where token tenant and scanned tenant differ; unusually large scans by a single token.
Root causeTenant identity was taken from request input instead of the authenticated principal.
DetectionToken-tenant versus key-prefix mismatch alerts, scan-size anomalies, security review of every endpoint taking a tenant parameter.
ContainmentDisable or restrict the endpoint, revoke suspect credentials.
RecoveryFix tenant derivation, scope exposure from audit logs, notify affected tenants per policy.
PreventionDerive the tenant from credentials, clamp ranges, reject empty prefixes, authorize at the storage layer too.
What still holdsAfter the fix, a credential can reach only its own prefix; already-read data cannot be un-read.
Cost of the fixA stricter API and slightly more server-side checking.
F33: the ten elements, in order, plus the posture of the reference design.

F34 — Key-size abuse or oversized writes exhaust memory and disk

The teaching limits are a key of at most 1 KiB and a value of at most 256 KiB, against an average record of about 1 KiB. A buggy client starts writing 256 KiB blobs at 1,000 writes/s. That is 256,000 KiB/s (about 250 MiB/s) of client ingest compared with about 1,000 KiB/s (under 1 MiB/s) for average records: 256× the bytes per write, before multiplying by three replicas and by compaction's rewrite factor. Memtables fill in seconds, flushes produce huge SSTables, and the Bloom filters and indexes grow with them. The limits only help if they are enforced before the log.

Enforcement lives at the earliest point that can see the size: the API edge returns 413 for oversized values and a validation error for keys above 1 KiB, before anything is appended, replicated or logged. Below that, a per-tenant byte-rate quota catches the case of many *valid* but large writes. Large objects that genuinely belong somewhere should go to an object store with the key-value record holding a pointer. A store that lets one big request in has accepted a replicated, compacted, backed-up copy of it, which is expensive to remove.

FacetF34 at a glance
Reference system during the incidentRejecting requests that exceed limits; the rest of the service is unaffected if limits and quotas hold.
TriggerA client sends many maximum-size values, or keys beyond the limit.
Timelinet0 rollout of a client change; t0+2 min memtable flush rate spikes; t0+5 min disk-growth alert; t0+6 min byte quota engages; t0+1 h client fixed.
Visible symptomIngest bytes per second rise 256× with a flat request rate; flush size and disk usage surge; 413 or 429 responses.
Root causeSize limits were not enforced early or not metered in bytes.
DetectionRequest-size histograms by tenant, ingest bytes per second, flush size, disk-growth slope.
ContainmentReject oversize at the edge; apply a byte-rate quota to the tenant.
RecoveryDelete or compact away the unwanted records; confirm disk and memtable pressure return to normal.
PreventionHard limits before the WAL, per-tenant byte quotas, pointer-to-object-store pattern for large payloads.
What still holdsAccepted writes are within contract; rejected writes are explicit.
Cost of the fixSome legitimate large payloads need a second storage path.
F34: the ten elements, in order, plus the posture of the reference design.
F19, F20, F21, F22

Leaders, Quorums and Reads

Leaders, terms and quorums are what turn replication into a promise instead of a hope. These four incidents are the cases that test that promise: a leader that cannot reach a majority, a leader that does not know it has been replaced, a follower that answers from the past, and two writers who both believe they are updating the same version.

F19 — A minority network partition attempts to accept writes

A network fault separates leader A from followers B and C. Clients on A's side can still reach A. A can append entries locally but cannot get a second acknowledgement, so nothing commits: its writes time out or return 503 UNAVAILABLE, never success. On the other side B and C time out waiting for heartbeats, hold an election, and B becomes leader at term 4 with C's vote. Two out of three is a majority; the majority side keeps serving.

Swipe to pan
Network partition: majority commits, minority rejectsPartition: only the majority side may commitRF 3; the link between A and the others is cut. CAP applies during this partition, not all the timeMinority side (1 of 3)A · old leaderterm 3, aloneClient 1Put → ApartitionMajority side (2 of 3)B · new leaderelected, term 4CfollowerClient 2Put → BClient 1 gets503 UNAVAILABLEA cannot reach a majority, so itcannot commit. It never says OKfor a write it cannot make durable.Client 2 gets200 OK, committed by B + CB and C elect within roughly anelection timeout (the Raft papersuggests values like 150–300 ms).Timing is not freeUntil the election finishes there isno leader: writes fail or wait.Reads in linearizable mode on Amust fail too: A cannot prove leadership.Two failures out of three leaves no majority anywhere: no commits and no strong reads. RF 5 needs 3 to commit and survives 2.
Only the side holding two of three replicas can commit; the isolated leader keeps its role on paper but cannot acknowledge anything.

The partition shows the cost of choosing consistency during a split. Clients stuck with A cannot write and cannot do linearizable reads either, because A can no longer confirm it is still the leader. They can still make follower-stale reads if the caller chose that mode, clearly labeled stale. Nothing here violates the reference design: A is unavailable for those operations by design. When the partition heals, A learns about term 4, steps down, and discards its uncommitted entries (which were never acknowledged), and clients redirected to B resume.

FacetF19 at a glance
Reference system during the incidentMajority side serves reads and writes; minority side rejects writes and linearizable reads, and offers only labeled stale reads.
TriggerA network partition isolates the leader with a minority of replicas.
Timelinet0 partition; t0+250 ms election timer on B/C; t0+300 ms B leads at term 4; t0+heal A sees term 4, steps down, drops its uncommitted tail.
Visible symptom503 for clients on A's side; leader change visible on the majority side; term numbers diverge until heal.
Root causeMajority commit requires two of three replicas; the isolated leader cannot reach one.
DetectionTerm changes, leaders-per-partition counters (should stay at one effective leader), unreachable-peer counts, write rejections by cause.
ContainmentNothing to force; the protocol is already doing the safe thing. Route clients to the majority side.
RecoveryHeal the network; confirm A rejoined as a follower and caught up.
PreventionFailure-domain placement (replicas in separate racks or zones), client retries with redirect, clear 503 semantics.
What still holdsNo acknowledged write is lost and no two leaders commit at once (I4).
Cost of the fixClients on the minority side lose write availability, which is the price of consistency.
F19: the ten elements, in order, plus the posture of the reference design.

F20 — A stale leader fails to be fenced

Leader A, term 3, stalls for 12 seconds in a long pause (a VM freeze, say). B and C elect B at term 4. When A wakes, it has no idea any of this happened: its clock says it is leader, and its lease, if it relies on one, may *look* valid. The write path is protected by terms: any append A sends to B or C carries term 3 and is rejected, because they have seen term 4. Reads are where it can bite. If A serves a linearizable read from local state under a lease it believes is valid, it may return a value that B has since overwritten.

Swipe to pan
Term fencing rejects a stale leaderTerms fence off the old leaderEvery message carries a term; replicas ignore anything older than the highest term they have seenAwas leader, term 4Bleader, term 5CfollowerA is cut off (pause or partition); B and C elect B for term 5B: append entry (term 5)ack → entry committedA wakes up and still believes it is the leaderA: append entry (term 4)reject: my term is 5A steps down, adopts term 5What the term doesIt is a monotonic fencing token.Higher term wins every comparison.A leader that cannot gather a majorityin its own term cannot commit.What it preventsTwo leaders both committing the same indexwith different contents. The writethe old leader tried is neveracknowledged as committed.Clients tooA client still routed to A gets arejection or redirect and re-resolvesthe leader. Retries must carry anidempotency key.
Terms order leaders: a paused leader's old-term messages are refused, so it cannot commit; reads need their own check.

There are two defences and a system should use at least one. The read-index approach has the leader confirm its leadership with a majority before each linearizable read batch, which costs a round trip but trusts no clock. The lease approach lets the leader answer locally while its lease is valid, but the lease must be shorter than the election timeout with a margin for clock drift, and a paused process must re-check on wake. For work *outside* the store, such as another system acting on a lease grant, the term is passed along as a fencing token so the downstream resource rejects the older holder. This incident is the reason that 'I think I am leader' is never sufficient on its own.

FacetF20 at a glance
Reference system during the incidentWrites safe via term checks; a stale leader's local reads may be wrong unless protected by read-index or a valid lease.
TriggerA leader pauses past the election timeout and resumes believing it is still the leader.
Timelinet0 A pauses; t0+250 ms B elected at term 4; t0+12 s A resumes; t0+12.001 s A attempts append at term 3 and is rejected; any local read served on the old lease would be stale.
Visible symptomTerm-3 appends rejected by peers; a short window of stale linearizable reads if reads trusted a lease; a step-down log line on A.
Root causeA process cannot observe that time passed while it was paused; its belief in leadership outlived the lease.
DetectionRejected-append-for-old-term counters, long-pause detectors, linearizability checker (e.g. Jepsen-style tests) in staging.
ContainmentFence the old leader by killing or restarting it if it does not step down; prefer read-index mode for sensitive reads.
RecoveryA steps down on seeing the higher term and resyncs its log.
PreventionTerm checks on every message, read-index or conservative leases, pause detection, fencing tokens for external actions.
What still holdsA stale leader cannot commit; linearizable reads hold if read-index or a valid lease is used.
Cost of the fixRead-index costs a round trip per read batch; leases depend on a bounded clock drift.
F20: the ten elements, in order, plus the posture of the reference design.

F21 — A follower serves a stale read during lag

Follower C is 2 seconds behind after a compaction pause. A user in Meridian's admin console saves new settings (v8 for atlas/user42/settings), the page reloads, and the read goes to C under the follower-stale mode. The response shows v7. From the user's side, the save did not work, though it committed on a majority.

Nothing is wrong, provided the API told the truth. A follower-stale read advertises exactly this trade: lower latency and offloaded leaders in exchange for being behind by some bounded or unbounded lag. The product problem is the user who just wrote. read-your-writes fixes it with a version floor: the write response returns committedVersion=8 (the session token), the client sends minVersion=8 on the read, and a follower that has not applied v8 forwards to the leader or waits briefly. Dashboards should publish replication lag per follower, and the router should drop followers beyond a staleness bound from the read pool.

FacetF21 at a glance
Reference system during the incidentAvailable; the read result is stale by design, and read-your-writes needs a version floor.
TriggerA reader is served by a follower whose applied index trails the committed index.
Timelinet0 write commits at v8; t0+20 ms page reload; read hits C at applied v7; user sees the old value; t0+2 s C catches up.
Visible symptomA user sees their own change missing for seconds; follower-lag gauge elevated; reads with minVersion routed to the leader.
Root causeFollowers apply the log asynchronously and may be behind the commit point.
DetectionPer-follower lag in entries and seconds, stale-read ratio by mode, read-your-writes forwards.
ContainmentRemove laggards from the read pool; route session reads to the leader.
RecoveryLet C catch up; verify lag returns inside the bound.
PreventionRead modes named in the API, version-floor tokens, a staleness bound for the follower pool.
What still holdsLinearizable and read-your-writes reads remain correct; follower-stale reads are labeled.
Cost of the fixForwarded reads cost leader capacity; waiting costs latency.
F21: the ten elements, in order, plus the posture of the reference design.

F22 — Concurrent updates and the lost-update trap

Two admin tools edit the same settings record. Each does the obvious thing: Get, change one field in the client, Put. Tool 1 reads v7 and sets theme=dark; Tool 2 reads v7 and sets language=fr; each writes the whole record. Tool 2's write lands second and erases the theme change: a lost update, with both writes acknowledged. The store did nothing wrong, since a plain Put is an overwrite. The check-then-put lived in the application, where the check and the put are two separate operations with a gap.

Swipe to pan
Compare-and-set with one winnerCAS: the version check happens inside the logBoth clients read v7 of atlas/user42/settings, then race with Put ifVersion = 7Client 1Client 2Leaderlog + stateFollowersPut ifVersion=7 (theme)Put ifVersion=7 (locale)orders: first, then secondreplicate first, commit200 OK, committed v8412: expected 7, current 8re-read v8, merge, retryWhy it is atomicThe leader evaluates ifVersion when itapplies the second to the state machine, in logorder. Exactly one of two writers with thesame expected version can succeed.Followers apply the same decision.The broken versionClient does Get, checks the version, thenPut. Between the two calls another writercan commit: a lost update.CAS must be one server-side operation.
Both writers present version 7; the leader serialises them, one commits and the other gets 412 with the current version.

The cure is a conditional write inside the replicated state machine: Put(expectedVersion=7) is evaluated by the leader at the position in the log where it is applied, so exactly one of the two writes sees version 7 and the other sees 8 and gets 412 Precondition Failed. The loser rereads, merges and retries (the worked timeline later shows the exact steps). Two cautions: CAS on a hot key can turn into a retry storm, so retries need jitter and a limit, and a conditional write is only as strong as the replication underneath it: in a leaderless design the same call needs a different explanation (F23).

FacetF22 at a glance
Reference system during the incidentAvailable; unconditional writes lose updates, conditional writes serialize them.
TriggerTwo clients perform read-modify-write on one record without a version precondition.
Timelinet0 both read v7; t0+8 ms tool 1 writes v8; t0+11 ms tool 2 writes v9 on top, overwriting tool 1's change; user reports the theme reverted.
Visible symptomA field silently reverts; two acknowledged writes, only one visible; no error anywhere.
Root causeThe read and the write were separate non-atomic steps with no precondition.
DetectionAudit of write sequences on a key showing back-to-back overwrites from different writers; 412 rate (expected to be non-zero where CAS is used).
ContainmentSwitch hot record paths to conditional writes; add a retry loop with a cap.
RecoveryReplay the lost field from the log if the user reports it.
PreventionCAS with expectedVersion for records edited by multiple writers; merge logic on 412.
What still holdsWith CAS, exactly one writer wins each version; the loser always learns.
Cost of the fixMore client code and some contention retries.
F22: the ten elements, in order, plus the posture of the reference design.
F23, F24, F25, F26

Leaderless-Model and Clock Failures

These four incidents belong to the *alternative* leaderless design and to the clocks that every design leans on. They are here because the usual claims made for tunable quorums, that overlap gives consistency and that timestamps order writes, are exactly the claims that fail.

F23 — Leaderless divergent siblings and conflict resolution

In the leaderless alternative with N=3, W=2, R=2, two clients update atlas/user42/settings at the same time through different coordinators. Coordinator X gets acks from replicas A and B for a write that sets theme; coordinator Y gets acks from B and C for a write that sets language. Both met W=2, both succeeded, and the replicas now hold different values at the same logical position. Neither write knew about the other: they are concurrent, not ordered.

Swipe to pan
Concurrent writes become siblings and need reconciliationALTERNATIVE: concurrent writes, siblings, reconciliationBoth clients read version v9 of atlas/user42/settings, then write without seeing each otherClient Atheme = darkClient Blocale = deStore holds two siblingssibling 1{theme: dark} vv [A:1]sibling 2{locale: de} vv [B:1]neither vv dominates the other = concurrentRead returns bothClient or app merges:{theme: dark, locale: de}writes back with the contextof both siblingsnew vv [A:1, B:1]supersedes bothAlternative rule: last-write-wins (LWW)Higher timestamp wins; the other write isdropped with no signal to its author.Skewed clocks can even keep the older write.Simple and available, but it loses data.Choose it only where a lost update is acceptable.Reference design avoids the forkOne leader orders writes in the log.B's put with ifVersion=9 fails 412 onceA has committed v10, so B re-readsand retries: no siblings to merge.Cost: writes need a quorum and a leader.
Concurrent writes via different coordinators both reach W=2; the next read sees two siblings, not an error.

A later read with R=2 sees both versions and has to decide. With version vectors (or vector clocks) the store can tell the writes were concurrent and returns both as siblings, leaving the merge to the application: here, a field-wise merge that keeps theme and language. With last-write-wins, the store silently keeps the one with the later timestamp and discards the other, a lost update that no error reports (see F26 for why the timestamp may not be trustworthy). Siblings need a merge function that is correct for the data (a union for sets, a field merge for settings, and sometimes no good answer), and read repair or anti-entropy then writes the merged result back so siblings do not accumulate.

FacetF23 at a glance
Reference system during the incidentALTERNATIVE design: available under partitions, with divergent copies and application-level merges.
TriggerTwo writes to one key reach different replica subsets with no causal ordering between them.
Timelinet0 X writes {theme} to A,B; t0+3 ms Y writes {language} to B,C; t0+1 s a read with R=2 gets two concurrent versions; t0+1.02 s the application merges; read repair writes the merge.
Visible symptomReads that return multiple siblings (or silently, one version under LWW); sibling-count metrics rising on hot keys.
Root causeWithout a single serialization point, concurrent writes can both succeed.
DetectionSibling counts and size per key, LWW overwrites where timestamps are within a skew bound, anti-entropy divergence reports.
ContainmentPause automated writers on hot keys; apply a deterministic merge function.
RecoveryMerge siblings, write the result with the merged context, and let repair converge.
PreventionPick the merge semantic per data type up front; use CRDTs for mergeable data; use the reference (leader-based) design where CAS semantics are required.
What still holdsReplicas converge eventually once the merge is applied; no linearizability is claimed.
Cost of the fixMerge logic is application code, and siblings grow if clients never resolve them.
F23: the ten elements, in order, plus the posture of the reference design.

F24 — The quorum-overlap claim fails under sloppy replacements

N=3, R=2, W=2 gives R+W=4, greater than 3, and the textbook intuition is that any read quorum overlaps any write quorum. It is only an intuition, and a sloppy quorum breaks it. The preference list for a key is A, B, C. C is unreachable. To keep accepting writes, the coordinator uses stand-in node D, outside the preference list, for the third copy, and stores a hint for C. A write is acknowledged with acks from A and D, meeting W=2.

Now C returns before D's hint is delivered, and a read arrives that contacts B and C. Neither holds the new value: A and D do. R+W>N still holds on paper and the read returns the old value, with no error and no sibling. Hinted handoff is best-effort repair: Cassandra's documentation states hints do not guarantee eventual consistency the way anti-entropy repair does, and hints are only stored for a limited window (a max_hint_window default of 3 hours in the current Apache Cassandra docs and sample configuration; check your version). Cassandra counts a hint toward a write only at consistency level ANY, so the sloppy-quorum behavior in this incident is the original Dynamo design rather than what Cassandra does at its ordinary quorum levels. The correct statement is narrow. With *strict* quorums, overlap guarantees that a read sees the latest write *that has completed*, and even that is not linearizability, because of concurrent writes, failed partial writes, clock skew and membership changes.

FacetF24 at a glance
Reference system during the incidentALTERNATIVE design: stale reads possible after acknowledged writes; no false claim of linearizability.
TriggerA write is accepted via a stand-in node while a preference-list replica is down; a later read hits replicas that lack it.
Timelinet0 C down; t0+1 s write acked by A and D; t0+4 min C back, hint pending; t0+4.1 min a read from B and C returns the old value.
Visible symptomA client reads its own acknowledged write and gets the previous one; hint backlog and divergence reports non-zero.
Root causeQuorum overlap is a property of strict quorums over a stable membership, not of sloppy ones with hints.
DetectionHint queue depth and age, read-repair and anti-entropy mismatch rates, staleness probes.
ContainmentRun repair to propagate; for sensitive keys use strict quorums or the leader-based path.
RecoveryDeliver hints, run anti-entropy, verify convergence.
PreventionState the model honestly in the API; strict quorums where needed; scheduled anti-entropy repair, not hints alone.
What still holdsAfter repair replicas converge; no read-after-write promise holds under sloppy quorums.
Cost of the fixStrict quorums reduce availability when preference-list nodes are down.
F24: the ten elements, in order, plus the posture of the reference design.

F25 — A delayed tombstone resurrects an older value

A delete in a replicated store is a write: it records a tombstone with a newer version, and the old value is dropped later during compaction. Take the key atlas/user42/legacy-flag with three replicas. A deletes it and the tombstone is acknowledged by A and C; B was down and missed it, still holding the value. The retention period for tombstones, an assumed 10 days as in Cassandra's documented default gc_grace_seconds of 864,000 s, expires, compaction drops the tombstone on A and C, and then B, which has been offline for 11 days, comes back with the old value.

Swipe to pan
Tombstone garbage collected too early resurrects a deleted recordTombstone timeline and zombie resurrectionAssumed grace period 10 days (a Cassandra-style default; set it from your own repair cadence)day 0deleteday 10: GCday 11Replica Alive valuetombstone (delete marker)purgedReplica Blive valuenode down, never saw the delete, not repairedback: live valueReplica Clive valuetombstone (delete marker)purgedrepair copies itZombie: delete undoneA and C no longer have a marker, so B lookslike the only (newest) copy of the recordWhy the purge was legalAfter day 10 the tombstone was dropped by compaction.B stayed away for 11 days, longer than the grace.Rule: tombstone retention > longest time a replica can go unrepairedRepair every replica inside the grace window, or wipe and rebuild a node that was away longer.In the leader-based reference design the log and snapshot carry the delete to a returning follower;the zombie risk is sharpest in leaderless stores that rely on repair.
The replica that missed the delete still holds the value; once the tombstone is purged, repair sees the other replicas as the stale ones and copies the zombie back.

With the tombstones gone, A and C have no evidence of the delete, so repair compares B's value with their absence and concludes that A and C are *missing* a record. The deleted value is copied back to them: a zombie. The rule that prevents it is that tombstone retention must be longer than the longest time any replica can be offline or unrepaired before it is repaired, and a replica beyond that horizon must be wiped and rebuilt, not rejoined. In a log-replicated reference design the same hazard returns whenever a replica is restored from an old snapshot or backup past the retention horizon, so it holds there too.

FacetF25 at a glance
Reference system during the incidentAvailable; the logical state is wrong for the resurrected keys, which is a correctness failure rather than a visible outage.
TriggerA replica that missed a delete returns after the tombstone has been garbage-collected.
TimelineDay 0 delete on A,C (B down); day 10 tombstone purged; day 11 B returns and repair copies the old value to A and C.
Visible symptomA deleted record reappears; reads of it return 200 instead of 404; the version is older than the delete.
Root causeThe tombstone expired before every replica had seen it.
DetectionAlerts for replicas absent longer than the GC horizon, deleted-key reappearance audits, repair age.
ContainmentQuarantine the returned replica; stop repair from it; re-delete affected keys.
RecoveryWipe and rebuild any replica older than the horizon; re-apply a deletion ledger for the window.
PreventionTombstone retention greater than maximum repair interval plus outage tolerance; routine repair inside that window; refuse stale replicas.
What still holdsWithin the horizon, the tombstone wins and deleted values stay deleted.
Cost of the fixLonger retention keeps tombstones, and scanning cost, for longer.
F25: the ten elements, in order, plus the posture of the reference design.

F26 — Clock skew alters last-write-wins and TTL results

The US client library timestamps its own writes using the machine clock, which is running 90 seconds ahead. In a last-write-wins model, the US client's write at 12:00:00 (real time) carries 12:01:30. An EU client writes a legitimate update at 12:00:40; its timestamp is lower than the US one, so the store keeps the older, US write. The newer write was acknowledged and then lost, and no one is told.

TTL has the same weakness. If expiry is decided by the node's own clock, a fast node expires records early and a slow one serves them late, by as much as the skew. The reference design avoids both by not letting caller or node clocks order anything: versions come from the leader's replicated log, and a TTL is stored as an expiry timestamp assigned at commit and evaluated against a bounded clock, with expiry applied by replicated tombstone rather than each replica deciding alone. Where cross-cluster last-write-wins is used by a product, it should say whose time: Google's Bigtable documentation, for instance, describes two writes to the exact same cell (same row, column and timestamp) sent to different clusters as resolved by a deterministic last-write-wins rule based on server-side time, which is still a decision to discard one write, just a reproducible one.

FacetF26 at a glance
Reference system during the incidentAvailable; the winner of a conflict, or the moment of expiry, can be wrong by up to the skew.
TriggerPhysical timestamps from unsynchronised clocks decide ordering or expiry.
Timeline12:00:00 US write stamped 12:01:30; 12:00:40 EU write stamped 12:00:40; the store keeps the US value; the EU user's change disappears.
Visible symptomA later-in-time write loses to an earlier one; records expire early on nodes with fast clocks; version order disagrees with wall-clock order.
Root causeWall-clock time is not an ordering mechanism; skew is unbounded unless monitored.
DetectionClock offset monitoring, writes whose timestamps are far from server time, LWW overwrite audits.
ContainmentReject or clamp client timestamps; alert on skew over a limit.
RecoveryReapply the lost update from logs where available; fix the clock source.
PreventionServer- or leader-assigned versions, hybrid logical clocks, TTL assigned at commit and replicated as a state change, monitored time synchronization.
What still holdsOrdering is determined by log position, not by anyone's clock.
Cost of the fixLess flexibility for clients to set their own timestamps; clock monitoring remains necessary for TTL.
F26: the ten elements, in order, plus the posture of the reference design.
F30, F31, F32, F36

Regional Failures and Residency

Across regions, the speed of light and the legal system both show up as failure modes. The reference design keeps a home-region leader and ships its log to a remote region asynchronously, so a regional disaster raises questions the single-region incidents never had to ask: what was lost, who may take over, and where the data is allowed to live.

The context for all four: EU is the home region, US holds an asynchronous replica, and replication lag is assumed to be 300 ms. Remote reads are eventually consistent unless routed to the home region. Asynchronous cross-region replication does not give global strong consistency, and nothing below should be read as claiming it does. Each recovery number is an illustrative example, not a promise.

F30 — Cross-region replication lag after an abrupt disaster

EU loses power at once, and the US replica, 300 ms behind, holds atlas/user42/settings at v10 while EU had acknowledged v11. Assume EU takes 12,000 writes/s (30% of the 40,000 average, an illustrative share). A 300 ms lag means up to 12,000 × 0.3 = 3,600 acknowledged-but-unreplicated writes at risk at the instant of failure. That figure is exposure, not a measured loss: it depends on how much of the lag window actually held unreplicated data.

Swipe to pan
Region failure: RPO is lag-dependent exposure, and promotion needs fencingLosing the home region: RPO, promotion and fencingHypothetical inputs: 12,000 EU writes/s (30% of average 40,000) and 300 ms lagEU — lostacknowledged up tov11unreachable, state unknownUS replicahas applied up tov10≈300 ms behind at the cutExposure at the instant of loss12,000 writes/s × 0.3 s= up to 3,600 acknowledgedwrites not yet in the US.That is RPO: lag-bound, not 0.Operator decisionA. Wait for EU recoveryNo acknowledged write is lost; EU data staysin its region. Cost: EU records are read-only orunavailable for as long as the region is down.Best when the outage is short or RPO must be 0.B. Promote the US replicaAvailability returns, but up to ~3,600 writesare missing until EU data is reconciled.Needs the old leader fenced (higher epoch) soEU cannot accept writes on return, and residencyrules must permit EU records in the US.Safety rules for a promotion• Fence first: new term/epoch on the US group, so a returning EU leader cannot accept writes (no split brain).• Record the last-known EU version so lost writes (v11 and later) can be audited and replayed if EU recovers.• Do not silently serve EU-resident records from the US: residency is a policy decision, not a failover default.
EU acknowledged v11 but US received only v10: the difference is the exposure window, not a fixed recovery point.

Promoting US is a business decision with an engineering price: those acknowledged writes disappear from the live system, the version counter on the US side continues from v10, and users who saw v11 in EU now see v10. The honest RPO statement is lag-dependent and non-zero, and the response is a procedure rather than a reflex: decide to promote, record the last log position US applied, publish the lost-window boundary to tenants, and when EU's storage returns, extract the unreplicated tail from EU's log and offer it for manual or tenant-specific reconciliation, because it conflicts with what US accepted in the meantime. The worked timeline below walks the same event step by step.

FacetF30 at a glance
Reference system during the incidentReads and writes available in US after promotion; data up to the lag window may be missing (RPO > 0).
TriggerThe home region fails abruptly with a non-empty replication backlog.
Timelinet0 EU lost; t0+90 s failure declared; t0+5 min US promoted at v10; t0+hours EU tail recovered for reconciliation.
Visible symptomUsers see older versions; dashboards show US applied index behind the last EU acknowledged index; a gap recorded at promotion.
Root causeAsynchronous replication acknowledges a write before the remote copy exists.
DetectionReplication lag per region, last-replicated index, promotion log of the boundary.
ContainmentPromote US only under an approved decision with fencing (F31); communicate the lost window.
RecoveryReconcile the recovered EU tail against US writes; restore EU as a replica, not as the leader, until caught up.
PreventionMeasure and alert on lag, minimize it for critical tenants, consider synchronous replication or a second in-region copy for stricter RPO.
What still holdsWrites up to the last replicated index are present; later ones are exposure.
Cost of the fixTighter RPO (synchronous cross-region commit) costs latency on every write.
F30: the ten elements, in order, plus the posture of the reference design.

F31 — Incorrect regional failover creates split-brain

An automated failover script promotes US when its health check cannot reach EU for 60 seconds. EU is not down. The path between the monitoring system and EU failed, and EU's leaders keep serving local traffic. Now US also accepts writes. atlas/user42/settings receives v11 in EU and a different v11 in US, concurrently, with each side's clients entirely satisfied.

Failover must be a decision with proof, not a reaction to silence. The safeguards: a region's right to lead is a lease or ownership epoch granted by an authority that both sides respect; EU's leaders step down when their lease expires without renewal, and US may only take over *after* the previous lease has provably expired or EU has been fenced; failover is approved by more than one signal (multiple vantage points, a human for region-scale moves). Once split-brain occurs, the only recovery is reconciliation: pick the surviving history, then expose the other side's writes as conflicts for owners to resolve. There is no automatic merge that is correct for arbitrary data.

FacetF31 at a glance
Reference system during the incidentUnsafe until one region is fenced; correctness is restored by choosing a surviving history.
TriggerA failover is triggered by a monitoring-path failure while the old home region is alive.
Timelinet0 monitor loses EU; t0+60 s script promotes US; t0+61 s both regions accept writes; t0+20 min operator notices dual leaders; t0+25 min EU fenced.
Visible symptomTwo leaders for the same partitions in a global view; the same version number carrying different content in each region.
Root causeUnreachable was treated as dead, and ownership was not tied to an expiring lease.
DetectionDual-leader detector, conflicting-version audit, ownership epoch mismatches.
ContainmentFence the region that should lose (stop its leaders and reject at the edge); stop automatic failover.
RecoveryChoose the surviving history; extract the other side's writes as conflicts for tenants; rebuild the loser as a replica.
PreventionLease-based regional ownership, multi-signal and approved failover, regular failover drills.
What still holdsAfter fencing and reconciliation, one history is authoritative; writes in the abandoned history are not silently merged.
Cost of the fixSlower failover (waiting for lease expiry) and operational ceremony.
F31: the ten elements, in order, plus the posture of the reference design.

F32 — Region-residency policy blocks an otherwise convenient failover

Cedar's contract requires its records to stay in the EU. The US replica therefore does not hold Cedar's records, or holds only what policy permits. When EU fails, the platform *could* technically restore Cedar's data into US capacity from a backup copy and keep serving. It must not. The failover plan classifies tenants by residency, and for Cedar the correct state is unavailable until an EU-resident capacity returns.

This turns a purely technical decision into a product one, and it needs to be written down before the incident. Options that stay inside the rule: a second EU region or a second EU zone group so that EU-resident tenants have an in-jurisdiction failover target, accepting the cost; or an explicit contractual availability level that tolerates downtime during a regional disaster. Tenants without the restriction (Atlas, in this fiction) may use US failover with the RPO caveats of F30. The failure to avoid is silent: an operator under pressure restoring EU data into the US because it works. The restore tooling should refuse to place restricted tenants' data outside their allowed regions.

FacetF32 at a glance
Reference system during the incidentAvailable for unrestricted tenants after failover; EU-resident tenants reject requests rather than breach residency.
TriggerThe home region fails and the only existing remote replica is in a region the tenant's policy excludes.
Timelinet0 EU lost; t0+5 min US promoted for unrestricted tenants; Cedar returns 503 with a clear status; t0+hours EU capacity returns and Cedar resumes.
Visible symptomCedar requests fail with a documented error while others recover; an incident-status banner explains why.
Root causeA technically possible action violates a legal or contractual constraint.
DetectionA policy check at the failover tooling, residency tags on tenants and partitions, audit of data location.
ContainmentDo not move restricted data; communicate the status.
RecoveryRestore from in-jurisdiction capacity when available; verify the data stayed within bounds.
PreventionA second in-region failover target, residency-aware tooling, contractual availability expectations stated in advance.
What still holdsResidency holds; the availability promise for restricted tenants is lower in a regional disaster.
Cost of the fixExtra regional capacity costs money; without it the downtime is accepted.
F32: the ten elements, in order, plus the posture of the reference design.

F36 — Regional write-read mismatch and a read-your-writes violation

A user in Frankfurt saves a setting in EU (v11) and the next request is routed by a global load balancer to the US region because of a transient health blip. The US replica is 300 ms behind, and within that interval the read returns v10. For the user it is a save that did not stick. A second attempt on the same record may even produce a conflict, because the client's cached version is now confusing.

The contract for this has to be explicit: remote reads are eventual unless they are routed to the home region. The fixes are a session token or version floor (the write returns v11; the next read sends minVersion=11; a US replica that has not applied it forwards to EU or waits a bounded time), affinity routing so a session sticks to its home region, or choosing linearizable reads for the flows where it matters. Even then, be careful with promises: Bigtable's documentation, for example, notes that sending a workload's reads and writes to a single cluster gives read-your-writes in a replicated instance, while row-affinity routing across clusters raises the rate of read-your-writes without guaranteeing it (checked against its routing documentation). The lesson is to specify which of those you offer, not to imply it.

FacetF36 at a glance
Reference system during the incidentAvailable; the US read is stale by design, and read-your-writes needs a token or home routing.
TriggerA session's reads are served by a lagging remote replica right after a write in the home region.
Timelinet0 write at EU commits v11; t0+120 ms load balancer shifts the session to US; t0+130 ms read returns v10 (US applied index behind); t0+300 ms US catches up.
Visible symptom‘My save disappeared’ reports clustered around regional routing events; stale-read counters by region.
Root causeAsynchronous replication and routing that ignores the session's write history.
DetectionStale-read rate by region, version-floor misses, regional routing changes correlated with complaints.
ContainmentRoute session-sensitive reads to the home region; raise the stale bound.
RecoveryNothing to repair; data was never lost.
PreventionSession version tokens, region affinity, documented read modes per endpoint.
What still holdsHome-region reads remain linearizable; remote reads converge within the lag.
Cost of the fixExtra latency for home-region routing, or a wait when the replica must catch up.
F36: the ten elements, in order, plus the posture of the reference design.
Timelines

Five Worked Timelines

Five sequences that are worth following step by step, because the order of events is the whole story: two writers on one version, a replica group losing members, a delete that comes back, a region disappearing, and one key drowning a leader.

These timelines reuse incidents above (F22, F17–F20, F25, F30, F10) but slow them down to individual events. Log indexes, versions and timings are hypothetical teaching values.

Timeline 1 — Two writers on version 7

Writer W1 and Writer W2 both read atlas/user42/settings at v7 and both want to change it. Each sends Put(expectedVersion=7) at nearly the same moment, so the two requests race to the leader. The comparison runs on the leader while the entry is applied, not on the client, which is what makes the outcome unambiguous.

Swipe to pan
Compare-and-set with one winnerCAS: the version check happens inside the logBoth clients read v7 of atlas/user42/settings, then race with Put ifVersion = 7Client 1Client 2Leaderlog + stateFollowersPut ifVersion=7 (theme)Put ifVersion=7 (locale)orders: first, then secondreplicate first, commit200 OK, committed v8412: expected 7, current 8re-read v8, merge, retryWhy it is atomicThe leader evaluates ifVersion when itapplies the second to the state machine, in logorder. Exactly one of two writers with thesame expected version can succeed.Followers apply the same decision.The broken versionClient does Get, checks the version, thenPut. Between the two calls another writercan commit: a lost update.CAS must be one server-side operation.
The leader orders the two conditional writes; whichever is applied first moves the version to 8, and the second fails its precondition.
StepEventRecord versionReply to the client
1W1 and W2 read the recordv7200 with committedVersion=7 to both
2Leader receives W1's CAS(7) first, appends it as log index 412v7 (uncommitted entry at 412)Pending
3Leader receives W2's CAS(7); appended at 413v7Pending
4Entry 412 reaches a majority and commits; the state machine applies itv8W1 receives 200 with committedVersion=8
5Entry 413 is applied; its precondition (7) does not match the current v8, so no changev8W2 receives 412 Precondition Failed with currentVersion=8
6W2 rereads, merges its change onto v8, sends CAS(8)v9W2 receives 200 with committedVersion=9
Exactly one writer wins each version. A 412 is information, not a fault; the loser knows the current version and can retry safely.

Two details deserve attention. Entry 413 is still *in the log* and commits, but applying it does nothing; the failure is a deterministic outcome of the state machine, identical on every replica. And if W1's reply is lost (F03), W1 retrying CAS(7) would also get 412: the correct behavior is to reread and see its own value at v8.

Timeline 2 — RF=3 with A leader, B follower, C lagging

Replication factor 3 describes how many copies exist; it says nothing about who may decide. Here A leads term 4, B is in sync, and C trails: it has applied entries only up to 41. Majority is 2 of 3, so commitment needs A plus one other.

StepEventLog stateWhat the group can do
1A appends entry 42 (evt-024) and sends it to B and CA: 42, B: 41, C: 41Nothing committed yet
2B appends 42 and acknowledgesA: 42, B: 42, C: 412 of 3 hold 42, so A commits 42 and acknowledges the client
3A failsB: 42, C: 41Election: B and C form a majority of 3
4C asks B for a vote; B refuses because C's log is shorter. B asks C; C grants itB: 42, C: 41B wins term 5 with two votes; entry 42 is preserved
5B commits a no-op at 43, replicates it to CB: 43, C: 43 after catch-upWrites and linearizable reads resume
6B then fails too; only C remainsC: 431 of 3 is not a majority: writes return 503, linearizable reads return 503; labeled follower-stale reads may still be served
7A or B returnsLog shorter or equal; term learnedMajority restored; the returning replica catches up as a follower
Two failures in a row cost write availability. RF=3 tolerates one failure for commits, and the statement that any two can be lost while writes still succeed is false.

Timeline 3 — Delete resurrection

This is F25 as a sequence. The record is atlas/user42/legacy-flag, retained tombstones last 10 days (assumed, as in a documented Cassandra default), and replica B is offline for the delete. The design question is not *whether* the tombstone expires but whether it expires before every replica has seen it.

Swipe to pan
Tombstone garbage collected too early resurrects a deleted recordTombstone timeline and zombie resurrectionAssumed grace period 10 days (a Cassandra-style default; set it from your own repair cadence)day 0deleteday 10: GCday 11Replica Alive valuetombstone (delete marker)purgedReplica Blive valuenode down, never saw the delete, not repairedback: live valueReplica Clive valuetombstone (delete marker)purgedrepair copies itZombie: delete undoneA and C no longer have a marker, so B lookslike the only (newest) copy of the recordWhy the purge was legalAfter day 10 the tombstone was dropped by compaction.B stayed away for 11 days, longer than the grace.Rule: tombstone retention > longest time a replica can go unrepairedRepair every replica inside the grace window, or wipe and rebuild a node that was away longer.In the leader-based reference design the log and snapshot carry the delete to a returning follower;the zombie risk is sharpest in leaderless stores that rely on repair.
A purged tombstone looks the same as a record that never existed, so repair copies the older value back from the replica that missed the delete (the diagram labels that replica C; the table below uses B).
DayEventABC
0Client deletes the key; tombstone written at v12tombstone v12down; holds value v11tombstone v12
1–9B stays offline; repairs skip ittombstone v12downtombstone v12
10Tombstone retention ends; compaction drops tombstonesnothingdown; value v11nothing
11B returns; repair compares the three replicasnothingvalue v11nothing
11Repair concludes A and C are missing v11 and copies itvalue v11value v11value v11
11A Get returns 200 with a deleted recordresurrectedresurrectedresurrected
The wrong answer is acknowledged by every replica. The safe rules: retention must exceed the longest unrepaired absence; a replica older than the horizon must be rebuilt, not rejoined; and repairs must run well inside the retention window.

Timeline 4 — EU region loss with the US replica 300 ms behind

EU is the home region. US applies the EU log 300 ms late. The incident is the abrupt loss of EU at T=0, and the decisions are about what to claim.

TimeEventEU stateUS stateStatement that is true
T−0.3 sEU commits and acknowledges atlas/user42/settings v11v11v10US is behind by one write
TEU loses powerunavailablev10Up to about 3,600 acknowledged writes (12,000/s × 0.3 s) may not exist in US
T+90 sFailure confirmed from multiple vantage points; EU's lease has expiredunavailablev10Safe to consider promotion; EU cannot be leading
T+5 minUS promoted for unrestricted tenants (Atlas, Birch)unavailablev10, now leadingRPO is non-zero and equals the unreplicated tail; RTO here is minutes by assumption
T+5 minEU-resident records (Cedar) are not served from USunavailableabsent by policyResidency is preserved; those tenants are down
T+hoursEU returns; its tail with v11 is extractedv11 in logv10 plus new writesThe tail conflicts with US writes and goes to reconciliation
The write at T−0.3 s was acknowledged and may be gone from the surviving system. Asynchronous replication never gives a zero RPO; it gives one that depends on lag.
Swipe to pan
Region failure: RPO is lag-dependent exposure, and promotion needs fencingLosing the home region: RPO, promotion and fencingHypothetical inputs: 12,000 EU writes/s (30% of average 40,000) and 300 ms lagEU — lostacknowledged up tov11unreachable, state unknownUS replicahas applied up tov10≈300 ms behind at the cutExposure at the instant of loss12,000 writes/s × 0.3 s= up to 3,600 acknowledgedwrites not yet in the US.That is RPO: lag-bound, not 0.Operator decisionA. Wait for EU recoveryNo acknowledged write is lost; EU data staysin its region. Cost: EU records are read-only orunavailable for as long as the region is down.Best when the outage is short or RPO must be 0.B. Promote the US replicaAvailability returns, but up to ~3,600 writesare missing until EU data is reconciled.Needs the old leader fenced (higher epoch) soEU cannot accept writes on return, and residencyrules must permit EU records in the US.Safety rules for a promotion• Fence first: new term/epoch on the US group, so a returning EU leader cannot accept writes (no split brain).• Record the last-known EU version so lost writes (v11 and later) can be audited and replayed if EU recovers.• Do not silently serve EU-resident records from the US: residency is a policy decision, not a failover default.
The exposure window is the replication lag at the moment of failure, and it is converted into a loss only if the system promotes the lagging copy.

Timeline 5 — The hot partition

A simplified 100-partition illustration: 200,000 reads/s average, so 2,000/s per partition on average. One key, atlas/feature=checkout_policy, takes 60,000 reads/s. Assume 25,000 point reads/s per replica.

OptionCapacity for the keyUtilization at 60,000/sConsistency contractWhat it does not fix
Leader only (linearizable)25,000/s240%: saturatedLinearizableEverything else on the leader queues behind it
Leader plus two followers (follower-stale)75,000/s80%Stale, bounded by lag; labeledWrites still go through one leader
Short client or edge cache (TTL, optional)Most requests never arriveWell below budgetStale by up to the TTLNeeds invalidation thinking; not for every read
Request coalescing at the nodeOne read serves many waiting requestsReducedSame as the underlying modeDoes not help distinct keys
Key splitting (copies)Per copyDivided by number of copiesOnly for read-only dataNot for a key that is updated
Reads can be fanned out if the caller accepts the staleness; writes to one key serialize through one leader no matter how many reads are served.
Swipe to pan
Hot partition histogram: one key cannot be spread by hashingOne hot key defeats uniform hashingSimplified 100-partition illustration (the fleet plan uses 1,024); 20 of 100 bars drawn60k0reads/savg 2,000/s60,000/s (30×)partition index (hash of key picks one; a single key never splits)Why hashing can't helpThe hot key is one key, so itlives on one partition group.Fleet is 6% busy at peak, yetthis group is the bottleneck.Mitigations by operationReads (spread out)Need ≥ 3 replicas at 25,000/s:60,000 ÷ 25,000 = 2.43 × 25,000 = 75,000 → 80% busyFollower reads (labeled stale),near cache with TTL (optional),request coalescing.Writes (not spread)One key = one leader = oneserial order of writes.Salting or fan-out does not fixa single key's write rate.Key splitting only works forread-mostly data you can shard.Operate itDetect with per-key / per-partition heat metrics, notnode averages.Rate-limit the tenant, shedload, and tell the owner tocache or redesign the key.
The averages say 2,000/s; the hot key says 60,000/s, 30 times as much.
Operations

Operating the Store: Signals, Drills and Rules

Thirty-six failures reduce to a short list of signals, a handful of drills and a few standing rules. Without them, each incident above is a surprise; with them, most are routine.

Signals that deserve a page, and signals that deserve a ticket

SignalWhat it meansTypical responseIncidents
Under-replicated partitions, with ageA partition has lost a replica and is one failure from write unavailabilityPage; start or verify the rebuildF06, F09, F28
Leader changes per partition and election durationLeaders are flapping or elections are slowPage if sustained; check network and pausesF17, F18, F19, F20
Commit latency and P99 by tenantShared commit path is saturated, or compaction is behindPage on sustained breach; throttle the noisy tenantF08, F10, F11
Disk-free and growth slope per volume (target ≤ 60% full)Capacity is eroding, compaction headroom is shrinkingTicket at 70%, page at 80%F05, F06, F34
Compaction pending bytes and tables per partitionRead amplification is growingTicket; page if writes are being stalledF08
Follower lag in entries and secondsFollower-stale and read-your-writes reads will be staleRemove laggards from the read poolF21, F36
WRONG_EPOCH rate and decay after movesClients hold stale maps or the map is not propagatingTicket; page if it does not decayF14, F29
Migration state age, fenced partitionsA rebalance is stuckPage if a fence outlives its deadlineF13, F15
Cross-region replication lag and last-replicated indexThe exposure window for a regional lossTicket; page above the agreed boundF30, F36
Tenant-token versus key-prefix mismatchesAuthorization bypass or a key-composition bugPageF16, F33
Repair age and replica absence beyond the tombstone horizonResurrection riskPage when a replica passes the horizonF25
Restore drill result, including decryptionWhether backups are usable at allTicket on a missed drill; page on failureF27, F35
Signals worth watching, with the response they trigger. Thresholds are examples to tune, not benchmarks.

Service objectives work best when written about operations rather than uptime: for example the fraction of linearizable reads answered within a latency goal, the fraction of writes acknowledged after a majority flush (which should be all of them), and the age of the oldest under-replicated partition. A store that answers quickly from a stale replica has a good latency number and a bad correctness one, so the objectives should name the read mode.

Drills that exercise the failures

  • Kill a leader under peak write load and verify no acknowledged write is lost, the election time is within the target, and linearizable reads wait for the no-op commit (F17, F18).
  • Partition a leader from its followers and confirm it returns 503 while the majority side elects a new leader, then heal and check the old leader steps down (F19, F20).
  • Cut power to a node mid-write with the storage stack in its production configuration and replay the log, checking that only the tail record is affected (F02, F04).
  • Run a rebalance while a continuous writer and a checker verify every acknowledged write remains readable, and kill the controller during the fence (F13, F15).
  • Restore last night's backup into quarantine, decrypt with the production role, replay the archived log to a chosen time and verify that deleted keys stay deleted (F27, F35).
  • Flood one tenant with writes and confirm that others' P99 stays inside its objective (F11).
  • Take the control plane offline and start a new client pod (F29).
  • Simulate loss of the home region and walk the decision procedure, including the residency check and the fence (F30, F31, F32).
  • Acknowledging a write before the log flush, or before a majority has it, to win latency (F02, F17).
  • Retrying a mutating call after a timeout without an idempotency key, or treating a CAS 412 after a timeout as proof of loss (F03).
  • Trusting a tenant ID, a timestamp or a leadership belief supplied by something that could be wrong (F16, F20, F26, F33).
  • Promoting a region because monitoring cannot see it (F31).
  • Letting tombstone retention be shorter than the longest repair gap (F25).
  • Taking backups without ever restoring them (F27, F35).
  • Hand-deleting WAL segments to free disk (F05).
  • Claiming read-after-write, linearizability or zero data loss without saying which read mode, region and assumptions apply (F21, F24, F30, F36).
Key takeaways
  1. 01Durable and acknowledged are different events, and a timeout is an unknown outcome, so mutating calls need idempotency keys and CAS callers need to reread.
  2. 02Majority commit survives leader changes only because elections favor up-to-date logs, and a leader's belief in itself is never enough for reads.
  3. 03Ownership changes need an epoch, a fence and an ordered cutover; shortcuts lose acknowledged writes.
  4. 04Deletes are data: tombstone retention, repair cadence and restore procedure decide whether they stay deleted.
  5. 05Cross-region asynchronous replication gives a lag-dependent RPO and no global strong consistency; residency and fencing are policy decisions made before the incident.
  6. 06Averages hide hot keys and noisy tenants; limits belong at admission and per tenant.