Run protocol for a chaos experiment that has already been designed: the four pre-flight gates (non-production target, measured healthy baseline, live observability, a rollback that has actually been exercised), how to pick a conservative blast-radius bound, the sampling cadence and abort criteria fixed in writing before injection, and the recovery-validation step with its tolerance and timeout. Owns execution safety only, not experiment design: the steady-state hypothesis, the fault to inject, and the experiment file come from elsewhere. Use when an experiment definition exists and a fault is about to be injected into a running system, and the go/no-go gates, abort thresholds, and recovery check still need to be agreed and written down before the fault starts.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
This is the run protocol. It covers the window that opens when someone is about to inject a real fault into a running system and closes when the system has been confirmed back at baseline: what must be true before the fault starts (pre-flight gates), how wide it is allowed to reach (blast-radius bound), what is watched while it is live and what ends it early (sampling and abort criteria), and how recovery is confirmed rather than assumed.
Not owned here: designing the experiment. Choosing the steady-state hypothesis and its metric, choosing which real-world event to inject, and writing the experiment definition are a separate job that happens earlier. Per principlesofchaos.org, an experiment starts "by defining 'steady state' as some measurable output of a system that indicates normal behavior" and then hypothesizes "that this steady state will continue in both the control group and the experimental group." This protocol assumes a written hypothesis with a numeric threshold already exists. If it does not, stop and go do that first: a drill with no hypothesis produces a story, not a result. Also out of scope: aggregating results across drills into a trend, and the vendor-specific syntax of whichever injection tool is in use.
A chaos drill and an outage differ only by preparation: Google's SRE account of a test-induced emergency shows a test scoped to one database cascading into a multi-service outage because the dependency assumptions were incomplete and the rollback had never been tested in a test environment (sre.google/sre-book/emergency-response). Every gate below exists because skipping it is how that outcome happens.
Fill this in and get it agreed before anything is injected. Every field is a number or a name, never a judgment to be made later.
| Field | Meaning | Fixed before injection? |
|---|---|---|
hypothesis | The steady-state metric and its numeric threshold, from the experiment definition | Yes |
target | The exact scope: environment, namespace, service, replica count N | Yes |
fault | The single event being injected | Yes |
blast_radius | Max fraction of N affected, plus max duration | Yes |
abort_criteria | The numeric thresholds that end the drill early, each with a dwell time | Yes |
sample_interval | How often observability is read while the fault is live | Yes |
recovery_criteria | The signals that must return to baseline, with tolerance and timeout | Yes |
rollback | The named action that removes the fault, and who runs it | Yes, and rehearsed |
A drill missing any row is not ready to run. This mirrors how the Chaos Toolkit
experiment format treats safety as structural rather than optional: the format
has a first-class rollbacks property whose actions "attempt to put the system
back to its initial state", and if the steady-state hypothesis is not met up
front then "the Method element is not applied and the experiment MUST bail out"
(chaostoolkit.org/reference/api/experiment).
These four are gates, not a checklist to feel good about. Each one is pass or fail. A single failure stops the drill. There is no partial pass, no "we will watch it closely instead", and no waiving a gate because a maintenance window is expiring. A failed gate is fixed and the drill is rescheduled.
Read the environment identity from the tooling itself (the active cluster or account context), not from what someone typed in the ticket. If the resolved context names production, the drill stops.
Running in production is a legitimate long-term goal, not a starting point. principlesofchaos.org does state that "Chaos strongly prefers to experiment directly on production traffic", and Netflix's Chaos Monkey "is responsible for randomly terminating instances in production" (netflix.github.io/chaosmonkey). Both describe mature programs with years of tooling behind them. This protocol gates on non-production and treats promotion as a separate decision made after repeated clean runs.
Take a real baseline measurement immediately before injection. Not last week's dashboard screenshot: a fresh reading over a window long enough to be stable. Record the actual numbers for every signal the abort and recovery criteria reference, because those criteria are expressed relative to this baseline.
If the target is already degraded, the drill stops. The experiment method is to look "for a difference in steady state between the control group and the experimental group" (principlesofchaos.org), so a degraded starting point means any difference observed cannot be attributed to the injected fault: an uninterpretable result carrying full risk.
Gating on "at least 99% of instances healthy" is a practitioner convention, not a standard. Treat it as a starting default. The defensible version is stricter and local: the baseline must sit inside the range the service normally holds at this time of day, and be stable across the measurement window.
Confirm that fresh samples for every abort-criterion signal have arrived within the last sampling interval. Stale dashboards fail this gate exactly as hard as missing ones.
The signals to confirm are the ones the drill will abort on. A useful default set is the four golden signals - latency, traffic, errors, saturation - defined verbatim in references/golden-signals.md after sre.google/sre-book/monitoring-distributed-systems.
Without live signals there is no way to detect that a bound has been crossed, which means the drill has no abort path that fires on evidence. That is not a riskier drill. It is a fault injection with no stopping rule.
Verified means someone ran it and watched it succeed. A runbook entry, a tool's advertised time-to-live, or a colleague's recollection that "it worked last time" do not clear this gate.
Exercise it like this, before injection:
Where the injection tool offers an automatic expiry as well, set it, and treat it as a backstop behind the manual action rather than as the rollback itself.
This gate is the direct lesson of the SRE test-induced emergency, where the rollback path failed because it had never been tested in a test environment (sre.google/sre-book/emergency-response). An untested path lets an unrelated environment change silently alter its result (sre.google/sre-book/testing-reliability).
| Gate | Passes when | On failure |
|---|---|---|
| Non-production target | Resolved context does not name production | Stop. Re-target, or run the separate production-readiness decision |
| Healthy measured baseline | Fresh reading, inside normal range, stable across the window | Stop. Fix the service, take a new baseline, reschedule |
| Live observability | Fresh samples present for every abort-criterion signal | Stop. Restore telemetry first |
| Exercised rollback | Rollback action was just run successfully against this target | Stop. Rehearse it, record the duration, then reschedule |
Per principlesofchaos.org, "It is the responsibility and obligation of the Chaos Engineer to ensure the fallout from experiments are minimized and contained." That principle sets the direction but not the number. Derive the number instead of asserting it.
The load argument. If a fraction f of N replicas is removed, each
surviving replica absorbs the redirected share, so its load multiplies by
1 / (1 - f). That is arithmetic, and it is the reason a large bound quietly
turns into two experiments at once:
| Fraction removed | Surviving replicas | Load per survivor |
|---|---|---|
| 1 of 12 (about 8%) | 11 of 12 | about 1.09x |
| 25% | 3 of 4 | about 1.33x |
| 50% | 1 of 2 | 2.00x |
| 100% | none | no surviving capacity |
At 50% the drill is testing the injected fault and a doubling of per-replica load simultaneously, so a failure cannot be attributed to either one. At small fractions the confound is small and the result is readable. That is the whole argument for starting small and widening only after clean runs.
The reference point for "small". Netflix's Chaos Monkey, running against production, ships a deliberately tiny bound: every weekday it "flips a weighted coin to decide whether to terminate an instance from that group", and when the coin comes up heads it schedules one termination in a daytime window (netflix.github.io/chaosmonkey/Termination-behavior). One instance per group per day, on purpose, in the most mature chaos program there is. A first drill has no case for being bolder than that.
Expressing the error bound. State the error-rate ceiling against the service's remaining error budget rather than as a bare percentage. Per sre.google/sre-book/embracing-risk, the error budget is the difference between the SLO target and measured actual uptime, "the 'budget' of how much 'unreliability' is remaining". A drill spends that budget. Deciding in advance what share of it this drill may spend is what keeps a series of drills from consuming the quarter.
Duration. Bound it in wall-clock time, and make the bound short enough that the full duration plus the rehearsed rollback duration fits inside the window the team has agreed to stay attentive for. A drill that outlives its watchers has no abort path.
Every abort criterion is a signal, a threshold, and a dwell time, written into the drill contract before injection and unchanged once the fault starts.
This is not a formality. Once a fault is live, the person watching is under time pressure, has already invested effort in getting the drill scheduled, and is looking at a graph that is supposed to be moving. Those are the conditions under which a threshold gets renegotiated in the moment. Writing the number in advance removes the decision from that moment: the sampler compares, it does not deliberate. The scientific framing demands the same, since the experiment exists to "try to disprove the hypothesis" (principlesofchaos.org), and a hypothesis whose bounds move during the run cannot be disproved.
Nobody may loosen a criterion mid-run. Anyone may abort early. Those are not symmetric, and should not be.
Read every abort-criterion signal on a fixed interval. A 10-second interval is a practitioner convention, not a standard. Two constraints set the real value:
Pick the interval from those two constraints for the actual stack, then write it into the contract.
Each row is filled in with the specific numbers before injection. The example values below are illustrative shapes, not recommended defaults.
| Abort criterion | Shape | Why it ends the drill |
|---|---|---|
| Error rate above budget | errors > X% sustained for D seconds, where X came from the agreed error-budget share | The drill is spending more reliability than it was authorized to spend (sre.google/sre-book/embracing-risk) |
| Blast radius exceeded | More than the bounded fraction of N affected | The fault has spread past the scope the bound was reasoned against, so the load argument above no longer holds |
| Latency collapse | p99 > M x baseline sustained for D seconds | Sustained, not spiked, means the system is not absorbing the fault |
| Downstream breach | Any dependent service crosses its own SLO | The drill is now affecting systems that never consented to be in scope, which is how a contained test becomes an incident (sre.google/sre-book/emergency-response) |
| Manual abort | Any participant calls it | Human judgment may always stop a drill early; it may never extend one |
Never abort silently. An abort is the drill's most informative outcome: it is the moment the system told you where its bound is. Discarding it because the run "did not complete" throws away the only data the drill produced.
Recovery is confirmed, not assumed. The property under test is recoverability, defined in the ISTQB glossary (V4.7.2) as "the degree to which a component or system can recover the data directly affected by an interruption or a failure and re-establish the desired state of the component or system", after ISO 25010 (glossary.istqb.org/en_US/term/recoverability).
Removing the fault is not recovery. Recovery is the system returning to the baseline recorded at Gate 2.
| Check | Passes when |
|---|---|
| Capacity | Ready replica count is back to the Gate 2 baseline count |
| Errors | Error rate is back inside the recovery tolerance around the Gate 2 baseline |
| Latency | p95 and p99 are back inside tolerance around their Gate 2 baselines |
| Downstream | Every dependent service touched during the run is back inside its own SLO |
| Stability | All of the above hold continuously for a settle window, not at a single instant |
"Baseline plus 10%" is a practitioner convention, not a standard, and a poor final answer because it is unrelated to how noisy the specific signal is. The tolerance has two opposing constraints:
Derive it from the Gate 2 measurement: set the tolerance from the observed variation of that signal across the baseline window and write the number into the contract. Keep the 10% default only where no baseline variance was captured, and say so in the record.
Set a maximum wait too. Five minutes is a common convention; the right value depends on the system's own restart and warm-up behavior. Reaching the timeout is a result, not an error: recovery did not complete unassisted, which is a finding worth more than a clean pass.
| Verdict | Meaning |
|---|---|
RECOVERED | All checks passed inside the timeout, with no manual intervention |
RECOVERED_ASSISTED | Checks passed, but a human had to act. Record exactly what they did; that action is a gap in automated recovery |
PARTIAL | Some checks passed inside the timeout, others did not |
NOT_RECOVERED | Timeout reached with checks still failing. Treat as an incident and follow the normal incident process |
Never run the next drill from an unrecovered state. A follow-up launched before recovery validates starts from an unknown baseline, which fails Gate 2 by definition, and its blast-radius reasoning is void because the surviving-capacity arithmetic assumed a full complement of healthy replicas.
A full end-to-end run - the frozen drill contract, the pre-flight table, the
live-run trace ending in an error-budget abort, and recovery validation to
RECOVERED - is in
references/worked-example.md.
One record per drill, aborted runs included.
## Chaos drill record: checkout-pod-kill-2026-07-19
**Target:** staging / checkout-staging / checkout (N=12)
**Fault:** pod-kill, bound 3 of 12, max 5m
**Hypothesis:** checkout_completion_rate >= 95% over 5m
**Outcome:** ABORTED at T+1:00 | **Recovery:** RECOVERED (4m14s, unassisted)
### Pre-flight
| Gate | Result | Evidence |
|---|---|---|
| Non-production target | PASS | context checkout-staging |
| Healthy measured baseline | PASS | 12/12 ready, 5xx 0.2%, p99 240ms, stable 10m |
| Live observability | PASS | freshest sample 4s old, all 4 signals |
| Exercised rollback | PASS | rehearsed T-8m, success in 6s |
### Run
Injected T+0:00, aborted T+1:00 on `error-budget` (5xx > 2% sustained 30s),
rollback complete T+1:06. Signal values at abort: ready 8/12, 5xx 3.3%,
p99 1140ms.
### Observed blast radius
| Measure | Peak observed | Bound |
|---|---|---|
| Replicas unready | 4 of 12 | 3 of 12 |
| Error rate | 3.3% | 2% |
| p99 latency | 1140ms | 10x baseline (2400ms) |
| Downstream | payments within SLO throughout | any breach aborts |
### Recovery
Capacity 12/12 at T+3:20; errors 0.3% (inside baseline + 0.4%); p99 262ms
(inside baseline + 12%); payments within SLO; held through the 2m settle
window. Verdict `RECOVERED`.
### Findings and next step
- Hypothesis did not hold: completion rate fell below 95% before the bound.
- Cascade beyond the target: a fourth replica went unready without being
targeted. Investigate the shared dependency before widening the bound.
- Do not widen the blast radius; re-run the same contract once understood.The nine drill anti-patterns, each with why it fails and what to do instead, are in references/anti-patterns.md: 100% blast radius, deciding the abort threshold live, treating a documented rollback as verified, skipping a gate for a window, injecting onto a degraded baseline, stale telemetry, silent aborts, starting the next drill before recovery, and widening a bound after a breach.