Overview
It is 10:02, two minutes into the stadium on-sale. BoxOffice is holding up: the waiting room admits fans at a steady pace, inventory is at 60% of its capacity. Then the external payment provider starts answering in 4 seconds instead of 200 ms. Within a minute, order has no free threads; the gateway, waiting on order, has none either; and fans who only wanted to browse the catalog get errors. Nothing in BoxOffice is broken β no bug, no crash, no full disk. The system simply had no defence against a slow dependency, and the slowness spread until it took everything down.
This class is about building those defences, and about deciding with numbers how much failure is acceptable. Reliability is the probability that a system performs its required function without failure, under stated conditions, for a stated period of time1. In distributed systems that definition covers much more than hardware: software bugs, overloaded dependencies, network partitions, bad deploys, lost data centres and operator mistakes.
A reliable system is not one that never fails. It is one that fails gracefully, recovers quickly and limits the blast radius of any single failure β the blast radius being the set of users and components affected by one failure.
The story so far
Chapter 10 of the story of the course. BoxOffice spreads the 10:00 crowd over stateless replicas (class 4, class 5), sells each seat once (class 6), answers reads from a cache (class 7) and admits fans through a waiting room (class 8); class 9 made it visible, with an SLO per journey and one trace per checkout. Now the trace shows the problem: the payment provider answers in seconds instead of milliseconds, and nothing in BoxOffice is broken. This class answers: how do we keep one slow dependency from taking everything down, and how much redundancy does the SLO ask us to buy?
Learning objectives
By the end of this class you should be able to:
- Calculate availability from MTTF and MTTR, the downtime allowed by a number of "nines", and the end-to-end availability of components in series and in parallel.
- Explain how a slow dependency causes a cascading failure, and estimate the resources it exhausts with Little's Law.
- Choose and configure resilience patterns β timeouts, retries with backoff and jitter, circuit breakers, bulkheads, load shedding and rate limiting β for a call between two services, and predict what each one does under failure.
- Design a disaster-recovery strategy from RPO and RTO targets, choosing between backup and restore, pilot light, warm standby and active-active.
- Plan a chaos experiment and a game day with a steady-state hypothesis and a controlled blast radius.
Roadmap
- This overview β availability, MTTF/MTTR, the "nines", composite availability, redundancy and failover, failure modes and cascading failures.
- Resilience patterns β timeout, retry with backoff and jitter, circuit breaker, bulkhead, load shedding, rate limiting, health checks, with Resilience4j.
- Disaster recovery β RPO and RTO, backups, multi-AZ and multi-region, and the four DR strategies.
- Chaos engineering β injecting failures on purpose to prove that the defences work; game days.
- Quiz β check your understanding.
Slides
Click the slides and use β β to move through them; F goes full screen and P opens the presenter view, with the notes. Open the slides in a new tab
Availability
The most visible face of reliability is availability: the fraction of time (or of requests) in which the system works. Reliability and availability are related but not the same. A service that crashes for one second every minute fails often (poor reliability) but is still about 98% available; a service that fails once a year but takes a week to repair is very reliable and poorly available. Availability comes from two measurable quantities:
| Term | Full name | Meaning |
|---|---|---|
| MTTF | Mean Time To Failure | Average time the system runs before a failure |
| MTTR | Mean Time To Repair (or Recover) | Average time to detect, diagnose and restore service after a failure |
| MTBF | Mean Time Between Failures | MTTF + MTTR β the whole failure-and-recovery cycle |
BoxOffice: two ways to improve inventory
inventory crashes on average once every 30 days (MTTF = 720 h). Each time, it takes 2 hours for someone to be paged, find the problem and restart it (MTTR = 2 h).
Availability = 720 / (720 + 2) β 99.72%.
- Halve the failures (MTTF = 1440 h): 1440 / 1442 β 99.86%.
- Let Kubernetes detect the crash and restart the pod in 1 minute (MTTR β 0.017 h): 720 / 720.017 β 99.998%.
Recovering faster was the bigger win β and much cheaper than never failing.
Reliability work pulls one of two levers. Raising MTTF (fewer failures): testing, code review, redundancy, fault-tolerant design, careful deploys. Lowering MTTR (faster recovery): good alerts, dashboards and traces, runbooks, automatic restarts and failover, quick rollbacks. In modern systems MTTR is usually the cheaper lever, and MTTR is itself a sum β time to detect + time to diagnose + time to fix β each part attacked by a different practice (see Observability).
The "nines"
Availability targets are quoted as a number of "nines". The table shows the downtime each target allows (year = 365.25 days, month = 1/12 of a year β 30.44 days):
| Availability | Per year | Per month | Per 28 days | Per week |
|---|---|---|---|---|
| 99% ("two nines") | 3.65 days | 7.31 hours | 6.72 hours | 1.68 hours |
| 99.9% ("three nines") | 8.77 hours | 43.8 minutes | 40.3 minutes | 10.1 minutes |
| 99.95% | 4.38 hours | 21.9 minutes | 20.2 minutes | 5.04 minutes |
| 99.99% ("four nines") | 52.6 minutes | 4.38 minutes | 4.03 minutes | 1.01 minutes |
| 99.999% ("five nines") | 5.26 minutes | 26.3 seconds | 24.2 seconds | 6.05 seconds |
Each additional nine is 10Γ less downtime, and it is not an incremental improvement: it changes how you operate. At three nines, a human can be paged, open a laptop and roll back. At four nines, the whole monthly budget is shorter than it takes to wake up; detection and recovery must be automatic. At five nines, even automatic failover must complete in seconds, and a single bad deploy can spend a year's budget.
Always state the window
"99.9%" alone is ambiguous: 43.8 minutes in a calendar month, 40.3 minutes in a 28-day window. Whenever you quote availability, say over which window β and whether it counts time or requests (see SLOs).
Composite availability
A request crosses many components, and the availability the user sees depends on how they are combined2. Two rules cover almost every case (see also Availability math).
When a request needs every component, availabilities multiply:
Three services at 99.9% each give \(0.999^3 \approx 99.7\%\). The chain is worse than its worst link, and every synchronous dependency you add lowers the ceiling. A service can never promise more availability than the product of the dependencies it calls synchronously.
When any one of \(n\) independent replicas is enough, the system fails only if all fail together:
Two replicas at 99% give \(1 - 0.01^2 = 99.99\%\); three give 99.9999%. Redundancy turns mediocre components into very available ones β as long as their failures are independent. The same bug, the same bad config, the same host or the same zone breaks that assumption.
Apply both rules to the BoxOffice purchase path. The numbers are illustrative assumptions, but the shape of the result is general:
Reading the chart from top to bottom:
- With one instance of everything, the three services at 99.5% dominate: about 98.3%, over 12 hours a month without purchases.
- Three replicas of the stateless services make them practically perfect, and the bottleneck moves to the components that are not replicated: the database and the external payment provider.
- Automatic database failover (a standby in another zone) and a second payment provider attack those. Each step removes the current weakest link β and the next weakest becomes visible.
flowchart LR
fan(["fan"]) --> lb["load balancer<br/>99.99%"]
lb --> g1["gateway #1"]
lb --> g2["gateway #2"]
lb --> g3["gateway #3"]
g1 --> o["order Γ 3"]
g2 --> o
g3 --> o
o --> i["inventory Γ 3"]
i --> db[("SQL primary<br/>+ standby in AZ-b")]
o --> p1["payment provider A<br/>99.9%"]
o -.->|"fallback"| p2["payment provider B<br/>99.9%"] Redundancy has a price tag
Each step in the chart costs money (three times the instances, a standby database, a second provider contract) and complexity (failover logic, testing the fallback). Reliability is bought, not free: the error budget is how you decide how much to buy.
Redundancy and failover
Redundancy only helps if traffic actually moves away from the failed component β that is failover β and if the redundant copy is ready to take it.
| Pattern | How it works | Failover time | Cost | BoxOffice example |
|---|---|---|---|---|
| Active-active | All copies serve traffic; the load balancer stops sending to a failed one | Seconds (health-check interval) | Each copy must absorb the extra load | Stateless gateway, order, catalog replicas |
| Active-passive (hot standby) | A standby receives replicated state and is promoted on failure | Seconds to a few minutes | Standby is paid for but idle | PostgreSQL primary + synchronous standby in another zone |
| Cold standby | Copy is started (or restored) only after a failure | Minutes to hours | Cheapest | Analytics workers, rebuilt from images |
Two rules make redundancy work:
- N + 1 (or N + 2) capacity. If the flash sale needs 6
orderreplicas, run 7 or 8: after losing one (or during a rolling deploy), the rest must still carry the peak. With replicas spread over 3 availability zones, losing a whole zone removes a third of capacity β plan for it. - Stateless where possible. Stateless services fail over trivially. State (databases, caches, queues) needs replication, and replication brings the hard questions: synchronous or asynchronous, how much data can be lost, and how to avoid two primaries at once (split brain; see Consensus). The disaster recovery page treats failover across regions.
Availability zones and regions
An availability zone (AZ) is one or more data centres with independent power, cooling and networking, a few kilometres from the other zones of the same region; latency between zones is about a millisecond. Spreading replicas across zones protects against a data-centre failure at almost no latency cost. Spreading across regions (hundreds or thousands of kilometres apart) protects against a regional disaster, but every synchronous call between regions costs tens of milliseconds.
Failure modes
Distributed systems fail in ways a single machine does not. Partial failures β some components work and others do not β are far more common than total outages, and harder to handle.
| Failure class | Characteristics | Handling strategy |
|---|---|---|
| Crash (fail-stop) | The process dies and stays dead | Health checks, restart, failover to a replica |
| Transient | Self-resolves within seconds (network blip, leader election) | Retry with backoff and jitter |
| Slow / gray failure | Still answering, but slowly or with some errors; health checks may still pass | Timeouts, circuit breakers, outlier detection, latency SLIs |
| Intermittent | Appears and disappears (GC pauses, a leaking resource) | Percentile latency, anomaly detection, profiling |
| Byzantine / incorrect | Returns wrong data without crashing (bit flips, corrupted cache, bug) | Checksums, validation, reconciliation |
| Correlated | Many components fail together (bad deploy, expired certificate, zone outage) | Gradual rollouts, diversity, multi-zone, chaos tests |
The most dangerous of these is not the crash but the slow component. A crashed dependency answers immediately β with a connection error β and the caller moves on. A slow dependency holds the caller's resources while it thinks. The caller cannot tell slow from dead (see Distributed systems), so without a deadline it waits.
Cascading failures
A slow dependency exhausts a resource in its caller β threads, connections, memory β and the exhausted caller becomes slow for its callers. The exhaustion climbs the call graph until unrelated features fail. This is the most common way microservice systems go down3.
sequenceDiagram
participant F as Fans
participant G as gateway
participant O as order (200 threads)
participant P as payment provider
F->>G: POST /orders (100 per second)
G->>O: create order
O->>P: charge (normally 200 ms)
Note over P: degrades: 4 s per call
Note over O: 100/s Γ 4 s = 400 calls in flight > 200 threads
F->>G: GET /events/ev-88 (catalog)
G->>O: (gateway threads also waiting on order)
G-->>F: 503 β no free thread
Note over F,G: browsing fails too, although catalog is healthy The arithmetic is Little's Law (see Queueing): calls in flight = arrival rate Γ time each call takes.
| Payment latency | Calls in flight at 100 purchases/s | order thread pool (200) |
|---|---|---|
| 200 ms (normal) | 100 Γ 0.2 = 20 | 10% busy |
| 1 s | 100 Γ 1 = 100 | 50% busy |
| 4 s | 100 Γ 4 = 400 | exhausted: requests queue, then time out |
Nothing about the purchase rate changed. A 20Γ increase in a dependency's latency became a 20Γ increase in occupied threads. When the pool is full, every route of order waits β including the ones that never call payment β and the gateway, waiting on order, fills its own pool.
Three things stop the cascade, each covered in Resilience patterns:
- A timeout caps how long one call can hold a thread (1 s timeout β at most 100 threads, not 400).
- A circuit breaker stops calling a dependency that is failing, so calls fail in microseconds instead of seconds.
- A bulkhead gives payment calls their own limited pool, so they cannot take the threads of other routes.
Try it in the playground
The cascading-failure scenario has an app calling a slow 3rd-party API with no timeouts. Press Run and watch the app's busy threads climb, then its latency and errors, then the errors of routes that do not even use the 3rd party. Now add a timeout to the 3rd-party call and run again; then enable the circuit breaker and compare the error rate and p99 of the healthy routes. Open the full playground to try a bulkhead.
Other amplifiers
Cascades are one example of a broader family: failures that feed themselves.
- Retry storms. Every caller retries failed calls, multiplying the load on a dependency exactly when it is weakest (patterns).
- Thundering herd on recovery. A service comes back and every client, cache and queue consumer hits it at the same instant; it falls over again.
- Death spiral of autoscaling or health checks. Overloaded instances fail their health checks and are removed, so the remaining ones get more load and fail too.
- Metastable failures. The system stays broken after the trigger is gone, because the work generated by the failure (retries, timeouts, cache misses) keeps it overloaded4. The fix is to drop load until it recovers β load shedding, retry budgets, circuit breakers.
The reliability concept map
Reliability is not one component: it is a property you buy, piece by piece, across the whole palette. The map links each palette component (green) to the reliability concepts it carries (blue); the arrows show how the class builds up β redundancy and probes keep instances serving, patterns contain slow dependencies, DR covers what redundancy cannot, SLOs say how much is enough, and chaos proves it all works:
flowchart LR
lbapp["lb Β· app"]:::comp -.- red["MTTF/MTTR, nines, series vs parallel,<br/>redundancy, N+1, multi-AZ"]:::tech
tp["thirdparty Β· payment"]:::comp -.- casc["cascading failures<br/>(Little's Law)"]:::tech
casc --> pat["timeout, retry + jitter,<br/>circuit breaker, bulkhead, fallback"]:::tech
apigw["apigw"]:::comp -.- shed["rate limiting,<br/>load shedding"]:::tech
hc["healthcheck"]:::comp -.- probes["liveness, readiness,<br/>startup"]:::tech
data["sql Β· objstore"]:::comp -.- dr["RPO / RTO, PITR backups,<br/>DR strategies"]:::tech
dns["dns"]:::comp -.- dr
obs["metrics Β· alerting"]:::comp -.- slo["SLI, SLO, error budget,<br/>burn rate"]:::tech
probes --> red
pat --> shed
pat --> chaos["chaos experiments,<br/>game days"]:::tech
dr --> chaos
slo --> chaos
classDef comp fill:#1e9e5a22,stroke:#1e9e5a
classDef tech fill:#2f6fed22,stroke:#2f6fed | Palette component | Question it answers | Concepts | Taught in | Scenarios |
|---|---|---|---|---|
lb, app | Does losing a replica or a zone stop the purchase? | MTTF/MTTR, nines, series vs parallel, active-active, N+1, multi-AZ | this overview | scale-out |
thirdparty, payment | What if a dependency gets slow? | cascading failures, timeout, retry + backoff + jitter, circuit breaker, bulkhead, fallback | this overview, Resilience patterns | cascading-failure, retry-storm, payment-system |
apigw | Which work do we refuse under overload? | rate limiting per client, load shedding by priority, waiting room | Resilience patterns | rate-limiter, flash-sale |
healthcheck | Should traffic reach this instance? | liveness, readiness, startup; no dependencies in liveness | Resilience patterns | scale-out |
metrics, alerting | Are we reliable enough? | SLI, SLO, error budget, burn rate, error budget policy β the target every defence here is judged by | Observability β SLIs, SLOs and error budgets (class 9) | observability-stack |
sql, objstore | How much data and time can we lose? | RPO/RTO, PITR, 3-2-1 backups, delayed replica | Disaster recovery | db-failover, db-replicas |
dns | How do fans reach the surviving region? | multi-region, the four DR strategies, fencing, DNS TTL | Disaster recovery | multi-region |
| (all of them) | Do the defences actually work? | steady-state hypothesis, fault injection, blast radius, game days | Chaos engineering | flash-sale (chaos) |
In the interview
When you finish a design, interviewers often ask "what happens if X fails?" or "what if X becomes slow?". Answer with a chain: who calls X, what resource they hold while waiting, what the timeout is, what the fallback is, and what the user sees. For BoxOffice: "payment is called by order with a 2 s timeout, a circuit breaker and its own bulkhead of 50 concurrent calls; when it opens, the fan sees 'payment temporarily unavailable, your seats stay held for 10 minutes' β and the catalog is unaffected." Also quote availability math: "two dependencies at three nines in series cap us at 99.8%".
Best practices
- Write the availability ceiling of each user journey before choosing components β multiply what the path calls synchronously: two dependencies at 99.9% already cap the purchase at 99.8%. In BoxOffice: the purchase-path chart.
- Run N+1 replicas spread over at least two zones β after losing one replica, or a whole zone, the rest must still carry the 10:00 peak; a call between zones costs about 1 ms.
- Automate recovery before chasing a higher MTTF β restarting in 1 minute instead of 2 hours took
inventoryfrom 99.72% to 99.998%. In BoxOffice: probes and restarts in Hands-on 10. - Give every dependency a written failure plan: its timeout, its fallback and what the fan sees β decided at design time, not at 10:02. In BoxOffice:
paymentfails fast with503and the order staysPENDING_PAYMENT(Hands-on 9). - Move off the request path everything the fan does not wait for β each synchronous dependency lowers the ceiling of the journey. In BoxOffice: the e-mail goes through the outbox and Kafka (Hands-on 7).
- Track MTTR as three numbers: time to detect, to diagnose and to fix β each one is cut by a different practice (alerts, traces, rollback). In BoxOffice: the game-day timeline of Hands-on 9.
Common pitfalls
- No timeout on an HTTP client, or a default of minutes: one slow dependency holds threads indefinitely.
- Assuming independence: three replicas on the same node, or with the same bad configuration, fail together.
- Redundancy without failover tests: a standby nobody has ever promoted is a hope, not a plan.
- Shared pools: one thread or connection pool for every dependency lets the worst one decide for all.
- Measuring only crashes: a gray failure (slow, partially wrong) passes health checks and hurts users.
Related
- Hands-on (the week after this class and class 9): Hands-on 8 β Observability first β OpenTelemetry, a RED dashboard and SLO burn-rate alerts, so that the experiments have something to show; then Hands-on 9 β Resilience & chaos: Resilience4j between
orderandpayment, Toxiproxy latency and faults, a game day. Hands-on 10 β Kubernetes: probes and self-healing. - Project: Chaos game day.
- Case studies: Payment system, Ticketing (BoxOffice flash sale).
- Classes: Estimation β availability math, Observability (previous class, the same week: the SLOs, dashboards and alerts these defences are judged by), Messaging (asynchrony removes dependencies from the critical path), Cloud-Native (next class: probes, self-healing, autoscaling).
What breaks next
The patterns contain a slow provider, redundancy and the DR plan cover lost machines and regions, and chaos experiments prove both. But every defence on this page assumes that something restarts a dead replica, adds replicas before 10:00 and rolls a bad release back β and on one laptop with Compose, that something is you. The inventory example above improved from 99.72% to 99.998% only because the restart took 1 minute instead of 2 hours; that minute exists only if a machine does the restart. Healing, scaling at 09:45 and rolling back without a human is class 11 β Cloud-Native.
-
IEEE. IEEE Standard Glossary of Software Engineering Terminology, IEEE Std 610.12-1990.Β β©
-
BEYER, B. et al. Site Reliability Engineering: How Google Runs Production Systems. O'Reilly, 2016. Chapter 3, "Embracing Risk"; chapter 22, "Addressing Cascading Failures".Β β©
-
NYGARD, M. Release It! Design and Deploy Production-Ready Software, 2nd ed. Pragmatic Bookshelf, 2018. Chapter "Stability Antipatterns".Β β©
-
BRONSON, N. et al. Metastable Failures in Distributed Systems. In: Workshop on Hot Topics in Operating Systems (HotOS), 2021.Β β©