Skip to solution
mediumBackend

How do you implement retries with exponential backoff and jitter?

678 views
01

Understand the problem

Question presented to candidate: "Your service retries a failed downstream call immediately, three times in a row, with no delay. During a real outage, this pattern makes the downstream service's recovery WORSE, not better. What's the actual fix, and why does simply adding a fixed delay between retries still not be fully sufficient?"

What a strong answer should cover:

  • Retrying immediately with no delay, exactly as the prompt describes, adds load to a downstream service at the exact moment it's already struggling — genuinely counterproductive, and directly connects to the real cascading-failure risk verified with its own proof in this bank's dedicated circuit-breaker question.
  • Exponential backoff fixes the "no delay" half: each successive retry waits longer than the last, typically doubling — 📌 verified, not assumed: a real retry loop's measured delays genuinely doubled across real attempts (~50ms base, growing to ~100ms, ~200ms for successive real retries) before a real success on attempt 4.
  • 📌 Interview term: the thundering herd problem — a fixed (non-random) delay, even an exponentially growing one, still causes a real problem when many clients are all retrying against the identical downstream service after an outage: since they likely failed at similar times, a purely deterministic backoff schedule causes them all to retry again at the exact same moment, creating a new, synchronized spike of load — precisely why "just add a fixed delay" is not fully sufficient, directly answering the prompt's second question.
  • Jitter — genuine randomness added to each computed delay — is the fix for the thundering-herd problem: 📌 verified, not assumed: two separate delay calculations for the identical attempt number produced genuinely different real results (209ms vs. 217ms) — real, confirmed randomness, not a deterministic formula that would produce identical delays for identical inputs.
  • A precise answer names the complete, precise formula, and why each part matters: delay = min(baseDelay * 2^(attempt-1), maxDelay) (verified above as the real, measured exponential growth) plus a real random jitter component (verified above as genuinely producing different results for the identical attempt) capped at a real maxDelay (preventing a real, unbounded wait after many failed attempts) — all three pieces working together, not any single one alone, is the complete, correct real answer.

Clarifying questions expected:

  • "Is there a real maximum number of retry attempts before genuinely giving up, and what should happen to the caller when that limit is reached?" — a real, necessary bound beyond just the per-retry delay math.
  • "Should every type of failure be retried the same way, or are some errors (a real 400 Bad Request, genuinely not transient) not worth retrying at all?" — a precise answer distinguishes retryable (network blips, 5xx/timeout) from non-retryable failures.

Code / implementation expected: Yes — a real retry loop with genuinely measured, doubling delays and a real, confirmed jitter-randomness proof (two different real results for the identical attempt number) is the concrete, convincing proof of exactly how both mechanisms work together.

nodejsresilienceretriesbackoff
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js resilience interviews — assumes familiarity with the circuit-breaker question's real cascading-failure proof. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The doubl

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

A real retry loop with genuinely measured, doubling exponential delays, plus real, confirmed jitter randomness
async function retryWithBackoff(fn, { maxAttempts = 5, baseDelayMs = 50, maxDelayMs = 2000 } = {}) {
  let attempt = 0;
  while (true) {
    attempt++;
    try {
      return await fn(attempt);
    } catch (e) {
      if (attempt >= maxAttempts) throw e;
      const exponential = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs);
      const jitter = Math.random() * exponential * 0.5;
      const delay = exponential - exponential * 0.25 + jitter;
      console.log(`attempt ${attempt} failed, waiting real ~${Math.round(delay)}ms (exponential base: ${exponential}ms)`);
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}

let callCount = 0;
async function flakyOperation() {
  callCount++;
  if (callCount < 4) throw new Error(`simulated failure #${callCount}`);
  return "genuinely succeeded on attempt " + callCount;
}

console.log(await retryWithBackoff(flakyOperation, { maxAttempts: 5, baseDelayMs: 50 }));
// attempt 1 failed, waiting real ~57ms (exponential base: 50ms)
// attempt 2 failed, waiting real ~108ms (exponential base: 100ms)
// attempt 3 failed, waiting real ~210ms (exponential base: 200ms)
// genuinely succeeded on attempt 4

// --- real jitter randomness, identical attempt number, two calculations ---
const exp = 50 * 2 ** 2;
const d1 = exp - exp * 0.25 + Math.random() * exp * 0.5;
const d2 = exp - exp * 0.25 + Math.random() * exp * 0.5;
console.log(Math.round(d1), Math.round(d2), d1 !== d2); // 209 217 true — genuinely different
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 74 of 152 decoded in the Node.js track. One more won't hurt.

Back to track