Cloud · Resilience · DWG NO. 026

Retry (with Backoff)

Retry a failed call that's likely transient, waiting longer between each attempt so you don't hammer a struggling service.

TypeResilience
ScopeOne call
ComplexityLow
Common inNetwork calls, rate-limited APIs, distributed databases

Not every failure means "give up"

A blip in network connectivity or a momentary rate limit on the other end isn't a permanent failure — surfacing it to the user immediately, on the first failed attempt, throws away requests that would have succeeded a second later. But retrying instantly, in a tight loop, just adds load right when the dependency needs less of it.

Wait longer after each failure

Wrap the call in a loop with a maximum attempt count. After each failure, wait before retrying — typically doubling the wait time (exponential backoff), often with a small random jitter so many clients don't retry in lockstep.

try 1wait 1stry 2wait 2stry 3

Exponential backoff with jitter

Each failed attempt roughly doubles the wait, plus a little randomness, before the next try — capped at a maximum number of attempts.

retry-backoff.js
async function retryWithBackoff(fn, { maxAttempts = 4, baseDelayMs = 300 } = {}) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxAttempts) throw err;
      const backoff = baseDelayMs * 2 ** (attempt - 1);
      const jitter = Math.random() * baseDelayMs;
      const delay = backoff + jitter;
      console.log(`attempt ${attempt} failed, retrying in ${Math.round(delay)}ms`);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

let calls = 0;
const flakyFetch = async () => {
  calls++;
  if (calls < 3) throw new Error("503 service unavailable");
  return "ok";
};

const result = await retryWithBackoff(flakyFetch);
console.log(result); // "ok" — succeeded on the 3rd attempt

Trade-offs