Performance Implications
While the Retry Pattern can greatly enhance reliability, it also introduces performance considerations and risks that architects must carefully manage:
-
Increased Load on Dependencies: Every retry is an additional request. When failures are truly rare, this extra load is negligible (and well worth the improved reliability). However, if a downstream service is failing due to being overloaded, retries can make the problem worse. For instance, imagine Service A is hammering Service B which is slow to respond. If A starts retrying aggressively, B gets even more requests. In the worst case, many clients retrying a struggling service can create a retry storm: each wave of failures spawns a wave of retries, compounding load exactly when capacity is lowest (see the 1,200 → 1,400 → 1,800 rps example below). Within one layer the blowup is bounded by max attempts; stacked across N layers it grows as rN — which is why both a cap and single-layer ownership are required. This is why backoff is crucial – to give breathing room between retries – and why limited attempts are important. It’s also a reason to incorporate circuit breakers or throttling for protection.
-
Increased Latency for End Users: When a service retries an operation, the overall time to get a response becomes longer. A single failed attempt might add a few seconds of delay while we wait and retry. This means that users might experience higher latency. In some cases, this is acceptable – a slightly slower successful response is better than an immediate error. But it can also degrade the user experience if overdone. If you chain multiple retries (or if multiple services in a call chain each retry), the cumulative delay can add up. One must balance success rate vs. response time. Often there is a cutoff where it’s better to fail fast than to keep the user waiting too long. That’s why setting an overall timeout for an operation is important in conjunction with retries. For instance, you might decide that an API call should either succeed within 5 seconds (including any retries) or not at all, to maintain a responsive SLA.
-
Wasted Work and Resource Consumption: Every retry that ultimately fails was essentially wasted effort – CPU cycles, network bandwidth, and memory that didn’t produce a successful result. If the underlying issue is not transient, all retries will fail and all that work is for naught (plus possibly straining the system). Therefore, it’s critical to detect non-transient failures quickly and avoid retrying them. For example, if a response comes back “permission denied” or “invalid request,” a retry won’t help – the client should not waste resources trying again. This often means your retry logic should inspect error codes/exceptions and classify by an allowlist of provably transient errors (unknown errors fail fast — a blocklist of non-retryable errors silently retries every failure type you didn’t anticipate). Similarly, if the first attempt already took a long time (nearly hitting a client timeout), it might be counterproductive to start another attempt that will likely also time out – better to propagate an error up than to tie up threads on hopeless retries.
-
Cascading Effects in Complex Systems: In a microservice ecosystem, consider the scenario where Service A calls B, and B calls C. If both A and B have their own retry policies, a failure in C could cause B to issue multiple attempts, and A to also issue multiple attempts of its call to B. This multiplies the load on B and C. It’s wise to avoid uncoordinated retries at every layer. Own retries at exactly ONE layer — choose the layer that (a) can classify the error as transient vs permanent, (b) owns or carries the idempotency key so a repeat is safe, and (c) still has deadline budget to spend. For user-facing flows that is often the edge/client-facing service (it owns the UX deadline); when idempotency lives deep (e.g. the service that mints the dedupe key next to the datastore), retry there and let outer layers fail fast. Never both: every additional retrying layer multiplies worst-case load (r per layer → rN). The other pages in this chapter apply this same criterion — edge vs. inner placement are context-dependent outcomes of it, not competing defaults. This prevents cascading retries from blowing up traffic. Also, monitoring systems should be in place to detect when excessive retries are happening (as it could indicate a downstream incident).
-
Retry Storms & Thundering Herd: If a popular service experiences a glitch, many instances of many services might all start retrying around the same time. This sudden surge (a retry storm) can be dangerous. Backoff and jitter help avoid synchronization of retries. Jitter specifically tackles the thundering herd by de-syncing retry timing, which is a best practice to include. Another mitigation is to implement budgets or limits on retries – for example, allowing a certain number of retries per second, or gradually increasing the interval (which exponential backoff inherently does). Some systems employ adaptive algorithms that lengthen backoff if errors continue (to avoid constant pressure).
-
Idempotency and Side Effects (Revisited): A performance or correctness pitfall of retries is the risk of performing an action multiple times. We must ensure either the action is idempotent or use measures to prevent side-effect duplication. Without this, retries could corrupt data (e.g. applying a transaction twice). In terms of performance, duplicate side effects might also double-consume resources (e.g. sending the same email twice uses twice the email-sending resource). Always consider the nature of the operation: Do not retry non-idempotent operations blindly unless you have a way to guard against repeated side effects (such as deduplication keys or check-pointing).
Strategies to Mitigate Issues
To address the downsides above, use a combination of techniques:
- Implement exponential backoff (don’t hammer the service rapidly).
- Add jitter to avoid synchronization of retries.
- Limit retry attempts to a reasonable number (and possibly limit the total time spent retrying), and enforce a per-client retry budget (retries as a bounded fraction of successful traffic).
- Use circuit breakers or fail-fast switches for scenarios where the failure is likely persistent. For instance, after N failures in a row, you might stop retrying for a short window (let the circuit open) to give the system time to recover.
- Distinguish error types – retry only on errors that are transient. For example, network timeouts, connection refused, or 502 Bad Gateway errors from a load balancer. Do not retry on business logic errors or client errors.
- Monitor and tune – use metrics to detect when retries are happening frequently. If a particular service is causing lots of retries, that might indicate it’s struggling or that your retry policy needs adjustment.
- Ensure idempotency – as stressed, make sure the operations can handle being repeated safely, or implement request deduplication mechanisms on the server side, especially for critical actions.
- Test under failure conditions – it’s important to simulate scenarios (like a dependency going down, or high latency) and see how your retry logic behaves. Make sure it actually improves resilience and doesn’t unintentionally overwhelm the system or time out too late.
When Not to Use the Retry Pattern
There are scenarios where retries might not be the best approach. If an error is clearly non-transient (e.g. a configuration error or a fatal exception), retrying just delays the inevitable. Likewise, if the downstream service is known to be down for an extended period (say a planned outage), a retry loop will only burn resources — a circuit breaker or a fallback response is preferable. Real-time systems with strict latency requirements might opt to fail fast rather than retry and violate the latency SLA. Also, if an operation is extremely expensive or has side effects that can’t be repeated, you should avoid automated retries. In such cases, alternative patterns like manual compensation, eventual reconciliation (for asynchronous processes), or simply alerting a human might be better. In essence, use retries where they make sense (transient, recoverable errors) and avoid them where they don’t (permanent failures or scenarios where retries could cause harm).
Retry policy in numbers: a concrete example
Suppose a downstream service normally responds in 50 ms. We configure:
- base delay = 100 ms
- exponential backoff multiplier = 2
- max delay = 2,000 ms
- max attempts = 5 (1 original call + 4 retries)
- full jitter: actual delay = random(0, computed_delay)
The worst-case elapsed time for one caller is roughly 100 + 200 + 400 + 800 = 1,500 ms of computed delays, plus the original timeout wait. With full jitter the expected sum is about half that, but tail callers can still wait seconds.
Why retries can amplify load
If the downstream is failing because it is overloaded, retries make it worse. Example:
- Downstream capacity = 1,000 rps.
- Incoming load = 1,200 rps.
- Failure rate = ~17%.
- If every failed request retries once, retry traffic ≈ 200 rps.
- New total = 1,400 rps, pushing failure rate to ~29%.
- Second retry wave adds another ~400 rps → 1,800 rps total — a retry storm.
Retry vs. circuit breaker vs. bulkhead
| Pattern | Protects against | When to use | When not to use |
|---|---|---|---|
| Retry | Transient failures | Network blips, occasional 503s | Permanent errors, overloaded downstream |
| Circuit breaker | Sustained failures | Downstream is down or very slow | Every call must succeed (e.g., payment auth) |
| Bulkhead | Resource starvation | Isolate dependencies | In-process calls with no shared pool |
Decision checklist before retrying
- Is the operation idempotent? If not, use a deduplication key or do not retry automatically.
- Is the error retryable? 5xx, timeouts, and connection refused usually are; 4xx and authentication errors are not.
- Does the total retry time fit inside the caller’s timeout budget?
- Have you added jitter and a per-client retry budget to avoid thundering herds?
Drill ladder
- L1: Why is exponential backoff alone not enough to prevent a retry storm?
- L2: Calculate total wait time for 5 retries with base 100 ms and multiplier 2, capped at 1 s.
- L3: What is the difference between full jitter and equal jitter?
- L4: How do you coordinate retries across multiple layers of a call chain?
- L5: Design a retry policy for a payment gateway where duplicate charges are unacceptable.
Answer key
- L1: Exponential backoff without jitter keeps failed clients synchronized — they all failed at the same instant, so they all wait the same (widening) interval and collide again at the same widened instants. Jitter is what de-syncs them.
- L2: Computed delays: 100 + 200 + 400 + 800 + min(1600, 1000) = 100 + 200 + 400 + 800 + 1,000 = 2,500 ms of computed delay (the 1,000 ms cap clamps the fifth delay from 1,600 ms). With full jitter the expected total is about half: ≈ 1,250 ms.
- L3: Full jitter:
sleep = random(0, window)— widest spread, can draw near-zero. Equal jitter:sleep = window/2 + random(0, window/2)— keeps half the backoff as a guaranteed floor between attempts. - L4: Retry at exactly one layer — the one that can classify the error, owns the idempotency key, and still has deadline budget (see the criterion above); all other layers fail fast and propagate. Enforce a retry budget so even the owning layer is bounded.
- L5: Idempotency-keyed POST (server dedupes on the key) + capped attempts (2–3 total) + per-try timeout and overall deadline + per-client retry budget + circuit breaker for sustained failure. The key makes a repeat safe; the caps make it bounded.
🤖 Don't fully get this? Learn it with Claude
Stuck on Performance Implications? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Performance Implications** (System Design) and want to truly understand it. Explain Performance Implications from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Performance Implications** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Performance Implications** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Performance Implications** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.