Skip to content
About
Main content

Why failure recovery is a different kind of problem

Why can a system not simply find out what happened?

The authority disappears

Think of any system that works normally. Somewhere there is a component that knows the truth: a log that either contains the commit record or does not, a transaction manager that knows whether it committed, an accountant that holds the byte count. When we reason about the system, we ask that authority.

Fault tolerance is the part where the authority goes away in the middle of a sentence, and the component that must decide what to do next cannot find out what already happened. This is not a weakness of some product. It is a property of asynchronous networks (networks where messages can take any amount of time): a machine that is slow and a machine that is dead produce the same observation, which is silence. Every is therefore a timeout, and a timeout is a guess.

Here is the constraint that governs this mechanism:

What stops mattering in this setting: whether the recovery path is fast. Recovery paths can be slow because they run rarely. But that permission is exactly what makes them dangerous: rarely run code is rarely tested (we return to this in Recovery mechanisms). What starts mattering: how many times an effect can be applied without changing the result.

The governing question is not "how do we avoid failure?". Failure cannot be avoided. The governing question is:


Three error classes, and only one is interesting

Every failure a component sees belongs to one of three classes. The mechanisms of this mechanism exist because the third class exists.

  1. Deterministic or permanent. The same input will fail again. Examples: a message with a broken format (a ""), a schema violation, a bad partition key. Retrying is pure waste. The only choices are to skip, quarantine (set aside for a human) or fix.

  2. Transient and independent across attempts. A dropped packet, a spot instance (a cheap cloud machine that the provider can take back) being taken away, a lock timeout. A retry will probably work. If each attempt fails with probability ε and attempts are independent, then n attempts all fail with probability εⁿ.

    Worked example. ε = 0.01 (1% of calls fail). Three attempts all fail with probability 0.01³ = 10⁻⁶, one in a million.

  3. Ambiguous. The request may or may not have taken effect. The timeout fired, but a timeout tells you about your observation, not about the remote state.

The first two classes need almost no machinery. The third class generates nearly all of it. Retry, failover, task re-execution, and delivery are all the same move: act as if the operation did not happen. , deduplication windows, , fencing and are all the same payment: make acting twice indistinguishable from acting once.

This is Wanjiru's payment call. The timeout means: "I did not hear back." The provider may have charged the card and the reply was lost (then a retry charges twice), or it may never have received the request (then not retrying means never billing). Both of her complaints come from the same ambiguity, and no retry loop, however well written, can remove it.


Three quantities: RPO, RTO and the duplication factor

The fault-tolerance mechanisms are organized into three groups, and each group bounds one quantity.

Group (part)What it suppliesWhat it bounds
Durability (Persistence & Durability)something left to redo from: how much work can be lost
Recovery mechanisms (Recovery mechanisms)the act of redoing: how long until service resumes
Correctness under failure (Making a repeat harmless)the guarantee that redoing is safethe : how many times an effect can land

The first group is a mechanism of its own; this one covers the other two, and adds how a system keeps answering while a node is gone (Availability: masking a lost node). Read in a list, these look like three unrelated bags of techniques. Read as RPO, then RTO, then duplication, they are one argument: you chose how much you can lose; then you chose how fast you come back; and the third group is what the first two cost you in meaning.


The arithmetic that governs this mechanism

Two formulas decide most design questions.

Availability is dominated by repair time

average time between failures
detect + decide + act
Data table
Availability by repair time (one failure a month)
Repair time MTTRAvailability
60 s100%
65.7 s100%
72 s100%
78.8 s100%
86.3 s100%
94.5 s100%
104 s100%
113 s100%
124 s100%
136 s100%
149 s100%
163 s100%
179 s100%
196 s100%
214 s100%
235 s100%
257 s100%
281 s100%
308 s100%
337 s100%
370 s100%
405 s100%
443 s100%
485 s100%
532 s100%
582 s100%
638 s100%
698 s100%
765 s100%
838 s100%
917 s100%
1,000 s100%
1,100 s100%
1,200 s100%
1,320 s99.9%
1,450 s99.9%
1,580 s99.9%
1,730 s99.9%
1,900 s99.9%
2,080 s99.9%
2,280 s99.9%
2,490 s99.9%
2,730 s99.9%
2,990 s99.9%
3,280 s99.9%
3,590 s99.9%
3,930 s99.8%
4,300 s99.8%
4,710 s99.8%
5,160 s99.8%
5,650 s99.8%
6,190 s99.8%
6,780 s99.7%
7,420 s99.7%
8,130 s99.7%
8,900 s99.7%
9,750 s99.6%
10,700 s99.6%
11,700 s99.6%
12,800 s99.5%
14,000 s99.5%
15,400 s99.4%
16,800 s99.4%
18,400 s99.3%
20,200 s99.2%
22,100 s99.2%
24,200 s99.1%
26,500 s99%
29,000 s98.9%
31,800 s98.8%
34,800 s98.7%
38,100 s98.6%
41,800 s98.4%
45,700 s98.3%
50,100 s98.1%
54,800 s97.9%
60,100 s97.7%
65,800 s97.5%
72,000 s97.3%
78,900 s97%
86,400 s96.8%

When is small compared to , unavailability is approximately MTTR / MTBF.

Everyone who can chooses to divide MTTR.

MTTR itself has three parts: detect + decide + act. Detection usually dominates. A failover that takes 20 seconds to execute, but sits behind a 5-minute health-check window, is a 5-minute outage. The most valuable term in fault tolerance, detection, is really the job of monitoring and failure-detection systems rather than of the recovery mechanism. That is the first uncomfortable fact: the part you can build here is not the largest part of downtime.

Checkpointing has a clean optimum

A saves state so that recovery does not start from zero. Taking checkpoints costs time; not taking them means you lose more work when a crash comes. Let:

  • C = the cost (time) of taking one checkpoint,
  • I = the interval between checkpoints,
  • MTBF = average time between failures.

Expected overhead per unit time is:

I (checkpoint interval)
— how long you wait (or how much you gather) before paying the fixed cost
C (cost of one checkpoint)
— cost paid once per interval (a rewrite, an fsync)
1 / (2·MTBF) (lost work per unit interval)
— cost per unit of interval that accumulates while you wait (reads slowed by deltas, time spent waiting)

The first term is the price of taking checkpoints. The second is the expected lost work: a failure arrives, on average, in the middle of an interval, so on average you redo I/2 of work, and failures happen once per MTBF. Minimizing this gives

I* = √(2 · C · MTBF)

The shape matters more than the numbers. The optimum moves as a square root. If you are wrong by a factor of 4 about the failure rate, the best interval changes only by a factor of 2, and the overhead near the minimum is flat. , which is why almost nobody tunes it and almost nobody suffers.