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.
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.
Failure map: all 36 incidents
| ID | Incident (hypothetical) | Area | Posture of the reference design | Invariants |
|---|---|---|---|---|
| F01 | Process crash before the WAL append | Host & storage | Rejecting (retryable); no false success | I1, I6 |
| F02 | Crash after the append but before the flush | Host & storage | Safe if acks wait for flush; lossy only under an unsafe config | I1 |
| F03 | Crash after commit, before the response | Host & storage | Available; outcome unknown to the client | I1, I6, I11 |
| F04 | Torn or corrupt WAL entry | Host & storage | Available after truncation to the last valid record | I1 |
| F05 | Disk full on the leader | Host & storage | Rejecting writes (503); reads continue | I1, I12 |
| F06 | Disk full on a follower | Host & storage | Available with reduced fault tolerance (2 of 3) | I12 |
| F07 | SSTable checksum corruption | Host & storage | Available if another replica is healthy; the corrupt copy errors, never lies | I1 |
| F08 | Compaction backlog and the P99 spike | Host & storage | Available but slow; at the extreme, writes are throttled | I8 |
| F09 | Snapshot corruption and repair from another replica | Host & storage | Available (two healthy replicas); the rebuilding replica is excluded | I1, I8 |
| F10 | A hot key saturates one partition leader | Partitioning | Available but overloaded for the leader's partition; neighbors pay | I8, I12 |
| F11 | A hot tenant saturates its neighbors | Multi-tenancy | Available; fairness violated until per-tenant limits engage | I7, I8 |
| F12 | New node added with unsafe modulo remapping | Partitioning | Unavailable/incorrect for moved keys until reverted | I2 |
| F13 | Rebalance interrupted halfway through | Partitioning | Available if the move is a resumable state machine; unsafe if the fence is open-ended | I2, I8 |
| F14 | Stale partition-map epoch sends writes to the old owner | Partitioning | Rejecting with a redirect; correct after one extra round trip | I2 |
| F15 | Split or migration race loses an update when fencing is absent | Partitioning | Unsafe without fencing; safe with fence-then-flip | I2, I3 |
| F16 | A key replicated into the wrong namespace | Partitioning | Available; confidentiality and integrity broken for the affected keys | I7 |
| F17 | Leader crashes before quorum commit | Replication | Unavailable for a brief election; committed data intact | I3 |
| F18 | Leader crashes after commit, new leader must honor it | Replication | Available after election; reads wait for the new leader's first commit | I3 |
| F19 | A minority network partition attempts to accept writes | Replication | Minority side rejects writes (503); majority side available | I4 |
| F20 | A stale leader fails to be fenced | Replication | Safe for writes via terms; reads depend on the lease or read-index rule | I3, I4 |
| F21 | A follower serves a stale read during lag | Replication | Available; reads stale (labeled) for follower-stale mode | I3 |
| F22 | Concurrent updates and the lost-update trap | Replication | Available; correct only if the compare-and-set is part of the replicated write | I11 |
| F23 | Leaderless divergent siblings and conflict resolution | Alternative design | Available (AP-style); concurrent writes diverge until reconciled | I11 |
| F24 | The quorum-overlap claim fails under sloppy replacements | Alternative design | Available (AP-style); read-after-write not guaranteed despite R+W>N | I11 |
| F25 | A delayed tombstone resurrects an older value | Data lifecycle | Available; deleted data returns until reconciled | I5, I10 |
| F26 | Clock skew alters last-write-wins and TTL results | Data lifecycle | Available; wrong winners and early or late expiry | I5, I11 |
| F27 | Failed restore reintroduces deleted secrets | Ops & security | Available; semantic safety broken until deletes are replayed | I5, I10 |
| F28 | Replica repair overwhelmed by foreground traffic | Ops & security | Available with degraded latency; repair slower than planned | I8 |
| F29 | Control-plane or map-service outage: warm versus cold clients | Ops & security | Available for warm clients; cold starts fail; no rebalancing | I2, I12 |
| F30 | Cross-region replication lag after an abrupt disaster | Geo | EU unavailable; US available and stale (v10) if promoted | I9 |
| F31 | Incorrect regional failover creates split-brain | Geo | Unsafe: two regions accept writes for the same keys | I9 |
| F32 | Region-residency policy blocks an otherwise convenient failover | Geo | Unavailable for EU-resident tenants; available for others | I9, I7 |
| F33 | A multi-tenant scan bypasses authorization via a caller-supplied tenant ID | Ops & security | Available; confidentiality broken for the affected endpoint | I7 |
| F34 | Key-size abuse or oversized writes exhaust memory and disk | Ops & security | Rejecting oversize requests; available otherwise | I12 |
| F35 | Backup encryption key inaccessible during restore | Ops & security | Available while healthy; no recovery path until the key is regained | I1, I12 |
| F36 | Regional write-read mismatch and a read-your-writes violation | Geo | Available; remote reads stale (eventual) unless routed home | I9, I3 |
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.
| Facet | F01 at a glance |
|---|---|
| Reference system during the incident | Available after restart or failover; nothing to repair, nothing lost, because nothing was promised. |
| Trigger | The server process dies between request validation and the first WAL append. |
| Timeline | t0 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 symptom | Connection reset or timeout on one request; a short burst of 503/UNAVAILABLE from the partition while it has no leader. |
| Root cause | A process boundary sits between 'request received' and 'record durable'; the client was never told the write happened. |
| Detection | Process-exit and restart counters, OOM-kill events, leader-election count per partition, client timeout rate by partition. |
| Containment | Nothing to contain beyond keeping retries bounded with jitter so recovery is not met with a storm. |
| Recovery | Client retries with the same idempotency key; the new leader sees no record and applies it as new. |
| Prevention | Memory limits and admission control so the process is not killed under load; idempotency keys on every mutating call. |
| What still holds | No acknowledged write is lost (I1) and no write applies twice (I6). |
| Cost of the fix | Idempotency keys must be stored and retained for a documented window, and clients carry a retry loop. |
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.
| Facet | F02 at a glance |
|---|---|
| Reference system during the incident | Available 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. |
| Trigger | Power loss or kernel panic after write() has returned but before the log was flushed to stable storage. |
| Timeline | t0 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 symptom | Unacked 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 cause | Durability was not established before the acknowledgement; page cache is volatile, and volatile device caches can break an honest flush. |
| Detection | A 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. |
| Containment | If acked-then-lost writes are possible, freeze writes on affected nodes and compare acknowledged request IDs from client logs with replayed sequence numbers. |
| Recovery | In 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. |
| Prevention | Flush before acknowledging, group commit for throughput, hardware that honours flush, and a periodic power-pull test. |
| What still holds | With flush-before-ack on a majority, an acknowledged write survives any single node's power loss. |
| Cost of the fix | Acknowledgement latency includes a flush; group commit trades a millisecond of waiting for a flush that is shared. |
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.
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.
| Facet | F03 at a glance |
|---|---|
| Reference system during the incident | Available once a leader exists; the committed write is intact and may be returned from the dedup table. |
| Trigger | The leader crashes or loses its connection after the commit but before the client receives 200 with committedVersion. |
| Timeline | t0 commit at majority; t0+1 ms A dies; t0+500 ms client times out; t0+800 ms retry hits the new leader. |
| Visible symptom | A 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 cause | Commit and response are separate events; the network cannot tell the client which of them occurred. |
| Detection | Timeout-then-success rates by endpoint, repeat idempotency-key hits, 412s whose current value equals the attempted value. |
| Containment | None needed in the store; if clients are retrying hard, apply load shedding so the retry wave is bounded. |
| Recovery | Same-key retry; for CAS, reread and compare to the intended value before treating 412 as a lost race. |
| Prevention | Require idempotency keys on non-idempotent mutations, store them in the replicated state machine, and document the retention window. |
| What still holds | The write applies at most once for any key still inside its retention window. |
| Cost of the fix | Dedup state costs memory and replication traffic; after the window expires the guarantee lapses, so the window must outlast the client retry horizon. |
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.
| Facet | F04 at a glance |
|---|---|
| Reference system during the incident | Available if two intact replicas remain; a single damaged replica is excluded and rebuilt. |
| Trigger | Power loss during an append, or media corruption of an earlier log segment. |
| Timeline | t0 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 symptom | Tail tear: a replay log line and nothing user-visible. Mid-log corruption: the replica refuses to start and drops out of the quorum. |
| Root cause | Appends are not atomic at the device level; checksums detect the break, but only sequence numbers reveal gaps. |
| Detection | Replay checksum-failure counters split by tail versus middle, replicas excluded from voting, segment scrub results. |
| Containment | Do not hand-edit the log. Quarantine the damaged replica and check the group still has a majority. |
| Recovery | Tail tear: automatic truncation. Mid-log: wipe the replica's state and rebuild from a snapshot plus the leader's log. |
| Prevention | Checksums and sequence numbers per record, periodic log scrubbing, storage with end-to-end integrity where available. |
| What still holds | Committed entries remain on the other replicas; a corrupt copy never gets to vote with bad data. |
| Cost of the fix | Checksums cost a little CPU per record and scrubbing costs background I/O. |
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.
| Facet | F17 at a glance |
|---|---|
| Reference system during the incident | Writes pause until a new leader is elected, then resume; the minority-side copy of an uncommitted entry may be applied or discarded. |
| Trigger | The leader dies after replicating to fewer than a majority of replicas. |
| Timeline | t0 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 symptom | A timeout for the client, a 503 burst for the partition, and sometimes a successful Get shortly after the timeout. |
| Root cause | A log entry is not committed until a majority holds it; a leader's local append proves nothing. |
| Detection | Election count and duration, term bumps, the gap between last-log-index and commit-index at failover. |
| Containment | None; keep clients from retrying without idempotency keys while the election runs. |
| Recovery | Clients retry with the same key; the new leader dedups if entry 42 was kept. |
| Prevention | Randomized election timeouts to avoid split votes; idempotency keys. |
| What still holds | Anything acknowledged was committed and is never lost; anything unacknowledged may or may not be present. |
| Cost of the fix | Short unavailability on every leader loss, plus the client burden of treating timeouts as unknown. |
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.
| Facet | F18 at a glance |
|---|---|
| Reference system during the incident | Linearizable reads and writes pause briefly during election; the acknowledged write is preserved. |
| Trigger | A leader fails after a majority held the entry but before the client was answered, then B takes over. |
| Timeline | t0 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 symptom | Some clients see a timeout then 200 on retry; reads against the group briefly return 503 or wait. |
| Root cause | The old leader's commit knowledge was in its memory; the new one must rebuild it safely. |
| Detection | Time from leader loss to first successful linearizable read; reads rejected with 'not ready'. |
| Containment | Do not widen read modes to ‘serve something’ unless the API contract labels the answer stale. |
| Recovery | Resume normal service; the no-op commit marks recovery complete. |
| Prevention | Vote rules that favor up-to-date logs; no-op on election; read-index or bounded leases. |
| What still holds | Every acknowledged write is visible to every later linearizable read. |
| Cost of the fix | A small, bounded pause on every failover. |
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.
| Facet | F05 at a glance |
|---|---|
| Reference system during the incident | Reads served; writes for the affected partitions rejected; no false success. |
| Trigger | Disk usage on the leader's volume reaches the hard limit during an import. |
| Timeline | t0 usage 78%; t0+40 min 94%; t0+55 min 100%; appends fail; leadership moves or writes return 503 until space is freed. |
| Visible symptom | 503/UNAVAILABLE for writes to the partitions on that volume; compaction stalls; latency of reads rises as files pile up. |
| Root cause | Ingest outpaced both compaction and capacity planning; the engine has no space for its own cleanup. |
| Detection | Disk-free percentage per volume and its growth slope, compaction debt, append error counts. |
| Containment | Throttle the importing tenant (429), shift leadership to a replica with room, free the reserved headroom for compaction. |
| Recovery | Move some partition replicas to other nodes, add capacity, then resume writes and let compaction catch up. |
| Prevention | Plan 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 holds | No write is acknowledged without durable space; reads stay correct. |
| Cost of the fix | Idle headroom: the 60% target means paying for disk the steady state does not use. |
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.
| Facet | F06 at a glance |
|---|---|
| Reference system during the incident | Available and correct, but only a single further replica failure from rejecting writes. |
| Trigger | The follower's volume fills while the leader's does not. |
| Timeline | t0 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 symptom | No client errors; under-replicated-partition count rises; leader log retention grows; commit latency may creep if C was the faster follower. |
| Root cause | Placement put a growing partition next to a full volume; majority commit hid a dead third replica. |
| Detection | Under-replicated partitions, follower lag in entries and seconds, disk-free on followers. |
| Containment | Free space on C or evict other replicas from it; cap log retention for the lagging follower. |
| Recovery | Restore C via snapshot when it has space; verify it rejoined the voting set. |
| Prevention | Alert on under-replicated partitions as a page, not a ticket; balance placement by disk headroom, not just count. |
| What still holds | Committed data is held by two replicas; the partition tolerates no more failures while C is out. |
| Cost of the fix | Alerting on invisible degradation produces pages for incidents that clients never felt. |
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.
| Facet | F07 at a glance |
|---|---|
| Reference system during the incident | Available through replicas; the damaged replica returns errors for the affected range rather than wrong values. |
| Trigger | Silent media corruption of an SSTable block, discovered by a read, compaction or a scrub. |
| Timeline | t0 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 symptom | A handful of retried reads and a corruption counter; if all copies were damaged, errors for that key range. |
| Root cause | Storage returned bytes that differ from what was written, and only an end-to-end checksum can notice. |
| Detection | Block checksum failure counters, scrub results per volume, repairs per day. |
| Containment | Take the table out of read service; do not compact it until repaired. |
| Recovery | Stream the key range from a healthy replica at the same commit point and rebuild the table; replace the device if failures cluster. |
| Prevention | Verify 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 holds | Corrupt data is never returned as valid; repair needs at least one healthy replica. |
| Cost of the fix | Scrubbing and verification cost I/O and CPU. |
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.
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.
| Facet | F08 at a glance |
|---|---|
| Reference system during the incident | Available with elevated read latency; writes to the affected partitions may be throttled by the engine. |
| Trigger | A sustained write burst, above the compaction budget, perhaps while repair also streams data. |
| Timeline | t0 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 symptom | Rising P99 and P99.9 for reads, then write latency, then 503/429 from throttling; compaction pending bytes rising. |
| Root cause | Foreground write volume exceeded the rate at which background merging reduces read and space amplification. |
| Detection | Pending compaction bytes, tables per partition, write-stall counters, read P99 per partition and per tenant. |
| Containment | Throttle the heavy writer, pause repair or rebalance traffic, grant compaction more I/O budget within limits. |
| Recovery | Let compaction drain; verify table counts and P99 return to baseline before restoring the throttle. |
| Prevention | Capacity for write amplification (the page's assumed 10× planning factor), per-tenant ingest quotas, compaction rate limits that keep foreground P99 visible. |
| What still holds | No data is lost or reordered; reads remain correct, only slower. |
| Cost of the fix | Throttling a writer is a visible policy cost; extra compaction I/O budget slows foreground reads. |
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.
| Facet | F09 at a glance |
|---|---|
| Reference system during the incident | Available; C is rebuilt without becoming a voter until its state is verified. |
| Trigger | A snapshot transfer to a lagging or replaced replica is corrupted in transit or on disk. |
| Timeline | t0 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 symptom | A snapshot-failed event and a longer under-replicated period; no client-visible effect. |
| Root cause | Bytes changed between the source state and the installed state; installation without verification would turn that into permanent divergence. |
| Detection | Snapshot verification failures, rebuild duration, under-replicated partition age. |
| Containment | Keep the target as a learner; pick a different source if failures repeat. |
| Recovery | Reinstall from a healthy replica at the same or later commit index, then replay the log. |
| Prevention | Hash every chunk and the whole snapshot, record the last-included index and term, install only after verification. |
| What still holds | Committed data stays correct; a replica that fails verification never votes. |
| Cost of the fix | Verification adds a read pass over the snapshot and delays catch-up. |
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.
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.
| Facet | F27 at a glance |
|---|---|
| Reference system during the incident | Available; restored data is wrong in a security-relevant way until the delete is re-applied. |
| Trigger | An older backup is restored over newer state; deletes between the backup and the failure are not in it. |
| Timeline | Sun 02:00 backup taken; Mon 10:00 delete; Tue 09:00 restore from Sunday's backup; Tue 09:40 the token authenticates again. |
| Visible symptom | A deleted credential works again; audit logs show a Get of a key with a delete event on record. |
| Root cause | A backup captures state, not intent; deletes after its timestamp are not part of the restored data. |
| Detection | Post-restore reconciliation of the deletion ledger against restored keys; audit queries for keys with a ledger delete after the backup's timestamp. |
| Containment | Quarantine the restored partition before it serves, or block the specific secret namespaces. |
| Recovery | Replay WAL archive to the incident-minus-epsilon time; re-apply ledger deletes; rotate any secret that was live again. |
| Prevention | Point-in-time recovery, a deletion ledger, per-tenant key encryption so deletion can mean key destruction, restore drills that include deletes. |
| What still holds | After reconciliation, the logical state matches the pre-incident state; the old value may persist in old backups until retention expires. |
| Cost of the fix | Keeping a WAL archive and a ledger costs storage, and restores take longer because replay is part of the procedure. |
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.
| Facet | F28 at a glance |
|---|---|
| Reference system during the incident | Available; latency elevated; fault tolerance reduced on 32 partitions until the rebuild completes. |
| Trigger | A node is lost and replaced during peak traffic; rebuild streams compete with foreground I/O. |
| Timeline | t0 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 symptom | Under-replicated partitions peaking at 32; read and write P99 up on source nodes; compaction debt on targets. |
| Root cause | Repair and foreground work share the same disks and network, and no bandwidth policy separated them. |
| Detection | Rebuild throughput, per-node I/O utilization, under-replicated partition age, foreground P99 on source nodes. |
| Containment | Lower repair rate if P99 breaches the objective; avoid concentrating sources on one node. |
| Recovery | Resume at a higher rate off-peak; confirm replicas are caught up and rejoined. |
| Prevention | Bandwidth caps with priority classes, spreading rebuilds across sources, headroom planned for one node loss, rebuild-time budgets in the SLO. |
| What still holds | Committed data remains correct; only the margin for a second failure and latency suffer. |
| Cost of the fix | A slower rebuild extends the exposure window; a faster one costs foreground latency. |
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.
| Facet | F35 at a glance |
|---|---|
| Reference system during the incident | Normal service continues; disaster-recovery capability is silently absent until tested. |
| Trigger | A key policy change, scheduled deletion or key-service outage blocks decryption of backups. |
| Timeline | t0 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 symptom | Restore job fails with an access-denied or key-not-found error, while all backup jobs show success. |
| Root cause | Backups were verified for existence and size but never for decryptability by the restore role. |
| Detection | Scheduled restore drills with decrypt verification, alerts on key policy changes and deletion schedules, key-version inventory. |
| Containment | Cancel any pending key deletion; restore the role's access; recover the key from escrow if necessary. |
| Recovery | Decrypt and restore after access is regained; re-verify the full chain with a drill. |
| Prevention | Regular restore drills including decryption, two-person approval and waiting periods for key deletion, key versions retained through retention, escrow with tested access. |
| What still holds | Backup contents are confidential and intact; availability of the restore path depends on key custody. |
| Cost of the fix | Drills and escrow take operator time; split custody slows emergency access. |
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.
| Facet | F12 at a glance |
|---|---|
| Reference system during the incident | Wrongly routed reads return 404 and writes diverge for about 75% of keys until the mapping is reverted. |
| Trigger | The routing function's denominator changes while data still sits where the old function put it. |
| Timeline | t0 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 symptom | A 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 cause | Placement was a function of node count, so changing the count changed the owner of most keys. |
| Detection | 404 rate versus baseline, correlation with the router config change, per-node key-count shifts. |
| Containment | Roll the router back; freeze membership changes. |
| Recovery | Reconcile any divergent writes by version; replay moves through the safe cutover procedure. |
| Prevention | Virtual partitions with a versioned partition map; no placement function that depends on the node count. |
| What still holds | After revert, data placed by the old mapping is intact; only writes during the window need reconciliation. |
| Cost of the fix | A partition map is another piece of metadata to protect, version and distribute. |
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.
| Facet | F13 at a glance |
|---|---|
| Reference system during the incident | Writes to one partition pause for the fence period; the rest of the cluster is unaffected. |
| Trigger | The migration controller crashes between fencing the source and publishing the new epoch. |
| Timeline | t0 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 symptom | 503 for P7 writes for a bounded interval; a migration shown as ‘stuck in fence’ on the dashboard. |
| Root cause | A multi-step ownership change was not atomic and had no timeout on its unsafe intermediate state. |
| Detection | Migration state age by phase, fenced-partition count, writes rejected by the fence. |
| Containment | Let the fence timeout expire, or complete the flip if the destination is verified; pause further migrations. |
| Recovery | Restart the controller from the persisted state; resume or roll back, then delete partial copies. |
| Prevention | Persisted migration state machine, fence deadline, throttled concurrent moves. |
| What still holds | One owner per partition at all times; no acknowledged write is lost. |
| Cost of the fix | A short fence is a tiny write-unavailability window on every move. |
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).
| Facet | F14 at a glance |
|---|---|
| Reference system during the incident | Stale clients are redirected, not served; no write lands on a non-owner. |
| Trigger | A client acts on a cached map epoch that predates an ownership change. |
| Timeline | t0 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 symptom | A burst of WRONG_EPOCH responses after each rebalance, decaying as caches refresh; slightly higher latency for affected clients. |
| Root cause | Routing information is replicated by caching, so some readers are always a little behind the truth. |
| Detection | WRONG_EPOCH rate and its decay time after a move, clients stuck on old epochs, redirect-loop counters. |
| Containment | None needed unless the rate does not decay; check the map distribution path. |
| Recovery | Clients refresh and retry with the same idempotency key. |
| Prevention | Epoch on every request, owner-side epoch checks, bounded retry on redirect. |
| What still holds | No write is accepted by a non-owner. |
| Cost of the fix | Extra latency for stale callers and mandatory client map-refresh logic. |
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.
| Facet | F15 at a glance |
|---|---|
| Reference system during the incident | Silent data loss if the cutover has no fence; none if the cutover follows fence, catch up to the final index, flip. |
| Trigger | Ownership flips while the source can still accept writes that the destination has not applied. |
| Timeline | t0 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 symptom | A later Get returns an older version than one acknowledged earlier; version regressions on one partition after a rebalance. |
| Root cause | Two nodes were authoritative for an interval; the handoff did not carry the last acknowledged entries. |
| Detection | A continuous correctness checker, version-regression alerts, source and destination last-index comparison at cutover. |
| Containment | Stop migrations; identify moved partitions in the affected window. |
| Recovery | Recover missing entries from the retired source's log while it still exists, then re-apply in order. |
| Prevention | Fence, catch up to the final index, then flip; keep the old copy for a rollback window. |
| What still holds | With the correct protocol every acknowledged write exists on the new owner before it serves. |
| Cost of the fix | A brief write pause at cutover and a retention period for old copies. |
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.
| Facet | F16 at a glance |
|---|---|
| Reference system during the incident | Available and wrong: reads and writes succeed but are shared across tenants. |
| Trigger | A client change omits the tenant prefix when composing keys. |
| Timeline | t0 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 symptom | Users see another tenant's settings; versions jump unexpectedly; keys appear without a tenant prefix. |
| Root cause | Isolation depended on client-composed keys rather than the server enforcing namespace from identity. |
| Detection | A scan for keys without a valid tenant prefix, cross-tenant canary reads, anomaly checks on version jumps. |
| Containment | Roll back the client; block writes to keys without tenant prefix at the store. |
| Recovery | Find the affected key range by prefix and version history; restore from the WAL archive or backup; notify affected tenants. |
| Prevention | Server-side tenant namespace from verified credentials, key-shape validation, canary tests in CI. |
| What still holds | After enforcement, no tenant can address another's keyspace; previously overwritten values are recoverable only as far as history was retained. |
| Cost of the fix | Server-side namespacing makes the API a little less flexible and tenant IDs mandatory in every layer. |
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.
| Facet | F29 at a glance |
|---|---|
| Reference system during the incident | Warm clients serve normally; cold clients fail to bootstrap; topology changes are frozen. |
| Trigger | The control-plane service holding the partition map becomes unreachable. |
| Timeline | t0 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 symptom | Pod start failures on map fetch, a stale-map-age gauge climbing, no change in data-plane error rates. |
| Root cause | A metadata dependency sat on the bootstrap path of clients, and nobody had designed a degraded mode for it. |
| Detection | Map age at clients, bootstrap failure rate, control-plane health, migrations paused. |
| Containment | Freeze rebalances and deploys of new clients; extend cache lifetimes. |
| Recovery | Restore the control plane; resume migrations after verifying map consistency. |
| Prevention | Seed-node bootstrap, long-lived map caches, a replicated control plane, no hard dependency on it for steady-state requests. |
| What still holds | Ownership is unchanged while it is down, so single-owner semantics hold. |
| Cost of the fix | More client logic and a map-serving path on storage nodes. |
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.
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.
| Facet | F10 at a glance |
|---|---|
| Reference system during the incident | Available; the hot partition's leader is saturated and its other keys see higher latency. |
| Trigger | A single key receives 60,000 reads/s, above any one replica's assumed 25,000/s point-read budget. |
| Timeline | t0 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 symptom | One partition's CPU and P99 spike while fleet averages stay low; top-key report shows one key at 30× average. |
| Root cause | Load concentrates on a key, and linearizable access to one key is served by one leader. |
| Detection | Per-key and per-partition request rates, top-N key sampling, leader CPU and queue depth. |
| Containment | Enable stale-labeled follower reads for that key if the contract allows, add a short TTL cache, and shed or throttle abusers with 429. |
| Recovery | Keep the cache; confirm the leader is below budget and queues drained. |
| Prevention | Hot-key detection, documented read modes for config-like data, cache and coalescing guidance for clients. |
| What still holds | Linearizable reads stay correct for callers that choose them; stale-labeled reads are bounded and visibly stale. |
| Cost of the fix | Reads become stale by the TTL or follower lag, a semantic change the caller must accept. |
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.
| Facet | F11 at a glance |
|---|---|
| Reference system during the incident | Available for all tenants; the noisy tenant is throttled, others recover. |
| Trigger | One tenant's sustained write burst consumes shared commit and compaction capacity. |
| Timeline | t0 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 symptom | Elevated P99 for Birch and Cedar correlated with Atlas's write rate; Atlas receives 429. |
| Root cause | Isolation was by logical namespace only; the physical resources underneath were shared and not metered per tenant. |
| Detection | Per-tenant request rate, queue time and P99 by tenant, 429 rates by tenant. |
| Containment | Throttle the noisy tenant first; avoid global shedding that penalises quiet tenants. |
| Recovery | Raise Atlas's quota deliberately or move its partitions; verify neighbors' P99 returned to baseline. |
| Prevention | Per-tenant quotas at admission, weighted fair queuing, dedicated placement for very large tenants, accounting of compaction and repair per tenant. |
| What still holds | Correctness is unaffected; fairness is an enforced policy, not a given. |
| Cost of the fix | Quotas may throttle legitimate bursts; dedicated placement costs more hardware. |
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.
| Facet | F33 at a glance |
|---|---|
| Reference system during the incident | Available; the scan endpoint is exploitable until fixed. |
| Trigger | A client supplies another tenant's ID in the scan request and the server uses it. |
| Timeline | t0 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 symptom | Audit records where token tenant and scanned tenant differ; unusually large scans by a single token. |
| Root cause | Tenant identity was taken from request input instead of the authenticated principal. |
| Detection | Token-tenant versus key-prefix mismatch alerts, scan-size anomalies, security review of every endpoint taking a tenant parameter. |
| Containment | Disable or restrict the endpoint, revoke suspect credentials. |
| Recovery | Fix tenant derivation, scope exposure from audit logs, notify affected tenants per policy. |
| Prevention | Derive the tenant from credentials, clamp ranges, reject empty prefixes, authorize at the storage layer too. |
| What still holds | After the fix, a credential can reach only its own prefix; already-read data cannot be un-read. |
| Cost of the fix | A stricter API and slightly more server-side checking. |
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.
| Facet | F34 at a glance |
|---|---|
| Reference system during the incident | Rejecting requests that exceed limits; the rest of the service is unaffected if limits and quotas hold. |
| Trigger | A client sends many maximum-size values, or keys beyond the limit. |
| Timeline | t0 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 symptom | Ingest bytes per second rise 256× with a flat request rate; flush size and disk usage surge; 413 or 429 responses. |
| Root cause | Size limits were not enforced early or not metered in bytes. |
| Detection | Request-size histograms by tenant, ingest bytes per second, flush size, disk-growth slope. |
| Containment | Reject oversize at the edge; apply a byte-rate quota to the tenant. |
| Recovery | Delete or compact away the unwanted records; confirm disk and memtable pressure return to normal. |
| Prevention | Hard limits before the WAL, per-tenant byte quotas, pointer-to-object-store pattern for large payloads. |
| What still holds | Accepted writes are within contract; rejected writes are explicit. |
| Cost of the fix | Some legitimate large payloads need a second storage path. |
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.
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.
| Facet | F19 at a glance |
|---|---|
| Reference system during the incident | Majority side serves reads and writes; minority side rejects writes and linearizable reads, and offers only labeled stale reads. |
| Trigger | A network partition isolates the leader with a minority of replicas. |
| Timeline | t0 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 symptom | 503 for clients on A's side; leader change visible on the majority side; term numbers diverge until heal. |
| Root cause | Majority commit requires two of three replicas; the isolated leader cannot reach one. |
| Detection | Term changes, leaders-per-partition counters (should stay at one effective leader), unreachable-peer counts, write rejections by cause. |
| Containment | Nothing to force; the protocol is already doing the safe thing. Route clients to the majority side. |
| Recovery | Heal the network; confirm A rejoined as a follower and caught up. |
| Prevention | Failure-domain placement (replicas in separate racks or zones), client retries with redirect, clear 503 semantics. |
| What still holds | No acknowledged write is lost and no two leaders commit at once (I4). |
| Cost of the fix | Clients on the minority side lose write availability, which is the price of consistency. |
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.
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.
| Facet | F20 at a glance |
|---|---|
| Reference system during the incident | Writes safe via term checks; a stale leader's local reads may be wrong unless protected by read-index or a valid lease. |
| Trigger | A leader pauses past the election timeout and resumes believing it is still the leader. |
| Timeline | t0 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 symptom | Term-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 cause | A process cannot observe that time passed while it was paused; its belief in leadership outlived the lease. |
| Detection | Rejected-append-for-old-term counters, long-pause detectors, linearizability checker (e.g. Jepsen-style tests) in staging. |
| Containment | Fence the old leader by killing or restarting it if it does not step down; prefer read-index mode for sensitive reads. |
| Recovery | A steps down on seeing the higher term and resyncs its log. |
| Prevention | Term checks on every message, read-index or conservative leases, pause detection, fencing tokens for external actions. |
| What still holds | A stale leader cannot commit; linearizable reads hold if read-index or a valid lease is used. |
| Cost of the fix | Read-index costs a round trip per read batch; leases depend on a bounded clock drift. |
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.
| Facet | F21 at a glance |
|---|---|
| Reference system during the incident | Available; the read result is stale by design, and read-your-writes needs a version floor. |
| Trigger | A reader is served by a follower whose applied index trails the committed index. |
| Timeline | t0 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 symptom | A user sees their own change missing for seconds; follower-lag gauge elevated; reads with minVersion routed to the leader. |
| Root cause | Followers apply the log asynchronously and may be behind the commit point. |
| Detection | Per-follower lag in entries and seconds, stale-read ratio by mode, read-your-writes forwards. |
| Containment | Remove laggards from the read pool; route session reads to the leader. |
| Recovery | Let C catch up; verify lag returns inside the bound. |
| Prevention | Read modes named in the API, version-floor tokens, a staleness bound for the follower pool. |
| What still holds | Linearizable and read-your-writes reads remain correct; follower-stale reads are labeled. |
| Cost of the fix | Forwarded reads cost leader capacity; waiting costs latency. |
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.
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).
| Facet | F22 at a glance |
|---|---|
| Reference system during the incident | Available; unconditional writes lose updates, conditional writes serialize them. |
| Trigger | Two clients perform read-modify-write on one record without a version precondition. |
| Timeline | t0 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 symptom | A field silently reverts; two acknowledged writes, only one visible; no error anywhere. |
| Root cause | The read and the write were separate non-atomic steps with no precondition. |
| Detection | Audit 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). |
| Containment | Switch hot record paths to conditional writes; add a retry loop with a cap. |
| Recovery | Replay the lost field from the log if the user reports it. |
| Prevention | CAS with expectedVersion for records edited by multiple writers; merge logic on 412. |
| What still holds | With CAS, exactly one writer wins each version; the loser always learns. |
| Cost of the fix | More client code and some contention retries. |
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.
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.
| Facet | F23 at a glance |
|---|---|
| Reference system during the incident | ALTERNATIVE design: available under partitions, with divergent copies and application-level merges. |
| Trigger | Two writes to one key reach different replica subsets with no causal ordering between them. |
| Timeline | t0 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 symptom | Reads that return multiple siblings (or silently, one version under LWW); sibling-count metrics rising on hot keys. |
| Root cause | Without a single serialization point, concurrent writes can both succeed. |
| Detection | Sibling counts and size per key, LWW overwrites where timestamps are within a skew bound, anti-entropy divergence reports. |
| Containment | Pause automated writers on hot keys; apply a deterministic merge function. |
| Recovery | Merge siblings, write the result with the merged context, and let repair converge. |
| Prevention | Pick 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 holds | Replicas converge eventually once the merge is applied; no linearizability is claimed. |
| Cost of the fix | Merge logic is application code, and siblings grow if clients never resolve them. |
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.
| Facet | F24 at a glance |
|---|---|
| Reference system during the incident | ALTERNATIVE design: stale reads possible after acknowledged writes; no false claim of linearizability. |
| Trigger | A write is accepted via a stand-in node while a preference-list replica is down; a later read hits replicas that lack it. |
| Timeline | t0 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 symptom | A client reads its own acknowledged write and gets the previous one; hint backlog and divergence reports non-zero. |
| Root cause | Quorum overlap is a property of strict quorums over a stable membership, not of sloppy ones with hints. |
| Detection | Hint queue depth and age, read-repair and anti-entropy mismatch rates, staleness probes. |
| Containment | Run repair to propagate; for sensitive keys use strict quorums or the leader-based path. |
| Recovery | Deliver hints, run anti-entropy, verify convergence. |
| Prevention | State the model honestly in the API; strict quorums where needed; scheduled anti-entropy repair, not hints alone. |
| What still holds | After repair replicas converge; no read-after-write promise holds under sloppy quorums. |
| Cost of the fix | Strict quorums reduce availability when preference-list nodes are down. |
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.
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.
| Facet | F25 at a glance |
|---|---|
| Reference system during the incident | Available; the logical state is wrong for the resurrected keys, which is a correctness failure rather than a visible outage. |
| Trigger | A replica that missed a delete returns after the tombstone has been garbage-collected. |
| Timeline | Day 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 symptom | A deleted record reappears; reads of it return 200 instead of 404; the version is older than the delete. |
| Root cause | The tombstone expired before every replica had seen it. |
| Detection | Alerts for replicas absent longer than the GC horizon, deleted-key reappearance audits, repair age. |
| Containment | Quarantine the returned replica; stop repair from it; re-delete affected keys. |
| Recovery | Wipe and rebuild any replica older than the horizon; re-apply a deletion ledger for the window. |
| Prevention | Tombstone retention greater than maximum repair interval plus outage tolerance; routine repair inside that window; refuse stale replicas. |
| What still holds | Within the horizon, the tombstone wins and deleted values stay deleted. |
| Cost of the fix | Longer retention keeps tombstones, and scanning cost, for longer. |
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.
| Facet | F26 at a glance |
|---|---|
| Reference system during the incident | Available; the winner of a conflict, or the moment of expiry, can be wrong by up to the skew. |
| Trigger | Physical timestamps from unsynchronised clocks decide ordering or expiry. |
| Timeline | 12: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 symptom | A 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 cause | Wall-clock time is not an ordering mechanism; skew is unbounded unless monitored. |
| Detection | Clock offset monitoring, writes whose timestamps are far from server time, LWW overwrite audits. |
| Containment | Reject or clamp client timestamps; alert on skew over a limit. |
| Recovery | Reapply the lost update from logs where available; fix the clock source. |
| Prevention | Server- or leader-assigned versions, hybrid logical clocks, TTL assigned at commit and replicated as a state change, monitored time synchronization. |
| What still holds | Ordering is determined by log position, not by anyone's clock. |
| Cost of the fix | Less flexibility for clients to set their own timestamps; clock monitoring remains necessary for TTL. |
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.
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.
| Facet | F30 at a glance |
|---|---|
| Reference system during the incident | Reads and writes available in US after promotion; data up to the lag window may be missing (RPO > 0). |
| Trigger | The home region fails abruptly with a non-empty replication backlog. |
| Timeline | t0 EU lost; t0+90 s failure declared; t0+5 min US promoted at v10; t0+hours EU tail recovered for reconciliation. |
| Visible symptom | Users see older versions; dashboards show US applied index behind the last EU acknowledged index; a gap recorded at promotion. |
| Root cause | Asynchronous replication acknowledges a write before the remote copy exists. |
| Detection | Replication lag per region, last-replicated index, promotion log of the boundary. |
| Containment | Promote US only under an approved decision with fencing (F31); communicate the lost window. |
| Recovery | Reconcile the recovered EU tail against US writes; restore EU as a replica, not as the leader, until caught up. |
| Prevention | Measure and alert on lag, minimize it for critical tenants, consider synchronous replication or a second in-region copy for stricter RPO. |
| What still holds | Writes up to the last replicated index are present; later ones are exposure. |
| Cost of the fix | Tighter RPO (synchronous cross-region commit) costs latency on every write. |
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.
| Facet | F31 at a glance |
|---|---|
| Reference system during the incident | Unsafe until one region is fenced; correctness is restored by choosing a surviving history. |
| Trigger | A failover is triggered by a monitoring-path failure while the old home region is alive. |
| Timeline | t0 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 symptom | Two leaders for the same partitions in a global view; the same version number carrying different content in each region. |
| Root cause | Unreachable was treated as dead, and ownership was not tied to an expiring lease. |
| Detection | Dual-leader detector, conflicting-version audit, ownership epoch mismatches. |
| Containment | Fence the region that should lose (stop its leaders and reject at the edge); stop automatic failover. |
| Recovery | Choose the surviving history; extract the other side's writes as conflicts for tenants; rebuild the loser as a replica. |
| Prevention | Lease-based regional ownership, multi-signal and approved failover, regular failover drills. |
| What still holds | After fencing and reconciliation, one history is authoritative; writes in the abandoned history are not silently merged. |
| Cost of the fix | Slower failover (waiting for lease expiry) and operational ceremony. |
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.
| Facet | F32 at a glance |
|---|---|
| Reference system during the incident | Available for unrestricted tenants after failover; EU-resident tenants reject requests rather than breach residency. |
| Trigger | The home region fails and the only existing remote replica is in a region the tenant's policy excludes. |
| Timeline | t0 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 symptom | Cedar requests fail with a documented error while others recover; an incident-status banner explains why. |
| Root cause | A technically possible action violates a legal or contractual constraint. |
| Detection | A policy check at the failover tooling, residency tags on tenants and partitions, audit of data location. |
| Containment | Do not move restricted data; communicate the status. |
| Recovery | Restore from in-jurisdiction capacity when available; verify the data stayed within bounds. |
| Prevention | A second in-region failover target, residency-aware tooling, contractual availability expectations stated in advance. |
| What still holds | Residency holds; the availability promise for restricted tenants is lower in a regional disaster. |
| Cost of the fix | Extra regional capacity costs money; without it the downtime is accepted. |
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.
| Facet | F36 at a glance |
|---|---|
| Reference system during the incident | Available; the US read is stale by design, and read-your-writes needs a token or home routing. |
| Trigger | A session's reads are served by a lagging remote replica right after a write in the home region. |
| Timeline | t0 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 cause | Asynchronous replication and routing that ignores the session's write history. |
| Detection | Stale-read rate by region, version-floor misses, regional routing changes correlated with complaints. |
| Containment | Route session-sensitive reads to the home region; raise the stale bound. |
| Recovery | Nothing to repair; data was never lost. |
| Prevention | Session version tokens, region affinity, documented read modes per endpoint. |
| What still holds | Home-region reads remain linearizable; remote reads converge within the lag. |
| Cost of the fix | Extra latency for home-region routing, or a wait when the replica must catch up. |
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.
| Step | Event | Record version | Reply to the client |
|---|---|---|---|
| 1 | W1 and W2 read the record | v7 | 200 with committedVersion=7 to both |
| 2 | Leader receives W1's CAS(7) first, appends it as log index 412 | v7 (uncommitted entry at 412) | Pending |
| 3 | Leader receives W2's CAS(7); appended at 413 | v7 | Pending |
| 4 | Entry 412 reaches a majority and commits; the state machine applies it | v8 | W1 receives 200 with committedVersion=8 |
| 5 | Entry 413 is applied; its precondition (7) does not match the current v8, so no change | v8 | W2 receives 412 Precondition Failed with currentVersion=8 |
| 6 | W2 rereads, merges its change onto v8, sends CAS(8) | v9 | W2 receives 200 with committedVersion=9 |
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.
| Step | Event | Log state | What the group can do |
|---|---|---|---|
| 1 | A appends entry 42 (evt-024) and sends it to B and C | A: 42, B: 41, C: 41 | Nothing committed yet |
| 2 | B appends 42 and acknowledges | A: 42, B: 42, C: 41 | 2 of 3 hold 42, so A commits 42 and acknowledges the client |
| 3 | A fails | B: 42, C: 41 | Election: B and C form a majority of 3 |
| 4 | C asks B for a vote; B refuses because C's log is shorter. B asks C; C grants it | B: 42, C: 41 | B wins term 5 with two votes; entry 42 is preserved |
| 5 | B commits a no-op at 43, replicates it to C | B: 43, C: 43 after catch-up | Writes and linearizable reads resume |
| 6 | B then fails too; only C remains | C: 43 | 1 of 3 is not a majority: writes return 503, linearizable reads return 503; labeled follower-stale reads may still be served |
| 7 | A or B returns | Log shorter or equal; term learned | Majority restored; the returning replica catches up as a follower |
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.
| Day | Event | A | B | C |
|---|---|---|---|---|
| 0 | Client deletes the key; tombstone written at v12 | tombstone v12 | down; holds value v11 | tombstone v12 |
| 1–9 | B stays offline; repairs skip it | tombstone v12 | down | tombstone v12 |
| 10 | Tombstone retention ends; compaction drops tombstones | nothing | down; value v11 | nothing |
| 11 | B returns; repair compares the three replicas | nothing | value v11 | nothing |
| 11 | Repair concludes A and C are missing v11 and copies it | value v11 | value v11 | value v11 |
| 11 | A Get returns 200 with a deleted record | resurrected | resurrected | resurrected |
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.
| Time | Event | EU state | US state | Statement that is true |
|---|---|---|---|---|
| T−0.3 s | EU commits and acknowledges atlas/user42/settings v11 | v11 | v10 | US is behind by one write |
| T | EU loses power | unavailable | v10 | Up to about 3,600 acknowledged writes (12,000/s × 0.3 s) may not exist in US |
| T+90 s | Failure confirmed from multiple vantage points; EU's lease has expired | unavailable | v10 | Safe to consider promotion; EU cannot be leading |
| T+5 min | US promoted for unrestricted tenants (Atlas, Birch) | unavailable | v10, now leading | RPO is non-zero and equals the unreplicated tail; RTO here is minutes by assumption |
| T+5 min | EU-resident records (Cedar) are not served from US | unavailable | absent by policy | Residency is preserved; those tenants are down |
| T+hours | EU returns; its tail with v11 is extracted | v11 in log | v10 plus new writes | The tail conflicts with US writes and goes to reconciliation |
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.
| Option | Capacity for the key | Utilization at 60,000/s | Consistency contract | What it does not fix |
|---|---|---|---|---|
| Leader only (linearizable) | 25,000/s | 240%: saturated | Linearizable | Everything else on the leader queues behind it |
| Leader plus two followers (follower-stale) | 75,000/s | 80% | Stale, bounded by lag; labeled | Writes still go through one leader |
| Short client or edge cache (TTL, optional) | Most requests never arrive | Well below budget | Stale by up to the TTL | Needs invalidation thinking; not for every read |
| Request coalescing at the node | One read serves many waiting requests | Reduced | Same as the underlying mode | Does not help distinct keys |
| Key splitting (copies) | Per copy | Divided by number of copies | Only for read-only data | Not for a key that is updated |
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
| Signal | What it means | Typical response | Incidents |
|---|---|---|---|
| Under-replicated partitions, with age | A partition has lost a replica and is one failure from write unavailability | Page; start or verify the rebuild | F06, F09, F28 |
| Leader changes per partition and election duration | Leaders are flapping or elections are slow | Page if sustained; check network and pauses | F17, F18, F19, F20 |
| Commit latency and P99 by tenant | Shared commit path is saturated, or compaction is behind | Page on sustained breach; throttle the noisy tenant | F08, F10, F11 |
| Disk-free and growth slope per volume (target ≤ 60% full) | Capacity is eroding, compaction headroom is shrinking | Ticket at 70%, page at 80% | F05, F06, F34 |
| Compaction pending bytes and tables per partition | Read amplification is growing | Ticket; page if writes are being stalled | F08 |
| Follower lag in entries and seconds | Follower-stale and read-your-writes reads will be stale | Remove laggards from the read pool | F21, F36 |
| WRONG_EPOCH rate and decay after moves | Clients hold stale maps or the map is not propagating | Ticket; page if it does not decay | F14, F29 |
| Migration state age, fenced partitions | A rebalance is stuck | Page if a fence outlives its deadline | F13, F15 |
| Cross-region replication lag and last-replicated index | The exposure window for a regional loss | Ticket; page above the agreed bound | F30, F36 |
| Tenant-token versus key-prefix mismatches | Authorization bypass or a key-composition bug | Page | F16, F33 |
| Repair age and replica absence beyond the tombstone horizon | Resurrection risk | Page when a replica passes the horizon | F25 |
| Restore drill result, including decryption | Whether backups are usable at all | Ticket on a missed drill; page on failure | F27, F35 |
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).
- 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.
- 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.
- 03Ownership changes need an epoch, a fence and an ordered cutover; shortcuts lose acknowledged writes.
- 04Deletes are data: tombstone retention, repair cadence and restore procedure decide whether they stay deleted.
- 05Cross-region asynchronous replication gives a lag-dependent RPO and no global strong consistency; residency and fencing are policy decisions made before the incident.
- 06Averages hide hot keys and noisy tenants; limits belong at admission and per tenant.