S080 · Incident Response — Runbooks, Postmortems, On-Call
Turning outages into learning. The IMOC roles, the blameless postmortem template, and why the best incident responses feel boring.
🎯 Run a real incident from detection to postmortem, know the three canonical roles, and write a blameless retro someone would actually read.
Why this session exists
The best day at any company is the day nothing breaks. The second-best is the day something breaks and everyone handles it calmly — clear roles, clean comms, tight fix, honest write-up, and by next week the same incident can't happen again. The third-best is the day something breaks and it's chaos. The worst is when it stays broken for hours because nobody knows who's in charge. This session teaches you the boring, essential craft of incident response — the runbooks, the roles, the postmortem template, the on-call rituals. It's the difference between an engineering org that learns from failure and one that hides from it.
- Name the three IMOC roles (IC / Comms / Ops) and describe what each is responsible for.
- Write a runbook that a stranger could execute at 3 AM.
- Run a real incident: declare, triage, mitigate, resolve, post-mortem.
- Write a blameless postmortem that focuses on systems, not people.
- Design an on-call rotation that doesn't burn out your team (SLOs on the pager itself).
Prerequisites
- S077 · 3 pillars of observability — you can't respond to what you can't see.
- S079 · SLIs / SLOs — SLO burn IS the trigger for most incidents.
(a) Intuition · 5 min
An ambulance rolls in with a critical patient. The trauma bay doesn't devolve into chaos — everyone knows their role. One person is the trauma team leader (doesn't touch the patient, just calls the shots). Another manages the airway. Another does compressions. Another documents everything. A nurse stands at the door managing the family and updating hospital admin.
Without those roles, ten people would crowd the patient and none of them would agree who's giving orders. With them, a life gets saved in 20 minutes and everyone can debrief afterwards on what to do better next time.
A tech incident is the same. The Incident Commander (IC) doesn't touch keyboards — they run the room. Ops / Tech Lead does the actual fixing. Communications keeps the rest of the company + customers informed. Scribe documents timeline for the postmortem. Everyone else supports.
Companies without these roles have incidents that last hours longer than they should, because five engineers are all typing at the same time and no one is deciding whether to rollback.
The IMAG roles you must know
- Incident Commander (IC) — decides. Doesn't touch code. Holds the room, delegates tasks, calls the shots on rollback / escalation / communications cadence. Rotates every 4 hours for long incidents.
- Operations / Tech Lead — does. The most senior engineer familiar with the failing system. Actually types the commands, reads the logs, deploys the fix. Reports to IC.
- Communications Lead — informs. Writes status page updates, notifies internal stakeholders (leadership, support, sales), coordinates customer-facing responses. Every 15-30 min status update.
- (For long or complex incidents: add a Scribe who logs the timeline in real time, and Planning who coordinates people joining/leaving.)
Anatomy of an incident — the five phases
- 1DetectAlert fires, user reports, or engineer notices. Time-to-detect (TTD) is the first metric.
- 2DeclareSomeone officially says 'this is an incident'. Assign IC. Open a war-room channel. Time-to-declare matters — hours are lost debating whether to declare.
- 3MitigateStop the bleeding. Rollback, disable feature flag, route around. Root cause can wait. Time-to-mitigate (TTM) is the number that matters most.
- 4ResolveFully restore. Then close the incident channel with a summary.
- 5PostmortemWithin 5 business days: timeline, contributing factors, action items with owners + due dates. Blameless.
(b) Visual walkthrough · 15 min
The incident flow
The war-room comms cadence
The 5 steps every runbook should have
'You're here because X alert fired'. Include the alert name, expected dashboard state.
'This means users experience Y. Business impact: $Z/hour.' Justify wake-up urgency.
Copy-pasteable: `kubectl get pods -n foo`, `psql -c "select ..."`. No 'you should probably check X'.
Numbered, ordered, each with 'expected result' + rollback if it fails.
'If steps 1-4 don't work in 15 min, page @team-name and prepare for X'.
The blameless postmortem template
Every good postmortem has these sections
On-call structures compared
The standard for small teams
- 5-10 person team, 1 week on-call per 5-10 weeks
- Secondary catches if primary misses
- Handoff meeting Monday morning
- Sustainable if page count < 3-5 per week
- Above that: fix pages, don't add rotation
For always-on services with global teams
- 3 rotations across timezones, ~8h shifts
- No middle-of-night pages ever
- Requires 15+ engineers total
- Handoff protocol between regions is CRITICAL
- Expensive but humane
Big enterprises
- L1: 24/7 team follows runbooks
- L2: subject-matter experts, paged only if L1 escalates
- Reduces load on senior engineers
- Requires excellent runbooks
- Risky if runbooks are stale
The mental model to hold
"Incident response is about fixing the bug fast. The best responder is the person who knows the system deepest — they'll find root cause quickest, and once we know root cause the incident is over."
Incident response is about restoring service, and that is usually a different activity from diagnosis. Mitigation almost never requires root cause: roll back, fail over, drain a region, disable a feature flag, shed load. Root cause analysis happens after the bleeding stops. Conflating the two is the single most common reason incidents run long — the deep expert disappears into a debugger while the customer-visible outage continues.
The myth is sticky because it is exactly correct in your day job. When you debug a pipeline in dev, root cause is the fix and finding it fast is the whole skill. That instinct is trained by thousands of hours of successful debugging. But debugging optimises for understanding; incident response optimises for time-to-mitigate under uncertainty, with a hard constraint that you may never fully understand what happened. The same person who is best at the first can be worst at the second, because their instinct is to keep pulling the thread instead of pressing the rollback button.
Run the counterfactual on your last three incidents. For each one, write down two timestamps and compare:
detected_at -> when a human/alert first knew
mitigated_at -> when customer impact stopped
root_caused_at -> when you actually understood why
TTM = mitigated_at - detected_at
TTRC = root_caused_at - detected_at
# If TTRC and TTM are nearly equal, you diagnosed before you mitigated.
# That gap is pure, avoidable customer impact.In a healthy process TTM is far smaller than TTRC, and often root cause lands days later in the postmortem.
Why does incident command insist on separating the Incident Commander from the person doing the technical work — even when there are only two people on the call, and the IC is "not doing anything"? This looks like process theatre. It isn't.
- 1During an incident the binding constraint is not engineering skill, it is human attention. The responder's working memory is fully consumed by the hypothesis they are currently testing.forced by · debugging is a depth-first search and holding the stack is what makes it work
- 2But an incident also generates a continuous stream of interrupt-driven work: stakeholder updates, "is this related?" pings, deciding whether to escalate, tracking which hypotheses were already ruled out, noticing that 20 minutes passed with no progress.forced by · an outage has an audience and a clock, neither of which the debugger can see from inside the debugger
- 3If one person holds both, the interrupts win. Every status request destroys the debugging stack, and the human resolves the conflict by either going silent (stakeholders escalate, more interrupts arrive) or by context-switching constantly (diagnosis stalls).forced by · context switching between a depth-first search and an interrupt queue has superlinear cost
- 4Therefore the roles must be held by different humans: one owns state and decisions and absorbs all interrupts, the others own hypotheses and hands-on-keyboard. The IC deliberately does not type, precisely so they remain interruptible.forced by · an interrupt absorber that is itself busy is not an absorber
- 5Given that split, the IC's real product is a single shared, written source of truth — timeline, current hypothesis, who owns what, what has been ruled out — because that is what lets responders join, hand off, or leave without the incident losing its memory.forced by · incidents outlast individual humans' attention spans and shift boundaries
Therefore the IC role exists to protect attention, not to add hierarchy. The IC is the incident's memory and its interrupt handler; the responders are its CPU.
And note what this predicts: the value of an IC should grow with the number of responders and the number of stakeholders, and should be near zero for a solo responder with no audience. That is exactly the observed pattern — nobody appoints an IC for a five-minute self-resolved blip, and every large incident that ran badly has a postmortem line reading "it was unclear who was making decisions".
Picture an incident as two disjoint phases with a hard wall between them. Phase one is a trauma bay: the only question is "what action, available right now, most reduces customer impact?" — and the acceptable answers are almost always the boring reversible ones. Phase two is the autopsy: unhurried, blameless, written down, and it is where all the learning lives.
The wall matters because the two phases reward opposite behaviours. Phase one rewards decisive action under uncertainty and punishes curiosity. Phase two rewards curiosity and punishes decisiveness. Doing them at the same time gets you the worst of both.
- Mitigate before you diagnose. Rollback, failover, flag-off, and load-shed are first-class fixes, not cop-outs.
- One IC, explicitly named out loud, who does not touch a keyboard. Roles: IC · Ops (hands-on) · Comms · Scribe. Merge roles when small, but never merge IC and Ops.
- Everything goes in one channel/doc with timestamps. If it isn't written, it did not happen and the next responder will redo it.
- Postmortems are blameless and about mechanisms, never people. "Human error" is the beginning of the analysis, never the end — the question is what made the error easy and the recovery slow.
Fire this the moment you see: a call where three people are debugging and nobody is deciding · "let me just check one more thing" while customers are down · a status page that hasn't updated in 30 minutes · a postmortem action item naming a person · an incident where the fix is known but nobody will authorise the rollback · a handoff at shift change with no written state.
You have a customer-visible outage and a plausible but unconfirmed cause. Do you roll back immediately, or hold and diagnose first?
Default to rollback, but spend the 60 seconds to snapshot evidence first — logs, a heap or thread dump, the current config, a sample of failing requests. That tiny cost buys back nearly all the diagnostic value and is what makes "roll back first" a sustainable policy rather than a way to accumulate mystery outages.
The genuinely senior move is upstream of the incident: invest in containment primitives — feature flags, regional drain, kill switches — so the third option is available. Teams that have them stop having this argument, because containment dominates both alternatives. Teams that don't have it re-litigate rollback-vs-diagnose in every single incident, at the worst possible moment.
(c) Hands-on · 25 min
Let's build a runbook + do a tabletop incident exercise. The 'code' here is a runbook + a scripted role-play. Real learning happens through repetition.
<!-- runbook_high_error_rate.md — a real runbook you'd store next to your service -->
# Runbook: HighErrorRate on checkout-api
**Alert name:** `HighErrorRate` (from prometheus alert rules)
**Severity:** Page (SEV-1 if >5%, SEV-2 if 1-5%)
**Owner team:** Payments team (@payments-oncall)
## Symptoms
- Alert `HighErrorRate` fires with expression `error_rate_5m > 0.01`
- Grafana dashboard `checkout-api-overview` shows 5xx bar red
- Users may see "sorry, please try again" on checkout
## Business Impact
- Every 1% of error rate = ~$X lost revenue per hour
- Customer-facing: checkout may fail entirely if error rate >20%
## Diagnostic Commands
Run in order; stop when you find the cause.
1. Check recent deploys — most incidents are caused by the last deploy:
```bash
argocd app history checkout-api --limit 5→ If a deploy happened within the last 30 min and error rate spike aligns, GOTO Mitigation Step 1 (rollback).
-
Check upstream dependencies:
kubectl get pods -n payments -l app=payment-service curl -s http://payment-service:8080/health | jq→ If unhealthy, GOTO Mitigation Step 2 (open incident with @payment-service-oncall).
-
Check database:
psql -h checkout-db.prod -c "SELECT COUNT(*) FROM pg_stat_activity WHERE state='idle in transaction';"→ If >100 idle transactions, GOTO Mitigation Step 3 (restart connection pool).
-
Check recent errors:
logcli query '{app="checkout-api"} |= "ERROR"' --tail --limit 50→ Look for a repeating error message — root-cause hint.
Mitigation Steps
1. Rollback recent deploy
argocd app rollback checkout-api --revision <previous-revision>Expected: error rate drops within 60s. Verify on dashboard.
2. Open dependency incident
Page @payment-service-oncall in #incidents. Set our service to 'degraded' on status page. Do NOT try to fix a downstream service yourself.
3. Restart connection pool
kubectl exec -it deploy/checkout-api -n payments -- \
curl -X POST http://localhost:8081/admin/reset-poolExpected: idle transactions drop, error rate recovers within 2 min.
Escalation
If Mitigation Steps 1-3 don't help within 15 min:
- Page @payments-eng-lead
- Page @sre-oncall (for cross-team coordination)
- Consider declaring SEV-1 and paging @exec-oncall
Now a Python simulator to practice the incident lifecycle:
```python
#!/usr/bin/env python3
"""incident_tabletop.py — tabletop incident simulator.
Prints a scenario, walks through the phases, prompts for decisions,
and simulates the outcome. Great for team practice; run this together
with your team once a quarter.
Run: python incident_tabletop.py
"""
from __future__ import annotations
import random
import time
from dataclasses import dataclass
@dataclass
class Scenario:
name: str
trigger: str
hidden_root_cause: str
good_actions: list[str] # keywords in a good response
time_to_mitigate_ideal: int # minutes
SCENARIOS = [
Scenario(
name="Deploy-triggered 500s",
trigger="Alert: 5xx rate 12% on checkout-api, started 2m ago.",
hidden_root_cause="A refactor of the payment client shipped in the last deploy has a null-check bug.",
good_actions=["rollback", "revert", "previous", "argocd", "deploy"],
time_to_mitigate_ideal=5,
),
Scenario(
name="Database saturation",
trigger="Alert: p99 latency 8s on user-service. CPU on the DB is 100%.",
hidden_root_cause="A background job started 15m ago is running an unindexed query on the users table.",
good_actions=["kill", "pg_terminate", "cancel", "job", "query"],
time_to_mitigate_ideal=10,
),
Scenario(
name="Third-party outage",
trigger="Alert: SendGrid webhook failures 100%. Emails are queuing in dead-letter queue.",
hidden_root_cause="SendGrid is having an outage (confirmed on their status page).",
good_actions=["status page", "third-party", "wait", "communicate", "queue"],
time_to_mitigate_ideal=15,
),
]
def role_check(role: str) -> None:
print(f"\n>>> Confirm your role: {role}")
input("[press ENTER to continue] ")
def phase(name: str, prompt: str) -> str:
print(f"\n{'='*60}\nPHASE: {name}\n{'='*60}")
print(prompt)
return input("Your action: ").lower().strip()
def evaluate_action(action: str, expected_keywords: list[str]) -> bool:
return any(k in action for k in expected_keywords)
def run_scenario(s: Scenario) -> None:
print(f"\n\n{'#'*60}\nSCENARIO: {s.name}\n{'#'*60}")
print(f"\n[t=0] 🚨 {s.trigger}")
# Phase 1: Detect + declare
role_check("Incident Commander")
action = phase("Declare",
"Is this an incident? What's your first move? "
"(hint: assign roles, open channel, page comms)")
if any(k in action for k in ["declare", "channel", "comms", "role"]):
print("✅ Good — you declared and set up roles.")
else:
print("⚠️ You should DECLARE and assign roles before diving in.")
# Phase 2: Diagnose
action = phase("Diagnose",
"What do you check first? (hint: recent deploys? "
"dashboards? logs? status page?)")
if any(k in action for k in ["deploy", "dashboard", "log", "status"]):
print("✅ Good — you looked at real data.")
# Phase 3: Mitigate
print(f"\n[t=+8min] Root cause found: {s.hidden_root_cause}")
action = phase("Mitigate",
f"How do you STOP THE BLEEDING right now? "
f"(remember: mitigate first, root-cause later)")
if evaluate_action(action, s.good_actions):
outcome_time = s.time_to_mitigate_ideal
print(f"✅ Mitigation successful. TTM = {outcome_time} min.")
else:
outcome_time = s.time_to_mitigate_ideal * 3
print(f"❌ That's not the fastest mitigation. TTM = {outcome_time} min "
f"(3x ideal — customers hurt for longer).")
# Phase 4: Postmortem outline
print(f"\n[t=+{outcome_time}min] Service recovered. Schedule postmortem.")
action = phase("Postmortem outline",
"Name three action items you'd include. "
"(examples: fix root cause, improve runbook, add alert)")
if "," in action or "\n" in action or "and" in action:
print("✅ You have concrete action items with clear scope.")
else:
print("⚠️ Try to have 3+ specific action items, each with an owner and due date.")
def main() -> None:
print("=" * 60)
print("INCIDENT TABLETOP EXERCISE")
print("=" * 60)
print("\nYou'll run through one randomly-selected scenario.")
print("Answer as if this were a real incident.\n")
scenario = random.choice(SCENARIOS)
run_scenario(scenario)
print("\n\n" + "=" * 60)
print("Debrief")
print("=" * 60)
print("- What went well?")
print("- What was harder than expected?")
print("- What would you add to your team's runbook after this?\n")
if __name__ == "__main__":
main()
What each block does
Anatomy of the runbook + simulator
Runbook template to fill in:
Save to runbooks/ in your repo. Link from the alert itself so the on-call sees it immediately.
(d) Production reality · 15 min
A misconfigured WAF rule caused catastrophic regex backtracking, spiking CPU to 100% on every edge server globally. All Cloudflare-fronted sites returned 502s. Detection was ~1 min (alerts fired immediately). Declaration and mitigation took ~27 min — the mitigation required rolling back the WAF rule globally.
rm -rf on what they thought was the secondary. It was the primary. All 5 automated backup mechanisms silently didn't work. They restored from a 6-hour-old snapshot.Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
Teach these three, no notes:
- The three IMAG roles — what each does, why they're separate.
- Why 'mitigate first, root-cause later' — one sentence.
- What 'blameless' means — and why it produces better postmortems than blame culture.
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.