Designing for Failure
How resilient systems contain failure instead of cascading it â timeouts, backoff+jitter retries, circuit breakers, bulkheads, redundancy/failover, and graceful degradation.
In a large system, components fail constantly â so resilience isn't about preventing failure, it's about containing it. The toolkit is layered: bound every call with a timeout, retry transient errors with backoff and jitter, trip a circuit breaker when a dependency is sick, isolate resources with bulkheads, run redundant instances with automatic failover, and always have a graceful fallback. Together they turn outages into degradations.
At scale, failure is constant â disks die, networks partition, dependencies slow down, instances crash. Designing for failure means building so that individual failures are contained and expected, not catastrophic. The goal is graceful degradation, not perfect uptime.
Never wait forever: every remote call needs a timeout so a slow dependency can't exhaust your threads. Retry transient errors â but with exponential backoff and jitter, or synchronized retries become a self-inflicted DDoS (a retry storm) on an already-struggling service.
When a dependency keeps failing, a circuit breaker 'trips' (OPEN) and fast-fails subsequent calls with a fallback â sparing the sick service and freeing your resources. After a cooldown it goes HALF-OPEN to probe recovery, then CLOSES when healthy. This is what stops one failure from cascading across services.
Eliminate single points of failure with redundancy across instances and availability zones, automatic failover to standby replicas, and bulkheads (isolated resource pools) so one overloaded component can't sink the rest. Combine with graceful degradation: serve a cached or simplified response rather than an error.
Key Concepts
Trips OPEN after repeated failures to fast-fail and spare a struggling dependency, then probes (HALF-OPEN) to recover.
Retrying with increasing, randomized delays so clients don't retry in lockstep and create a retry storm.
Serving a reduced experience (cached/default data) when a dependency is down, instead of failing the whole request.
Isolating resources (thread/connection pools) per dependency so one overloaded call path can't starve the others.
Running standby replicas across zones and switching to them automatically when the primary fails â no single point of failure.
Making operations safe to retry by ensuring repeated execution has the same effect as one â essential when retrying.
1// Resilient call: timeout â retry (with backoff+jitter) â circuit breaker â fallback2async function getRecommendations(userId) {3 return breaker.run(4 async () => {5 // 1. Bound the wait â never hang forever on a slow dependency6 return await withTimeout(300, () =>7 // 2. Retry transient errors with EXPONENTIAL BACKOFF + JITTER8 retry({ attempts: 3, backoff: "exponential", jitter: true }, () =>9 recoService.fetch(userId)10 )11 );12 },13 // 3. FALLBACK when the breaker is open or all retries fail â14 // degrade gracefully instead of erroring the whole page15 () => getPopularItems() // generic, cached, always-available16 );17}18// breaker: trips after N failures, fast-fails while open, probes to recover
Reliability is the feature users notice when it's missing. Cascading failures and retry storms cause the biggest outages, and the patterns that prevent them â circuit breakers, backoff, bulkheads, graceful degradation â are core to SRE practice and a staple of senior system-design interviews. Designing for failure is what separates a demo from production.
Common Pitfalls
1The Cascading Failure
A recommendations service slows to a crawl. The product page calls it synchronously, so page threads block waiting, thread pools fill, and soon the entire site is down â not just recommendations.
One slow, non-critical dependency consumed all the request-handling threads of a critical service. With no timeout, no breaker, and no fallback, a localized slowdown cascaded into a full outage.
Wrap the call in a tight timeout, a circuit breaker, and a bulkhead (separate thread pool). When recommendations degrade, the breaker opens and the page instantly falls back to generic 'popular items' from cache. The product page stays fast and fully functional.
Takeaway: Isolate and time-bound every dependency, especially non-critical ones. A circuit breaker plus a fallback converts 'the whole site is down' into 'one widget shows generic data'.
2The Retry Storm
A database has a brief hiccup. Thousands of clients all retry immediately and simultaneously, hammering the database the instant it tries to recover and knocking it back down â repeatedly.
Naive fixed-interval retries meant every client retried in perfect lockstep. The synchronized surge turned a 2-second blip into a prolonged outage â clients DDoS'd their own backend.
Switch to exponential backoff with jitter so retries spread out over time, cap the number of attempts, and add a circuit breaker so clients stop retrying entirely once failures pile up. The database gets breathing room to recover.
Takeaway: Retries without backoff and jitter make outages worse. Randomized, capped, backoff-based retries â plus a breaker â let a struggling system actually recover instead of being re-overwhelmed.
Join the discussion
Comments
Loading comments...
Cookie preferences
We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.