Failure & Recovery in Production

The Steady-State Hypothesis and the Abort Condition

An experiment on a live system has six parts: a hypothesis, a measured steady state, an injected failure, an observation, an abort condition and a conclusion. The abort condition is not optional paperwork — it is the entire difference between an experiment and an outage you caused on purpose.

▶ Run the lab

The question this answers

The question

How do I test a failure assumption on a real system without the test becoming the incident?

The guarantee — the property claimed, and its scope

A bounded experiment guarantees exactly two things: that the deviation from steady state is measured against a baseline recorded before the injection, and that the injection stops automatically when a pre-declared threshold is crossed. It guarantees nothing about the system under test — that is the point of running it.

Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.

What a node knows — observation versus inference

The experiment runner knows the metrics it is subscribed to and the injection it has applied. It does not know whether a deviation it observes was caused by its injection or by something unrelated happening at the same moment — a deploy, a traffic spike, another team’s experiment. Attribution is inference, not observation, which is why an experiment needs a pre-recorded baseline and a control period rather than a before-and-after glance.

A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.

What guarantee?What does a node know?How does it work?What can fail?How does it fail?Where is coordination?What holds under failure?How does it recover?How would you know?What is the simpler thing?
chaos engineeringexperimentssteady stateabort condition

The six parts, in order

An experiment that is written down has six parts, and skipping any of them turns it into something else. Hypothesis: a falsifiable statement about what the system will do. Steady state: the measurable normal behaviour, recorded *before* anything is touched. Injection: the specific fault, at a specific blast radius, for a specific duration. Observation: the metric that decides the hypothesis. Abort condition: the threshold at which the injection is automatically reversed. Conclusion: what was learned, including "the hypothesis held", which is a real result.

The hypothesis must be falsifiable and specific. "The system is resilient to a node failure" is not a hypothesis; it cannot be shown false by any measurement. "When one of the three API replicas is killed, checkout success rate stays above 99.5% and p99 stays under 800ms for the full five minutes" is a hypothesis, because a single run can refute it.

Note the direction of the hypothesis. You state what you *expect*, then try to break the expectation. An experiment run without a stated expectation cannot fail — whatever happens, someone will explain afterwards why that was the expected behaviour. This is the operational version of pre-registration, and it exists for the same reason.

The experiment loop, with the abort path that makes it an experiment
baseline recordedyes — this is the findingno — keep observingduration elapsedHypothesis (falsifiable)Measure steady stateInject fault (bounded radius)Observe vs baselineAbort condition crossed?Auto-revert injectionConclusion (held / refuted)
ClientGateway / LBServiceWorkerDatabaseCacheQueue / LogObject storageCDNExternal system

If you cannot measure steady state, that is the finding

The second step defeats more experiments than any other. To claim the system deviated, you need a number that describes normal, with its normal variance, before you touch anything. Not "it feels fine" — a measured distribution: checkout success rate at 99.7% ± 0.1 over the last fourteen days, p99 at 480ms ± 60, queue depth under 200.

Teams routinely discover at this step that no such number exists. The success-rate metric mixes two products; the latency metric is an average, so a doubled tail is invisible in it; the queue-depth gauge is scraped every 60 seconds and the interesting behaviour lasts 20. Stop there and report it. "We cannot measure our steady state" is a more valuable result than any injection would have produced, because it means that during a real incident nobody will be able to tell whether the system is recovering.

It is also the cheapest finding available. You reached it with zero risk to production, before injecting anything, and the work it generates — a real SLI, a percentile instead of a mean, a scrape interval shorter than the phenomenon — is work you needed anyway. This is the honest answer to "we are not mature enough for chaos engineering": the first experiment you cannot run is already telling you what to fix.

# usable: a distribution, a window, and a variance
checkout_success_rate   99.71%  +/- 0.08  (14d, 5m buckets)
checkout_p99_ms            482  +/- 61    (14d, 5m buckets)
order_queue_depth           <200          (never above 340 in 14d)

# not usable, and each for a different reason
avg_latency_ms             120   <- a mean; hides exactly the tail we are testing
success_rate            "good"   <- not a number
queue_depth_gauge       scraped every 60s <- shorter than the effect we expect
error_rate           mixes checkout + browse <- deviation cannot be attributed
A steady state worth writing down, versus one that is not

The abort condition is what makes it an experiment

An abort condition is a pre-declared threshold, expressed in the same metrics as the steady state, that automatically reverses the injection when crossed. It must be automatic. A human watching a dashboard is not an abort condition — during the ten seconds it takes to notice, interpret and act, the thing you were worried about has already happened, and the human is the person least willing to admit their experiment is going badly.

The rule is uncomfortable but simple: without an automatic abort condition, you have not run an experiment, you have caused an outage on purpose. The distinction is not moral, it is operational: an experiment has a known worst case, and the abort condition is what makes the worst case known. It converts an unbounded risk into a bounded one, and bounding the risk is the only reason it is acceptable to do this in production at all.

Two details matter. First, the abort path must be tested before the experiment — a revert that has never been exercised is exactly as reliable as an untested fallback, which is to say unknown. Second, hitting the abort condition is a *successful* experiment: you learned that the hypothesis is false, and you learned it in a bounded window at a time of your choosing with the whole team watching, rather than at 03:00 on a holiday weekend.

  • Automatic: the runner reverts the injection itself, on a threshold, without a human in the loop.
  • Expressed in user-visible metrics: abort on checkout success rate, not on CPU on the injected node.
  • Time-bounded independently: the injection also reverts after N minutes even if no threshold is crossed.
  • Independent of the injected component: an abort that reads its signal from the thing you broke cannot fire.
  • Tested first: run the revert path once with no fault injected, and confirm it restores the system.
1const experiment = {
2 hypothesis:
3 'Killing one of three checkout replicas keeps success rate above 99.5% ' +
4 'and p99 below 800ms for the full 5 minutes.',
5
6 steadyState: { // measured BEFORE injection, not asserted
7 window: '14d',
8 checkoutSuccessRate: { mean: 0.9971, sd: 0.0008 },
9 checkoutP99Ms: { mean: 482, sd: 61 },
10 },
11
12 injection: { kind: 'node-crash', target: 'checkout-replica-2', blastRadius: '1 of 3' },
13 duration: { minutes: 5 },
14
15 // Not optional. The runner enforces the threshold; no human is in this loop.
16 abort: {
17 when: [
18 { metric: 'checkout_success_rate', below: 0.99 },
19 { metric: 'checkout_p99_ms', above: 1500 },
20 { metric: 'order_queue_depth', above: 1000 },
21 ],
22 action: 'restore-replica',
23 // read from a path that does not traverse the injected component,
24 // or the abort signal dies with the thing you broke.
25 signalSource: 'edge-metrics',
26 },
27
28 rollbackTested: true, // the revert path was exercised dry first
29} as const
The declaration, with abort as a required field rather than a comment

Blast radius is a dial, and it starts small

The other bound is scope. Every experiment declares how much of production it can affect, and the first run of any new hypothesis takes the smallest setting that can still refute it: one instance, one shard, one percent of traffic, one non-peak hour, staging first if the hypothesis is about a mechanism rather than about production scale.

The dial only turns up after the hypothesis has held at the smaller setting. This ordering is what makes the practice defensible to the people whose revenue is at stake — you are not asking permission to break production, you are asking permission to break 1% of it for four minutes with an automatic stop, and you have already shown the same test passing at a smaller radius.

The uncomfortable truth about scaling the radius: many failure modes only appear at scale, because they are about resource exhaustion, retry amplification or correlated capacity. A one-instance experiment cannot refute a hypothesis about [[cascading-failure]]. So the dial has to move eventually, and the discipline is that it moves one notch at a time with the abort condition re-derived at each notch.

Key points

  • Six parts: hypothesis, steady state, injection, observation, abort condition, conclusion.
  • The hypothesis must be falsifiable — "the system is resilient" is not one.
  • Steady state is measured before the injection. If it cannot be measured, stop: that is the finding.
  • The abort condition must be automatic and must not depend on the component being broken.
  • Hitting the abort condition is a successful experiment, not a failed one.
  • Blast radius starts at the smallest setting that could still refute the hypothesis.

The chain, answered

Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.

How it works
  • Write the hypothesis as a statement a single run can show false.
  • Record the steady-state distribution over a window long enough to include normal variance.
  • Declare the injection, its blast radius and its maximum duration.
  • Declare the abort thresholds in user-visible metrics, with a signal path independent of the injected component.
  • Dry-run the revert path with no fault injected.
  • Inject, observe against baseline, and let the runner revert on threshold or on elapsed duration.
  • Record the conclusion, including "held" — and re-run it later, because a hypothesis that held is only true of the system as it was that day.
What can fail at the boundary
  • The abort signal is served by the component under test and goes silent exactly when it is needed.
  • The revert path has never been exercised and does not restore the system.
  • A deploy, traffic spike or unrelated incident coincides with the injection and the deviation is misattributed.
  • The blast radius is larger than declared because the injected component is shared by something not on the diagram.
  • The injection does not actually apply — the fault is swallowed by a layer in between and the "held" conclusion is meaningless.
How it fails — what an operator sees
  • Experiment becomes incident: the operator sees error rate climbing past the abort threshold with the injection still active, because the abort was a human watching a dashboard who was in a meeting.
  • Unattributable result: the operator sees a deviation during the injection window and cannot say whether it was the experiment or the deploy that shipped four minutes earlier, because no control period was recorded.
  • False pass: the operator sees no deviation at all, and later discovers the fault never reached the target — the injected node was already out of rotation.
  • Abort blindness: the operator sees the abort condition never fire during a genuine failure, because the metric it reads is scraped through the injected service.
  • Radius overrun: the operator sees two unrelated services degrade during an experiment scoped to one, because all three share a connection pool that appears on no architecture diagram.
Where coordination is required
  • Announcing the experiment window is coordination with humans, and it is mandatory: an unannounced experiment consumes another team’s on-call time diagnosing your injection.
  • Deploy freeze during the window is cheap coordination that buys attribution — one fewer variable changing at the same time.
  • The runner needs no coordination with the system under test, and should not have any: an injection that requires the target’s cooperation cannot model the target being gone.
  • The abort path must be coordination-free in the failure case — it cannot require a quorum, a lock or a call into the broken component to reverse itself.
What still holds under failure
  • While the injection is active, the system provides whatever guarantee it genuinely has — that is what is being measured, and it may be less than the documented one.
  • The abort condition bounds the *duration* of the deviation, not its severity within the threshold window.
  • State that drifted during the experiment window is real drift and needs the same reconcile step as a real incident — an experiment is not exempt from [[reconciliation]].
  • If the abort path itself fails, the fallback is the declared maximum duration, which is why the duration bound is a second independent safety net.
How it recovers
  • Detect: the runner compares live metrics to the recorded baseline continuously, not at the end.
  • Contain: the abort condition fires and the injection is reverted automatically; the maximum duration bounds the case where the abort logic fails.
  • Recover: confirm the injected component is back in rotation and serving, and that dependent queues have drained.
  • Reconcile: repair anything derived that drifted during the window, exactly as after a real incident.
  • Verify: re-measure steady state and compare it to the pre-experiment baseline before declaring the window closed.
How you would know
  • The baseline itself, recorded and stored with the experiment record — an experiment without its baseline is not reproducible.
  • Deviation from baseline in the user-visible metric named by the hypothesis, sampled finer than the effect you expect.
  • Whether the abort condition fired, when, and how long the revert took to take effect.
  • A control period immediately before the injection, so a coincident unrelated change is visible as a pre-existing deviation.
  • Proof that the injection actually applied — a signal from the target, not from the runner’s intent.
When it helps
  • Testing a specific resilience assumption that the team has stated but never checked: "we can lose a zone", "the breaker opens in time", "the replica promotes in 30 seconds".
  • Validating a fix after an incident, where the hypothesis is precisely the failure that just happened.
  • Discovering that steady state is unmeasurable — a finding you get before taking any risk at all.
When it hurts
  • On a system already known to be fragile: if you are confident the experiment will fail, you do not need the experiment, you need the fix.
  • Without an automatic abort and a bounded duration, in which case it is not an experiment.
  • During a period of high business risk — a launch, a sale, a filing deadline — where the cost of being wrong is not the cost you priced when you designed the experiment.
Simpler alternatives
  • A game day: humans walk through the failure scenario against a runbook with no injection. Cheaper, safe, and it finds missing runbooks and unclear ownership — but it cannot find behaviour nobody predicted.
  • Staging or a load-test environment: safe and repeatable, but it does not reproduce production’s scale, traffic mix or shared infrastructure, which is where most of the interesting failures live.
  • Reading the incident history: your past incidents are free experiments that already ran. Re-deriving hypotheses from them is the highest-value place to start.
  • Failure-mode review on paper (an FMEA-style walk of the dependency graph) to pick which hypothesis is worth testing before spending risk on any of them.

Build an experiment — the abort condition is not optional

Build an experiment — and the builder will not let you skip the abort condition
Hypothesis, steady state, injection, observation, abort, conclusion. Six parts; one of them is what separates an experiment from an outage.
simplifiedThe run is a deterministic model of a deviation: an exponential ramp scaled by the injection and the blast radius, plus a small seeded wobble. It shows how the abort condition behaves — it predicts nothing about your system.
1 · Hypothesis
Write it so a single run could refute it. "The system is resilient" cannot be refuted and is therefore not a hypothesis.
2 · Steady state
3 · Injection
blast radius
4 · Observation
5 · Abort condition· required
revert plan
6 · Owner of the finding· required
This is not an experiment yet. Missing: a hypothesis one run could refute; an abort condition; a hard maximum duration; a revert plan; a named owner for the finding. An injection without an automatic abort is not a test with a safety margin — it is an outage you scheduled. The abort condition is the field that makes the difference, which is exactly why a builder that lets you skip it teaches the wrong habit.
seed 7

What people believe, and what is true

Claim

The abort condition is paperwork we can add later.

Reality

It is the property that bounds the worst case. Without it the activity has no known upper bound on harm, which is the definition of an outage rather than an experiment.

Claim

A human watching the dashboard is the abort condition.

Reality

Noticing, interpreting and acting takes tens of seconds, and the person watching is the one most invested in the experiment continuing. Abort must be automatic.

Claim

If the experiment aborts, it failed.

Reality

It succeeded. You refuted a false belief inside a bounded window at a time you chose, instead of discovering it during a real incident.

Claim

We cannot measure steady state, so we cannot do this yet.

Reality

You just did the first experiment and it produced a finding: during a real incident nobody will be able to tell whether the system is recovering. Fix that, and you have both a better system and a baseline.

Claim

A hypothesis that held means the system is resilient.

Reality

It means the system was resilient to that fault, at that radius, on that day, at that traffic level. Every deploy invalidates the result, which is why experiments are repeated rather than ticked off.

Go deeper

Only the levels this lesson can honestly fill — a missing level is a claim nobody had.

Overview

Hypothesis, steady state, injection, observation, abort, conclusion. The abort condition must be automatic; without it you are not experimenting, you are causing an outage deliberately.

Practical

Write the hypothesis so one run can refute it. Record the baseline distribution before touching anything. Give the runner abort thresholds in user-visible metrics read through a path that does not cross the injected component, plus a hard maximum duration. Dry-run the revert. Start at the smallest radius that could still refute the hypothesis.

Advanced

The structure is an attribution argument. You are claiming a causal link between an injection and a deviation, on a system with many uncontrolled variables, from a single run. The baseline supplies the counterfactual, the control period and deploy freeze remove the obvious confounders, and the blast radius bounds the cost of being wrong about all of it. Read the result as evidence about that configuration on that day — not as a property of the system.

Apply it

Build it, then break it
  • 🔧 Take a resilience claim your team makes in a design document and rewrite it as a hypothesis with a baseline, an injection, an abort condition and a blast radius.
  • 🔧 Pick an existing abort condition and trace its metric path. Confirm the signal does not travel through the component the experiment breaks.
Reason about this
  • An experiment scoped to one replica of one service degrades two unrelated services. Nothing on the architecture diagram connects them. Where do you look first?
Interview questions
  • 💬 Write a falsifiable hypothesis for the claim "our service survives losing a zone".
  • 💬 Your experiment has no automatic abort, but an experienced engineer will be watching the dashboard. What is wrong with that?
  • 💬 You go to record steady state and find only an average latency metric over a mixed set of endpoints. What do you do next?