Fault tolerance
Fault tolerance for system design interviews: redundancy, timeouts and deadlines, retries with exponential backoff and jitter, idempotency, circuit breakers, bulkheads and shuffle sharding, graceful degradation and load shedding, and when each one applies.
On this page
- Picture it first: which fault are you tolerating?
- Redundancy: the baseline
- Timeouts: never wait forever
- Retries with backoff and jitter
- Circuit breakers: stop calling what's broken
- Bulkheads: contain the blast radius
- Graceful degradation and load shedding
- Prove it: inject failures on purpose
- How it shows up in design problems
- How to say it in the interview
🛡️ What is fault tolerance?
Fault tolerance is a system's ability to keep doing its job, perhaps in a reduced way, while some of its parts are broken. You don't try to prevent every fault; you assume disks die, networks drop packets and dependencies slow down, and you design so those faults don't become user-visible failures. A twin-engine plane is the classic picture: lose one engine and it keeps flying, not as fast, but it lands.
For this lesson, we'll walk through the toolkit: redundancy, timeouts, retries with backoff and jitter, idempotency, circuit breakers, bulkheads, graceful degradation and load shedding, and when each one earns its place. Interviewers ask "what happens when service X is slow or down?" in almost every design round, because it separates people who've drawn boxes from people who've been paged at 3 a.m.
Below the line (out of scope)
- Definitions, SLOs and fault vs failure: Reliability
- Switching to a standby when a stateful node dies: Failover
- The breaker's state machine in depth: Circuit breaker
- Keys and dedupe for safe retries: Idempotency
- One-line reference of every pattern: Reliability patterns cheat sheet
The 30-second answer
I assume every dependency will fail or get slow, and I make sure no single fault takes the whole request path down. Every remote call gets a timeout based on the dependency's real latency, plus at most a few retries with exponential backoff and jitter, and only for operations that are idempotent. If a dependency keeps failing, a circuit breaker stops calling it for a while. Each dependency gets its own bulkhead (a separate pool or cell), so one slow service can't use up every thread. Non-critical features degrade to a cached or default answer, and when we're overloaded we shed low-priority work early instead of slowing everyone down. Then we prove it works by injecting failures on purpose.
What you’ll learn
- Name the fault shape (crash, omission, slow, Byzantine) before choosing a defence
- Set timeouts from a dependency's latency percentile and propagate the remaining deadline
- Explain why retries multiply (243x across 5 layers) and make them safe with backoff, jitter, budgets and idempotency
- Contain the blast radius with circuit breakers, bulkheads, cells and shuffle sharding
- Plan graceful degradation and load shedding, and prove them with failure injection
01Picture it first: which fault are you tolerating?
Faults come in a few shapes, and each one needs a different defence. Name the shape before you name the pattern.
| Fault | What you see | First defence |
|---|---|---|
| Crash | The node stops and stays stopped | Redundancy and failover |
| Omission | A request or reply is lost; you hear nothing | Timeouts, then retries |
| Timing (slow) | The reply comes, but far too late | Timeouts, bulkheads, load shedding |
| Byzantine | The node replies with wrong or conflicting data | Checksums and validation; full Byzantine tolerance is rarely in scope for a typical web backend |
Slow is the hardest. A crashed node is easy to spot and route around. A slow one still accepts connections, still answers health checks, and quietly holds every caller's threads while it thinks. Most of the toolkit below exists because of slowness.
02Redundancy: the baseline
Run more than one copy of everything that matters, in places that don't fail together: separate machines, racks, zones. Stateless services sit behind a load balancer; stateful ones replicate and fail over. This is the foundation, and it's cheap to say, but it only helps if the copies are independent. Two replicas on the same rack or the same bad config file are one failure waiting to happen. Finding those hidden shared parts is the job of Single point of failure; the maths of how much redundancy buys you lives in Availability.
03Timeouts: never wait forever
Every call across a network, and ideally every call across processes, needs a timeout. Without one, a hung dependency holds your thread, your connection and your caller's thread, all the way up the stack.
Picking the value is the hard part. Amazon's Builders' Library describes a practical rule: choose an acceptable rate of false timeouts, such as 0.1%, and set the timeout at the matching latency percentile of the dependency, here its p99.9. Too high and the timeout protects nothing; too low and healthy-but-slow requests get retried, adding load exactly when the dependency is struggling.
Two refinements show depth in an interview:
- Budgets, not fixed numbers. If the user-facing request has 2 seconds, and 1.4 seconds are already gone, the next call gets at most 0.6 seconds. Google's SRE book calls this deadline propagation: pass the remaining time down the stack so no one works on a request the caller already gave up on.
- Inner timeouts shorter than outer ones. If the gateway times out at 3 seconds but the service behind it waits 10 seconds on the database, the service keeps working for 7 seconds on an answer nobody will read.
04Retries with backoff and jitter
Many faults are transient: a dropped packet, a node restarting, a brief leader election. Retrying fixes those. But retries are also how small incidents become large ones, because every retry is extra load sent to something that's already failing.
Try it yourself first
Before reading on, try this. A user request passes through 5 services in a chain, and the last one calls a database. Each layer retries a failed call up to 3 times in total (the original plus 2 retries). The database starts failing every query. How many database queries does one user request now cause?
The answer is 3 x 3 x 3 x 3 x 3 = 243. Each layer multiplies the attempts of the layer above. This exact example appears in Amazon's guidance, which concludes that a database under that pressure is unlikely ever to recover. So the rules for retries are:
- Retry at one layer, usually the one closest to the user or the one that owns the operation. Others fail fast.
- Cap the attempts (2 or 3), and cap them globally too: a retry budget stops retrying when too many recent calls were retries. Google's SRE book suggests a per-process limit, for example 60 retries a minute. Amazon's SDK limits retries locally with a token bucket.
- Back off exponentially, doubling the wait each time, so a struggling service gets breathing room.
- Add jitter, a random part of the wait, so thousands of clients that failed together don't retry together.
- Only retry what's safe. Reads are usually safe. Anything with side effects, like charging a card, needs an idempotency key so the second attempt doesn't charge twice.
Synchronised retries vs jittered retries
A common form is "full jitter": wait a random time between zero and the current backoff cap. Notice in the figure that the very first burst is identical in both cases. Jitter can't undo the original failure; it stops you from recreating it on every retry. For the patterns side by side, see Distributed systems patterns, and for how these failures look in production, Backend failure modes.
05Circuit breakers: stop calling what's broken
Retries assume the fault is brief. When it isn't, a circuit breaker watches the error rate to a dependency and, past a threshold, fails calls immediately for a cool-off period, then lets a few trial calls through to see if it has recovered. Callers get a fast error they can handle, and the sick dependency gets room to recover.
Breakers aren't free. Amazon's Builders' Library points out that they add modal behaviour that's hard to test and can lengthen recovery, which is why Amazon often prefers limiting retries with a token bucket. My default: bounded retries everywhere, breakers on dependencies that fail for long stretches or are expensive to wait on. The state machine is covered in Circuit breaker.
06Bulkheads: contain the blast radius
Ships are split into watertight compartments so one breach doesn't sink the whole hull. In software, a bulkhead gives each dependency, tenant or feature its own slice of resources: a thread pool, a connection pool, a set of servers, or a whole cell.
One shared pool vs bulkheads
The same idea scales up. Cells run complete, independent copies of a service, each serving a slice of users, so a bad deploy or a poison request hurts one cell. Shuffle sharding goes further by giving each customer a random small set of workers.
Amazon's Builders' Library walks through shuffle sharding with 8 workers and each customer assigned 2. There are 28 possible pairs, so if one customer's traffic takes down its two workers, only customers sharing that exact pair suffer: about 1/28 of them. Route 53 applies it at scale: capacity is arranged into 2,048 virtual name servers and each customer domain gets a shuffle shard of 4, giving about 730 billion possible shards. A DDoS attack on one domain spikes its four name servers while other customers don't notice.
07Graceful degradation and load shedding
When something breaks, a smaller answer usually beats no answer.
- Graceful degradation means deciding in advance what each feature does when its dependency is down. Recommendations fall back to a cached "popular items" list. Reviews disappear from the product page while checkout keeps working. A feed shows slightly stale posts from a cache.
- Load shedding means refusing some work early when you're over capacity, so the work you accept still completes in time. Reject with a quick 429 or 503 at the edge, drop low-priority traffic (batch jobs, prefetches) first, and drop requests that have already waited longer than their deadline. Google's SRE book describes the goal well: keep the server out of overload while it still does as much useful work as it can, for example by returning 503 once too many requests are in flight. Rate limiting and message queues are the neighbouring tools: one caps what a client may send, the other absorbs bursts to process later.
Many candidates list fallbacks ("if recommendations fail we show cached ones") and stop there. The fallback path runs rarely, so it's the code most likely to be broken when you need it: the cache was never warmed, the default list is empty, or the fallback itself calls the failing service. The same goes for timeouts set longer than the caller's timeout, and for retries added at every layer "to be safe". Say how you'd exercise the fallback, for example a feature flag that forces it on in a test environment and periodically in production.
08Prove it: inject failures on purpose
You don't know a system tolerates a fault until you've watched it do so. Chaos engineering turns that into a habit: define steady state as a measurable output (orders per minute, error rate), predict it'll hold, inject a real-world fault such as a killed server or a severed connection, and look for a difference. Netflix's open-source Chaos Monkey does the simplest version, randomly terminating production instances so every team has to build services that survive it. Start small: one instance, off-peak, with a way to stop the experiment.
Numbers worth having ready. Retries multiply: 3 attempts at each of 5 layers is 243x load on the bottom layer. A timeout at the dependency's p99.9 accepts roughly 0.1% false timeouts. Shuffle sharding with 8 workers and 2 per customer limits a bad customer's impact to about 1/28 of others. Each one turns "we'll make it resilient" into a claim an interviewer can check.
09How it shows up in design problems
Every design problem has at least one "what if this is slow?" moment. Three where it decides the design:
- Design a payment system: retries to the payment provider must be idempotent, timeouts must be explicit about "unknown" outcomes, and you reconcile instead of guessing.
- Design a notification system: each channel (email, SMS, push) gets its own queue and worker pool, so a slow SMS provider doesn't delay push notifications, with retries and backoff per channel.
- Design a flash sale: load shedding and queueing at the edge protect inventory, and non-critical pages degrade so the purchase path keeps its capacity.
At this point, you should ask your interviewer which features are critical. The answer tells you where to spend redundancy and where a fallback is good enough.
10How to say it in the interview
When I ask "what if this dependency is slow?", I'm listening for timeouts with a reason behind the number, bounded retries that are safe to repeat, isolation so the problem stays local, and a plan for what the user sees. Candidates who mention retry amplification or jitter unprompted usually have real production experience.
In the interview
"I'll assume every dependency can fail or slow down. Every remote call gets a timeout set from the dependency's p99.9 latency, and I pass the remaining request deadline down so nobody works on an abandoned request. Retries happen at one layer only, at most twice, with exponential backoff and full jitter, inside a retry budget so a bad minute doesn't triple our load. The payment call carries an idempotency key, so a retry can't charge twice. Payments, search and recommendations each get their own connection pool, so a slow provider can't take every thread, and a circuit breaker opens if the provider keeps failing. Recommendations degrade to a cached popular list, and under overload we shed batch and prefetch traffic first. Finally, I'd run failure drills, killing instances and adding latency, to prove the fallbacks actually work."
On the whiteboard
- Fault shapes: crash · omission · slow (hardest) · Byzantine (rare)
- Redundancy across independent failure domains
- Timeout = dependency p99.9 · propagate deadlines · inner < outer
- Retry: one layer, 2-3 attempts, budget, exp. backoff + jitter, idempotent only
- 3 tries x 5 layers = 243x load
- Circuit breaker for long outages · bulkheads / cells / shuffle sharding
- Degrade non-critical features · shed load early (429/503, low priority first)
- Prove it: chaos experiments, exercise fallbacks
Further reading (5)
- Timeouts, retries, and backoff with jitter (Amazon Builders' Library): How Amazon picks timeouts, why retries are selfish, the 243x example and retry token buckets.
- Addressing Cascading Failures (Google SRE book): How overload spreads, and the defences: load shedding, retry budgets and deadline propagation.
- Workload isolation using shuffle-sharding (Amazon Builders' Library): Blast-radius reduction with clear worked numbers, and how Route 53 uses it.
- Architecture design patterns that support reliability (Azure Well-Architected Framework): A catalogue of reliability patterns (bulkhead, circuit breaker, retry, throttling and more) with links to each.
- Principles of Chaos Engineering: The short manifesto for testing fault tolerance with controlled experiments in production.