Incidents need a structure, not heroics
The instinct during an outage is for everyone competent to pile in and start debugging. That produces duplicated work, contradictory changes, a stakeholder channel nobody is updating, and a decision-making process where the loudest voice wins. Incident command exists to prevent all four.
| Role | Owns | Explicitly does not |
|---|---|---|
| Incident Commander | Decisions, priorities, delegation, declaring severity and resolution | Debug. The IC's hands stay off the keyboard |
| Operations lead | Making changes to the system, one person at a time | Communicate externally |
| Communications lead | Status page, stakeholder updates, customer support liaison | Debug or make changes |
| Scribe | Timeline of what was observed, decided, and done, with timestamps | Anything else |
| Subject matter experts | Answering specific questions when asked | Making unilateral changes |
For a small incident one person holds every role, and that is fine. The value of naming them is that when an incident grows, promotion is a decision rather than a scramble. The single most common failure is an IC who is also debugging — at which point nobody is running the incident.
Severity levels that mean something
| Severity | Definition | Response |
|---|---|---|
| SEV-1 | Complete outage or data loss affecting most users | Page immediately, all hands, exec notification, status page within 15 min |
| SEV-2 | Major functionality degraded or a subset of users fully broken | Page, dedicated IC, status page, hourly updates |
| SEV-3 | Minor degradation, workaround exists, budget burning slowly | Ticket, business hours, no status page |
| SEV-4 | Cosmetic or internal-only issue | Backlog |
Write the definitions down and make declaring an incident cheap and blameless. The expensive failure mode is not over-declaring; it is an engineer spending ninety minutes deciding whether something counts as an incident while customers are affected.
The first fifteen minutes
- Declare. Open the channel, name the IC out loud. Do not wait for certainty about severity — start high and downgrade.
- Assess impact. Which SLO is burning, how fast, and who is affected? This determines everything that follows.
- Stop the bleeding before understanding it. Roll back, fail over, shed load, disable the feature flag. Diagnosis is a luxury you buy with mitigation.
- Communicate. First stakeholder update within fifteen minutes even if it only says 'we are investigating'. Silence is read as incompetence.
- Only then diagnose. With impact contained, use the triage sequence from Part 5: RED at the edge, walk the call graph, USE at the suspect.
Mitigation before diagnosis is the hardest discipline to instil, because it feels like giving up on understanding. It is not. A rollback that restores service in four minutes and leaves you puzzled is unambiguously better than a root cause found in forty minutes of customer impact. You can reproduce it afterwards in staging, calmly.
Postmortems that produce change
A postmortem is worth writing only if something is different afterwards. Most are not, and the reasons are consistent: they are written by one person days later, they blame an individual, and their action items have no owner or date.
Blameless means structurally blameless
'Blameless' does not mean politely avoiding names. It means starting from the assumption that everyone acted reasonably given the information they had, and asking why the system made the wrong action look right. If an engineer ran a destructive command against production, the finding is not that they were careless. It is that a destructive command was runnable against production without a confirmation, a review, or a guard.
postmortem-2026-07-14-checkout.md
# Incident 2026-07-14: Checkout unavailable for 34 minutes
**Severity:** SEV-1
**Duration:** 14:02 - 14:36 UTC (34 min)
**Error budget consumed:** 71% of the 28-day budget
**Customer impact:** ~18,400 failed checkout attempts, est. GBP 240k deferred revenue
## Timeline (UTC)
| Time | Event |
|-------|-------|
| 13:58 | Deploy of checkout-api v2.14.0 begins |
| 14:02 | CheckoutErrorBudgetBurnFast fires (14.4x). Paged. |
| 14:05 | On-call acknowledges; declares SEV-2, opens incident channel |
| 14:09 | Escalated to SEV-1 - error rate at 94% |
| 14:11 | Trace shows connection pool exhaustion on the orders DB |
| 14:18 | Rollback to v2.13.4 initiated |
| 14:31 | Rollback complete; error rate begins falling |
| 14:36 | Error rate normal. Incident resolved. |
## What happened
v2.14.0 added a per-request call to the loyalty service inside the
checkout transaction. Each call held a database connection for the
duration of the external call. Under production concurrency the pool
of 20 was exhausted within four minutes.
## Why it was not caught
- Load testing ran against a staging loyalty service responding in 8ms;
production responds in 180ms at p50.
- Connection pool saturation was not on any dashboard, and had no alert.
- The change was reviewed by two engineers; neither had context on the
transaction boundary.
## Why detection took 4 minutes and mitigation 29
- Detection was correct and fast; the alert worked as designed.
- Rollback took 13 minutes because the deploy pipeline had no one-click
rollback and required a manual Argo sync with an approval step.
## What went well
- Burn-rate alert fired within 4 minutes with an accurate summary.
- Trace-based diagnosis identified the cause in 6 minutes.
- Comms lead posted to the status page at 14:12.
## Action items
| # | Action | Owner | Due | Ticket |
|---|--------|-------|-----|--------|
| 1 | Add connection pool saturation to the service dashboard and a ticket-level alert | @priya | 2026-07-21 | OBS-441 |
| 2 | One-click rollback in the deploy pipeline, no approval for reverts | @sam | 2026-07-28 | PLT-102 |
| 3 | Load test against production-representative dependency latency | @jo | 2026-08-11 | CHK-889 |
| 4 | Lint rule: forbid external calls inside a DB transaction | @priya | 2026-08-11 | CHK-890 |
| 5 | Error budget policy: freeze until budget recovers above 25% | @lead | immediate | - |
- Every action item has one named owner and a date. A team is not an owner.
- Action items go in the normal backlog with the same priority mechanics as everything else. A separate 'postmortem tracker' is where actions go to be forgotten.
- Review completion rate monthly. If under half of postmortem actions ship, the postmortems are theatre and everyone knows it.
- Publish them internally. The learning value is mostly for people who were not in the incident.
On-call as a system to be measured
On-call quality determines retention more than almost anything else in an engineering organisation, and it is measurable. Track these and review them quarterly like any other SLO.
| Metric | Healthy | Investigate when |
|---|---|---|
| Pages per shift | < 2 per week | > 5 per week, or any out-of-hours page two nights running |
| Out-of-hours pages | < 1 per week | More than 2 — this is what burns people out |
| Actionable page ratio | > 90% | < 75% — the rotation is being trained to ignore alerts |
| Time to acknowledge | < 5 minutes | Consistently longer means notification routing is broken |
| Rotation size | 6– 8 people | Fewer than 5 means shifts come round too often to sustain |
| Postmortem action completion | > 80% within 30 days | < 50% — the process is not producing change |
- Compensate on-call, in money or in time off. Unpaid out-of-hours work is a hidden cost that eventually gets paid in resignations.
- Run a proper handover at each shift change: what fired, what is degraded, what is deploying, what is deferred.
- Give the on-call engineer explicit permission to have no sprint commitments. On-call is the work.
- Rotate people through it. An on-call rotation that excludes the developers who write the code removes the feedback loop that makes the code better.
Scaling the platform without scaling the bill
As the estate grows, the observability platform becomes one of the larger line items in the infrastructure budget. Four levers control it, in descending order of effect.
1. Aggregate before long-term storage
Downsampling by aggregation
groups:
- name: longterm-aggregation
interval: 60s
rules:
# Per-pod detail is valuable for 15 days and worthless for a year.
# These aggregates typically cut retained series by 20-50x.
- record: cluster_service:http_requests:rate5m
expr: |
sum by (cluster, service, route, code_class) (
label_replace(rate(http_requests_total[5m]),
"code_class", "$1xx", "code", "(.)..")
)
- record: cluster_service_le:http_request_duration_seconds_bucket:rate5m
expr: |
sum by (cluster, service, le) (
rate(http_request_duration_seconds_bucket[5m])
)
2. Set retention per signal and per tier
| Data | Hot (queryable, fast) | Cold (object storage) | Delete |
|---|---|---|---|
| Raw metrics, 15s | 15 days | — | 15 days |
| Aggregated metrics, 60s | 90 days | 13 months | 13 months |
| Application logs | 7 days | 30 days | 30 days |
| Audit / security logs | 30 days | 12 months+ | per compliance requirement |
| Traces (sampled) | 3 days | 14 days | 14 days |
3. Attribute the cost
Multi-tenancy with per-team limits does two things: it contains the blast radius of one team's cardinality accident, and it makes the cost visible to the people who can reduce it. A monthly report of active series and log volume by team changes behaviour in a way that no amount of documentation does.
Who is using what
# Cost attribution queries
# Active series by team
sort_desc(
count by (team) ({__name__=~".+"})
)
# Log volume by namespace over 24h, in GB
sort_desc(
sum by (namespace) (
bytes_over_time({namespace=~".+"}[24h])
) / 1e9
)
# The 20 most expensive metrics by series count
topk(20, count by (__name__) ({__name__=~".+"}))
4. Prune what nobody queries
A significant fraction of ingested metrics are never queried by anyone. Grafana usage insights and the Mimir query analyser will tell you which metrics appear in dashboards and rules; anything appearing in neither, for ninety days, is a candidate for a drop rule at the collector.
A maturity path, and where to go next
| Stage | You have | Next step |
|---|---|---|
| 0 — Reactive | Customers tell you when things break | Basic uptime monitoring and centralised logs |
| 1 — Monitored | Dashboards and threshold alerts, high noise | Define SLIs and SLOs for your top service (Part 2) |
| 2 — Objective-driven | SLOs with an error budget policy, burn-rate alerts | Full RED and USE coverage, structured logs (Parts 5, 8) |
| 3 — Correlated | Metrics, logs and traces linked by trace ID and exemplars | Cardinality budgets, tail sampling, cost attribution (Parts 7, 9) |
| 4 — Self-service | Teams define their own SLOs on a shared platform with golden paths | Automated production readiness, capacity forecasting, chaos testing |
Most organisations sit between 1 and 2, and the jump to 2 is where nearly all of the value is. It requires no new tooling: it requires deciding what 'working' means, measuring it honestly, and agreeing what happens when you miss.
What this series did not cover
- Chaos engineering. Deliberately injecting failure to verify your assumptions and your alerting actually fire.
- Capacity planning and load testing. Forecasting demand and finding the knee in your latency curve before your users do.
- Production readiness reviews. The checklist a service passes before it is permitted to page a human.
- Progressive delivery. Canary and blue-green deploys gated automatically on SLO burn rate.
- Continuous profiling. Pyroscope and the fourth signal, which closes the loop from 'this service is slow' to 'this function is slow'.
Hands-on: labs/12-incidents in the companion repo includes an incident simulation you can run against the lab stack, the postmortem template above, and the cost-attribution dashboards.
Key takeaways
- Incident command separates deciding from debugging. The IC's hands stay off the keyboard.
- Mitigate before you diagnose. A rollback that restores service beats a root cause found during ongoing impact.
- Blameless means asking why the system made the wrong action look right, not politely omitting names.
- Every postmortem action needs one named owner, a date, and a place in the normal backlog.
- Measure on-call health - pages per shift, out-of-hours pages, actionable ratio - and treat it as an SLO.
- Control platform cost with aggregation for long-term storage, per-signal retention, per-team attribution, and pruning unqueried metrics.
- The jump from threshold alerting to SLO-driven operation needs no new tools, only a decision about what 'working' means.
Quick answers
Who should be the incident commander?
Whoever declared the incident, until they hand it over. The IC does not need to be the most senior person or the deepest expert - in fact the expert is usually the wrong choice, because they will be pulled into debugging. The IC needs to be able to run the process, delegate, and make decisions with incomplete information.
How do we make postmortems actually blameless?
Structure enforces it better than intent. Ask 'what made this action seem correct at the time' rather than 'why did you do this'. Require every action item to change a system, a process, or a guard rail - never a person's behaviour. If a finding reads as 'be more careful', it is not a finding.
How long should we keep metrics, logs and traces?
A common split is metrics for 13 months so year-on-year comparisons work, application logs for 7 to 30 days, and sampled traces for 3 to 14 days. Audit and security logs follow your compliance requirement and should be stored separately, since their retention is usually far longer and their query patterns are different.
Our observability bill is out of control. What do I change first?
Look at cardinality before anything else - it is almost always the dominant cost, and Part 7 covers the mechanics. Then aggregate away instance-level labels before long-term storage, set per-signal retention, and drop metrics that appear in no dashboard and no alert. Attribute the remaining cost per team and publish it; visibility does more than any single technical change.