Emergency Kill-Switch & Instant-Rollback Runbook

This how-to is part of Managing Flag Deprecation & Cleanup. It is the runbook you reach for when a release behind a flag is actively causing an incident and you need the blast radius gone in seconds — not after a revert, rebuild, and redeploy.

The scenario: a flagged feature (checkout.payments.express-pay) is throwing errors in production, error budget is burning, and you are on call. A kill switch flips the feature to its safe variant for every request instantly, bypassing all targeting logic, because the change lives in the control plane rather than in your deploy pipeline. This runbook covers flipping it, proving it propagated, and recovering cleanly.

The reason a kill switch beats a code revert during an incident is arithmetic: a revert has to pass CI, build an artifact, roll through your deploy stages, and drain connections — a fifteen-to-forty-minute path on most pipelines, and every one of those minutes burns error budget and customer trust. A control-plane flip removes the feature from the request path in the time it takes your sync transport to fan the change out to every replica, which is sub-second on streaming and one poll interval otherwise. Treat this document as muscle memory to rehearse in a game day, not as reference material you read for the first time at 03:00 with a pager screaming — the whole value of a kill switch evaporates if you have to think about the commands while the incident is live.

Kill-switch state transitions A flag moves from normal targeting to a forced safe variant during an incident, then back to normal after the fix is verified. Normal targeting rules evaluated Kill switch ON forced safe variant Recovered targeting restored incident fix verified
The kill switch forces a safe variant during the incident; targeting is restored only after the underlying fix is verified.

Prerequisites

What you need in hand before an incident Four prerequisites: write access or break-glass to the control plane, a documented safe variant, a way to query each replica's resolved variant, and a known propagation budget. Control-plane access or break-glass Safe variant known in flag metadata Per-replica query debug / trace Propagation budget known number
Rehearse these before the incident — discovering that you lack replica visibility or a documented safe variant mid-outage is the worst time to learn it.

Step-by-Step Procedure

Three moves, in order: identify the flag and its safe variant, force that variant for all traffic, and confirm every replica actually flipped. The critical nuance is force, not disable.

Force the safe variant, do not disable the flag Disabling a flag falls back to the in-code SDK default, which may differ from the control-plane safe variant; forcing the explicit variant is deterministic and keeps the flag auditable. Disable the flag falls back to in-code default may not be the safe value intent lost from audit log Force the safe variant every eval returns "off" deterministic + auditable recovery is one inverse command
Forcing the explicit safe variant keeps behaviour deterministic and the flag object intact for audit — disabling gambles on the in-code default matching the safe state.

Step 1 — Identify the flag and its safe variant

Confirm the exact flag key and the variant that disables the failing behavior before touching anything.

flagctl get checkout.payments.express-pay --env prod -o json | jq '{state, defaultVariant, variants}'

The defaultVariant is your target state. If the safe value isn’t obvious, the flag taxonomy metadata should record which variant is fail-safe. Reading the current state first also tells you whether someone has already touched the flag — if it is already forced to some other variant, you may be looking at a mid-flight change from a teammate rather than the natural targeting state, and blindly overwriting it can mask what actually triggered the incident. Copy the full JSON output into your incident channel before you change anything; that snapshot is the “before” half of the diff you will want during the retro, and it is the fastest way to prove exactly what production was serving at the moment the kill switch fired.

Step 2 — Force the safe variant for all traffic

Override targeting entirely so every evaluation returns the safe variant, regardless of context.

flagctl set checkout.payments.express-pay \
  --env prod --force-variant off --reason "INC-4821 express-pay 5xx" --actor "$USER"

Forcing the variant (rather than disabling the flag) keeps the flag object intact for audit and makes recovery a single inverse command. The --reason and --actor land in the audit trail. Put the incident ticket ID in --reason unconditionally — six months later, when someone runs a cleanup audit and finds a flag pinned to off, that string is the only thread connecting the override to the outage that justified it, and a flag pinned “just in case” with no ticket behind it is exactly the kind of debt that never gets paid down. If your control plane supports it, prefer a targeting override that pins the variant at the top of the rule stack rather than editing the existing rules in place; the override is a single object you delete to recover, whereas hand-edited rules have to be reconstructed from memory, and reconstruction under pressure is where second incidents are born.

Step 3 — Confirm propagation across every replica

A kill switch you can’t confirm is a guess. Query each replica until they all report the safe variant.

for host in $(cat replicas.txt); do
  printf '%s ' "$host"; curl -s "$host/debug/flags/checkout.payments.express-pay" | jq -r '.variant'
done | sort | uniq -c     # expect every line to read "off"

If some replicas lag, you are watching your sync transport’s propagation window — streaming clears in under a second, polling within one interval. The sort | uniq -c at the tail is deliberate: it collapses hundreds of hosts into a two-line summary so you can see at a glance whether the fleet is uniform, and the moment one line still reads the old variant you have a concrete list of hosts to investigate rather than a vague suspicion. Do not stop querying the instant the first replica flips — a single healthy node proves the change reached the control plane, not that it reached the fleet, and the whole point of Step 3 is to distinguish “I sent the command” from “every request now honours it.” If you run behind a CDN or edge worker that evaluates flags at the edge, remember those points of presence are replicas too; a debug endpoint that only reaches your origin fleet will report all-clear while edge caches keep serving the failing variant to real users.

Verification Step

Confirm the error signal actually stops. Watch the service’s 5xx rate or the failing metric for one full propagation window plus a safety margin:

# Error rate should fall to baseline within the propagation window
watch -n 5 'curl -s http://metrics.internal/q?expr=rate_5xx{service="checkout"} | jq .value'

The incident is mitigated — not resolved — once the metric returns to baseline. Recovery (Step: restore targeting) happens only after the root-cause fix ships and is verified in staging.

Watch a second signal alongside the error rate, because a kill switch can move the symptom without fixing the user. If express-pay was failing, forcing it off will stop the 5xx spike, but customers now fall back to the standard checkout path — confirm that path is actually carrying the traffic and not silently dropping conversions, or you have swapped a loud failure for a quiet one. Give the metric a full propagation window plus a margin of at least one more window before you declare mitigation; latency percentiles and cache-warmed error counters both lag the underlying change, and calling it too early means you announce “resolved” in the incident channel just as a delayed metric ticks back up. If the error rate does not fall after a full window, do not assume the switch failed — first re-run Step 3, because an unpropagated switch and an ineffective switch look identical on the dashboard and demand opposite responses.

Error rate falls to baseline within the propagation window After the kill switch fires, the 5xx rate drops back to baseline within one propagation window plus a safety margin; the incident is mitigated, not resolved, at that point. kill switch fires 5xx burning error budget baseline restored within one propagation window → mitigated (not yet resolved)
Watch the failing metric for a full propagation window plus margin; a return to baseline confirms mitigation, but resolution waits on the verified root-cause fix.

Gotchas & Edge Cases

Gate recovery on a verified fix, not the clock Restoring targeting before the root-cause fix is verified re-triggers the incident; recovery must wait for the fix to ship and pass verification in staging. fix verified? in staging no → keep the kill switch on restoring early re-triggers the incident yes → restore targeting record recovery in the audit trail
Recovery is gated on a verified fix, never on elapsed time — flipping targeting back too soon simply restarts the incident and erodes trust in the switch.

Troubleshooting & FAQ

The kill switch fired but errors continue on a few hosts — why?

Those hosts are still serving a cached or pre-switch rule set. Check their resolved variant directly (Step 3); if they lag, your local cache TTL or a dropped streaming connection is the cause. A failed sync connection is the usual culprit — verify each replica’s connection state.

Should I disable the flag or force a variant?

Force the safe variant. Disabling reverts to the in-code default, which is not guaranteed to match the control-plane safe state, and it loses the explicit intent in the audit log.

How do I make sure a kill switch is always fast enough?

Keep the failing-feature flags on a streaming transport with a tight fallback poll, and rehearse the runbook so propagation latency is a known number before an incident, not a discovery during one.

What if the control plane itself is down when I need the kill switch?

Then you fall back to the last value each replica cached and to the in-code default, which is exactly why the code default should always be the safe state for high-risk flags. Design the SDK initialization so a control-plane outage fails static on the safe variant rather than blocking startup or flapping, and keep a documented secondary path — a config-map override or an environment flag the deploy can set — for the case where you cannot reach the control plane at all.

Can I automate the kill switch instead of paging a human?

Yes, and for well-understood failure signals you should: wire an alert on the feature’s own error rate to a webhook that forces the safe variant automatically, so mitigation happens in seconds without a human in the loop. Keep the automation narrow — one flag, one clear signal, one safe variant — require the same audit metadata a human would supply, and always leave the manual runbook intact for the cases the automation was never scoped to cover.

Who is allowed to fire the kill switch, and how do I prevent misuse?

Scope kill-switch authority to the on-call rotation and incident commanders through a break-glass role, not to everyone with control-plane read access. Every forced override should carry an actor and a reason, alert a shared channel on use, and be reviewed in the incident retro — the goal is a fast path that is fully attributable after the fact, not an unlogged back door.

How is a kill switch different from a normal gradual rollback?

A gradual rollback walks a percentage down over minutes to limit churn and watch metrics between steps; a kill switch is the opposite — it forces the safe variant for 100% of traffic in one move because the feature is actively harmful and there is nothing to gain by easing off. Reserve the instant, all-traffic flip for genuine incidents, and use a staged rollback for the routine “this isn’t performing as hoped” cases.