Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
- 12Relational Databases and Data Modeling for System Design
- 13Database Indexes and Query Performance: How to Make Reads Fast
- 14Transactions, Isolation Levels and Concurrency Control: Preventing Race Conditions
- 15Database Replication: Primary-Replica, Read Replicas, Lag and Failover
- 16Database Sharding and Partitioning: How It Works and When You Need It
- 17SQL vs NoSQL: Data Models and How to Choose the Right Database
6 · Distributed Systems
- 18Why Distributed Systems Are Hard: Partial Failure, Timeouts and Clocks
- 19CAP Theorem and Consistency Models Explained Without the Myths
- 20Idempotency, Retries and Message Delivery Guarantees
- 21Distributed Transactions: Two-Phase Commit, the Outbox Pattern and Sagas
- 22Distributed Locks, Leases, Leader Election and Consensus
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
- 33Design a Notification System: Email, SMS and Push at Scale
- 34Design a Chat System Like a Messaging App
- 35Design a News Feed: Fan-Out, Ranking and the Celebrity Problem
- 36Design Search Autocomplete (Typeahead)
- 37Design a Distributed Job Scheduler
- 38Design a Ticket Booking System Without Overselling
- 39Design a Payment System: Idempotency, Ledgers and Reconciliation
11 · Large-Scale Case Studies
- 40Design a Video Streaming Platform: Upload, Transcoding and Adaptive Streaming
- 41Design a Ride-Hailing System: Location, Matching and Real-Time Updates
- 42Design a Food Delivery Platform: Orders, Restaurants and Delivery Partners
- 43Design an E-Commerce System: Catalog, Cart, Checkout, Inventory and Flash Sales
- 44Design a Web Search Engine: Crawling, Inverted Indexes and Ranking
12 · Interview Readiness
Lesson 23 · Reliability and Operations
Fault Tolerance: Timeouts, Circuit Breakers, Bulkheads, Backpressure and Graceful Degradation
How one slow dependency takes down a whole system, and the techniques that stop it: timeouts and deadlines, retry budgets, circuit breakers, bulkheads, rate limiting, load shedding, backpressure and graceful degradation, with a runnable circuit breaker and a degradation plan for a real page.
For
SDE-2 candidates and backend developers responsible for services that call other services, databases and third-party APIs, and that must stay up when those dependencies misbehave.
Level
Before you start
Read Why Distributed Systems Are Hard (article 18) and Idempotency, Retries and Message Delivery Guarantees (article 20). Little's Law from article 02 explains why slowness becomes overload.
On this page
Key takeaways
- Most large outages are cascading failures: a slow dependency holds resources, callers back up, retries add load, and failure spreads upstream.
- Timeouts with propagated deadlines, limited retries with budgets, and circuit breakers stop callers from waiting on or hammering a failing dependency.
- Bulkheads isolate resources per dependency or tenant, so one failure cannot consume everything.
- Under overload, shed load early and deliberately, prioritising critical traffic; it is better to serve most users well than everyone badly.
- Design graceful degradation in advance: decide what each page shows when each dependency is down, and make the fallbacks cheap.
At 9:02 p.m. the recommendation service, a non-essential widget at the bottom of Pixly's home page, starts responding in 8 seconds instead of 80 ms because of a bad deployment. By 9:05 the entire app is down. Uploads fail, feeds do not load, logins time out. The recommendation service never crashed, and nothing else had a bug.
What happened: every feed request waited 8 seconds on recommendations. With requests arriving at 3,500 per second, the API servers' threads all filled with waiting requests (Little's Law from article 02: 3,500 x 8 s = 28,000 requests in flight). New requests queued, then timed out at the load balancer. Clients retried. Load doubled. Health checks timed out, so the load balancer removed servers, pushing their traffic onto the rest, which collapsed faster.
This is a cascading failure, and it is how most large outages unfold. This article covers the techniques that contain failures so that a problem in one corner stays in that corner.
What you will learn
#- The vocabulary: faults, failures, fault tolerance, cascading failure.
- How cascading failures happen, step by step.
- Timeouts and deadline propagation, and retry budgets.
- Circuit breakers, with a runnable simulation.
- Bulkheads and shuffle sharding for isolation.
- Rate limiting, load shedding and admission control under overload.
- Backpressure: making producers slow down.
- Graceful degradation and static stability, with a worked plan.
- How these mechanisms themselves fail, and how to test them.
Vocabulary
#- A fault is something going wrong in a component: a disk error, a bug, a slow network link.
- A failure is the system as a whole not doing its job for users.
- Fault tolerance is preventing faults from becoming failures.
- A cascading failure is a failure that spreads: one component's trouble overloads or starves others, which then fail too. Google's SRE book describes cascading failures as failures that grow over time through positive feedback, and the most common cause as overload (Addressing Cascading Failures).
How a cascade unfolds
#1. A dependency becomes slow (or fails slowly)2. Callers wait; their threads, connections and memory fill up3. Callers become slow to their own callers -> the slowness moves upstream4. Timeouts fire; clients and services retry -> load increases5. Health checks time out -> load balancers remove servers -> survivors get more load6. Survivors exceed capacity and crash -> back to step 5, fasterTwo features make it vicious. Positive feedback: each stage makes the next one worse. Slow is worse than down: a dependency that fails instantly costs callers almost nothing; one that hangs for 8 seconds costs them 8 seconds of resources per request.
Every technique below breaks one link in this chain.
1. Timeouts and deadlines
#Every remote call needs a timeout (article 18), so a slow dependency costs a bounded amount of time per request. Better still, propagate a deadline: the request arrives with "must finish by T", and each downstream call gets only the remaining time. A call made with 30 ms left should not wait 2 seconds.
In the opening scenario, a 200 ms timeout on recommendations would have limited each request's extra wait to 200 ms: painful but survivable.
2. Retries with limits and budgets
#Retries recover from transient faults but multiply load during real outages (article 20). Contain them:
- retry at one layer, a small number of times, with backoff and jitter;
- use a retry budget: allow retries only while they are a small fraction of total requests (for example 10%); beyond that, fail fast;
- never retry after the deadline has passed.
3. Circuit breakers
#Even with timeouts, calling a dependency that is clearly down wastes a full timeout on every request and keeps hammering a service that is trying to recover. A circuit breaker sits in front of the call and stops calling a dependency that keeps failing, for a while.
Microsoft's description of the pattern uses three states (Circuit Breaker pattern):
- Closed: calls pass through; failures are counted. When failures exceed a threshold within a period, the breaker opens.
- Open: calls fail immediately, without touching the dependency, and the caller uses a fallback. After a timeout, the breaker becomes half-open.
- Half-open: a limited number of trial calls pass through. If they succeed, the breaker closes; if any fails, it opens again.
The same guidance notes that a circuit breaker serves a different purpose from retries: retries assume the operation will succeed soon; the breaker prevents calls that are likely to fail. Retry logic should stop when the breaker reports it is open.
A circuit breaker in code
#This Java program simulates a breaker with a failure threshold of 3 and an open period of 5 seconds, using a fake clock so the output is repeatable. The dependency is down from 1 to 12 seconds.
// File: CircuitBreakerDemo.javapublic class CircuitBreakerDemo { enum State { CLOSED, OPEN, HALF_OPEN } static class CircuitBreaker { private final int failureThreshold; private final long openMillis; private State state = State.CLOSED; private int consecutiveFailures = 0; private long openedAt = 0; CircuitBreaker(int failureThreshold, long openMillis) { this.failureThreshold = failureThreshold; this.openMillis = openMillis; } String call(long now, boolean dependencyHealthy) { if (state == State.OPEN) { if (now - openedAt < openMillis) { return "rejected fast, fallback served"; // no call made } state = State.HALF_OPEN; // let one trial request through } boolean ok = dependencyHealthy; // the real remote call happens here if (ok) { consecutiveFailures = 0; state = State.CLOSED; return "success"; } consecutiveFailures++; if (state == State.HALF_OPEN || consecutiveFailures >= failureThreshold) { state = State.OPEN; openedAt = now; } return "failure"; } State state() { return state; } } public static void main(String[] args) { CircuitBreaker breaker = new CircuitBreaker(3, 5_000); for (long t = 0; t <= 15_000; t += 1_000) { boolean healthy = t < 1_000 || t >= 12_000; // the dependency is down from 1 s to 12 s String result = breaker.call(t, healthy); System.out.printf("t=%2d s dependency %-4s -> %-30s state %s%n", t / 1000, healthy ? "up" : "down", result, breaker.state()); } }}t= 0 s dependency up -> success state CLOSEDt= 1 s dependency down -> failure state CLOSEDt= 2 s dependency down -> failure state CLOSEDt= 3 s dependency down -> failure state OPENt= 4 s dependency down -> rejected fast, fallback served state OPENt= 5 s dependency down -> rejected fast, fallback served state OPENt= 6 s dependency down -> rejected fast, fallback served state OPENt= 7 s dependency down -> rejected fast, fallback served state OPENt= 8 s dependency down -> failure state OPENt= 9 s dependency down -> rejected fast, fallback served state OPENt=10 s dependency down -> rejected fast, fallback served state OPENt=11 s dependency down -> rejected fast, fallback served state OPENt=12 s dependency up -> rejected fast, fallback served state OPENt=13 s dependency up -> success state CLOSEDt=14 s dependency up -> success state CLOSEDt=15 s dependency up -> success state CLOSEDRead it closely. After three failures the breaker opens, and for the next several seconds callers get an instant fallback instead of a timeout. At 8 seconds the half-open trial fails, so it reopens. At 12 seconds the dependency has recovered but the breaker is still open, so one more second passes before the trial at 13 seconds succeeds and traffic resumes. That short delay in recovery is the price of protecting the dependency from a flood the moment it comes back.
Production breakers (libraries such as Resilience4j in Java, or service-mesh features) usually use failure rates over a sliding window rather than consecutive counts, count slow calls as failures, and keep a breaker per dependency (and sometimes per shard or endpoint), so one bad partition does not block healthy ones.
4. Bulkheads: isolate resources
#On a ship, bulkheads divide the hull so one leak does not sink everything. In software, a bulkhead gives separate, limited resources to separate dependencies or workloads, so one cannot consume them all. Microsoft's Bulkhead pattern describes isolating elements into pools so that if one fails, the others continue to function (Bulkhead pattern).
Examples:
- Separate thread pools or concurrency limits per dependency. Calls to recommendations may use at most 50 concurrent slots; when they are full, further calls fail fast. The feed and profile calls have their own slots and are unaffected.
- Separate connection pools for the critical database path and for reporting queries.
- Separate queues and worker fleets for urgent and bulk work.
- Separate deployments per customer tier or per region.
You can size a bulkhead with Little's Law. If recommendations normally take 80 ms and receive 3,500 calls per second, about 3,500 x 0.08 = 280 calls are in flight; a limit of, say, 400 handles normal peaks, and in the 8-second scenario the excess fails fast instead of consuming every thread.
Shuffle sharding
#For multi-tenant systems, Amazon's Builders' Library describes shuffle sharding: each customer is assigned to a small, random combination of workers out of a larger fleet, so one customer's bad traffic (a "poison" request pattern) affects only the few workers in its shard, and almost no other customer shares exactly the same combination (Workload isolation using shuffle-sharding). It is an advanced form of bulkheading worth mentioning in SDE-2 discussions about noisy neighbours.
5. Rate limiting and throttling
#Protect the system from clients that send too much: per-user, per-API-key or per-IP limits, answered with 429 Too Many Requests. Microsoft's Throttling pattern frames this as controlling resource consumption so the system keeps meeting its service levels under heavy demand (Throttling pattern). Article 31 designs a distributed rate limiter.
6. Load shedding and admission control
#Rate limits protect against individual clients. Sometimes the total load, from many well-behaved clients, exceeds capacity: a big event, a failover that moved traffic, a retry storm. A server that tries to serve everything then serves everything slowly, so almost every request times out, and useful throughput collapses. Google's SRE book discusses this, recommending that servers protect themselves by rejecting excess load early and cheaply (Handling Overload). The Amazon Builders' Library makes the same argument: shedding excess requests keeps latency low for the requests you accept, which keeps goodput high (Using load shedding to avoid overload).
How to shed:
- Measure saturation with something that reacts quickly: concurrent requests, queue length, or queue waiting time, rather than CPU alone.
- Reject early and cheaply: a fast
503(or429) before doing expensive work. A rejected request costs microseconds; a timed-out one costs seconds. - Prioritise. Shed the least important traffic first: background prefetches, analytics events, non-critical widgets, before logins and purchases. Health checks and internal control traffic should be protected.
- Drop requests that have already waited too long. If a request has spent 3 seconds in a queue and the client times out at 2, processing it is pure waste.
7. Backpressure
#In asynchronous pipelines, the equivalent of load shedding is backpressure: a slow consumer signals producers to slow down rather than letting buffers grow without limit.
- Bounded queues: when a queue is full, producers block, slow down, or receive an error, instead of memory growing until a crash.
- Pull-based consumption: consumers fetch work when they have capacity (most queue consumers already work this way).
- Propagate the signal upstream: an API whose work queue is too old returns
503withRetry-Afterfor new non-urgent jobs (article 11's backlog discussion).
Unbounded queues do not remove overload; they hide it until memory runs out or the work is too old to matter.
8. Graceful degradation
#Graceful degradation means that when a dependency fails, the system continues with reduced functionality rather than failing entirely. It must be designed in advance, because nobody designs good fallbacks in the middle of an incident.
Common fallbacks:
Fallback | Example |
|---|---|
Serve cached or stale data | Show yesterday's recommendations; show the last known like count |
Default or generic content | "Popular this week" instead of personalised suggestions |
Hide the feature | Remove the recommendations widget from the page |
Read-only mode | Browsing works; posting is temporarily disabled with a clear message |
Queue for later | Accept the order, confirm payment later (article 05) |
Reduced quality | Lower video resolution; fewer search results |
Two supporting ideas:
- Kill switches and feature flags let operators turn off expensive or failing features instantly, without a deployment.
- Static stability: the Amazon Builders' Library describes designing systems so they keep operating in their current state when a dependency (such as a control plane) is impaired, for example by pre-provisioning enough capacity in each availability zone so that losing one zone does not require launching new resources during the emergency (Static stability using Availability Zones). The general lesson: avoid recovery plans that depend on the very systems most likely to be impaired during the incident.
Worked example: degrading Pixly's home page
#First list the dependencies of the home-page request and decide, in advance, what happens when each fails.
Dependency | Critical? | Timeout | If it fails | Mechanisms |
|---|---|---|---|---|
Authentication / session store | Yes | 50 ms | Show a "please sign in again" page; do not guess | Redundant store; no degradation possible |
Feed service | Yes | 300 ms | Serve the user's cached feed from earlier; if none, "recent photos from popular accounts" | Breaker, cached fallback |
Photo metadata cache | No (database fallback) | 20 ms | Read from replicas, with request coalescing; limit concurrency to protect the database | Bulkhead on database fallback |
Like counts | No | 50 ms | Hide counts or show last known values | Breaker, stale values |
Recommendations | No | 200 ms | Hide the widget | Breaker, bulkhead of 400 slots |
Ads | No | 150 ms | Show no ads | Breaker |
Edge protections:
- Rate limits per user and per IP at the API gateway.
- Load shedding with three priority classes: (1) login, upload and other user actions; (2) feed loads; (3) prefetches, analytics events and recommendations. Under overload, class 3 is rejected first.
- Retry policy: the mobile app retries idempotent reads with backoff and jitter, once; services do not retry each other.
Replaying the 9:02 incident with these in place: recommendation calls start timing out at 200 ms; the breaker opens after a few seconds; the widget disappears; every other part of the page loads normally. Users notice a missing widget, not an outage, and the on-call engineer is paged by the breaker-open alert rather than by a total failure.
When the protections themselves fail
#Problem | Effect | Mitigation |
|---|---|---|
Timeouts longer than the caller's own deadline | Protection never triggers in time | Derive timeouts from the latency budget |
Breaker counts client errors (4xx) as failures | Opens because of bad requests, blocking good ones | Count only server errors and timeouts |
One breaker for a whole sharded dependency | One bad shard blocks all | Breakers per shard or endpoint |
Fallback calls another overloaded service | Fallback becomes a second cascade | Fallbacks must be local and cheap (cache, defaults) |
Load shedding rejects health checks | Load balancer removes healthy servers | Exempt health checks and control traffic |
Unbounded internal queues | Memory exhaustion, ancient work | Bounded queues and deadlines |
Untested fallbacks | Broken exactly when needed | Fault-injection tests and game days |
Test it before production does
#These mechanisms only prove themselves under failure, so create failure on purpose: inject latency and errors into dependencies in staging, run game days where a team deliberately disables a component and watches the system's response, and run load tests above expected peak to see where shedding begins. This practice is often called chaos engineering. The goal is not drama; it is discovering a broken fallback on a Tuesday afternoon rather than during the biggest sale of the year.
Trade-offs
#Mechanism | You gain | You pay |
|---|---|---|
Short timeouts | Bounded resource use | More false failures on slow-but-healthy calls |
Circuit breakers | Fast failure, protection for the dependency | Slower recovery; tuning; false opens |
Bulkheads | Failure isolation | Less resource sharing; more pools to size |
Load shedding | High goodput under overload | Some users get errors |
Graceful degradation | Partial service instead of outage | Design and testing effort; reduced features |
Backpressure | Stable memory and latency | Producers must handle "slow down" signals |
Common mistakes
#- No timeouts, or timeouts longer than the user's patience.
- Retries at every layer without budgets.
- Treating every dependency as critical, so any failure is a total failure.
- Fallbacks designed during the incident.
- Unbounded queues "to absorb spikes".
- Health checks that fail when a dependency fails (article 08).
- Never testing failure paths.
In an interview
#- For each dependency on the critical path, state the timeout and what happens when it fails.
- Mention circuit breakers for unreliable dependencies, with a fallback.
- Mention bulkheads when one slow dependency could consume shared resources.
- Under "what if traffic is 10 times normal?", talk about autoscaling limits, rate limiting and prioritised load shedding.
- Show a degradation table for the main page or flow. Interviewers rarely see one, and it signals production experience.
Exercises
#Exercise 1
Read the cascade
#An API calls a payment-status service with a 30-second timeout and 3 retries. Each API server has 200 worker threads and handles 500 requests per second normally, with 20% of requests calling payment status. The payment-status service starts hanging. Estimate how long until an API server's threads are exhausted, and list three changes that would contain the failure.
Exercise 2: Design the degradation
#A product page calls: catalogue (title, images, price), inventory (in stock?), reviews, recommendations, and a delivery-date estimator. Decide which are critical, what each fallback is, and what the page looks like when inventory is down.
Exercise 3: Size a bulkhead
#A search service is called 2,000 times per second with a p99 latency of 150 ms. You want calls to it to never use more than a bounded number of concurrent slots on each of 10 API servers, while handling normal peaks comfortably. Choose a limit per server and explain what happens when the search service slows to 3 seconds.
Exercise 4: Shed the right load
#During a festival sale, total traffic is 3 times capacity. Traffic is: 40% product browsing, 25% search, 15% add-to-cart and checkout, 10% recommendations and analytics beacons, 10% order-status checks. Propose a shedding policy.
Show solutionHide solution
Payment-status calls arrive at 20% of 500 = 100 per second per server. With the service hanging, each call holds a thread for up to 30 seconds, and retries make it worse (up to 4 attempts, so up to 120 seconds per request). Ignoring retries, 100 new stuck threads per second exhaust 200 threads in about 2 seconds. After that, every request on the server, including the 80% that never touch payment status, waits for a thread and times out. The whole API fails within seconds.
Changes:
- A short timeout (for example 300 to 500 ms, based on the service's normal latency) so each stuck call holds a thread briefly.
- A circuit breaker on payment-status calls, with a fallback ("status updating, check back shortly"), so after a few failures calls stop entirely.
- A bulkhead: cap concurrent payment-status calls at, say, 20 per server, so even with failures they cannot take more than 10% of threads.
- No retries inside the request for this non-critical read (or one retry within the deadline).
Exercise 2
Show solutionHide solution
Dependency | Critical? | Fallback |
|---|---|---|
Catalogue | Yes | Serve the cached product page (CDN or cache); if no cache, show an error page |
Inventory | Partly | Show "check availability at checkout" instead of a stock status; the add-to-cart button stays, and real availability is checked at checkout (where it must be correct) |
Reviews | No | Hide the reviews section, or show the cached average rating |
Recommendations | No | Hide, or show static best-sellers |
Delivery estimator | No | Show a generic range ("usually 3 to 5 days") |
With inventory down, the page shows title, images, price and a neutral availability message; the customer can still add to cart. The final stock check happens at checkout, where a hard dependency on inventory is acceptable (and where a clear "temporarily unable to complete checkout" message is better than overselling).
Exercise 3
Show solutionHide solution
Per server: 2,000 / 10 = 200 calls per second. At p99 of 150 ms, the in-flight count is about 200 x 0.15 = 30 at the tail (and fewer on average). A limit of about 60 concurrent calls per server (twice the normal tail) handles normal peaks with headroom.
When search slows to 3 seconds, the demand would be 200 x 3 = 600 in flight per server. With the bulkhead, only 60 are allowed; the rest fail fast immediately and the page shows a fallback ("search is temporarily slow, try again") or the search box returns cached popular results. Threads for every other feature stay free. Combine with a timeout (say 500 ms) and a circuit breaker so most calls do not even wait that long once search is clearly unhealthy.
Exercise 4
Show solutionHide solution
Capacity is one third of demand, so roughly two thirds of traffic must be shed or reduced. Policy, in priority order (keep the most valuable):
- Keep add-to-cart and checkout (15%) at full priority: it is the revenue path. Reserve capacity for it explicitly.
- Keep order-status checks (10%) but serve them from a cache with a short TTL, so they cost little.
- Shed recommendations and analytics beacons (10%) entirely: hide widgets; buffer or drop beacons.
- Product browsing (40%) served mostly from the CDN and cache (pages are largely static); shed origin requests that miss the cache beyond a limit, showing a cached version or a lightweight "high traffic" page.
- Search (25%): rate-limit per user, return cached results for popular queries, and shed the rest with a friendly message.
Also: put a virtual waiting room in front of the sale entrance (article 43) so arrivals are admitted at a rate the system can serve, which is a form of admission control.
Where to go next
#- Next: High Availability and Disaster Recovery: Redundancy, Failover, RPO, RTO and Multi-Region widens the view from one dependency to whole zones and regions.
- Then: Observability shows how to see these failures as they happen.
Sources & further reading
- Technology
- Software Engineering
- #availability
- #distributed systems
- #fault tolerance
- #reliability
- #roadmap
- #scalability
- #system design
- #system design interview
Connected lessons
Linked from 2 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
Start Here: Your Roadmap from Zero to Job-Ready DSA
Guide · Technology
Heaps and Priority Queues
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

