Cascading Failure
How a small slowdown in one dependency spreads upstream, service by service, until a localized problem becomes a system-wide outage — and how engineers contain the blast radius before it gets there.
What Is a Cascading Failure?
A cascading failure occurs when a problem in one component causes the components that depend on it to degrade, which then causes the components that depend on THOSE to degrade — turning a small, localized problem into a broad, system-wide failure.
The chain is often as ordinary as it gets: Service A calls Service B, which calls a database. If Service B becomes slow, Service A waits longer on every call it makes. Those slower calls keep A's worker threads, outbound connections, or concurrency slots occupied for longer than planned. Once enough of that shared capacity is tied up, Service A starts queueing or timing out its own requests — and now its callers start waiting on IT.
The critical thing to understand is that the original failure does not need to be a complete outage. A dependency going from 40 milliseconds to 2 seconds is enough. It is still "up." It is still returning correct answers. It is simply asking everyone who depends on it to wait much longer than they budgeted for — and that wait is what spreads.
Why Does It Happen?
A cascading failure needs a slow dependency, a caller that shares finite resources across all of its work, and nothing in place to bound the wait or isolate the damage.
Slow Dependencies
Dependencies rarely fail instantly — they slow down first. A downstream service becomes slow enough that every caller now waits far longer than it was designed to.
Resource Exhaustion
Every waiting request holds something open in the caller: a worker thread, an outbound connection-pool slot, or queue and buffer memory. The longer the wait, the longer those resources stay occupied — and the caller's own capacity for OTHER work shrinks.
Unbounded Queues
Without a limit, requests waiting for a scarce resource just keep piling up instead of being rejected. The queue grows, memory grows, and by the time a request is finally served, its caller has often already given up.
Missing Timeouts and Isolation
If a caller has no timeout — or one so long it is effectively unbounded — it holds resources until the dependency finally responds. And if there's no isolation between resource pools, one struggling dependency can consume capacity that unrelated operations needed.
Put together, this is a resource-sharing problem, not a single-point failure. Service B didn't take Service A down directly — Service A ran out of its own worker, connection, or queue capacity because too much of that capacity was tied up waiting on Service B. Aggressive, uncontrolled retries make it worse: they add traffic onto a dependency at the exact moment it can least absorb it.
Watch It Happen
Press play to watch a cascading failure unfold in a live request chain — from a healthy system, through a gradual downstream slowdown, resource exhaustion, upstream propagation, mitigation, and full recovery.
Cascading Failure
⤢ Expand for synchronized narration timeline
Why Is It Dangerous?
A cascading failure can turn one degraded dependency into a system-wide outage — and the symptom users see is often several hops away from the actual cause.
Because every hop in the chain shares finite resources with every other hop, the damage doesn't stay contained to where it started. Threads and connections held open on one service are threads and connections that service can't use for anything else — including requests that have nothing to do with the failing dependency.
Response times climb at every hop that waits on the one below it — a two-second dependency can turn an 80ms request into several seconds, several layers up.
Thread pools and connection pools exhaust at each layer, causing timeouts and error responses to appear at the edge — far from where the real problem lives.
Every in-flight call to a slow dependency holds a resource open in the caller, starving unrelated requests that share the same pool.
With no isolation or backpressure, a single degraded dependency can eventually make every layer above it unhealthy — even ones that never call the failing service directly.
The same causes recur in many published cascading-failure postmortems:
- No timeout on a downstream call — or one so long it fails to release resources before they pile up.
- A shared resource pool with no isolation between dependencies.
- Unbounded queues that let requests pile up instead of getting rejected.
- Aggressive retries that add more traffic onto an already-struggling dependency.
- No circuit breaker — every caller keeps attempting calls to a service that clearly cannot serve them.
- No backpressure — upstream callers are not told to slow down, so work keeps arriving faster than the system can drain it.
- No prioritization — critical and non-critical traffic compete for the same shrinking capacity.
How to find the real cause
Start from the edge errors, then walk down the call chain and look for the deepest hop that went bad first. Four signals, checked per hop, usually point to it:
- Per-hop latency (from traces or per-dependency client metrics) — the culprit is the lowest hop whose own latency rose; every hop above it only inherits that wait.
- Thread or connection-pool saturation — in-use at its maximum and callers waiting for a slot, first at the hop next to the culprit, then one hop up at a time.
- Queue depth — a backlog that grows without draining means arrivals exceed what the hop can complete.
- Circuit-breaker state — a breaker that never opened (or doesn't exist) explains why callers kept waiting; one that opened tells you which dependency was judged unhealthy.
- T+0 s: database query latency 40 ms → 2 s. This is the first and only anomaly, and nothing alerts yet.
- T+~1 s: B needs 200 × 2 = 400 concurrent slots (it needed 200 × 0.04 = 8) but has 100, so it can complete only 100 ÷ 2 = 50 req/s. B's pool saturates and its queue grows by about 150 requests per second.
- T+~2 s: A's calls to B now take seconds, so A's own pool fills the same way. Because A shares that pool with work that never touches B, unrelated endpoints slow down too, and no breaker has opened.
- T+tens of seconds: gateway timeouts and 5xx appear. This is the first thing users and dashboards notice, even though the cause sits two hops down.
How Do Engineers Fix It?
Five techniques address different parts of the mechanism. Resilient systems almost always combine several of them.
Timeouts
Never wait indefinitely for a downstream call. When a call exceeds its timeout, the caller gives up and frees the resource immediately.
Set a timeout that reflects the call's realistic latency budget — long enough to tolerate normal variance, short enough to release resources before they pile up.
A request may fail that would have eventually succeeded. But holding resources open indefinitely is worse: it lets one slow dependency exhaust the caller's entire capacity.
Circuit Breaker
When a dependency is clearly unhealthy, stop calling it entirely for a cool-down period, and fail fast instead of attempting the call.
Track recent error rates or latency per dependency. Once a threshold is crossed, the breaker opens: calls fail immediately without reaching the dependency. After a cool-down, it goes half-open to test recovery, then closes if the test succeeds.
Requests fail immediately while the breaker is open — even ones that might have succeeded. In exchange, the dependency stops receiving pressure and gets real room to recover.
Bulkheads
Give different dependencies their own dedicated resource pools, instead of sharing one pool across everything a service does.
Partition thread pools, connection pools, or concurrency limits per downstream dependency (or per traffic class). A pool dedicated to a struggling dependency can fill up without touching the pools used for unrelated work.
More infrastructure and more configuration to manage — multiple smaller pools instead of one large one. The payoff is a dramatically smaller blast radius when one dependency degrades.
Backpressure
Signal overload upstream so producers slow down or stop sending more work, instead of hiding the problem behind an ever-growing queue.
Use bounded queues, concurrency limits, or protocol signals so that once a limit is hit, upstream callers quickly see 'busy' and can slow down, retry later, or stop producing temporarily.
Throughput is intentionally constrained during overload. That is healthier than pretending more work can be absorbed and discovering too late that latency and memory have exploded.
Load Shedding
When the system is overloaded, intentionally reject lower-priority work to protect capacity for critical operations.
Classify traffic by priority ahead of time. Under overload, reject or defer low-priority requests first, keeping capacity available for critical paths like checkout or authentication.
Some users experience a deliberate failure for non-critical functionality. Graceful, partial degradation is almost always better than an uncontrolled, total failure.
| Technique | Protects Against | Main Benefit | Trade-off |
|---|---|---|---|
| Timeout | Slow dependency | Releases resources | Request may fail |
| Circuit Breaker | Repeated dependency failure | Stops pressure | Temporary unavailability |
| Bulkhead | Resource exhaustion | Limits blast radius | More infrastructure |
| Backpressure | Excess work | Controls load | Requests may wait/reject |
| Load Shedding | Overload | Protects critical paths | Some requests rejected |
| Retry Controls | Retry amplification | Reduces extra traffic | Slower recovery |
Graceful Degradation
Containing a cascade protects the system. Graceful degradation protects the response — the goal usually isn't "never fail," it's "fail in a controlled way."
When a non-critical dependency is unavailable, the entire request does not necessarily need to fail with it. A product page can render without a recommendations widget. A dashboard can load without analytics. An API response can go out without profile enrichment. Deciding in advance what "good enough" looks like, without a given dependency, is what makes this possible.
- Recommendation service unavailable → show the page without recommendations.
- Analytics unavailable → continue serving the core request.
- Profile enrichment unavailable → return a basic response instead of failing entirely.
Engineering Takeaway
- A cascading failure is not necessarily caused by one catastrophic failure — a small degradation is usually enough.
- The trigger is almost always a slowdown, not a crash. A dependency can still be 'up' and still cause a cascade.
- The mechanism is resource sharing: waiting requests consume threads, connections, and memory in their callers, and that consumption is what spreads.
- Timeouts bound how long a caller waits, so missing or overly long timeouts do not let a slow dependency hold resources until the caller exhausts its own capacity.
- Circuit breakers go further — they stop calling an unhealthy dependency entirely, giving it real room to recover.
- Bulkheads isolate resource pools so one bad dependency cannot starve unrelated work.
- Backpressure tells upstream systems to slow down or stop sending more work when capacity is gone, while load shedding deliberately rejects lower-priority requests to protect critical paths.
- The goal of resilient architecture is to contain the blast radius — a dependency can fail without taking the entire system with it.