Error handling patterns can stop dependency failures from cascading, but only when they are designed with isolation, timeouts, and fallbacks in mind. A single well-placed try/catch or circuit breaker can contain a failure, while a missing check can let one bad service take down an entire application. This article explains how these patterns work, where they succeed, and where they fall short.
Formula
The calculator above estimates two simple metrics for a dependency with a given timeout, retry count, and failure rate. The first output, Availability, is simply 100 minus the failure rate (p). The second, Effective retry time, is the timeout (t) multiplied by the number of retries (r). These are not a full reliability model, but they illustrate the core trade-off: retries add latency, and failures reduce availability.
In symbols: Availability = 100 - p (in percent), and Retry time = t × r (in seconds). The variables are:
- t – timeout per attempt, in seconds.
- r – number of retries after the first attempt.
- p – percentage of requests that fail on the first try.
Worked Example
Using the defaults: timeout = 2 seconds, retries = 3, failure rate = 5%.
Availability = 100 – 5 = 95%.
Retry time = 2 × 3 = 6 seconds.
So with a 5% failure rate, you can expect about 95% of requests to succeed on the first attempt. If a request does fail and you retry 3 times with a 2-second timeout, the retries alone can add up to 6 seconds of waiting before giving up.
What Are Dependency Failures and Cascades?
A dependency is any external service or component your application calls: a database, a REST API, a message queue, or a third-party payment gateway. When that dependency becomes slow, unresponsive, or returns errors, your application must handle the situation. If it doesn’t, the failure can spread. A cascade happens when one failed dependency causes other components to fail in turn, often because threads pile up, connections exhaust, or timeouts are set too long.
For example, if a web service calls a database that is down, and the service has no timeout, every incoming request might hang waiting for a response. As more requests arrive, the service’s thread pool fills up. Then other services that call this service also start hanging, and the failure spreads outward. This is the classic cascading failure pattern.
Core Error Handling Patterns That Prevent Cascades
Several patterns are specifically designed to stop this spread. Each addresses a different part of the problem:
- Timeouts – Set a maximum time to wait for a dependency. This prevents a single slow call from occupying a thread forever.
- Retries with backoff – Retry failed requests, but with increasing delays (exponential backoff) to avoid hammering a struggling service.
- Circuit breakers – When a dependency fails repeatedly, the circuit breaker opens and subsequent calls fail fast without even attempting the call. After a cooldown period, it allows a few test calls to see if the service recovered.
- Bulkheads – Isolate different dependencies into separate thread pools or connection pools, so a failure in one pool doesn’t starve the others.
- Fallbacks – Provide a default response or cached data when a dependency fails, so the user still gets a usable result.
These patterns are often combined. For instance, a typical setup might include a 2-second timeout, 3 retries with exponential backoff, and a circuit breaker that opens after 5 consecutive failures.
How to Choose the Right Pattern
Choosing the right pattern depends on the nature of the dependency and the business requirements. Ask these questions:
- Is the dependency critical or optional? If it’s optional (like a recommendation engine), a fallback to a default value is fine. If it’s critical (like a database), you need a circuit breaker to avoid waiting on a dead service.
- How fast does it need to respond? For user-facing requests, keep timeouts short (1-2 seconds). For background jobs, longer timeouts may be acceptable.
- Is the failure transient or permanent? Transient failures (like a network blip) benefit from retries. Permanent failures (like a bad request) should not be retried.
- What is the cost of a retry? If retries consume expensive resources or cause side effects (like processing a payment), you need idempotency keys and careful retry logic.
In general, start with timeouts and retries for all dependencies, then add circuit breakers for dependencies that have shown a tendency to fail in clusters.
Common Mistakes That Undo Error Handling
Even with the right patterns, mistakes can still cause cascades. Here are the most common ones:
- Setting timeouts too long. A 30-second timeout might seem safe, but if you have 100 concurrent requests, that’s 3000 seconds of thread time. Keep timeouts short and measure your thread pool capacity.
- Retrying too aggressively. Retrying immediately and infinitely can overwhelm the failing service and your own resources. Use exponential backoff and a maximum retry count.
- Not handling the “no response” case. A timeout is not the same as a connection refused. You must handle both, and also handle partial responses (e.g., a connection drops mid-stream).
- Ignoring the circuit breaker state. If the circuit is open, you should fail fast, not still try to call the dependency. Some libraries do this automatically, but custom code often forgets.
- Forgetting about thread pool exhaustion. Even with timeouts, if your thread pool is too small, a burst of slow calls can still exhaust it. Bulkheads help here.
A classic example: a service calls a database with a 5-second timeout. Normally, the database responds in 50ms. But when the database is under heavy load, it takes 4 seconds to respond. The service’s thread pool has 200 threads. With 200 concurrent requests, all threads are busy waiting 4 seconds each. New requests queue up, and the service becomes unresponsive to everything, including health checks. A circuit breaker that opens after 3 failures would have stopped the wait after a few seconds.
When Error Handling Isn’t Enough
There are situations where error handling patterns alone cannot prevent cascades. For example, if a dependency is so slow that it exhausts memory or CPU even while waiting, timeouts won’t help. Similarly, if the failure is due to a bug in your own code that causes an infinite loop, no pattern will save you. Also, if the dependency is a shared resource like a database connection pool that is not properly configured, your error handling might just shift the problem.
Another limitation is that error handling patterns add complexity. Each pattern has its own configuration and failure modes. A misconfigured circuit breaker can cause false positives, blocking a healthy service. A retry storm can make a minor outage worse. Therefore, these patterns must be tested under failure conditions, not just in happy-path tests.
Comparison: Timeouts vs. Circuit Breakers vs. Retries
These three patterns are often confused, but they solve different problems:
- Timeouts limit the duration of a single call. They prevent a call from hanging forever.
- Retries handle transient failures by repeating the call. They are useful when the failure is likely to be temporary.
- Circuit breakers stop calls to a failing dependency for a period. They prevent repeated attempts to a service that is likely to keep failing.
Think of it as: timeouts are the seatbelt, retries are the airbag, and circuit breakers are the emergency brake. You need all three for a safe ride.
Frequently Asked Questions
What is the difference between a timeout and a circuit breaker?
A timeout limits how long you wait for a single call. A circuit breaker stops making calls altogether after a certain number of failures. Timeouts prevent a single call from hanging; circuit breakers prevent a series of calls to a failing service.
How many retries should I configure?
It depends on the dependency and the failure mode. For transient network errors, 2-3 retries with exponential backoff (e.g., 100ms, 200ms, 400ms) is a common starting point. For idempotent operations, you might retry more, but always cap the total time you’re willing to spend.
Can error handling patterns cause more harm than good?
Yes, if misconfigured. Too many retries can overload a struggling service. A circuit breaker with a very short cooldown might not give the service time to recover. Always test under failure conditions and monitor the effects.
What is a bulkhead pattern?
Bulkhead is an isolation pattern where different dependencies use separate thread pools or connection pools. If one dependency exhausts its pool, it doesn’t affect the pools for other dependencies. This prevents a single failure from consuming all resources.
Do I need a fallback for every dependency?
Not necessarily. Fallbacks are useful for non-critical dependencies where a default value is acceptable. For critical dependencies, you might prefer to fail fast and let the user know, rather than returning stale or incorrect data.
How do I test my error handling patterns?
Use fault injection tools to simulate slow responses, timeouts, and connection errors. Run chaos experiments where you deliberately kill a dependency and observe how your system behaves. Ensure your monitoring captures error rates, latency, and thread pool usage.
Related
Formula used
in t | Timeout (seconds) | s | 2 | 0.1 | 60 in r | Retry count | - | 3 | 0 | 10 in p | Failure rate (%) | % | 5 | 0 | 100 out a | Availability (%) | % | 100 - p | 2 out rt | Effective retry time (s) | s | t * r | 2
Each line is one input (in, sel, list) or one result (out: label, unit, formula, decimals). How the calculators work
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- Valheim Server Not Showing in Community List Fixes
- DisplayPort vs HDMI: Which Cable Should You Use?
- Soundbar vs 2.1 Speaker System: Which Is Better?
- Survive Lava for Brainrot Tier List170 points
- MySQL Dump Import: Fix Common Errors
- Mouse Moving on Its Own? Causes & Fixes