Engineering Notes
Home/Advanced Engineering/Advanced/Failure Handling and Retry Strategies
Advanced Engineering · Advanced

Failure Handling and Retry Strategies

Classify every failure, retry only what's transient, and keep retries from multiplying across layers. Calculate retry amplification in a call chain, set timeouts everywhere, and fail gracefully.

🚀 Advanced Engineering● Intermediate⏱ 4 min read▶ 2 interactive🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Classifying failures: transient, permanent, unknown
  • A good retry policy: backoff, jitter, caps, idempotency
  • Retry amplification across layers
  • Timeouts, deadlines and graceful failure

distributed-systems/fundamentals/06-retry_with_exponential_backoff and 07-circuit_breaker introduced the mechanisms. This topic is the engineering discipline around them: classifying failures, choosing what to retry, budgeting retries across a system, and failing in ways that are safe for users and data.

1Classify Every Failure

ClassExamplesRight response
transienttimeout, connection reset, 503, 429, DB failover, deadlock victim (03-deadlocks/)retry with backoff + jitter (bounded)
permanent400/422 validation, 401/403, 404, business rule violation, malformed datafail fast, return a clear error, no retry
unknown outcometimeout after sending a non-idempotent request (did the charge happen?)retry only if idempotent (idempotency key) or reconcile (query status)
resource exhaustionpool exhausted, out of memory, queue fullshed load, back off, don't amplify
bugsTypeError, null dereferencefail, alert, don't retry (it'll fail the same way)

Make classification explicit in code (error types or a retryable flag), not ad-hoc catch blocks.

2A Good Retry Policy

ts
async function withRetry<T>(op: () => Promise<T>, opts = { attempts: 4, baseMs: 100, maxMs: 2000, deadlineMs: 5000 }) {
  const start = Date.now();
  for (let attempt = 1; ; attempt++) {
    try {
      return await op();
    } catch (err) {
      if (!isRetryable(err) || attempt >= opts.attempts) throw err;
      const backoff = Math.min(opts.maxMs, opts.baseMs * 2 ** (attempt - 1));
      const delay = Math.random() * backoff;                           // full jitter
      if (Date.now() - start + delay > opts.deadlineMs) throw err;     // respect the caller's deadline
      await sleep(retryAfter(err) ?? delay);                           // honour Retry-After when given
    }
  }
}

Checklist: bounded attempts, exponential backoff, jitter, only retryable errors, only idempotent operations (or with idempotency keys), respect Retry-After, an overall deadline (don't retry past the point where the caller has given up).

3Retry Amplification

Retries multiply across layers. If each of 4 layers retries 3 times (4 attempts each), one failing call at the bottom can be attempted 4⁴ = 256 times. During an overload, retries add load exactly when the system can least afford it — turning a brownout into an outage.

Mitigations:

  • Retry at one layer (usually the one closest to the user or to the failing dependency) — others fail fast.
  • Retry budgets: allow retries only up to e.g. 10% of request volume per client (gRPC/Envoy support this); once exhausted, fail immediately.
  • Circuit breakers stop calling a dependency that's clearly down.
  • Deadline propagation: pass the remaining time budget downstream (grpc-timeout, a deadline header) so no layer works on requests whose caller has already timed out.

4Timeouts Everywhere

Every network call needs a timeout — the default in many clients is infinite. Set them from the latency SLO downward: if the user-facing budget is 1 s, a downstream call might get 300 ms with one retry. Separate connect timeouts (short) from request timeouts. A missing timeout turns a slow dependency into exhausted thread/connection pools upstream.

5Failing Gracefully

Under the HoodWhy Full Jitter Works▾

With pure exponential backoff, every client that failed at time t retries at exactly t + 100ms, t + 300ms, t + 700ms… — synchronized waves that hit the recovering server together. Full jitter picks the delay uniformly in [0, base·2ⁿ], which spreads retries evenly over the window. AWS's analysis ("Exponential Backoff and Jitter") found full jitter minimizes both total work and completion time compared with no jitter or "equal jitter" (base·2ⁿ/2 + random(0, base·2ⁿ/2)). Decorrelated jitter (delay = random(base, prev × 3), capped) is a popular alternative that avoids very short delays repeating. In all variants the key property is the same: destroy the correlation between clients' retry times.

Errors are classified as transient, permanent, unknown-outcome or exhaustion; only idempotent transient failures are retried with capped exponential backoff, full jitter and a deadline, at a single layer with a retry budget and circuit breaker, while others fail fast or fall back
Errors are classified as transient, permanent, unknown-outcome or exhaustion; only idempotent transient failures are retried with capped exponential backoff, full jitter and a deadline, at a single layer with a retry budget and circuit breaker, while others fail fast or fall back

⚠️ Common Pitfalls

  1. Retrying non-idempotent operations without idempotency keys — duplicate charges.
  2. Retrying permanent errors — wasted capacity, delayed failure.
  3. Retries at every layer — amplification during incidents.
  4. No timeouts — hung requests and exhausted pools.
  5. No jitter — synchronized retry storms.
  6. Swallowing errors (catch {}) — silent data loss.

📋 Quick Reference

text
classify: transient → retry · permanent → fail fast · unknown outcome → idempotency/reconcile · exhaustion → shed
retry: bounded · exponential · full jitter · Retry-After · deadline · idempotent only · ONE layer · budget
timeouts on every call (connect < request) · circuit breakers · fallbacks · compensations

🧠 Key takeaways

  • Classify failures: transient (retry), permanent (don't), unknown (retry idempotent operations only).
  • Retry policy: exponential backoff + jitter, attempt and time caps, idempotency keys.
  • Retries multiply across layers (3 layers × 3 attempts = 27×): retry in one layer, use retry budgets and breakers.
  • Set timeouts everywhere with deadline propagation, and degrade gracefully.

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1How should a service handle failures of its dependencies?core▾

Set timeouts on every call, retry transient failures with exponential backoff and jitter (only idempotent operations), use circuit breakers for sustained failures, bulkheads to isolate resources, and fallbacks to degrade gracefully.

Follow-ups interviewers ask
  • What's a fallback? A cheaper alternative response: cached data, defaults, or a reduced feature.
Q2Transient vs permanent failures: how do you tell them apart?core▾

Transient: timeouts, 503, 429, connection resets, deadlocks: retry may succeed. Permanent: 400, 401, 403, 404, validation errors: retrying won't help. Classify errors explicitly in client code.

Follow-ups interviewers ask
  • What about 500? Ambiguous; retry cautiously only if the operation is idempotent.
Q3How do idempotency keys enable safe retries?core▾

The client sends a unique key per logical operation; the server stores the key with the result and returns the stored result for duplicates, so retrying after a timeout never performs the action twice.

Follow-ups interviewers ask
  • How long are keys stored? Typically 24 hours or longer than the client's retry window.
Q4What's the danger of retrying at every layer?senior▾

Retry amplification: 3 retries at each of 4 layers can multiply load by up to 256x on the failing component, prolonging outages. Retry at one layer (usually closest to the client), with budgets.

Follow-ups interviewers ask
  • What's a retry budget? Limiting retries to a percentage of requests.
Q5How should failures be surfaced to users?core▾

Clear, actionable messages without internal details, consistent error formats in APIs (e.g. RFC 7807 problem details), correlation IDs for support, and graceful degradation instead of blank pages.

Follow-ups interviewers ask
  • Why include a correlation ID? Support can find the exact request in logs and traces.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.

✅ Check yourself

Answer the questions — feedback is instant.

1. 3 layers each try 3 times. One user request can cause how many calls to the bottom service?
3³.
2. A 400 Bad Request should be…
Permanent error.
3. Why propagate deadlines downstream?
Avoid wasted work.
4. What is a retry budget?
Prevents retry storms.