SRE
The 3am Page That Taught Me Alerts Need Runbooks, Not Just Thresholds
The alert fired correctly. The threshold was right. I still spent 40 minutes at 3am guessing, because nobody had written down what to actually do about it.
The page said: "PodCrashLoopBackOff — payments-worker — namespace: prod." That's it. Correct alert, correct threshold, correct routing — it woke me up exactly when it should have. And I still spent the first 15 minutes of a 3am page doing something no on-call engineer should ever have to do: figuring out from scratch what a crash-looping payments-worker pod even means for the business, and what I was allowed to safely do about it.
What actually happened
A payments-worker pod had started crash-looping after a dependency (an internal fraud-check service) started returning malformed responses under load. The worker's client library didn't handle the malformed response gracefully — it panicked, Kubernetes restarted it, it panicked again. Textbook crash loop. The fix, in hindsight, took about 90 seconds: scale down the worker to stop hammering the broken dependency, page the fraud-check team, and let payments queue safely rather than process incorrectly.
But at 3am, half-awake, alone, with no context, here's what actually happened instead: I spent 10 minutes confirming it wasn't safe to just restart-and-hope (it wasn't — it would keep crash-looping). Then 15 minutes reading through the worker's code trying to understand what it does and whether scaling it to zero was safe (is it processing anything that can't be re-queued? I genuinely didn't know). Then I paged the fraud-check team's on-call, who confirmed the malformed-response issue in about 2 minutes because they already knew about it internally. Total time to actual resolution: 40 minutes. Total time that mattered: about 90 seconds, once someone with the right context was involved.
Nothing was wrong with the alerting rule. The threshold, the routing, the paging — all correct. The gap was entirely in what happened *after* the page: there was no written answer to "what do I do about this," so I had to reconstruct it live, at 3am, under time pressure, from a codebase I don't own.
The real cost of a missing runbook isn't the outage — it's the exhaustion
Payments queued safely the whole time; nothing was actually lost. The 40 minutes didn't cost the business much. But it cost me: I was awake, stressed, and context-switching through unfamiliar code for 40 minutes at 3am for a problem that had a known, 90-second fix — known by someone else, on another team, who wasn't the one who got paged. That mismatch — the person who gets paged isn't always the person who has the answer — is exactly what a runbook is supposed to bridge.
What a runbook actually needs to contain (and what it doesn't)
I used to think runbooks were bloated wiki pages nobody reads. They are, when they try to explain the whole system. A useful runbook answers exactly one question fast: given this specific alert, what do I do in the next 2 minutes? Everything else is noise.
- Is this safe to ignore/wait on, or does it need action now? — a one-line severity/impact statement, written for someone with zero context.
- The single safest first action — not a menu of options. One default move that's almost always correct (in our case: scale the worker down, don't try to fix the root cause live).
- Who actually owns the root cause, by name or team/Slack channel — not "the payments team" in the abstract, but the specific on-call rotation to page.
- What NOT to do — the thing that seems reasonable but makes it worse (in our case: don't just restart the pod repeatedly — it re-triggers the same crash against the same broken dependency).
- How to confirm it's actually resolved — a specific metric or log line to check, not "it looks fine now."
## PodCrashLoopBackOff: payments-worker
**Impact:** Payments queue safely during this — no data loss, but
processing is delayed. Not an emergency, but resolve within 30 min
to avoid queue backlog alerts.
**First action:** Scale payments-worker to 0 replicas immediately.
kubectl scale deploy/payments-worker -n prod --replicas=0
This stops it from hammering whatever it's failing against.
**Root cause owner:** fraud-check team (#fraud-check-oncall).
Page them — this crash loop is almost always caused by their
service returning malformed responses under load.
**Do NOT:** repeatedly restart/delete the pod. It will crash-loop
again immediately against the same broken dependency and wastes
time.
**Resolution check:** scale payments-worker back to its normal
replica count and confirm error rate on `payments_worker_errors_total`
drops to baseline within 5 minutes.Notice what's missing: no explanation of the whole payments architecture, no history of why the fraud-check dependency exists. Just the four things a half-asleep on-call engineer needs in the next 2 minutes.
The actual fix wasn't just documentation — it was automation
Writing the runbook stopped the 40-minute version of this incident. But the deeper fix was recognizing that "scale the worker down when this specific crash loop appears" is a deterministic, safe, well-understood action — which means it doesn't need a human awake at 3am to execute it at all. That's the exact class of problem I've since started encoding as automated, guarded remediation instead of a runbook a human has to read and follow manually: match the known failure signature, run the known-safe first action automatically, and only escalate to a human with full context if the automated fix doesn't resolve it. A good runbook is the right first step. An automated one is the actual goal.
Pick your last 5 pages. For each one, ask: was there a written, specific runbook a half-asleep engineer could follow in under 2 minutes? If the honest answer is no for more than one of them, that's your highest-leverage on-call improvement — higher leverage than tuning any alert threshold.