failure-models · diff
git:20260911.37b054b to git:20260923.832673e
4 added, 3 removed. Audit A to A.
---
name: failure-models
description: >
Stating a system's fault model before designing against it: crash-stop, crash-recovery,
omission, timing and Byzantine faults; partial failure and the third outcome of every
remote call (unknown); gray failure and the slow node whose health check stays green; the
eight fallacies as a checklist; blast radius, correlated versus independent failure, and
the availability arithmetic of a dependency chain. Use when a design says "if the service
is down" without defining down, when a retry is added to a call whose outcome is unknown,
when a node is slow rather than dead, when replicas share a host, an AZ or a database, or
when an availability target is quoted for a service built on ten others. Does not cover
what the model implies about messages (delivery-semantics) or reads (consistency-models),
how a failure spreads (cascading-failures), the named shapes
(distributed-failure-catalogue), what an orchestrator does with a failed pod
(kubernetes-service-lifecycle), or failures as types (java-exception-design).
---
# Failure Models
## Purpose
Make the relevant fault model explicit before relying on it in a design. Every downstream decision —
whether a retry is safe, whether a read may be stale, how many replicas are enough — is an
answer to "which faults do we tolerate?", and a design that never asked the question has
answered it by accident.
The failure this prevents is the one-word fault model. "The service can go down" is not a
- model: it silently assumes crash-stop, which lets a developer write recovery code that is
- not idempotent, treat a timeout as a definite failure, and count three replicas on one host
- as three. Naming the class turns each of those into a visible, arguable claim.
+ model: it leaves restart behavior, outstanding effects and shared failure domains unspecified.
+ Even a crash-stop peer may have applied a write before its acknowledgement was lost; the
+ fault class alone does not resolve that outcome. Naming the class and its assumptions makes
+ recovery, retry and redundancy claims reviewable.
## Workflow
1. **Inspect the contract and write or update the relevant boundary's fault-model card.**
Start with the requested operation, its success/degraded-success criteria, existing model,
client/driver behavior and deployment evidence. Reuse established facts; ask only for gaps
that change safety, recovery or the recommendation. Name the fault classes, failure domains,
synchrony assumption, recovery source, detection mechanism, and maximum tolerated
combination. Crash-stop, crash-recovery, omission, timing and Byzantine are not labels
for the whole system: a trusted database replica may be crash-recovery while an
Internet-facing client is arbitrary or hostile. A process that restarts from durable
state is crash-recovery; any in-flight operation whose completion was not durably
recorded must be retried, reconciled, or abandoned by an explicit rule.
2. **Distinguish known applied, known not applied and unknown effects.** Name the effect
boundary: a multi-effect operation can be partly complete, so preserve each effect's
status instead of calling the whole operation rejected or rolled back. A read timeout,
reset after dispatch or missing broker ack normally leaves the effect unknown. Decide
what happens on unknown: repeat safely, reconcile, or escalate. Proven non-application
can permit retry even for a non-idempotent operation, subject to cause and budget; a
rejected later attempt does not clear an earlier unknown effect. See
`references/the-unknown-outcome.md`.
3. **Check a relevant gray failure.** For example, a node passes its health check while its
responses miss the caller's deadline. Assess detection, resource occupancy and remaining
capacity under the actual load; distinguish a tolerable slowdown from a violated contract.
4. **Draw the failure domains.** For a process, a host, a rack, an AZ, a dependency and a
deploy, write what a specified fault can affect. Replicas sharing a domain fail together for that
cause; they may still tolerate independent process faults.
5. **Do conditional availability arithmetic** on the request path before promising a
number. Required dependencies in series multiply availability only when their events are
independent and their SLI windows and success definitions align; genuinely independent
redundant alternatives multiply _unavailability_. Correlated and conditional failure
needs a measured joint distribution or an explicit common-cause model. See
`references/failure-domains-and-arithmetic.md`.
6. **Walk the eight fallacies as a checklist** — reliable network, zero latency, infinite
bandwidth, secure network, unchanging topology, one administrator, zero transport cost,
homogeneous network. Use code patterns as investigation leads: check whether an overall
deadline bounds a client without a separate read timeout, admission bounds queue growth,
and discovery/connection refresh handles topology changes. Report the effective behavior
and violated contract, not a defect inferred from one missing API call.
Inspect the actual client/driver, retry, durability and deployment configuration before assigning
outcomes. The conceptual Java type uses sealed classes/records (Java 17); exhaustive pattern
switch without preview requires Java 21. These examples do not set a platform requirement;
preserve the project's target. Deliver the relevant card, evidence versus assumptions, outcome
and recovery policies, and a recommendation compared with retaining the current design and
any materially relevant alternative. Name what evidence would change it. Include a focused
fault-injection case with an observable invariant; distinguish proposed checks from executed
results. Missing protocol or topology evidence means a conditional claim, not a replica count
or availability promise.
## Fault classes
```text
Assume crash-stop when:
- the algorithm may treat a stopped participant as never returning. Replacing its process
identity with a fresh replica does not make the original participant crash-recovery.
Assume crash-recovery when:
- the same logical participant can return after a crash and recover durable state — local
log, database rows, committed offsets, epochs or leases. Volatile state is lost; durable
state may lag acknowledged work unless the durability contract proves otherwise.
Assume omission (a message or a response silently lost) when:
- the underlying transport, queue, proxy or load balancer can drop sends or receives.
A higher-level reliable-channel abstraction may mask omissions, but its retry,
deduplication and terminal-failure assumptions then become part of the model.
Assume timing/performance failure when:
- correctness or usefulness depends on a deadline. A correct-but-late response can be a
failure to the caller. Allocate the caller's end-to-end deadline across attempts,
queueing and cleanup; a timeout is not automatically useful merely because it is shorter.
Include Byzantine faults when:
- input crosses a trust boundary — a client, another tenant, a third party — where a
participant may send arbitrary, inconsistent or hostile data. Input validation protects
an API but does not make its replication protocol Byzantine-fault tolerant. Under common
quorum protocols, tolerating `f` crash failures typically needs `2f+1` voting members and
Byzantine agreement commonly needs `3f+1`; the exact bound depends on synchrony,
authentication, quorum and protocol assumptions. State those assumptions instead of
transplanting a replica count.
```
## Fault-model card
For every important operation, make these fields reviewable:
| Field | Question that must have an answer |
| ------------------ | ---------------------------------------------------------------------------------------------------- |
| Safety invariant | What must remain true even during a partition, retry or recovery? |
| Liveness condition | Under which timing and quorum assumptions must progress resume? |
| Faults tolerated | Crash-stop, crash-recovery, send/receive omission, delay, corruption, arbitrary peer? |
| Bound | How many simultaneous faults, and in which independent domains? |
| Detector | Timeout, lease, heartbeat, quorum observation, operator signal? Which false suspicion is acceptable? |
| Durable truth | Which log, row, offset, epoch or manifest reconstructs state after restart? |
| Ambiguous effect | How is an unknown outcome deduplicated, queried, reconciled or escalated? |
| Recovery objective | What RTO/RPO and backlog-drain time are required, and under what load? |
| Re-entry | How is a recovered or partitioned participant fenced before it can mutate state again? |
Do not merge **fault**, **error** and **failure**. A fault is the hypothesised cause; an error
is incorrect internal state; a failure is externally visible deviation from the service
contract. One host reboot can cause ten failed requests: count the incident once and the ten
request failures in a request-based SLI. A masked reboot may cause no service failure. State
the counting unit, and do not dismiss a latency SLO failure because responses were eventually
correct.
## Rules
- **Partial failure creates outcome uncertainty.** A local exception does not imply rollback
either; remote calls additionally decouple caller observation from peer execution. A remote
call can leave you not knowing — and that third outcome is where many
distributed bugs arise. Code that maps a timeout onto "the effect never applied" has erased it:
a timeout alone does not establish the callee's state, and the callee may complete the
work after the caller gave up.
- Crash-recovery makes the recovery path a correctness surface. For each recovery step,
state whether it is idempotent and under which key. The mechanics are `idempotency`; the
requirement to have an answer is here.
- **A slow node can be more damaging than a dead one.** A definitively stopped endpoint is
eventually excluded; a slow endpoint may retain traffic and consume caller threads,
connections and deadline budget. But aggressive suspicion can eject a healthy node and
destroy quorum or capacity. That the system's view of health can differ from the client's
— _differential observability_ — is why "fail fast" is a policy, not a fact. In a fully
asynchronous network a detector cannot distinguish crash from unbounded delay; practical
systems assume some eventual timing bound and trade false suspicion against detection
delay. Record that trade-off for readiness checks, leases and failover.
- **Redundancy must name the fault it tolerates.** Three replicas on one host cannot survive
losing that host, but may tolerate an individual process crash. Ask what they share: host, rack, AZ, control plane,
image, config, deploy, certificate, downstream dependency. A shared deploy is the one most
often missed — rolling one bad artefact to every replica introduces a common cause, which
makes deploy strategy an availability control rather than a release convenience.
- **Adding a required dependency multiplies availability under an independence model and
therefore increases total unavailability.** Ten independent dependencies at 99.9% in
series produce about 99.0% path availability. For a time-based SLI on a 365-day year,
that corresponds to roughly 87 unavailable hours, not 8.8; request failure fractions do
not directly convert to outage hours. Real incidents are often correlated, so use this as a comparison model, not a
forecast. Either define a tested degraded mode that removes the dependency from the
required path, or stop quoting the higher number. Redundant alternatives multiply
unavailability only under independence; common causes set an unavailability floor.
## Decision framework
```text
If the operation crosses a process boundary:
classify timeout/cancellation/disconnect as Unknown unless protocol or authoritative
state establishes the relevant effect's outcome.
If progress requires suspecting a peer:
separate suspicion from authority to act; preserve the protocol's safety assumptions.
Tune detection for recovery speed and false suspicion, and state any timing/clock bounds
required for safety, especially with leases.
If replicas share any host, zone, control plane, deploy, credential or dependency:
model that cause once as a common failure domain;
do not multiply replica availability as if independent.
If stale or recovered writers could violate the invariant:
identify how the protected resource rejects their effects, for example through a validated
fencing protocol or atomic conditional write. Verify the actual claim/effect contract;
a client-side check followed by a pause and an unchecked write is not enforcement.
Detailed lease/fencing protocol design belongs to distributed-locks-and-leases.
If the design claims availability during partition:
state which operations remain safe, which side may progress, and what reconciliation
occurs after healing. "The service stays up" is not a consistency contract.
```
## Failure injection and recovery proof
Select fault cases relevant to the card. Before injection, establish an isolated or explicitly
authorized target, observable invariants, blast radius, abort conditions and recovery steps:
- inject loss separately before send, after apply/before acknowledgement, and during
response transfer; assert downstream state and duplicate count;
- pause a process and add latency/jitter rather than testing only clean termination;
- partition asymmetrically (`A` reaches `B`, `B` cannot reach `A`) and isolate data plane
from control plane;
- crash after every durable-write boundary, restart from persisted state, and verify the
safety invariant plus bounded recovery;
- expire credentials, deploy incompatible versions, exhaust pools/disk/file descriptors,
and restore backups into an isolated environment;
- run at realistic load: failover that takes 20 seconds when idle can create hours of
recovery backlog at saturation.
Observe detection latency, false-positive rate, unknown outcomes, duplicate/reconciliation
counts, quorum loss, recovery backlog, RTO and recovered data point (RPO). A test that only
asserts the client exception does not validate the distributed outcome.
## Anti-patterns
| Anti-pattern | Why dangerous / symptom | Better alternative |
| ------------------------------------ | ----------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| One fault model for the whole system | Trust and durability assumptions leak across boundaries | Model each operation and participant role, then compose them |
| Timeout means rollback | Retried writes duplicate after lost acknowledgements | Preserve `Unknown`; use idempotency, status lookup or reconciliation |
| Health check means truth | Gray/asymmetric failures stay green or healthy nodes flap | Compare client-view signals; define detector error costs |
| Replica count means availability | Common deploy, zone or datastore defeats all replicas | Draw domains and measure joint/common-cause failures |
| Failover equals recovery | Traffic moves but stale owners write, data is missing, backlog explodes | Fence old owners; prove state recovery and capacity during catch-up |
| Chaos without invariants | Generates outages but no falsifiable learning | Declare safety/liveness hypotheses, blast radius and abort conditions first |
## References
- [The unknown outcome](references/the-unknown-outcome.md) — the three-outcome model in Java,
how a JDBC, HTTP and Kafka call each maps onto it, and what an unknown write forces the
design to provide. Read when adding a retry, handling a timeout, or writing across a
process boundary.
- [Failure domains and availability arithmetic](references/failure-domains-and-arithmetic.md)
— series and parallel composition worked through, correlated failure, and the questions
that expose a hidden shared dependency. Read when promising an availability number, sizing
replicas, or reviewing a topology.
- [Chubby locks and sequencers](https://static.usenix.org/events/osdi06/tech/full_papers/burrows/burrows_html/)
and [Redis lock assumptions](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/)
— consult when checking stale-owner enforcement and a lease protocol's timing assumptions.