BOOK DEEP-READ · CHAOS ENGINEERING · CH 6
Chaos Engineering: System Resiliency in Practice · Ch 6 · Microsoft · 2020
The last two chapters were about how to stage a drill — Slack scripting one for a legacy system, Google scaling one up to the whole company. This chapter steps back and asks something more basic: which failure should you break first?
Any moderately complex production system has more ways to break than you can count, and you only get a few dozen good experiments a year. Where should those few dozen go?
A hospital can run a thousand different tests. You have one morning.
The smart move is not to pick at random, and not to use the most expensive machine on every organ. It is to ask two things: how likely is this problem at your age and lifestyle, and how bad is it if you have it. Heart and blood pressure go first; tropical parasites go last — not because parasites are harmless, but because the odds on you are tiny. Chaos experiments get ranked by the same ruler.
Many teams start chaos work by installing a tool, then let the tool's feature list become the experiment list: it can kill machines at random, so they keep killing machines.
Two hundred kills, nothing broke. That is reassuring — but it may only mean you already knew nothing would break. The handful of things that could actually take the system down were never touched.
The other half is more awkward: a complete list is impossible to write. There are twenty or thirty dependencies, each can go wrong in several distinct ways, and multiplying gives thousands.
You spend a limited number of shots where they can actually teach you something. One more rule saves a lot of effort: do not run an experiment whose answer you already know. If you are sure it will break, do not break it — go fix it; breaking it is just scheduling your own incident. One honest cost: the ranking rests on estimates of probability and impact, and estimates are wrong, so the failures you ranked last will still come for you.
There are infinitely many ways to break and only a few dozen experiments — so the real craft of chaos engineering is not injection but ordering: start from the dependency list, favour the "slow" and "wrong data" modes, rank by likelihood × pain, and skip the ones whose answer you already know.
Want the mechanism, the numbers and the diagrams? → Switch to the deep read
You think the barrier to chaos engineering is injection capability — can you kill a process, cut a network link, add latency. This chapter moves the barrier: injection was never the bottleneck; selection is. The fault space explodes combinatorially with dependencies times failure modes, while the experiment budget is bounded on three sides at once — people, risk and time windows. So the real engineering problem is variation and prioritization: start from a complete dependency inventory, spread variation along the failure-mode gradient (dead → slow → wrong data), rank by probability × impact ÷ cost, and never run an experiment whose answer you already know — the product of an experiment is removed uncertainty, not a staged incident.
Chapter 6, the third piece in Part II on principles in practice, written from Microsoft's vantage point. It divides labour with the previous two as "how to run one" versus "which one to run": Ch4 has Slack script a drill for a legacy system, Ch5 has Google scale drills to company size, and this one steps back to ask where the budget should go. Ahead, Ch7 (LinkedIn) governs the ethical ceiling on experiments, and Ch12 uses lineage-driven fault injection (LDFI) to promote this chapter's human judgement into an algorithm. In the real world it is aimed at teams whose tooling is already in place but whose experiment list contains exactly one item: kill a random instance.
Start with the scale. For a mid-sized production service, the dependencies on a single request path run to a dozen explicit ones (database, cache, auth, a few downstream RPCs, a queue, object storage) plus a dozen implicit ones (DNS, TLS certificates, service discovery, config and feature-flag rollout, clocks, the metrics backend, the image registry). Call it twenty-five, each with at least 4 failure modes, and single-dependency single-mode candidates already number in the hundreds; allow two dependencies to fail together and it is tens of thousands; layer on load level, region and failure ratio and it keeps climbing.
The budget, meanwhile, is bounded on three sides: people (a decent experiment needs a hypothesis, metrics and an abort plan — Ch4 showed how heavy that is), risk (every production injection can turn into a real incident) and time windows (off-peak, humans in the loop). A team doing this seriously runs experiments well in the dozens per year, not the thousands.
What happens if you skip the problem? Three typical degenerations:
So the question is: in a fault space you cannot enumerate, what explainable rule picks the next ten experiments?
One relation is enough: candidate experiments ≈ dependencies × failure modes × other variation axes. Each factor is merely in the tens; multiplied, you land in the thousands. That settles it — chaos engineering cannot chase coverage the way unit tests do. It is sampling by nature, and all of its quality lives in the sampling rule.
It also explains why injection capability is not the bottleneck: your tooling can already generate far more experiments than you can run. Judgement is the scarce resource.
Where the list comes from sets your ceiling: start from what the tool can do and the list looks like a feature list; start from dependencies and it finally looks like your system.
The test is one sentence: if it breaks and your request gets slower or fails, it is a dependency. Run that filter and you usually dredge up a batch of never-tested things: DNS, TLS certificate expiry and revocation checks, service discovery, config and feature-flag rollout, NTP, the metrics backend (does a thread block when writes fail?), the image registry (used only when scaling out, so it fails precisely when you most need to scale out).
The most valuable finding hides right here: the dependency labelled "non-critical." "Analytics failing doesn't affect checkout" is a hypothesis; inject once and you often discover the call is synchronous, has no timeout, and shares a thread pool with the main path. Criticality is an experimental result, not a claim in a design doc — so "everyone says it doesn't matter" is a high-priority signal.
This is the thing to take away. For one dependency, ordered by how hard it is for fault-tolerant code to absorb, there are four rungs:
Why rung ③ is the most dangerous can be computed: at 200 QPS upstream with a downstream p99 of 50ms, and concurrency ≈ QPS × latency, steady state needs about 10 slots; degrade to 3s and you need 600 slots while the connection pool holds 50 — within seconds the pool is full and requests that never touch that downstream start failing too. A "non-critical" dependency is thereby promoted to a site-wide outage, having never returned a single error.
So the first cut of variation should follow this gradient: every dependency you have already killed still owes you two experiments — inject latency, inject wrong data. Far cheaper than finding another dependency to kill, and far more productive.
Priority = probability × impact ÷ cost. All three need inputs you can state out loud:
Two filters after the ranking save more budget than the ranking itself:
One: skip the known answers. If you are convinced it will fail (no timeout, no fallback) → file a bug and fix it, do not inject. If you are convinced it will pass and you have verified that → it does not deserve another window either. The product of an experiment is removed uncertainty, so what deserves running is exactly the batch you are confident about but have never verified — every surprise lives there.
Two: one injection collects three things. The same run can verify system behaviour, whether the alert fires, and whether people and runbooks keep up. Attaching monitoring verification costs almost nothing at the margin and is the thing most often forgotten. That also raises the priority of some "I'm confident" experiments — you run them not to watch the system but to watch the pager.
The first table lands the gradient: for one dependency, what each of the four injections verifies and why it gets skipped.
| Failure mode | What it verifies | Usual defence | Why it goes untested | Priority |
|---|---|---|---|---|
| ① Refused / fail fast | Whether breaker and fallback paths actually work | Circuit breaker, retry, default value | It isn't skipped — nearly everyone starts here | Low (likely already verified) |
| ② Timeout | Whether a timeout exists and was computed | Explicit timeouts + budget propagation | The default hides inside a client library nobody read | Medium-high |
| ③ Degradation (latency) | Whether pool saturation spreads failure to unrelated requests | Bulkheads, load shedding, adaptive concurrency | No error code; the availability dashboard is all green | Highest |
| ④ Wrong / partial data | Validation, reconciliation and cache-poisoning defences | Schema validation, checksums, reconciliation jobs | Costly to inject, and needs a correctness oracle | High (valuable but hard) |
The trap is spending the whole budget on rung ①. That is precisely the one rung you have probably already absorbed.
The second table runs the chapter's ruler over a candidate list, to show what "which few first" looks like.
| Candidate experiment | Probability from | Impact | Cost | Verdict |
|---|---|---|---|---|
| 3s latency into a "non-critical" dependency | Criticality is a claim, never verified | Possible pool saturation → site-wide | Low, one instance will do | First |
| Reproduce the last incident's failure class | Measured — it already happened | Known and quantified | Medium, but the clearest hypothesis | Near the top, doubles as regression |
| Fail a recently changed dependency | Change frequency is the best prior | Scales with the path's traffic | Low | Make it routine, put it in the pipeline |
| Kill a random instance | High, but the outcome is verified many times over | Usually unnoticed | Very low | Keep as continuous background noise |
| Clock skew / split brain | Two orders of magnitude lower on most systems | Can be severe | High, hard to orchestrate | Worth it for stateful stores |
| Inject into a call you are sure will break | — | — | — | Don't: file a bug and fix it |
Concretely:
Three lines that change review conclusions. One: the quality of chaos engineering is not injection capability, it is the quality of the sampling rule. Two: "everyone says it's non-critical" is a high-priority signal. Three: every dependency you have killed still owes you latency injection and bad-data injection.
Asked in an interview how you do chaos engineering, "we kill random instances weekly" is the tooling layer; "we complete the dependency inventory including DNS, certificates and config rollout, rank by probability × impact, inject latency into the dependencies believed non-critical first, and use the same injection to verify the alert" is another level — it shows you know the bottleneck is selection, not injection.
Evidence from industry
① Core claim: injection is not the bottleneck, selection is. Candidates ≈ dependencies × failure modes × other variation axes; three factors in the tens multiply to 10³–10⁴ while the budget is a few dozen per year — chaos engineering is sampling by nature, and all its quality is in the sampling rule.
② Build the list from dependencies, not from the tool. The test: if it breaks and your request gets slower or fails, it is a dependency — DNS, certificates, service discovery and config rollout all count.
③ Failure modes form four rungs: refused → timeout → degradation → wrong data. Fault-tolerant code is essentially only tested on rung ①; outages happen on ③ and ④.
④ Why rung ③ is lethal, in one calculation: 200 QPS × 50ms needs about 10 slots; degraded to 3s it needs 600 while the pool holds 50 — saturation turns a dependency that never errored into a site-wide outage.
⑤ The first cut of variation: every dependency you have killed still owes you latency injection and bad-data injection — much cheaper than finding a new one to kill.
⑥ Rank by probability × impact ÷ cost: the hardest input for probability is incident history, impact comes from QPS and money. A dependency called "non-critical" is a high-priority signal.
⑦ Two filters: never run a known answer (if you're sure it breaks, go fix it), and let one injection also verify the alert — near-zero marginal cost, most often forgotten.
⑧ All-green is evidence of sampling bias, not of resilience; and a ranking by expected loss needs a separate list for the low-probability, high-loss tail.