Skip to the solution
MediumBackend8 min read

How do you implement retries with exponential backoff and jitter?

678 views

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.

  • nodejs
  • resilience
  • retries
  • backoff

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 8 sections

Read the code

JavaScriptA 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

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 74 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextMedium

How do you manage database schema migrations in a Node.js project?

Continue