Skip to content

Retry & Backoff

fetch-smartly implements a robust retry strategy using exponential backoff with full jitter.

Default Behavior

typescript
import { fetchWithRetry } from 'fetch-smartly';

// Uses default retry policy: 3 retries, 1s base delay, 2x backoff, jitter enabled
const res = await fetchWithRetry({ url: 'https://api.example.com/data' });

Default retry policy:

OptionDefaultDescription
maxRetries3Maximum retry attempts
baseDelay1000Base delay in ms
maxDelay30000Maximum delay cap in ms
backoffFactor2Exponential multiplier
jittertrueRandomize delay to prevent thundering herd
retryOn[408, 429, 500, 502, 503, 504]HTTP status codes to retry
retryOnNetworkErrortrueRetry on DNS/connection failures

What Gets Retried

Error TypeRetried?Reason
500, 502, 503, 504YesServer errors are often transient
429 (Rate Limit)YesRespects Retry-After header
408 (Request Timeout)YesServer-side timeout
400, 401, 403, 404NoClient errors won't fix themselves on retry
Network errorsYesDNS failures, connection refused
Timeout errorsYesRequest exceeded configured timeout
User abortNoIntentional cancellation

Retry-After Header

When a 429 response includes a Retry-After header, fetch-smartly uses that delay instead of the computed backoff:

typescript
// If server responds with: Retry-After: 60
// fetch-smartly waits 60 seconds (capped at maxDelay)

Both numeric seconds and HTTP-date formats are supported.

Custom Retry Logic

typescript
await fetchWithRetry({
  url: 'https://api.example.com/data',
  retry: {
    maxRetries: 5,
    baseDelay: 500,
    shouldRetry: (ctx) => {
      // Only retry the first 2 attempts
      return ctx.attempt <= 2;
    },
  },
  onRetry: (ctx) => {
    console.log(`Attempt ${ctx.attempt}, waiting ${ctx.delay}ms`);
  },
});

Backoff Formula

Without jitter: delay = min(maxDelay, baseDelay * backoffFactor ^ attempt)

With jitter (full jitter): delay = random(0, min(maxDelay, baseDelay * backoffFactor ^ attempt))

Released under the MIT License.