Courses / System Design Interview / Chapter 38
Module 4 · Wrap-Up · Chapter 38 of 39
Lessons from Real Outages
Builds on Abstractions That Hide the Network, Availability, Reliability, Scalability and Latency, DNS: How a Name Becomes an Address, Load Balancers, Databases: Replication, Partitioning and Choosing a Store, Monitoring: Metrics, Logs, Traces and Alerts, Rate Limiters and Design a Payment System. Next up: How to Practise: A 45-Minute Checklist.
Part 1 · How Big Outages Happen
Small changes, big outages
It is 9:40 pm on the night of a cricket final, and a tea stall near Pune station is packed. A fan scans the stall's QR code to pay ₹20. The payments app spins, then fails, and across India millions of payments fail the same way. No server has died. Ten minutes ago, an engineer shipped a one-line config change that cut the timeout on database calls from 2 seconds to 200 milliseconds. Every server loaded it within seconds. At peak many queries take longer than that, so calls time out, every layer retries them, and the database sinks under many times its normal load.
This payments app is the chapter's running example, because its bad night holds most of the lessons. Disks and servers die every day at scale, and the redundancy from Availability, Reliability, Scalability and Latency absorbs them; the outages that make the news start with something ordinary, like that config change.
After a serious incident, well-run teams write a postmortem: the timeline, the impact, the trigger, why the damage spread, and the actions that stop a repeat. Some publish theirs. This chapter tells seven patterns from public postmortems as short stories: what happened, why it spread, and what stops it. Six follow one published write-up each, at the level it describes; the seventh, overload, appears in too many to need one.
- 1What triggered it?A command, a config push, a deploy, a date
- 2Why did it spread?A global push, a shared part, retries that kept going
- 3How was it found?Ideally by an alert on what users feel
- 4Why was recovery slow?Hard rollback, cold restarts, or broken tools
Triggers are small and cannot be ruled out: people mistype, and tests miss cases. Spread and recovery are where design helps. Most lessons in this chapter make sure one mistake reaches only a small part of the system and can be undone quickly. The next page asks how small that part can be.
Blast radius
The tea-stall outage reached every user because every server took the change at once. Blast radius is how much of a system, and how many users, one failure or one change can affect. Think of the sealed compartments in a ship's hull: a leak floods one compartment, not the ship. Systems get compartments in two ways: split the system into parts that fail independently (in space), and change one part at a time (in time).
These are the usual units of space, from smallest to largest:
| Unit | What it is | When one fails |
|---|---|---|
| Server | One machine behind a load balancer | Health checks route around it |
| Availability zone | One or more data centres in a region, with separate power and network | The other zones in the region keep serving |
| Region | A group of zones in one area, such as a city | Another region takes over, if data was copied there |
| Cell | A full copy of the service (servers, cache, database) that serves a fixed slice of users | Only that slice of users is affected |
Servers, zones and regions mostly contain hardware faults; only cells also contain a bad change, because each cell can be changed on its own. Take the payments app's 20 million users. As one big deployment, a bad change reaches all 20 million at once. Split into 10 cells of 2 million and change one cell at a time, and a mistake reaches at most 2 million before anyone reacts. Start each rollout on 1% of the first cell, and about 20,000 people meet it first.
Cells are not free: ten copies of every component to run, watch and upgrade, plus a routing layer that maps each user to a cell. Below a few million users, zones and staged rollouts give most of the protection for far less work. Ask of every component and every tool: what is the most this one thing can break?
Cells and regions contain a failure only if they share nothing. One global config service, login service or database behind every cell is a hidden path to everywhere.
Part 2 · Seven Patterns from Real Incidents
A command that removed too much
The first story shows how far one typed command can reach when the tool behind it sets no limit. In 2017, a large cloud provider was debugging a slow billing process in its storage service in one region. Following an established procedure, an authorised engineer ran a command to take a few servers out of service. One input was entered incorrectly, and the command removed a larger set of servers than intended, including servers that two core parts of the storage service depended on.
Why it spread. Both parts needed a full restart, which had not been done in the larger regions for many years. The service had grown a lot since, so the restart and its safety checks took longer than expected. Many websites and services that kept files there broke too. For a while even the provider's status page could not be updated, because its admin tool depended on the same storage service.
What stops it. Each weakness has a matching fix:
| What went wrong | What stops it |
|---|---|
| The tool removed too much capacity, too quickly | Make big actions slow, so a mistake can be seen and stopped halfway |
| Nothing checked that enough capacity would be left | Refuse any action that would leave a system below its minimum |
| A full restart had not been done there for years | Practise rare operations, so you know they work and how long they take |
| The status page relied on the broken service | Keep the tools you need in an outage off the systems they report on |
The provider's published follow-ups matched three of these rows: the tool now removes capacity more slowly and refuses to go below a subsystem's minimum, and the status page's admin tool runs across several regions. It also said it would speed up splitting the slowest part to recover into cells.
If one wrong input can take out a core part of a service, the tool is the bug, not the typist. Build the payments app's operations scripts like APIs for strangers: validate inputs, preview the effect, and limit what one call can do.
A bad update sent everywhere at once
The command removed capacity. The next story added a file, and the file reached every machine at the same moment. In 2024, a cybersecurity company sent a routine content update to the security agent that runs on its customers' Windows computers. The update was a data file the agent reads, not new program code. The file triggered a bug in the agent, which runs deep inside the operating system, and the computers crashed.
Why it spread. The update went to customers' machines all at once, instead of to a small group first, so no small group could reveal the problem. The company soon pulled the bad file, but the agent loads during start-up, so many machines crashed again on every restart, before they could fetch the fix. Airlines, hospitals, banks and broadcasters were among those hit, and a large number of machines had to be repaired by hand, one at a time.
The pattern reaches far beyond security software. Many teams stage code carefully, yet treat config, feature flags, rules and data files as safe to change everywhere at once. A config change can break a system as badly as a code change, and config systems are built to be fast, so a bad value reaches every machine in seconds. The payments app's timeout was exactly this: one value in a config file, sent down a fast path that skipped the stages its code went through.
A rule that pinned CPUs worldwide
The third story is another fast path for data, with burnt CPU instead of crashes. In 2019, a company whose network sits in front of a great many websites, to speed them up and protect them, added a rule to its web application firewall, the layer that inspects requests for attacks. The rule contained a regular expression that made the matching engine do an enormous amount of work. CPUs on the machines serving web traffic went to nearly 100%, and visitors to the sites behind the network got errors.
Why it spread. The company's software releases went out in stages, but firewall rules were deployed to every server in one go. Service came back within about half an hour, when the team used a global switch to turn off the firewall's managed rules; the bad rule was then removed and the rules switched back on.
The engine was working as designed. Most regex engines in popular languages backtrack: when part of a pattern fails, they go back and try another way to match. With some patterns, such as a repeat inside a repeat, the number of ways to try doubles with each extra character. This is catastrophic backtracking.
pattern ^(a+)+$ 'a' × 40 matches at once 'a' × 20 + 'b' fails after ~1 million tries: milliseconds 'a' × 30 + 'b' fails after ~1 billion tries: seconds 'a' × 40 + 'b' fails after ~1 trillion tries: minutes to hours
A textbook pattern, not the incident's rule. Times are rough and depend on the engine and machine.
What stops it. Input that matches is instant, so tests that only check what a rule catches will pass it. Use an engine built on the RE2 design, such as Go's standard regexp package, whose time grows only linearly with the input, at the cost of features such as backreferences. Otherwise cap the time one match may take. Keep a kill switch per feature, as the company did, and stage rules too: even a fast lane can send a rule to 1% of servers and watch CPU for a few minutes. The payments app's firewall rules need the same care.
A network change that cut a company off
The next story shows a safety check doing harm because every site ran it at once. In 2021, a large social network vanished from the internet for several hours. During routine maintenance, a command meant to check the capacity of its backbone, the private network joining its data centres, instead took down every backbone connection. An audit tool built to stop commands like this had a bug and let it through.
Why it spread. With the backbone gone, the data centres were cut off from each other and from the internet. The company's DNS servers were designed to withdraw their BGP (Border Gateway Protocol) announcements, the messages that tell other networks how to reach them, whenever they could not reach the data centres. That is a sensible check for one sick site, but every site tripped it together, so the rest of the internet could no longer find any of the company's names.
Recovery was slow. As DNS: How a Name Becomes an Address warned, everything that depends on names breaks along with DNS, and that included many internal tools; remote access, even the backup path, was down too. Engineers had to go to the data centres in person, where machines are deliberately hard to reach and change. Once the backbone returned, services came back in stages to avoid a dangerous surge, something the company had practised in drills.
The first rule applies to the payments app's load balancers: if most servers fail their health check at once, the balancer keeps them all in rotation, because a broken check is more likely than a dead fleet.
Backups that turned out not to work
The stories so far were about spread; this one is about recovery when the last line of defence was never tested. In 2017, a company that hosts code for developers was fighting a database problem late at night: spam was loading the primary database, and the replica copying it had fallen behind. While rebuilding the replica, an engineer ran a command to delete its data directory, in a terminal connected to the primary instead. Within seconds, almost all of the production database was gone.
Why recovery failed. The regular dumps had been failing silently, because the backup tool's version did not match the database's, and the emails reporting the failures were being rejected. Other backup methods turned out to be missing or broken too. The restore came from a copy an engineer had happened to take some hours earlier for a test environment, so changes made since, such as new issues and comments, were lost; the code in the repositories was not affected.
What stops it. Four habits would have turned this into a short outage:
| Habit | In practice |
|---|---|
| Replication is not a backup | Replicas copy a bad delete within seconds. Keep point-in-time copies where production commands cannot reach them |
| Alert on missing success | Failure emails can fail too. Alert when no backup has succeeded in, say, 25 hours |
| Only a restore proves a backup | Restore into a test environment on a schedule, check the data, and time it |
| Make the wrong server look wrong | Show the host and its role in the prompt, and ask for the server's name before deleting |
The RPO and RTO from Availability, Reliability, Scalability and Latency turn these habits into requirements. For the payments app's ledger, nightly dumps could lose up to a day of payments, which no bank would accept, so it also ships the database's log of changes to backup storage every few minutes: an RPO of minutes even for a mistake the replicas copied. Only a timed restore drill tells you your real RPO and RTO.
A certificate that expired everywhere
Every story so far began with a change someone made. The next kind of outage needs no change at all: it waits for a date. In 2018, a certificate built into software from a telecom equipment maker expired. The software ran inside the networks of several mobile operators in more than one country, and when the date passed it stopped working. Millions of people lost mobile data for most of a day, until fixed software was installed.
Why it spread. This is a time bomb: a failure that waits for a date or a count. Common ones are a TLS certificate or API credential that expires, a licence date, and a counter stored as a signed 32-bit integer, which tops out at about 2.1 billion. Every copy of the software shares the same calendar and the same counters, so a time bomb goes off everywhere at once. No canary can catch it, because nothing was deployed.
The payments app carries time bombs too: the certificates on its connections to each bank, and any transaction id stored in 32 bits.
Overload and retry storms
The last pattern is the one behind the tea-stall outage, and it runs through so many public postmortems that it needs no single company. A dependency slows for a moment: a database fails over, a burst of traffic arrives, or a timeout is set too short. Calls time out, and every layer that sees a timeout tries again. The retries land when spare capacity is lowest, so more calls time out and more retries follow.
Why it spread. Each retry setting looks harmless alone. In the payments app, a payment goes from the phone app to an API gateway, then to the payments service, then to its database, and each of the three layers tries a failing call up to 3 times. The overload can outlast its trigger, because the retries alone keep the database past its limit.
Predict before you watch: every database call is timing out. How many database calls can one tap on Pay become?
- 3 × 3 × 3 = 27. Each app try makes the gateway try 3 times, and each gateway try makes the service try 3 times.
Load at its slowest moment: 27× normal (One retrying layer within a 10% budget: about 11,000 /s)
Read it as steps
Watch for how three harmless-looking retry settings turn 10,000 payment requests a second into 270,000 database calls.
A database that was already struggling at 10,000 calls a second now gets 270,000, and most of that work is wasted: by the time it answers, the caller has given up and tried again. A retry that rescues one lost packet becomes an attack on your own system when a whole dependency is slow.
What stops it. Availability, Reliability, Scalability and Latency teaches these defences in full. Here, only the gateway retries, within a budget of about 10% extra calls and with jittered backoff, so the database sees at most 1.1 times its load, not 27. A circuit breaker stops calls to the stalled database for a few seconds, low-value work is shed, and traffic returns in steps. Timeouts are set a little above the healthy p99, never guessed. And every retry of a payment carries the same idempotency key, as Design a Payment System explains, so a retry never charges anyone twice.
Part 3 · Lessons That Keep Repeating
Staged rollouts and canaries
Line the stories up and one lesson stands out: in most of them, one change reached everything at once. A staged rollout sends a change to a small part of the system, watches it, then widens step by step. The first small group is the canary, named after the birds miners once carried underground as an early warning of gas.
Here is the payments app's timeout change, config v42, shipped two ways: to every region at once, then to a watched 1% canary.
Plan A: v42 goes to every region at once, and payments time out everywhere
Read it as steps
Watch for how many users see failed payments each time: the same bad config goes to every region at once, then through a 1% canary.
The bug was identical both times. What changed was how many people met it before the system reacted: everyone, or about 1%. Staging does not prevent the mistake; it caps the damage and buys time to undo it.
A typical plan, with example waits, looks like this:
| Stage | Gets the change | Watch for at least |
|---|---|---|
| Canary | 1% of servers in one region | 30 minutes |
| First region | One whole region, ideally a quieter one | 1 hour |
| Wider | Half the regions, never both regions of a failover pair | A few hours |
| Everywhere | All remaining regions | Keep watching for a day |
As Monitoring: Metrics, Logs, Traces and Alerts explains, the canary is compared with a baseline, a same-sized group still on the old version, and the rollout rolls back on its own when the canary is clearly worse. Some bugs, like the short timeout, show only at peak, so let a stage run through a busy hour, and freeze risky changes before known peaks such as a final or a festival sale.
Kill switches, rollback and slow automation
The next question is how fast you can undo a change that got past the canary. When a change goes wrong, the first job is to stop the harm, not to understand it. This is mitigation; the root cause can wait until users are safe.
Two tools make mitigation fast. A kill switch turns one feature off at runtime, without a deploy, usually through a flag the code checks; one restored service in the firewall story. If the payments app's new cashback feature starts failing, a switch turns cashback off within a minute while payments keep flowing. Rollback returns to the last version that worked, and it is usually faster and safer than writing a fix under pressure. Keep every change reversible: keep the previous build ready, version every config file, and change database schemas in steps the old code can still run against.
Read flags from a local cache so they still work when the flag service is slow or down, make the off state safe, and flip each switch in a drill now and then. A kill switch that depends on the thing it switches off fails exactly when you need it.
Brakes on automation are the other half, the lesson of the command that removed too much. Automation makes bad changes as fast as good ones, so any tool that can touch thousands of servers needs limits. It is the idea of Rate Limiters, applied to your own operations:
def remove_servers(pool, servers): if len(servers) > max(1, pool.size // 20): raise Refused("more than 5% of the pool in one command") if pool.healthy - len(servers) < pool.minimum: raise Refused("would leave the pool below its minimum") for batch in chunks(servers, size=2): # slow on purpose drain_and_remove(batch) if not healthy_for(pool, minutes=5): # stop at the first sign of harm raise Refused("pool unhealthy, stopping here")
Three brakes: a cap per command, a floor the pool never drops below, and a slow pace that halts on bad signals.
When it happens anyway
Stages, switches and brakes make outages rarer and smaller, but something will still break. How the first hour goes depends on preparation done months earlier: who does what, and whether your repair tools work when production does not. Most teams name four roles:
| Role | Job |
|---|---|
| Incident commander | Runs the response: sets priorities, assigns work, decides on rollback. Does not debug, so someone keeps the whole picture |
| Responders | Investigate and apply fixes, and tell the commander before any big change |
| Communications lead | Updates the status page, support and leaders on a fixed schedule, such as every 30 minutes |
| Scribe | Keeps a timeline of what was seen, tried and decided; the postmortem is built from it |
Clear roles keep five people from trying five fixes at once. On the night of the final, the payments app's commander would order the rollback of v42 first and ask why later. The tools for that night must not depend on production:
Within a few days the team writes a blameless postmortem, which asks why the system let one slip become an outage, not who to punish. Each action item changes the system and has an owner and a date: "the removal tool refuses to go below minimum capacity", not "be more careful". Then practise: restore a backup every week, fail over a region every quarter, and switch off a dependency on purpose. These drills, often called game days, are how the social network had rehearsed coming back in safe steps.
In the interview
Outage questions rarely come by name; they arrive as "how would you deploy this?" or "what if the database slows down?", and these lines answer them with numbers.
- Say
- "Config goes through the same pipeline as code: a canary on 1% of servers for 30 minutes, then one region at a time, with automatic rollback."
- Say
- "Only the gateway retries, within a 10% budget and with jittered backoff, so a stalled database never sees 27 times its load."
- Say
- "We restore a backup into a test environment every week, and alert if no backup has succeeded in 25 hours."
- Avoid
- "We have replicas, so we do not need backups." A bad delete is replicated in seconds.
- Avoid
- "We test everything, so it will not break." Tests lower the odds; stages limit the damage.
Wrap-up
| Term | Meaning |
|---|---|
| Blast radius | How much of the system, and how many users, one failure or change can reach |
| Cell | A full copy of the service that serves a fixed slice of users |
| Canary | The small first group that gets a change, compared with a baseline |
| Kill switch | A runtime flag that turns one feature off without a deploy |
| Retry storm | Retries at several layers multiplying load on a slow dependency until it collapses |
| Postmortem | The blameless write-up of an incident: timeline, trigger, spread and action items |
| Time bomb | A failure that waits for a date or a count, such as an expiring certificate |
Next comes the last chapter, How to Practise: A 45-Minute Checklist, which turns the whole course into a timed routine you can rehearse.
Part 4 · Check yourself
Quick check
Q1The payments app's ledger has replicas in three zones, so a teammate calls backups a waste of money. Why are they wrong, and how would you prove the backups work?
Think it through, then check.
- Replicas copy a bad delete within seconds; they protect against losing a machine, not a mistake.
- Backups are point-in-time copies that production commands cannot reach.
- Prove them with scheduled, timed test restores, and alert if none has succeeded in 25 hours.
Q2At 9:00 the payments database fails over in 30 seconds. Traffic is normal, yet at 9:20 it is still at 100% CPU and most calls time out. What is happening, and what do you do now and later?
Think it through, then check.
- A retry storm: retries at several layers multiply the load, mostly for callers that already gave up.
- Now: open breakers or turn retries off, shed low-value traffic, then ramp traffic back.
- Later: retry at one layer within a 10% budget, with jittered backoff and deadlines.
Q3The payments app's transactions table uses a signed 32-bit id. It holds 2.0 billion rows and gains about 100 a second. How long until inserts fail, and what should have warned you?
Think it through, then check.
- 2³¹ − 1 ≈ 2.147 billion, so about 147 million ids remain.
- 147 million ÷ 100 per second ≈ 1.47 million seconds ≈ 17 days.
- Then every insert fails. An alert at 70% of the range would have given about 2.5 months to move to 64-bit ids.
Q4An attack is hitting the payments app's login page right now, and the security team has a firewall rule that blocks it. Your normal staged rollout takes hours. How do you ship the rule?
Think it through, then check.
- A staged fast lane: 1% of servers for 5 minutes, watching CPU and errors, then everywhere.
- A linear-time regex engine, or a cap on work per match, so the rule cannot pin CPUs.
- Cost: a few more minutes of attack. Gain: a 1% blast radius if the rule is bad.
Q5From Load Balancers: a change to the /health endpoint makes every server fail its check at once. What should the balancer do, and which story here made the same mistake at a larger scale?
Think it through, then check.
- Fail open: keep every server in rotation, because a broken check is more likely than a dead fleet.
- The network outage: every DNS site withdrew its routes at once, a check meant for one sick site.
- Lesson: cap automatic reactions, and hand bigger ones to a person.