typescript 95 lines · 2 tabs

Exponential backoff with jitter for retries

Shared by codesnips Jan 2026
2 tabs
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;
}

export function computeDelay(attempt: number, baseMs: number, maxMs: number): number {
  const cap = Math.min(maxMs, baseMs * 2 ** attempt);
  return Math.floor(Math.random() * cap); // full jitter in [0, cap]
}

export function isRetryable(err: unknown): boolean {
  if (err instanceof TypeError) return true; // network / DNS failure
  const status = (err as { status?: number }).status;
  if (status === undefined) return false;
  return status === 408 || status === 425 || status === 429 || status >= 500;
}

function sleep(ms: number, signal?: AbortSignal): Promise<void> {
  return new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms);
    signal?.addEventListener('abort', () => {
      clearTimeout(id);
      reject(new DOMException('Aborted', 'AbortError'));
    }, { once: true });
  });
}

export async function withRetry<T>(
  op: (attempt: number) => Promise<T>,
  opts: RetryOptions
): Promise<T> {
  let lastErr: unknown;
  for (let attempt = 0; attempt <= opts.retries; attempt++) {
    opts.signal?.throwIfAborted();
    try {
      return await op(attempt);
    } catch (err) {
      lastErr = err;
      if (attempt === opts.retries || !isRetryable(err)) break;
      const delay = computeDelay(attempt, opts.baseMs, opts.maxMs);
      opts.onRetry?.(attempt, delay, err);
      await sleep(delay, opts.signal);
    }
  }
  throw lastErr;
}
2 files · typescript Explain with highlit

This snippet shows a small, self-contained retry layer built around the exponential backoff with full jitter strategy, the standard approach for retrying transient failures against remote services without turning a blip into a stampede. The core problem it solves is thundering herds: if many clients fail at the same instant and all wait the same fixed delay, they retry in lockstep and hammer a recovering service. Backoff spaces retries out geometrically, and jitter randomizes each wait so callers spread across the window instead of synchronizing.

In backoff.ts, computeDelay derives an exponential ceiling of baseMs * 2^attempt, clamps it to maxMs, then applies full jitter by picking a uniform random value in [0, cap]. This is the AWS-recommended variant: it trades a little worst-case latency for much better decorrelation between clients. isRetryable centralizes the policy for what deserves another attempt — network errors and the retryable status codes 408, 425, 429, and 5xx — so the decision lives in one place. sleep is a trivial promise wrapper, and withRetry is the generic driver: it loops up to retries + 1 times, awaits the operation, and on a retryable failure computes a delay and waits before the next pass. It also honors an AbortSignal so an in-flight retry sequence can be cancelled, and it rethrows immediately on non-retryable errors rather than wasting attempts.

One subtlety worth noting: the delay is computed from the upcoming attempt index, and the final failure is rethrown after the loop exhausts, preserving the original error for the caller.

In fetchWithRetry.ts, the policy is wired to a real HTTP client. fetchJson throws a typed HttpError carrying the status so isRetryable can inspect it, and it forwards a per-request AbortController timeout that is cleared in a finally. It also reads the Retry-After header when a server sends one, letting the server's own guidance override the computed jitter — a common courtesy that respects rate limiters. The withRetry call wraps this so callers simply await fetchWithRetry(url) and get bounded, jittered retries for free.

The trade-offs are deliberate: retries only make sense for idempotent operations, maxMs caps tail latency, and a retry budget prevents unbounded loops. This pattern is the right reach whenever a client talks to a flaky network, a rate-limited API, or a service that may briefly return 503 during deploys.


Related snips

Share this code

Here's the card — post it anywhere.

Exponential backoff with jitter for retries — share card
Link copied