Skip to content
Software Atlas

Reliability & SRE

How teams keep a service working for users: targets you can measure, alerts worth waking up for, calm incident response, blameless learning, and less manual toil.

⏱ ~50 min read16 sections10 interactive widgetsYAML · shell · Go · JavaScript
FoundationsPractitionerStaff-level
Foundations§01

The 30-second version

In one sentenceReliability isn't "nothing ever breaks". It's knowing how reliable users need you to be, noticing quickly when you aren't, fixing it calmly, and making the same failure less likely next time.

You've pushed code that someone else deployed. This topic is about what that "someone else" does once the code is running: how they decide whether the service is healthy, who gets woken up when it isn't, and what happens the morning after. The discipline has a name, SRE, coined at Google in the early 2000s. Today many companies use it, whether or not anyone there has "SRE" in their job title.

The reliability loop
Click a stage. Each one feeds the next, and the last one feeds back into the first.
🎯 Set a target. Decide what “working” means for users and write it down as a number: an SLO such as “99.9% of checkouts succeed”. The gap to 100% is the error budget, and a policy says what happens when it runs low.

The words you'll meet

  • An SLI is a measurement of what users experience, e.g. "the share of checkouts that succeed".
  • An SLO is the target for it: "99.9% over 30 days".
  • The Error budget is the 0.1% you're allowed to miss. It's a budget because you spend it, on risky releases, experiments and the occasional bad day.
  • An Incident is an unplanned event that hurts users. It's handled by whoever is on call, using a Runbook as a starting point.

Before you start

This topic builds on Observability. You don't need all of it, but §09 SLIs, SLOs and error budgets and §10 Alerts that deserve to wake someone are the foundation; we'll recap them briefly and then go further. Load testing has its own topic, and so do deployment strategies such as canaries and feature flags. We'll link to them rather than repeat them.

Foundations§02

“Reliable” is a number, not a feeling

In one sentenceA service is reliable when users can do what they came to do, fast enough, often enough. Write that down as a number, and arguments about reliability turn into arithmetic.

Start from the user, not the server

Ask a team "is the checkout service reliable?" and you'll hear "CPU is fine", "no errors in the logs", "it felt slow yesterday". None of those answers the question. The question users care about is: when I press Pay, does it work, and quickly?

So reliability is measured from the outside, as a ratio of good experiences to all experiences:

SLI = good events ÷ valid events

For checkout that might be "checkouts that returned success in under 1 second ÷ all checkouts". That number is the SLI. The target you set for it, say "at least 99.9% over the last 30 days", is the SLO. The Observability topic shows how to compute it from metrics; here we focus on using it.

Why not 100%?

100% sounds like the right goal. It's the wrong one, for four reasons:

  1. Users can't tell the difference. Their phone's Wi-Fi, their mobile network and their browser fail far more often than 0.01% of the time. Above a certain point your extra reliability disappears in their noise.
  2. Your dependencies aren't 100%. If your card-payment provider promises 99.95%, checkout can't do better than that without extra work, like a second provider.
  3. Each nine costs about ten times more. Going from 99.9% to 99.99% means redundant everything, slower and more careful releases, and people on call who must react within minutes.
  4. 100% leaves no room to change anything. Every deploy carries some risk. A 100% target, taken seriously, means never deploying.

That last point is the heart of SRE. The gap between your SLO and 100% is the Error budget, and it is meant to be spent: on shipping features, on experiments, on the occasional bad deploy. We'll make that concrete in §04.

What the nines look like

Toggle the SLO and watch how many failures it allows. Each square is one request:

What the SLO allows
Each square is one request. Red squares are the failures the SLO allows out of 1,000.
failures allowed per 1,000
1
per month at 1,000,000 requests
1,000
as a full outage per 30 days
43.2 min
if every request failed during it

The same arithmetic over a 30-day month (43,200 minutes):

SLOFailures allowedAs one full outage per 30 days
99%1 in 1007 h 12 min
99.5%1 in 2003 h 36 min
99.9% ("three nines")1 in 1,00043 min
99.99% ("four nines")1 in 10,0004 min 19 s

Four nines means the whole month's budget is gone in a single 5-minute outage. Only a page that wakes someone within a minute or two, plus systems that fix themselves, can defend that. Most products don't need it, and that's fine.

SLO, SLA: which one has lawyers?

An SLA is a contract with paying customers: "99.9% or you get 10% of your money back". An SLO is an internal target your team uses to make decisions. Keep the SLO stricter than the SLA (say, SLO 99.95% against an SLA of 99.9%). Then you start fixing things while missing the target only hurts your pride, not your revenue.

Practitioner§03

Choosing SLIs and writing the SLO down

In one sentenceAn SLI is a promise about one thing a user does. Pick two or three per service, define "good" and "valid" so precisely that two engineers would count the same number, and write it all down.

What to measure depends on what the service does

Different kinds of systems disappoint users in different ways, so they need different SLIs:

Kind of systemUsers are unhappy when…Typical SLI
Request/response (an API, a website)it errors, or it's slowAvailability: share of requests that succeed. Latency: share of requests faster than a threshold, e.g. under 300 ms.
Data pipeline (orders flowing into a dashboard)the data is old, or wrongFreshness: share of reads where the data is less than 5 minutes old. Correctness: share of records that pass a check.
Batch job (nightly invoices)it didn't finish in timeShare of runs that complete successfully before their deadline.
Storage (file uploads)a file comes back damaged or missingDurability: share of stored objects that can be read back intact.

Notice that a latency SLI is also written as a ratio: not "p99 under 300 ms" but "99% of requests complete in under 300 ms". The ratio form works the same way as availability, so the error budget and burn-rate math from §04–§05 apply to both. (If percentiles are fuzzy, Observability §06 explains them.)

Where you measure changes what you see

The same request can be counted in several places, and each one misses something:

Measured at…SeesMisses
Your server's metricsevery request your code handledrequests that never reached your code, e.g. because the server had crashed
The Load balancerrequests that reached your infrastructure, including ones to dead serversproblems before it: DNS, the user's network, a broken page that never sends the request
The client (browser or app reporting back)what users really experiencedit's noisy (bad phone networks), delayed, and some clients never report
Synthetic monitoring (a script that acts like a user every minute)whether a full journey works, even at 3 a.m. with no real trafficit only tests the paths you scripted, from a few places

A common, sensible start: SLIs from load balancer logs or server metrics, plus a synthetic check for your most important journey, so you notice outages even when traffic is too low to measure.

"Good" and "valid" need fine print

The ratio is easy. Agreeing on what goes on each side of it is the real work. Some questions every SLO has to answer:

  • Which requests are valid? Do health checks (the load balancer's automatic "are you alive?" requests) count? Bots? Requests from your own load tests? Usually not, since they aren't users.
  • Is a 4xx "bad"? A 404 because the user typed a wrong URL is the user's mistake, not an outage. But a sudden flood of 401s after a deploy is your bug. Most teams count 5xx as bad and watch 4xx rates separately.
  • What does "fast enough" mean? Pick the threshold from what users notice (a page that loads in under 1 s feels instant), not from what you happen to achieve today.
  • Over what window? A rolling 30 days (always "the last 30 days") is the usual choice: it moves smoothly instead of resetting on the 1st of the month.

Write it down

An SLO that lives only in an alert rule gets forgotten. Teams keep a short document next to the code, reviewed like code. Here's a plain example. The exact format matters less than the fields:

slo/checkout.yaml
service: checkout
owner: team-payments            # who answers for this SLO
slos:
  - name: checkout-availability
    description: Users can complete a checkout.
    sli:
      good: responses to POST /checkout with status < 500     # ← what counts as success
      valid: all POST /checkout requests, excluding health checks and synthetic load tests
      measured_at: load balancer logs                          # ← where we count
    objective: 99.9%            # the target
    window: rolling 30 days

  - name: checkout-latency
    description: Checkout feels instant.
    sli:
      good: successful POST /checkout answered in under 1000 ms
      valid: all successful POST /checkout requests
      measured_at: load balancer logs
    objective: 99%
    window: rolling 30 days

error_budget_policy: docs/error-budget-policy.md   # what we do when budget runs low (§04)
review: every quarter, with product                # targets aren't forever

Fewer, better SLIs

A service with fifteen SLOs has none, because nobody can keep fifteen numbers in their head during an incident. Start with one availability and one latency SLI for the most important user journey. Add more only when users complain about something your SLIs didn't catch. That complaint is a sign your SLIs measure the wrong thing.

Practitioner§04

Error budget policies: spending on purpose

In one sentenceAn error budget is only useful if everyone agreed beforehand what happens when it runs out. That agreement is the error budget policy.

The argument it ends

Without an SLO, every release turns into the same argument. Product wants to ship the new feature this week. The engineers who get paged want to slow down because "things feel fragile". Both sides are guessing, and the louder voice wins.

With an Error budget, the question becomes arithmetic: how much budget is left? If plenty is left, ship; the users are clearly fine. If it's nearly gone, users are already feeling pain, and the next risky release should wait. The Error budget policy writes down that rule before anyone is upset.

What a policy says

It's usually one page, signed by the engineering and product leads. A typical shape:

Budget left (rolling 30 days)What the team does
More than 50%Business as usual. Ship features, run experiments.
20–50%Ship carefully: risky changes go out as a canary (a small slice of users first) and need a second reviewer.
Under 20%Feature freeze: only bug fixes and reliability work ship, until the budget recovers.
Spent (0% or less)Freeze continues. The top reliability problems from recent postmortems become the team's priority.
One incident used more than 20% on its ownA postmortem is required, with at least one high-priority action item.

And a few rules for the edge cases:

  • Exceptions: who can approve an urgent launch during a freeze (e.g. a security fix, or a launch with a legal deadline)?
  • Not our fault: if a cloud provider's outage burned the budget, does that count? Most policies say yes for the SLO, since users felt it, but the freeze might be relaxed if your own changes weren't involved.
  • Who decides when it's disputed: usually a named director, so the decision doesn't drag on for days.

Play a quarter

The game below compresses this into 13 weeks. Each week you either ship a feature or spend the week on reliability. Shipping piles up risk; each feature makes the next incident a bit more likely. The policy above is enforced: in the red zone, the ship button turns off.

Run a quarter under an error budget policy
13 weeks. Each week, ship a feature or spend the week on reliability. Shipping adds risk that piles up; reliability work pays it down.
error budget left this quarterGreen: ship freely
100%
0%20% freeze50% careful100%
1
2
3
4
5
6
7
8
9
10
11
12
13
features: 0 · incident risk if you ship: 21%
More than half the budget left. Normal releases, experiments welcome.

Play it two or three times with different rhythms. We simulated 200 quarters of this game. Shipping every week averaged about 9 features, because the freeze stops you partway, and missed the SLO in over a third of quarters. Two weeks of features, then one week of reliability also averaged about 9 features, and met the SLO in every one of the 200 quarters. That's the claim SRE makes: reliability work and feature work aren't enemies; the budget tells you the right mix.

Rolling windows give budget back

In the game, the quarter is fixed, so spent budget never comes back. Real SLOs usually use a rolling 30-day window: the SLI always covers "the last 30 days". A bad day 31 days ago falls out of the window, and its cost is returned to the budget. That's how a freeze ends by itself. After a few clean weeks, the old incident ages out and the budget climbs back above the threshold.

Spending budget on purpose

A leftover budget at the end of the month isn't a trophy. It's risk you could have taken and didn't. Teams with plenty of budget left can spend it deliberately:

  • release more often, or with smaller canaries and shorter waits between steps,
  • run planned maintenance (a database upgrade with a few seconds of errors),
  • run chaos experiments in production (§12),
  • or lower the SLO, if you keep beating it by a mile and users don't notice the difference.
Practitioner§05

Alerting on burn rate, in practice

In one sentenceDon't page on "errors are high". Page on "at this pace, we'll blow the month's error budget soon". That pace is the burn rate.

The idea in one division

Observability §10 builds burn-rate alerts step by step, including the Prometheus rules. Here's the core again, because everything in this section rests on it:

burn rate = current error rate ÷ error rate the SLO allows

A 99.9% SLO allows 0.1% of requests to fail. If 1.44% are failing now, the Burn rate is 1.44 ÷ 0.1 = 14.4. Two numbers follow from it:

  • Budget used per hour = burn rate ÷ 720 (a 30-day window has 720 hours). 14.4 ÷ 720 = 2% of the month's budget every hour.
  • How long a full budget lasts = 720 hours ÷ burn rate = 720 ÷ 14.4 = 50 hours.

A burn rate of 1 means "exactly on pace to use the whole budget by the end of the window", which is fine. Anything well above 1, sustained, is trouble, and the higher it is, the sooner someone needs to look.

The standard tiers

These starting points come from Google's SRE Workbook (chapter "Alerting on SLOs"). Each rule checks a long window, to ignore short blips, and a short window, so the alert stops soon after the problem does:

Burn rateLong window / short windowBudget gone if it lasts the long windowAction
14.41 h / 5 min2%📟 page
66 h / 30 min5%📟 page
13 days / 6 h10%🎫 ticket

A page interrupts a human right now (an Page); a ticket waits for work hours.

Why two windows, and why both must agree

A "1 h window" means "the error rate averaged over the last hour", recomputed every minute. A rule fires only while the burn rate is above its threshold in both windows at the same moment. Each window covers the other's weakness:

  • The long window ignores blips. One bad minute barely moves a 1-hour average, so it can't page on its own.
  • The short window ends the page quickly. After the errors stop, the 1-hour average still contains the bad minutes and stays high for a long time. The 5-minute average forgets them within minutes. Because the rule needs both, the page clears as soon as the short window recovers.

Worked example, with a 99.9% SLO: from 14:20 to 14:40, 5% of requests fail, a burn rate of 50.

  1. At 14:21 the 5-minute average is already above 14.4, but the 1-hour average isn't: one bad minute in sixty is too little. No page.
  2. By 14:36 the last hour holds 17 bad minutes: 17 × 50 ÷ 60 ≈ 14.2, plus the background errors, puts it just over 14.4. Both windows are above the line, so the rule pages.
  3. At 14:40 a rollback stops the errors. By 14:43 the 5-minute window holds mostly good minutes, so it falls below 14.4, and the page clears.
  4. A 1-hour-only rule would keep paging until 15:23, when the bad 20 minutes have mostly slid out of the hour.

Switch between the three rules and watch the shaded "page firing" area:

Why each rule needs two windows
Errors at a burn rate of 50 from 14:20 to 14:40, then a one-minute blip at 15:45. Lines are the burn rate averaged over each window.
14.460014:0014:2014:4015:0015:2015:4016:00
1-hour average5-minute averagepage firing
1 h AND 5 min: pages 14:36–14:43. Pages at 14:36, once the 1-hour average passes 14.4, and stops at 14:43, a few minutes after the errors end at 14:40, because the 5-minute average has dropped and the rule needs both. The one-minute blip at 15:45 is ignored: the 1-hour average barely moves.

Try the numbers yourself

Burn-rate calculator
Pick an SLO and today's error rate. See how fast the 30-day budget goes and which alert tier that pace triggers.
allowed error rate
0.1%
100% − 99.9%
burn rate
14.4×
1.44% ÷ 0.1%
budget used per hour
2.0%
burn ÷ 720 hours
full budget lasts
2.1 days
720 h ÷ burn
100%50%0%day 0day 10day 20day 30
📟 Page (fast burn). ≥ 14.4× over 1 h and 5 min: at least 2% of the month's budget per hour.
The dashed line is the allowed pace (burn rate 1). Your line hits zero on day 2.1.
low-traffic check · requests per minute
One failed request in a 5-minute window = 5,000 requests with 1 failure = 0.02% errors = burn rate 0.2×. One failure is a small blip here; the windows filter it out naturally.

Two things to try. First, set the SLO to 99.99% and the error rate to 0.2%: a failure rate most people would call "fine" is a burn rate of 20, and the month's budget is gone in a day and a half. Stricter SLOs make alerts far more sensitive. Second, look at the low-traffic check at 10 requests per minute. A single failed request makes the 5-minute window look like a fast burn. More on that below.

The same math in code

You'll rarely compute this by hand, since your monitoring system does it. But writing it once makes it stick, and it's handy for scripts and dashboards. (New to Go? The 90-second Go primer covers the basics used here.)

package main

import (
	"fmt"
	"time"
)

// SLO is the promise: e.g. 99.9% of requests good over 30 days.
type SLO struct {
	Target float64       // 0.999
	Window time.Duration // 30 days
}

// BurnRate: how many times faster than allowed we are using the budget.
func (s SLO) BurnRate(bad, total int) float64 {
	if total == 0 {
		return 0 // no traffic, nothing burned
	}
	errorRate := float64(bad) / float64(total)
	allowed := 1 - s.Target   // 0.001 for a 99.9% SLO
	return errorRate / allowed // ← the whole idea is this one division
}

// BudgetUsed: the share of the whole window's budget spent during `lookback`.
func (s SLO) BudgetUsed(burn float64, lookback time.Duration) float64 {
	return burn * lookback.Hours() / s.Window.Hours()
}

// TimeLeft: how long a full budget lasts at this burn rate.
func (s SLO) TimeLeft(burn float64) time.Duration {
	return time.Duration(float64(s.Window) / burn)
}

func main() {
	slo := SLO{Target: 0.999, Window: 30 * 24 * time.Hour}

	// The last hour: 72,000 requests, 1,037 of them failed.
	burn := slo.BurnRate(1037, 72000)
	fmt.Printf("burn rate:          %.1f\n", burn)
	fmt.Printf("budget used (1 h):  %.1f%%\n", 100*slo.BudgetUsed(burn, time.Hour))
	fmt.Printf("full budget lasts:  %v\n", slo.TimeLeft(burn).Round(time.Hour))
}
// burn rate:          14.4
// budget used (1 h):  2.0%
// full budget lasts:  50h0m0s

(Both run as shown with Go 1.24 and Node 22. The output comments are real.)

Low-traffic services: when one request is 2%

Burn-rate alerts assume lots of traffic. At 10 requests per minute, a 5-minute window holds 50 requests; one failure is 2% errors, a burn rate of 20 at 99.9%. The short window alone would page for a single unlucky request. The long window helps (one failure in an hour of 600 requests is "only" a burn rate of 1.7), but quiet services still page more often than they should. Common fixes:

  • Require a minimum number of failures in the rule, e.g. "and at least 5 failed requests in the last hour". It's one extra condition in the alert expression (using the recording rules from Observability §10):

    expr: |
      slo:checkout_errors:ratio_rate1h > (14.4 * 0.001)
      and slo:checkout_errors:ratio_rate5m > (14.4 * 0.001)
      and sum(increase(http_requests_total{route="/checkout", code=~"5.."}[1h])) >= 5   # ← the new line

    increase(…[1h]) is the number of failed requests counted during the last hour.

  • Add Synthetic monitoring. A script that does a fake checkout every minute gives you steady traffic, so you notice an outage at 3 a.m. even when no real user does.

  • Group small services that share a user journey into one SLO, so there are enough requests to measure.

  • Accept a looser SLO or a longer window. A service with 100 requests a day can't meaningfully promise 99.9%: a single failure is already 1%.

What a good alert contains

When a burn-rate alert fires at 3 a.m., the person reading it is half asleep. The notification itself should answer the first questions:

the annotations of an alert rule
annotations:
  summary: "Checkout is burning its 30-day error budget 14× too fast"
  impact: "About 1 in 70 checkouts is failing; users see 'payment failed'."
  dashboard: "https://grafana.example.com/d/checkout-slo"
  runbook_url: "https://wiki.example.com/runbooks/checkout-burn"   # ← what to do first

The runbook_url is the most important line. §06 shows what goes in a Runbook.

Foundations§06

On-call without burnout

In one sentenceBeing on call means you are the person who answers when production needs a human. Done well, that happens rarely, every page is real, and the pager rotates fairly.

The rotation

An On-call rotation spreads the job across the team. The common setup:

  • Primary: gets paged first, for one week at a time.
  • Secondary: the backup, paged if the primary doesn't respond, and available to help in big incidents.
  • Weekly turns: with 8 engineers, each person is primary roughly one week in eight. Google's SRE book recommends at least 8 people for a rotation at one site (or 6 per site when two sites share it), so that nobody spends more than about 25% of their time on call. With fewer, on call comes around so often that people burn out.
  • Follow-the-sun: teams in, say, Europe and the US each cover their own daytime, so nobody is woken at night. It needs a team in more than one time zone.

The paging tool (PagerDuty, Opsgenie, Grafana OnCall and similar) holds the schedule and an Escalation policy: if the primary doesn't acknowledge the page within, say, 5 minutes, it pages the secondary, then a manager. Pages never vanish just because one phone was on silent.

What "being on call" asks of you

  • Be reachable and able to act within the agreed time (often 5–15 minutes to acknowledge): laptop nearby, network access, not on a plane.
  • You don't have to know everything. Your job is to respond, judge how bad it is, follow the runbook, and pull in help when you're stuck. Escalating early is a skill, not a failure.
  • Hand off cleanly. At the end of your week, a short On-call handoff (a note or a 15-minute call) tells the next person what fired, what's still open and what to watch.
  • Rest afterwards. Good teams give time off after a night of pages, and pay or compensate on-call time. A tired engineer makes the next incident worse.

Paging hygiene: every page must earn its noise

The fastest way to wreck on call is too many alerts. People who are paged for nothing learn to ignore pages, which is called Alert fatigue. Sooner or later they'll ignore a real one. The rule of thumb:

A page must be urgent, about something users feel (or will feel very soon), and actionable by the person who receives it. Everything else is a ticket for work hours, or a graph on a dashboard.

You've just joined a team and inherited these alert rules. Decide what each one should do:

Page, ticket, or delete?
You inherited these eight alert rules. Decide what each one should do when it fires.
Checkout burn rate 14.4× over 1 h (and over 5 min)
CPU above 90% on api-3 for 10 minutes (latency and errors normal)
TLS certificate for api.example.com expires in 10 days
Disk on db-1 will be full in about 3 hours at the current rate
One pod of 30 restarted; users saw no errors
Search latency SLO burning at 1.2× over 3 days
Synthetic login check failing from all 3 regions for 5 minutes
Nightly backup job failed once
0/8 decided · 0 match the suggestion

Measure the pager itself

Teams that keep on call healthy track a few numbers every week and review them together:

MetricHealthy-ish targetIf it's worse
Pages per on-call weeka handful. Google's SRE book treats 2 incidents per 12-hour shift as the ceiling, not the goaldelete, merge or downgrade alerts
Pages that needed actionclose to 100%every "nothing to do" page is a rule to fix
Pages outside working hoursas few as possiblefix the cause during the day; consider follow-the-sun
Time to acknowledgewithin the agreed minutescheck the paging setup and escalation

Runbooks: the first five minutes, written down

A Runbook is a short page linked from the alert. It turns "I've never seen this alert before, at 3 a.m." into a checklist. A good one fits on one screen:

runbooks/checkout-burn.md
# CheckoutBudgetBurnFast

## What it means
Checkout is failing ≥ 14× faster than the 99.9% SLO allows. Users can't pay.

## Check first (2 minutes)
1. Dashboard: https://grafana.example.com/d/checkout-slo
   Is the error rate still high? Which status codes? All regions or one?
2. What changed? Deploys and flag flips are the vertical lines on the graph.
3. Payment provider status: https://status.payments.example

## Mitigate
- Bad deploy in the last hour?  → roll back (see below)
- Payment provider down?         → switch to backup provider (flag `payments.backup`)
- Traffic spike?                 → scale out; reject low-priority traffic (/recommendations)

## Commands
    kubectl rollout undo deployment/checkout -n shop      # back to the previous version
    kubectl rollout status deployment/checkout -n shop    # wait until it's done

## Escalate
Not better in 15 minutes, or unsure → declare an incident (§07) and page #payments-oncall.

(The kubectl commands are what you'd run on a Kubernetes cluster; the Kubernetes topic explains them. They weren't run for this page.)

Notice what's there: what it means, how to confirm, what to do, when to escalate. Notice what isn't: the full architecture, the history of the service, a theory of the bug. And update the runbook after every incident where it was wrong. A stale runbook is worse than none, because people trust it.

Foundations§07

Incident response: roles, severity, communication

In one sentenceAn incident goes better when one person coordinates, each helper has exactly one job, and users hear from you before they have to ask.

What counts as an incident

An Incident is anything unplanned that hurts users or puts them at risk: an outage, a slow or broken feature, data loss, a security breach. The key decision is declaring one: saying out loud, "this is an incident, we're switching to incident mode." That switch brings in a process: a dedicated chat channel, roles, regular updates.

Declare early. It costs almost nothing to declare an incident and close it 10 minutes later as "false alarm". It costs a lot to have three engineers quietly poking at production in different chat threads for an hour, each assuming someone else is telling the support team.

Severity: how big is it?

A Severity level tells everyone how much to drop. Every company defines its own, but most look like this:

LevelExampleResponse
SEV1Checkout down for everyone; data being lost; a security breachAll hands. Leadership informed. Status page updates every 30 minutes or so.
SEV2A major feature broken for many users; checkout failing for 20%Incident team assembled now. Status page updated.
SEV3A minor feature broken, or a workaround existsFixed during work hours by the owning team.

When unsure, pick the higher severity. Downgrading later is easy; finding out an hour in that it was a SEV1 is not.

Roles: one job each

In a small incident, one person may do everything. Once more than two people are involved, split the work. The structure is borrowed from how fire services run emergencies:

1

Incident commander

The Incident commander coordinates. They keep the big picture, assign work, make the calls ("we roll back now"), and decide when it's over. They deliberately don't debug, because someone has to keep looking up from the logs.
2

Operations lead

Investigates and changes the system: reads dashboards, runs the rollback, tries the fix. Brings in specialists as needed. Announces every change in the incident channel before making it.
3

Communications lead

Keeps everyone else informed: posts to the Status page, updates support and leadership on a schedule, and shields the responders from "any news?" messages.

Bigger incidents add a scribe, who keeps a timestamped log of what was seen, tried and decided. That log becomes the backbone of the postmortem (§09). The commander can hand over the role ("I'm handing incident command to Ben") if they're tired or needed elsewhere. They just have to say it explicitly.

Watch one unfold

Step through a real-shaped SEV2. Watch the roles appear, the first status update go out, and when the team stops investigating and starts fixing:

An incident, minute by minute
A SEV2 on a checkout service, from page to resolution. Step through it.
time
14:02
who does what
📟 Ana (on-call) is paged
status page
All systems operational
budget used
2% of 30-day budget
what users feel
checkout errors: 18%
The CheckoutBudgetBurnFast alert fires: about 18% of checkouts fail, which is a burn rate of about 180 (18% ÷ 0.1%), far above the 14.4 page threshold. Ana, the on-call engineer this week, gets a phone notification and has a few minutes to acknowledge it (if she doesn't, it escalates to the secondary).
1 / 9

Communicating: short, honest, regular

Users and colleagues mostly want to know three things: is it being handled, what should I do, and when will I hear more? A good status update answers exactly that:

Investigating — Payments failing for some customers (14:12 UTC) Some card payments are failing at checkout. Orders are not charged when this happens, so it's safe to retry. We're investigating and will update by 14:45 UTC.

Notice what's missing: guesses about the cause, internal names, blame. Then keep the promise. Update by 14:45 even if the update is "still investigating". Silence feels worse to users than slow progress.

Ending an incident on purpose

An incident is mitigated when users are no longer hurt, and resolved when the commander declares it over, usually after a quiet period to make sure the fix holds. Before everyone goes back to normal work:

  1. post a final status update ("resolved, here's what happened in one sentence"),
  2. make sure any risky temporary changes are tracked (a disabled feature, an extra 20 servers),
  3. assign an owner and a date for the postmortem.
Practitioner§08

Mitigate first, root cause later

In one sentenceDuring an incident your job is to make users OK again, not to understand the bug. Understanding comes later, when nobody is failing to pay.

Why mitigation comes first

Every minute of an incident burns error budget and hurts real users. Finding a Root cause can take an hour of reading code under pressure. A Mitigation is anything that stops the harm without that understanding: put back the old version, send traffic somewhere healthy, switch the broken thing off.

It feels wrong to engineers. We want to know why. But the bug will still be there tomorrow, in the old version's diff, ready to be studied. Users won't wait.

Ask "what changed?" first

Most incidents are caused by a change: a deploy, a config edit, a feature flag flip, a certificate expiring, a traffic pattern shifting. So the cheapest question in any incident is "what changed just before it started?" Good dashboards draw deploys and flag changes as vertical lines on every graph, so the answer takes seconds (Observability §11 puts "what changed?" on its list of questions every dashboard should answer).

The mitigation toolbox

SituationMitigationTypical time
A deploy just went outRoll back to the previous versionminutes
A new feature misbehavesTurn it off with its Kill switch (a Feature flag)seconds
A dependency is downFail over to a backup; or degrade gracefully (hide the feature)minutes
Too much trafficAdd servers; shed low-priority trafficminutes
One server, zone or region is badDrain it: take it out of the load balancerminutes
Bad data was writtenStop the writer first; repair the data afterwardsvaries

A Rollback is the workhorse. It's fast, because the previous version was already built and tested, and safe, because it ran fine an hour ago. The Deployment strategies topic covers how blue-green releases (old and new versions running side by side, traffic switched in one step) and canary releases make rollbacks nearly instant.

Try it: race the clock

Three incidents, eight possible moves. Every move takes time, and the red area is budget burned while you made it. Try the "deploy" scenario twice: once by digging for the root cause first, once by rolling back first.

Mitigate or investigate?
Each action takes time, and users keep failing while it runs. Get the error rate under 1% while burning as little budget as you can.
Checkout errors jumped to 30% at 14:02. Something is wrong — you don't know what yet.
60%30%0%0 min30 min60 min
time since alert
0 min
error rate now
30%
month's budget burned
0%
red area ÷ 43.2 bad minutes (99.9%)
Pick your first move. Hint: the cheapest question in any incident is three minutes long.

Notice that the right mitigation depends on the situation, and only the 3-minute "what changed?" look tells you which situation you're in. Rolling back does nothing for a provider outage, and adding servers does nothing for a bad deploy. Restarting is the move people reach for when they don't know what's wrong. It helps only when a process is stuck (a memory leak, a deadlock, connections that never close); it does nothing for bad code or too much traffic, and under heavy load it makes things worse by removing servers for a while.

What a rollback looks like

How you roll back depends on how you deploy. Two common forms:

Kubernetes: go back to the previous version (not run here)
kubectl rollout undo deployment/checkout -n shop
kubectl rollout status deployment/checkout -n shop   # waits until every copy (pod) runs the old version
git: revert the bad commit, let the pipeline deploy it (run for real)
$ git log --oneline
7654ba1 Turn on pricing cache
7d5609e Add pricing service config
$ git revert --no-edit HEAD
[main 61828e1] Revert "Turn on pricing cache"
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline
61828e1 Revert "Turn on pricing cache"
7654ba1 Turn on pricing cache
7d5609e Add pricing service config

git revert doesn't delete history. It adds a new commit that undoes the bad one, so the normal pipeline can test and deploy it like any other change (CI/CD covers that pipeline). The kubectl route is faster because it skips the build. The git route is slower but leaves the repository matching what's running. Many teams do the fast one first, then the git revert, so the next deploy doesn't bring the bug back.

Mitigations that make things worse

Under pressure, some moves feel productive but backfire:

  • Restarting everything at once. You lose all capacity for a minute, and every client reconnects at the same moment (a Thundering herd).
  • Turning up retries. If a service is overloaded, more retries add load. That's a Retry storm, and it turns a slowdown into an outage.
  • Two people changing things at once. If both a rollback and a config change happen, nobody knows which one helped, or which one broke things further. The operations lead announces each change in the channel before making it.
  • "Quick fix" deploys under pressure. A hand-written fix that skips review and tests has a real chance of making things worse. Roll back first; fix forward only when rollback is impossible.
Practitioner§09

Blameless postmortems

In one sentenceA postmortem asks "what about our system made this failure possible, and hard to fix?", never "who screwed up?"

Why blameless?

Imagine an engineer ran a script that deleted a production table. If the postmortem concludes "Sam made a mistake; Sam has been told to be more careful", what changes? Nothing. The script still accepts production credentials without asking. The next tired person will do the same thing. And everyone has learned to keep quiet the next time they break something, so you'll understand less about your next incident.

A blameless Postmortem assumes everyone acted reasonably given what they knew at the time, and asks why the system let a reasonable action cause harm. People talk openly when they aren't afraid, and open talk is the only way to learn what really happened.

When to write one

Most teams write a postmortem when an incident:

  • hurt users noticeably (any SEV1 or SEV2),
  • used a big chunk of error budget (the §04 policy might say "more than 20%"),
  • needed a human to fix data or intervene by hand,
  • or was a near miss: nothing broke, but only by luck.

What goes in it

A postmortem is a document of a few pages, written within a week while memories are fresh. A common structure:

  1. Summary: two or three sentences a manager can read in 20 seconds. What broke, for whom, for how long.
  2. Impact: numbers. "About 1 in 5 checkouts failed from 13:58 to 14:17, then fewer during the rollback until 14:24; ~3,100 users affected; about 10% of the monthly error budget." (Check it the §05 way: roughly 19 minutes × 19% + 7 minutes × ~10% ≈ 4.3 bad minutes, out of the 43.2 a 99.9% SLO allows.)
  3. Timeline: timestamped facts, mostly copied from the scribe's log: when it started, when the alert fired, when someone acknowledged it, each action, when it was mitigated. The gaps are often the most useful part: why did 4 minutes pass between the first errors and the page, and another 10 after the acknowledgement before anyone asked what had changed?
  4. Contributing factors: everything that made the incident possible or worse.
  5. What went well: the rollback took 4 minutes; the runbook was right. These are worth keeping.
  6. Action items: concrete changes, each with an owner and a due date.

Here's the §07 incident's timeline as a strip. The bands are the gaps worth asking about:

The gaps in the timeline
The incident from §07 as a strip. Each gap is a question for the postmortem.
detect
ack
diagnose + mitigate
watch, then resolve
13:58
14:02
14:05
14:24
14:54
13:58 errors start (v2.31 deployed)
14:02 alert fires
14:05 acknowledged
14:24 mitigated (rollback done)
14:54 resolved
time to detect
4 min
errors start → alert
time to mitigate
26 min
errors start → users OK
time to resolve
56 min
errors start → incident closed
Averaged over many incidents, these become MTTD (mean time to detect) and MTTR (mean time to recovery, here the 26 minutes until users were OK). Ask about each gap: could an alert have fired sooner? Could the rollback have started at 14:15 instead of 14:17?

Contributing factors, not the root cause

Real incidents almost never have a single cause. The table-deletion incident above might have all of these:

  • the cleanup script accepted production credentials without a confirmation step,
  • production and staging credentials were stored in the same file,
  • the backup restore had never been tested, so recovery took 3 hours instead of 20 minutes,
  • the alert for "table missing" didn't exist; a customer noticed first.

Remove any one of them and the incident is smaller or doesn't happen. That's why many teams write "contributing factors" instead of "root cause". The popular Five whys technique is useful for digging, but it follows a single chain of "why?" and tends to stop at a person ("why did the script run? Because Sam ran it."). When you reach a person, ask one more question: why was it easy for a person to do that?

Practise the rewrite

Each sentence below is the kind of thing that shows up in first drafts. Click to see a blameless version and the question it opens up:

Rewrite it without blame
Each sentence is from a real-style postmortem draft. Click it to see the blameless version — and the question it opens up.
Notice the pattern: the blameful sentence ends the investigation (“Sam messed up”). The blameless one starts it, and points at something the team can change. Swap Sam for anyone else and the failure would have happened anyway.

Action items that actually happen

The point of the document is the action items. Too often they're vague ("improve monitoring") and quietly die in a backlog. Good ones are:

WeakStrong
Be more careful with scriptsCleanup script refuses production connection strings unless --prod is passed and the table name is typed again (owner: Sam, due 14 May)
Improve monitoringPage when any core table's row count drops more than 50% in 5 minutes (owner: Priya, due 21 May)
Test backupsWeekly automated restore of last night's backup into a scratch database, alert on failure (owner: Ben, due 31 May)

Prefer fixes that make the mistake impossible or harmless (a guard in the script) over fixes that rely on people remembering (a new rule in the wiki). And keep the list short. Three action items that get done beat twelve that don't.

Share it

Postmortems are most valuable when people outside the team read them. Many companies hold a weekly or monthly review where a postmortem is presented and discussed, and keep all of them in one searchable place. The same contributing factors (untested restores, missing alerts, manual deploy steps) show up again and again across teams. Spotting that pattern is how an organization, not just a team, gets more reliable.

Practitioner§10

Toil, and automating it away

In one sentenceToil is the manual, repetitive work a machine could do. It grows as your service grows, so if you don't automate it, it eventually eats the whole team.

What counts as toil

Not all operations work is toil. Google's SRE book gives a checklist. Toil is work that is:

  • manual: a human runs the commands,
  • repetitive: you've done it before and you'll do it again,
  • automatable: a script could do it, since no human judgment is needed,
  • tactical: done because something interrupted you, not because you planned it,
  • without lasting value: afterwards the service is exactly as good as before,
  • growing with the service: twice the traffic, twice the work.

Restarting a stuck worker every morning is toil. So is resizing a disk by hand whenever it hits 80%, or copying a customer's data between systems on request. Writing a postmortem, designing a new cache, or reviewing a pull request aren't toil; they need judgment, or they leave something better behind.

Google's teams aim to keep toil under 50% of each engineer's time, so the other half goes into engineering that removes future toil. Without a cap like that, toil grows until there's no time left to automate anything.

When does automating pay off?

Automation costs time up front and some upkeep afterwards. Toil costs a little time, forever, and more each month as traffic grows. Play with the numbers:

Is it worth automating?
A manual task that grows with traffic vs. a script you build once and maintain. Hours over one year.
0h62h124hwk 0wk 13wk 26wk 39wk 52by handautomated
toil per week
1.7 h → 3.0 h
today → in 12 months
hours by hand, year 1
118 h
automation pays off
week 16
build 24 h + 1 h/month
The red line curves upward: toil grows with traffic, the automated line doesn't. And the chart leaves out what doesn't fit in hours — mistakes made by tired humans on step 7 of 12, and the engineer who quits because half their week is copy-paste.

With the defaults, a 20-minute task done 5 times a week (about 1.7 hours a week, growing to about 3 hours) pays back a 24-hour automation project in about 16 weeks. And the chart doesn't count the hidden costs: the mistakes people make when they're bored, and the context switch every time it interrupts real work.

A real example: certificate expiry

An expired Certificate is one of the most embarrassing outages, because it's completely predictable. Browsers refuse the connection the moment a TLS certificate passes its expiry date. Many teams have a calendar reminder and a person who checks by hand. That's toil, and it fails the one time the person is on holiday.

The fix is a tiny program that connects to each host, reads the certificate's expiry date, and fails if any expires within 14 days:

package main

import (
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"time"
)

const warnBefore = 14 * 24 * time.Hour // renew at least two weeks ahead

func main() {
	failed := false
	dialer := &net.Dialer{Timeout: 10 * time.Second} // never hang on an unreachable host
	for _, host := range os.Args[1:] {               // e.g. go run . api.example.com:443 shop.example.com:443
		conn, err := tls.DialWithDialer(dialer, "tcp", host, nil) // nil config = normal certificate checks
		if err != nil {
			fmt.Printf("FAIL %s: %v\n", host, err)
			failed = true
			continue
		}
		cert := conn.ConnectionState().PeerCertificates[0] // the server's own certificate
		conn.Close()

		left := time.Until(cert.NotAfter)
		status := "ok  "
		if left < warnBefore {
			status, failed = "WARN", true // ← a human gets a ticket, weeks before users notice
		}
		fmt.Printf("%s %s expires %s (in %.0f days)\n", status, host, cert.NotAfter.Format(time.DateOnly), left.Hours()/24)
	}
	if failed {
		os.Exit(1) // non-zero exit fails the scheduled CI job
	}
}

We ran both against a local HTTPS server with a certificate made to expire in 5 days, then against the same server without trusting that certificate, against a port with nothing listening, and against a port that accepts the connection but never answers:

$ SSL_CERT_FILE=cert.pem go run . localhost:18443
WARN localhost:18443 expires 2026-11-04 (in 5 days)
exit status 1

$ go run . localhost:18443          # certificate not trusted
FAIL localhost:18443: tls: failed to verify certificate: x509: certificate signed by unknown authority (…)

$ NODE_EXTRA_CA_CERTS=cert.pem node check.mjs localhost:18443
WARN localhost:18443 expires 2026-11-04 (in 5 days)

$ node check.mjs localhost:18443 localhost:18444
FAIL localhost:18443: self-signed certificate
FAIL localhost:18444: connect ECONNREFUSED 127.0.0.1:18444

$ node check.mjs localhost:18445     # accepts, then says nothing
FAIL localhost:18445: timeout after 10 s
$ go run . localhost:18445
FAIL localhost:18445: context deadline exceeded

(SSL_CERT_FILE and NODE_EXTRA_CA_CERTS tell Go and Node to trust our home-made test certificate. Against real public hosts you don't need them. An already-expired certificate also shows up as FAIL, because the Handshake (the opening exchange where the client checks the certificate) refuses it.)

Run it on a schedule

A checker nobody runs is still toil. Let CI run it every morning. In GitHub Actions, a workflow can be triggered by a schedule instead of a push:

.github/workflows/cert-expiry.yml
name: cert-expiry
on:
  schedule:
    - cron: "17 7 * * *"      # every day at 07:17 UTC (minute hour day month weekday)
  workflow_dispatch:           # also adds a "Run workflow" button

permissions:
  contents: read
  issues: write                # needed to open an issue below

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-go@v7
        with:
          go-version: "1.26"
      - run: go run ./tools/certcheck api.example.com:443 shop.example.com:443
      - if: failure()          # only runs when the check above failed
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
        run: |
          # one open issue is enough: don't open a new one every morning
          open=$(gh issue list --label cert-expiry --state open --json number --jq length)
          if [ "$open" -eq 0 ]; then
            gh issue create --label cert-expiry --title "TLS certificate expiring soon" \
              --body "See $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
          fi

The cert-expiry label must exist in the repository once (create it in the Issues tab, or with gh label create cert-expiry). Because the step checks for an open issue with that label first, a certificate that stays unrenewed for a week produces one issue, not seven.

A ticket (here, a GitHub issue) is the right severity: it's real but not urgent, exactly like the "certificate expires in 10 days" alert in §06. The CI/CD topic explains workflows, jobs and permissions in depth.

The automation ladder

Most toil isn't removed in one step. Teams climb a ladder, and every rung is better than the one below:

  1. Written down: a runbook says exactly what to do, so anyone can do it.
  2. Scripted: one command runs all the steps.
  3. Self-service: the people who need it (another team, support) can trigger it without you.
  4. Automatic: the system detects the condition and runs it, and tells you afterwards.
  5. Designed away: the condition can't happen any more. Certificates renew themselves (e.g. with an ACME client such as cert-manager or Let's Encrypt's certbot), and disks grow automatically.

The certificate checker is rung 4 for noticing. Automatic renewal is rung 5, and the checker then becomes a safety net that should never fire.

Practitioner§11

Capacity: having enough before you need it

In one sentenceAn outage caused by "too much traffic" is usually a planning failure, not bad luck: the growth was visible in the graphs for months.

What capacity planning is

Capacity planning answers one question: will we have enough of everything for the busiest moment coming up, even if something breaks? "Everything" means servers, but also database connections, disk, queue throughput, third-party rate limits and cloud quotas.

The method has three inputs:

  1. How much one server can safely handle. You measure this with a Load test, not by guessing. The Load testing topic walks through finding that number with k6, and keeping Headroom below it.
  2. How big the peak will be. Today's busiest hour, grown by your monthly growth rate, plus any known events (a sale, a launch, a TV ad).
  3. What failure you must survive. One server? A whole Availability zone?

Plan for a zone, not just a server

The load testing topic adds one spare server ("N+1 redundancy") so a single crash doesn't hurt. In the cloud, servers are spread across a few availability zones: separate data centers in the same region. A zone can fail as a unit (power, network, a bad config pushed to one zone). If you run in three zones and lose one, the other two must carry everything.

Worked example: peak needs 12 servers' worth of capacity. Spread over 3 zones that's 4 per zone. Lose a zone, and 8 servers are left to do the work of 12, running at 150% of their safe limit. To survive, each zone needs 12 ÷ 2 = 6 servers, so 18 in total. That's 50% more than the bare minimum. With only 2 zones, surviving one means each zone must carry the whole load alone: double.

Try it, and press "lose zone B":

Capacity planner
How many servers for the peak you'll have in N months — and does the plan survive losing a whole zone?
peak in 6 months: 4,256 req/s
naive: just enough89% busy
zone A
zone B
zone C
peak ÷ 400 = 11 servers, rounded up to 4 per zone × 3 zones = 12.
N+1 zone: survive a zone loss59% busy
zone A
zone B
zone C
18 servers: each zone gets peak ÷ (400 × 2) = 6, so any 2 zones can carry everything.
Normal day: the naive plan runs at 89%, the N+1 plan at 59%. That unused capacity looks like waste — until you press lose zone B.

The limits that aren't servers

Adding app servers is usually the easy part. Incidents often come from the parts that don't scale with a button:

  • The database. More app servers means more database connections. A database that allows 500 connections, with 30 servers each keeping a Connection pool of 20, has already run out (600).
  • Cloud quotas. Accounts have limits on instances, IP addresses and API calls per second. Autoscaling that hits a quota at peak simply stops adding servers.
  • Third-party rate limits. Your payment or email provider might allow 100 requests per second on your plan. Your own scaling doesn't change theirs.
  • Things that fill up: disks, log volume, message queues. Forecast these like traffic: "at the current rate, the disk is full on 14 March".

Autoscaling is a tool, not a plan

Autoscaling adds servers when load rises. It's great for daily ups and downs, but it doesn't remove the need to plan:

  • it takes minutes to notice the load, start servers and warm them up, so a sudden spike hits before help arrives,
  • it has a maximum (which you set, or a quota sets for you),
  • and it only scales what it controls. It won't add database capacity or raise a partner's rate limit.

So you still decide the minimum (enough for a zone loss at normal peak) and the maximum (below every downstream limit), and you check both against forecasts.

Before a big day: test it

Before a launch, a sale or a marketing event, teams do a capacity review. They estimate the peak (often 3–10× normal for a big sale), check every limit above against it, and run a load test at that level in an environment like production. Then they pre-scale on the day instead of trusting autoscaling to react in time. The Load testing topic covers testing somewhere that resembles production.

Staff-level§12

Chaos engineering in brief

In one sentenceEvery system has failure modes nobody has seen yet. Chaos engineering finds them on your schedule, with small experiments, instead of at 3 a.m. on the system's schedule.

The idea

You've added health checks, timeouts and failover. But have you ever seen them work? Chaos engineering means deliberately injecting failures (kill a server, slow down a dependency, cut a network link) and watching whether the system copes. Netflix made it famous around 2011 with Chaos Monkey, a tool that randomly shut down production servers during work hours, so every team had to build services that survived it.

It's not random destruction. It's a scientific experiment:

  1. Define the Steady state: a measurable "everything's fine", usually your SLIs. "Checkout success ≥ 99.9%, p99 under 800 ms."
  2. Form a hypothesis: "If one database replica dies, the steady state holds."
  3. Inject the failure, starting with a small Blast radius: one server, 1% of traffic, or a staging environment.
  4. Compare the steady state during the experiment with normal. Stop automatically if the SLIs dip below a limit.
  5. Learn: if the hypothesis was refuted, you found a weakness cheaply. Fix it, then rerun the experiment.

Try some experiments

The system below has a load balancer, six app servers across three zones, a database with a replica, and a recommendations service on the checkout page. Pick which defenses exist, then break something:

Chaos lab
Pick the defenses the system has, then break something on purpose. Does the hypothesis hold?
defenses in place
experiment
Hypothesis: Checkout stays fast when the recommendations service is slow.
Choose an experiment and press run. Start small: 1% of traffic limits how many real users a refuted hypothesis can hurt.

The "add latency to recommendations" experiment is the classic lesson. A slow dependency is often worse than a dead one. A dead service fails fast, while a slow one makes every caller wait, until callers run out of connections or memory and fail too. A Timeout plus a fallback (show "popular items") is Graceful degradation: the page keeps working with a bit less.

A fault injector in a few lines

You don't need special tools to start. A small Middleware that fails a share of requests on purpose, switched on by environment variables, lets you check how callers behave when your service misbehaves:

package main

import (
	"log"
	"math/rand/v2"
	"net/http"
	"os"
	"strconv"
	"time"
)

// chaos wraps a handler and breaks a fraction of requests on purpose.
// CHAOS_ERROR_RATE=0.1 → 10% get a 503. CHAOS_DELAY_MS=300 → every request waits 300 ms.
func chaos(next http.Handler) http.Handler {
	errRate, _ := strconv.ParseFloat(os.Getenv("CHAOS_ERROR_RATE"), 64) // unset → 0, so chaos is off
	delayMS, _ := strconv.Atoi(os.Getenv("CHAOS_DELAY_MS"))
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(time.Duration(delayMS) * time.Millisecond)
		if rand.Float64() < errRate { // ← the injected fault
			http.Error(w, "chaos: injected failure", http.StatusServiceUnavailable)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok\n"))
	})
	log.Fatal(http.ListenAndServe(":18081", chaos(hello)))
}

We ran both with CHAOS_ERROR_RATE=0.1 CHAOS_DELAY_MS=300 (Go on port 18081, Node on 18082) and sent 200 requests to each (measured on this machine):

$ for i in $(seq 200); do curl -s -o /dev/null -w '%{http_code}\n' localhost:18081/; done | sort | uniq -c
    187 200
     13 503          # Go: 6.5% failed (random, so not exactly 10%)
$ curl -s -w 'time: %{time_total}s\n' localhost:18081/
ok
time: 0.301007s      # the 300 ms delay, on every request

$ for i in $(seq 200); do curl -s -o /dev/null -w '%{http_code}\n' localhost:18082/; done | sort | uniq -c
    181 200
     19 503          # Node: 9.5% failed
$ curl -s -w 'time: %{time_total}s\n' localhost:18082/
ok
time: 0.300438s

Now point a caller at it. Does the caller time out, retry with Retry with backoff, show a fallback, or hang?

Real tools, real environments

Once the basics work, dedicated tools inject failures at the infrastructure level, without code changes:

  • Toxiproxy: a proxy you put between a service and a dependency to add latency, drop connections or limit bandwidth. Handy in tests.
  • Chaos Mesh and LitmusChaos: kill pods, partition networks and stress CPU inside a Kubernetes cluster.
  • AWS Fault Injection Service, Azure Chaos Studio: managed experiments against cloud resources, with built-in stop conditions.

And start with people, not tools. A Game day is a scheduled session where the team runs one experiment together, everyone watching dashboards, the runbook open, a rollback ready. It tests the system and the humans at once.

Staff-level§13

Staff-level: reliability trade-offs

In one sentenceReliability is a design choice with a price tag. Senior engineers don't ask "how do we make it more reliable?" but "how reliable does this need to be, and what's the cheapest way to get there?"

Chains multiply

A checkout request might pass through a CDN, a load balancer, the app, a database and a payment API. It succeeds only if every step succeeds, so their availabilities multiply. Five parts at 99.9% each give 0.999⁵ ≈ 99.5%, five times the failures of any single part. A system is less reliable than its least reliable dependency, often a lot less.

Two tools fight back:

  • Redundancy (parallel copies): if any one of n copies can serve the request, it fails only when all of them fail at once. Two app servers at 99.5% each: 1 − 0.005² = 99.9975%.
  • Graceful degradation: take a non-essential part off the critical path. If recommendations fail, show a default list instead of an error page. Then it drops out of the multiplication entirely.
Availability math
A request needs every box in the chain to work. Multiply. Then see what redundancy and graceful degradation buy you.
CDN
99.990%
Load balancer
99.990%
App × 1
99.50%
one server, 99.5%
Database
99.950%
Payments API
99.90%
Recommendations
99.50%
checkout availability
98.83%
expected failure per 30 days
8 h 24 min
Six boxes that each look reliable multiply to about 98.83% — worse than the weakest one. Add app replicas, then make the non-essential call optional.Assumes the parts fail independently. Replicas in the same zone, on the same bad deploy or the same config share failures — real gains are smaller.

The catch: failures aren't independent

That replica math assumes the copies fail independently, like two coins flipped separately. In real life they often fail together: same buggy deploy, same expired certificate, same zone, same overloaded database underneath. That's a Correlated failure, and it's why "we have three replicas" doesn't mean 99.9999%. The defenses are about breaking the correlation:

  • spread replicas across zones (and for the highest tiers, regions),
  • deploy gradually (canaries, one zone at a time), so a bad version never reaches every copy at once,
  • avoid shared single points: one config service, one DNS provider, one certificate for everything, one person who knows how the system works. Each is a Single point of failure.

Every nine has a price

Moving from 99.9% to 99.99% doesn't make things a bit better. It cuts the allowed downtime from 43 minutes a month to about 4. In practice that requires most of:

To get…You need roughly…
99%one server, restart it when it dies, deploy whenever
99.9%redundancy across zones, health checks, rollbacks in minutes, an on-call rotation
99.99%automatic failover everywhere, gradual deploys with automatic rollback, on-call that acts within minutes, dependencies that are themselves 99.99%+
99.999%multi-region Active-active, a dedicated reliability team, years of work: rarely worth it outside infrastructure, payments or emergency services

Before asking for another nine, ask what the business loses per hour of downtime and compare it with the cost of the nine. Sometimes the honest answer is that 99.5% is right, and the money is better spent elsewhere.

Your SLO depends on your dependencies'

You can't promise users more than your critical dependencies give you, unless you design around them. If the payment provider is 99.95% and on your critical path, a 99.99% checkout SLO is fiction. Options: add a second provider (and fail over), make the dependency asynchronous (accept the order, charge the card a minute later from a queue), or set your SLO to what's achievable. The same applies inside a company. Ask internal teams for their SLOs, and give yours to the teams that depend on you.

Reliability vs. speed is mostly a false choice

It sounds like teams must choose between shipping fast and being reliable. Research on software delivery (the DORA reports behind the DORA metrics) has repeatedly found the opposite: teams that deploy most often also tend to have the lowest change failure rates and the fastest recovery. The mechanism is the toolbox from this topic. Small, frequent changes are easier to review, canary and roll back than big monthly releases. The error budget tells you when the balance is off. It shouldn't feel like a permanent tug of war.

Don't forget the humans

A system that meets its SLO by paging the same three people every night isn't reliable. It's borrowing from their health, and that loan comes due when they quit. Treat on-call load (pages per week, night pages, time spent on toil) as a reliability metric with its own targets, and fund reducing it like any other reliability work.

Practitioner§14

Pitfalls gallery

#1

An SLO nobody uses

The team set a 99.9% SLO, built a dashboard, and never looked at it again. When the budget hit zero, they shipped the next feature anyway. An SLO without an Error budget policy is decoration. Fix: agree the policy with product (§04), and look at the budget in every planning meeting.

#2

Measuring what's easy instead of what users feel

"Availability = the health-check endpoint returns 200." The health check passes while checkout fails, because the health check doesn't touch the database or the payment provider. Fix: measure real user requests (§03), ideally at the load balancer, and add a Synthetic monitoring check that runs the real journey.

#3

Paging on causes

"CPU > 80%", "pod restarted", "queue length > 1000". Most fire when users are fine, so people learn to ignore pages. That's Alert fatigue, and sooner or later a real page gets ignored too. Fix: page on SLO Burn rate (§05); move cause alerts to dashboards and tickets; keep a cause-based page only for "certain outage soon" (a disk filling in an hour).

#4

Debugging while users burn

An engineer spends 40 minutes reading the diff of the deploy that broke checkout, instead of rolling it back in 5. It feels productive; it costs a third of the month's budget. Fix: ask "what changed?", mitigate, then investigate (§08). Put "roll back first" in the runbook so nobody has to decide it under pressure.

#5

Nobody's in charge

Four engineers jump into the same chat thread, each trying something different. Two of them change production at the same moment; nobody updates the status page; support learns about the outage from Twitter. Fix: declare an incident, name an Incident commander, give everyone one job (§07).

#6

“Root cause: human error”

The postmortem ends at a person. Nothing about the system changes, and the next person makes the same mistake, or hides it. Fix: treat "human error" as the start of the investigation: why was that mistake easy to make, and hard to notice? (§09)

#7

Action items that never happen

"Improve monitoring", "be more careful", no owner, no date. Six months later, the same incident repeats with the same action items. Fix: few items, each concrete, owned and dated; review overdue items in a regular meeting; prefer guards in code over rules in a wiki.

#8

Runbooks from last year

The runbook mentions a dashboard that was deleted and a command for the old cluster. At 3 a.m., the on-call engineer follows it anyway, and makes things worse. Fix: link runbooks from alerts, give each an owner, and update them as an action item after any incident where they were wrong.

#9

Toil that ate the team

Every week brings more manual work: restarting jobs, resizing disks, answering the same request by hand. No time is left to automate any of it, so it keeps growing. Fix: cap Toil (Google aims for under 50% of time), measure it, and spend the rest automating the biggest items (§10).

#10

Counting replicas as independent

"We have three replicas at 99.9% each, so we're at 99.9999%." Then one bad deploy reaches all three, or one zone takes two of them down. Fix: assume correlated failures; spread across zones, deploy gradually, and plan capacity for losing a zone (§11, §13).

Practitioner§15

Check yourself

Work out your answer before you click. Every question is covered above, and each explanation names the section.

QUESTION 1 / 8
What's the burn rate?

Your SLO is 99.5% over 30 days. Right now 2% of requests fail.

QUESTION 2 / 8
First move

Errors jumped to 25% three minutes after a deploy. You don't know why yet. What do you do first?

QUESTION 3 / 8
Which alert should page?
QUESTION 4 / 8
The incident commander's job

During a SEV1, the incident commander mostly…

QUESTION 5 / 8
Is it toil?

Which of these is toil?

QUESTION 6 / 8
Chain availability

A request passes through three services in a row, each 99.9% available. Assuming independent failures, about how available is the whole path?

QUESTION 7 / 8
Blameless or not?

Which postmortem sentence is blameless?

QUESTION 8 / 8
Capacity for a zone loss

Peak traffic needs 12 servers. You run in 3 availability zones and must survive losing one. How many servers?

Answered 0 / 8
Foundations§16

Cheat sheet

🎯
SLI, SLO, budgetSLI = good ÷ valid events, measured from the user's side. SLO = target over a rolling window (e.g. 99.9% / 30 days). Budget = 100% − SLO.
💸
Spend the budgetAn error budget policy, agreed in advance, says what happens at each level: ship freely, ship carefully, freeze.
🔥
Burn rateburn = error rate ÷ allowed error rate. Budget per hour = burn ÷ 720. Full budget lasts 720 h ÷ burn.
📟
Page only when it mattersUrgent + users hurt + actionable → page. Real but not urgent → ticket. Otherwise → dashboard or delete.
🎖
Incident rolesCommander coordinates, operations lead changes the system, comms lead talks to everyone else, scribe writes it down.
⏪
Mitigate firstAsk "what changed?", then roll back, flip the kill switch, fail over or shed load. Root cause later.
📝
Blameless postmortemsTimeline, impact, contributing factors, what went well, a few owned and dated action items.
🤖
ToilManual, repetitive, automatable, no lasting value, grows with traffic. Keep it under ~50%; automate the biggest first.
FormulaExample
Error budget (30 days)99.9% → 0.1% → 43.2 min of full outage
Burn rate1.44% errors ÷ 0.1% allowed = 14.4
Budget used per hour14.4 ÷ 720 = 2%
Chain of dependencies0.999 × 0.999 × 0.999 ≈ 99.7%
n independent replicas1 − (1 − a)ⁿ: two at 99.5% → 99.9975%
Survive losing 1 of z zonesper zone = peak ÷ (z − 1); 12 servers' worth over 3 zones → 6 per zone, 18 total
Alert tier (99.9%, 30 days)Long / short windowAction
burn ≥ 14.41 h / 5 minpage
burn ≥ 66 h / 30 minpage
burn ≥ 13 days / 6 hticket
SeverityTypical meaning
SEV1core feature down for most users, data loss, security breach
SEV2major feature broken for many users
SEV3minor feature broken, or a workaround exists

Status update template: what users see · what they should do · when the next update comes.

Runbook template: what it means · check first · mitigate · commands · when to escalate.

Chaos experiment: steady state → hypothesis → small blast radius → automatic stop → learn and fix.

Finished reading?Mark it complete to track your progress on a roadmap. Saved in this browser only.