javascript 108 lines · 3 tabs

Fetch With Exponential Backoff and Full Jitter Retries

Shared by codesnips Jul 2026
3 tabs
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);

export function fullJitter(attempt, baseDelay = 300, maxDelay = 10_000) {
  const ceiling = Math.min(maxDelay, baseDelay * 2 ** attempt);
  return Math.random() * ceiling;
}

export function isRetryable(errorOrResponse) {
  if (errorOrResponse instanceof Response) {
    return RETRYABLE_STATUS.has(errorOrResponse.status);
  }
  // fetch rejects with TypeError on network failure; AbortError on timeout
  const name = errorOrResponse?.name;
  return name === 'TypeError' || name === 'AbortError';
}

export function retryAfterMs(response) {
  const header = response.headers.get('Retry-After');
  if (!header) return null;
  const seconds = Number(header);
  if (!Number.isNaN(seconds)) return seconds * 1000;
  const date = Date.parse(header);
  return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
}

export function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms);
    signal?.addEventListener('abort', () => {
      clearTimeout(id);
      reject(new DOMException('Aborted', 'AbortError'));
    }, { once: true });
  });
}
3 files · javascript Explain with highlit

This snippet shows a small, reusable retry layer around fetch that survives flaky networks and transient server errors without hammering the backend. The core idea is exponential backoff: after each failed attempt the wait grows geometrically (base * 2^attempt), which spreads retries out and gives an overloaded service time to recover. On top of that it adds full jitter — instead of every client waiting exactly the same computed delay, each waits a random value between zero and that ceiling. Jitter is what prevents the thundering-herd problem where thousands of clients that failed at the same instant all retry in lockstep and re-synchronize the outage.

In backoff.js, fullJitter computes the capped exponential ceiling with Math.min(maxDelay, baseDelay * 2 ** attempt) and then samples a random point below it. isRetryable encodes the policy for what deserves another try: network errors (a thrown TypeError from fetch), request timeouts, and the standard transient HTTP statuses 408, 429, and 5xx. Client errors like 400 or 404 are deliberately not retried because repeating them only wastes time.

fetchWithRetry.js is the workhorse. It wraps each attempt in its own AbortController so a slow request can be cancelled once timeout elapses, translating a hang into a retryable error. It respects a server-supplied Retry-After header on 429/503 responses, honoring backpressure instead of guessing. When an attempt fails and retries remain, it sleeps for the jittered delay and loops; when attempts are exhausted it throws the last error. The signal passed by the caller is also observed, so an unmounting component can cancel the whole retry chain.

useFetchWithRetry.js adapts this into an idiomatic React hook. It tracks data, error, and loading, creates an AbortController in the effect, and aborts it on cleanup — critical for avoiding state updates after unmount. The trade-off worth noting is that retries multiply latency and can mask real failures, so callers should cap retries and maxDelay sensibly. This pattern is the right reach whenever a call touches an unreliable dependency: third-party APIs, rate-limited endpoints, or mobile connections.


Related snips

Share this code

Here's the card — post it anywhere.

Fetch With Exponential Backoff and Full Jitter Retries — share card
Link copied