Crash or Silence? Why a Dead Node Is Better News Than a Quiet One
Two incidents, same night. In the first, risk-engine segfaults — the container restarts, health checks flip to failing, the load balancer pulls it, dashboards go red in seconds. In the second, a firewall rule change silently drops 30% of packets between the payment service and its ledger: no errors, no restarts, just transactions that sometimes take 8 seconds and a retry storm nobody can explain. The crash was loud and the omission was silent — and the silent one cost four hours.
The vocabulary keeps design discussions precise. A fault is the defect or event at the component level (the crash, the dropped packet); an error is the incorrect internal state it produces (a stale balance, a truncated response); a failure is when the system's delivered service deviates from spec (the checkout 500s). Designing means: deciding which faults you tolerate, which errors you detect before they become failures, and which failures users are allowed to see.
The taxonomy you will be asked to recite:
| Fault class | What happens | Is it observable? | Typical detection |
|---|---|---|---|
| Crash (crash-stop) | Node stops executing, never returns | Yes — liveness probes fail | Health checks, heartbeats, restart |
| Crash-recovery | Node stops, later resumes with possibly lost state | Partially — did it lose memory/locks? | State validation on recovery, fencing |
| Omission — send | Node thinks it sent; message never leaves (or is dropped) | No — silence looks identical to slowness | Timeouts + sequence numbers, gap detection |
| Omission — receive | Message arrives but is dropped before processing | No | Same, plus checksums on the socket layer |
| Timing | Response arrives — early, late, or out of order | Only against a deadline | Latency SLOs, heartbeats with jitter |
| Byzantine | Arbitrary behavior: wrong values, lies, split answers | No — it may look plausible | Redundancy, cross-checks, audit |
The insight the table encodes: crash faults stop a node while omission faults silently drop messages or responses — and silence is worse than death because it is ambiguous. A crashed node cannot be confused with a healthy one. A dropped response is indistinguishable from a slow one, which is exactly the undecidable timeout from the fallacies topic, now wearing its formal name. This is why robust protocols carry sequence numbers and idempotency: a gap in sequence numbers proves omission where a timeout only suspects it, and a dedup key makes the retry that resolves it safe.
Practical consequences per class, because classification without response is trivia. Crash: run N ≥ 3 so a majority survives one death (the consensus topic's arithmetic), restart aggressively, but fence the old instance — a recovered node that still believes it holds a lock is a crash-recovery fault turned into corruption; leases and epochs (fencing tokens) deny it. Omission: add checksums and heartbeats so loss is detectable, tune retransmission with idempotent consumers, and monitor gaps rather than errors. Byzantine: raise the redundancy bar (next chapter) — or, far more often, decide honestly that you do not defend against it and say so in the design doc.
Interview framing: "what fault model does your system assume?" is the question hiding behind "how does it handle failure?" A complete answer names the classes you tolerate (crash + omission, with timeout-based detection), the classes you explicitly exclude (Byzantine — we trust our own nodes; the external API is authenticated), and one mechanism per included class (health checks, sequence numbers, fencing). Naming what you exclude is the part candidates miss, and it demonstrates that a fault model is a design decision rather than a mood.
The Slow Node: Timing Faults Where Success and Failure Coexist
The database replica answers every query — within 3 seconds, when it used to answer within 10 milliseconds. Its health check (a trivial SELECT 1, answered in 1 ms) stays green, the failover never triggers, and 20% of user requests crawl. Then the replica recovers, and the problem "fixes itself" before the on-call engineer finishes describing it. This is a timing fault: a response that succeeds late rather than a clear error — the hardest class to catch, because nothing is technically broken.
Timing faults are dangerous precisely because they violate our detection habits. Crash detectors watch liveness; omission detectors watch gaps; but a slow node passes both. Worse, slowness tends to be partial and correlated: one shard, one query shape, one network path. It spreads through the system as thread-pool exhaustion — callers pile up waiting, their own capacity disappears, and the observed failure surfaces in an unrelated service. Gray failures ("the node is up but degraded") are the incident-report name for this shape, and they are a recurring theme in post-mortems precisely because the obvious telemetry is green.
Detection has to be built deliberately, on time rather than on death:
1. latency SLOs per dependency, at p95/p99 — alert on latency BEFORE errors
(errors arrive late; slowness is the early signal)
2. heartbeats with expected cadence: 'answered SELECT 1 in 1 ms' AND
'answered the production query shape in < 150 ms' — check the real workload,
not the synthetic one
3. saturation signals: thread-pool queue depth, connection-pool wait time,
GC pause rate — the node is 'healthy' while it waits to even start
4. comparative baselines: one replica 10× slower than its siblings — the fastest
detector of partial degradation is a peer comparison, not an absolute threshold
The response policy matters as much as detection, and it differs from crash handling. You cannot restart your way out of slowness (the restart often does not reproduce it); instead, load shedding and hedging: reject or queue new work before the queue makes every request miss the deadline (protect the many for the one), and consider a hedged request — after the p95 of normal latency, fire the same read at a second replica and take the first answer — which converts tail latency from "additive" to "near-p95" for idempotent reads. For stateful writers, evict via the same fencing discipline as a crash: drain, then remove from the quorum, because a slow voter in a consensus group is a liveness bug, not a performance one.
Design-review framing: for every dependency, write down what "slow" looks like and what triggers the response — a threshold, an action, and an owner. Teams that only define "down" discover during an incident that their outage had no trigger: the system was never down, just uniformly 8× slower, and every threshold in the runbook looked at error rates that never fired. That gap — failure defined as errors, missing slowness entirely — is the timing-fault lesson, and it sets up the class nobody wants to discuss next: components that do not merely fail, but answer incorrectly.
When Nodes Lie: Byzantine Faults and Choosing Your Adversary
A Byzantine fault is arbitrary behavior: a node that returns a wrong balance with total confidence, tells two peers different versions of the same fact, or follows the protocol except for the one field that matters. The name comes from the generals coordinating an attack by messenger — some generals may be traitors, and messages may be forged — and the formal result that matters to architects: to tolerate f Byzantine faults among n replicas, you need n ≥ 3f + 1 (and quorums above 2f + 1), versus n ≥ 2f + 1 for mere crash tolerance with a simple majority.
That arithmetic is the whole design decision in one line. Three replicas tolerate one crashed node (majority of 2 survives) but zero liars — with three nodes, a traitor plus an honest-looking split can defeat a two-vote majority you cannot verify. Four replicas tolerate one liar because any correct quorum of three contains at least two honest nodes that agree. Byzantine faults involve arbitrary or malicious behavior and need stronger protocols — PBFT-style quorum exchanges with authentication, signature chains on messages, view changes when a leader misbehaves — each adding rounds of communication and cryptographic overhead to a path that, in the crash-only world, needed one append and a heartbeat.
The architect's first job is therefore to scope the adversary honestly:
| Scenario | Model that fits | Why |
|---|---|---|
| Your own services in one trust domain | Crash + omission | Nodes do not lie; they die or get partitioned — Raft is enough |
| One bad disk / memory bit flip | Crash-recovery + checksums | Corruption is caught at the storage layer, not by BFT |
| Replicas across organizations or cloud providers | Byzantine-lite | Peers can be compromised independently; authenticate and cross-check |
| Public/blockchain consensus, untrusted participants | Full Byzantine | Any participant may actively deceive — 3f+1 is the price of entry |
| Safety-critical control (avionics, pacemakers) | Byzantine + physical diversity | Independent hardware, triple modular redundancy, voters on the output |
Note what the table says about the common case: most internal systems are lying to themselves about needing BFT. A payments cluster inside one VPC with mutual TLS and a hardened supply chain should spend its budget on crash recovery, backups, and reconciliation — layers that also catch the accidental "Byzantine-ish" events (a buggy deploy writing wrong values) better than a BFT library would. Where a system does need to defend against independent trust domains, the honest engineering answer is often not "adopt PBFT" but "reduce the trust surface": authenticate every message, sign audit logs, run cross-checkers that compare outputs from independent implementations, and reconcile against an external source of truth. Diverse implementations matter because two copies of the same buggy code agree with each other perfectly — replication defends against death, not against a shared misconception.
Two clarifications interviewers probe. First, arbitrary does not mean random: a Byzantine node behaves strategically — plausible, consistent, wrong where it counts — which is why simple majorities and "the most common answer wins" heuristics fail. Second, BFT is not automatically "more reliable": it trades liveness complexity (view changes, stricter timing assumptions) for safety under adversarial faults; in partial synchrony, protocols can stall rather than commit wrongly. State both, and add your scoping rule — crash model for a single trust domain, authenticated redundancy plus reconciliation across domains, full BFT only when the participants genuinely cannot be trusted — and you have answered the fault-model question at the level it is actually asked. Next, the mechanism that must work despite all these faults: agreeing on order when there is no referee.
Quiz
1. A service's health check (`SELECT 1`) returns in 1 ms while production queries take 3 seconds and caller thread pools exhaust. Which fault class is this, and what is the correct detection?
2. Why is an omission fault generally more dangerous to diagnose than a crash?
3. How many replicas are needed to tolerate f Byzantine faults, versus f crash faults with a majority?
4. You are designing a payments cluster inside one VPC with mutual TLS and a single engineering team. Which fault model is the honest choice?
Flashcards
Question
Define fault, error, and failure, with one example each.
Click to reveal answer
Answer
Fault: the component-level cause — a segfault, a dropped packet. Error: the incorrect internal state it creates — a stale balance, a truncated response. Failure: delivered service deviates from spec — the checkout request 500s. Design names which faults it tolerates, which errors it detects, and which failures users may see.
Question
The six-class fault taxonomy, one line each.
Click to reveal answer
Answer
Crash-stop: node halts, never returns (observable — health checks). Crash-recovery: halts, resumes, state may be lost (needs fencing/leases). Omission: message silently dropped (ambiguous — needs sequence numbers + timeouts). Timing: answers early/late/out of order (needs deadlines). Byzantine: arbitrary or lying behavior (needs redundancy 3f+1).
Question
How do you detect a gray failure when liveness checks stay green?
Click to reveal answer
Answer
Measure time, not death: p95/p99 latency SLOs on the real query shape (alert before errors appear), saturation signals (pool wait, queue depth, GC pauses), and comparative baselines against sibling replicas — one node 10× slower than peers is the fastest partial-degradation signal. Respond with load shedding and hedged reads for idempotent traffic.
Question
Why does crash recovery need fencing, and what is a fencing token?
Click to reveal answer
Answer
A recovered node may still believe it holds a lock or leadership while the system moved on — its writes corrupt state that others assumed exclusive. Fencing tokens (epochs/leases) are monotonically increasing tokens granted by the coordinator; storage or downstream systems reject writes carrying a stale token, so the zombie's actions are denied regardless of its beliefs.
Question
State the 3f+1 result and what it costs in practice.
Click to reveal answer
Answer
Tolerating f Byzantine nodes requires n ≥ 3f+1 replicas with quorums above 2f+1 (so every correct quorum contains f+1 honest members). Cost: more nodes, authenticated/signed messages, extra communication rounds and view changes — traded only when participants sit in genuinely independent trust domains; a single trust domain uses 2f+1 crash-tolerant majority protocols instead.
Revision Notes
Key Takeaways
- 1. Classification drives response: crashes need restart and fencing, omissions need gap detection and idempotent retries, timing faults need latency SLOs, Byzantine needs redundancy
- 2. Silence is ambiguous and slowness is green — the two classes that evade standard monitoring are exactly the ones that cause the longest incidents
- 3. Health checks must exercise the real workload; `SELECT 1` passing while production queries take 3 seconds is the canonical gray failure
- 4. Recovery without fencing is a corruption bug: a resumed node's beliefs must be denied by epochs, not trusted
- 5. 3f+1 versus 2f+1 is the price of distrust — scope the adversary first; most internal systems should buy detection and reconciliation instead of BFT
Interview Tips
- • 'What fault model does your system assume?' — name included classes (crash + omission, timeout and gap detection) AND what you exclude (Byzantine — one trust domain) with a mechanism per class
- • 'How would you detect a failing-but-not-dead node?' — latency SLOs before error alerts, saturation metrics, peer comparison; mention hedged reads and load shedding as responses
- • 'Why 3f+1?' — quorums must contain f+1 honest members so liars cannot dominate; contrast with 2f+1 majority for crash-only, and note the communication-round cost
- • 'Tell me about a hard-to-diagnose failure' — narrate a gray failure: green health checks, real queries slow, thread pools exhausted upstream; the lesson is that 'down' was defined as errors, missing slowness
Cheat Sheet
Fault Models & Failure Modes
Chain: fault (component cause) → error (bad internal state) → failure (spec deviation seen by caller).
| Class | Signature | Detection | Response |
|---|---|---|---|
| Crash-stop | halted, never answers | health checks / heartbeats | restart, N≥3 majority survives |
| Crash-recovery | resumes, memory lost | state validation | fencing tokens/epochs — deny the zombie |
| Omission | silence, dropped msgs | timeouts + sequence gaps | idempotent retry, checksums |
| Timing | answers, too late | p95/p99 SLO on real workload, saturation, peer comparison | load shed, hedge reads, drain+evict |
| Byzantine | plausible, wrong | redundancy, cross-checks, audit | n≥3f+1, authenticated quorums |
Key asymmetry: crash = loud, omission = ambiguous (silence ≠ slowness), timing = green dashboards (health check passes while production queries take 3 s).
Gray-failure playbook: alert on latency before errors · check the real query shape, not SELECT 1 · pool wait/queue depth/GC · compare replicas to each other · hedged request after p95 for idempotent reads.
BFT scoping: single trust domain → crash+omission (Raft) · cross-org/provider → authenticate + cross-check + reconciliation · public/untrusted or safety-critical → 3f+1 with signatures. Same-buggy-code replicas agree perfectly: diversity, not count, catches shared misconceptions.
Design-doc line to write: 'we tolerate <crash+omission> via <checks/timeouts/fencing>; we explicitly exclude