Features

Retry

Rezo includes a built-in retry system for handling transient failures. Configure it as a simple boolean, a retry count, or a detailed object with backoff strategies, status code filters, and lifecycle callbacks.

Quick Start

import { Rezo } from 'rezo';

// Simple: retry up to 3 times
const { data } = await rezo.get('https://api.example.com/data', {
  retry: 3
});

// Boolean: use default retry settings (3 retries, 1s delay)
const { data } = await rezo.get(url, { retry: true });

// Detailed configuration
const { data } = await rezo.get(url, {
  retry: {
    maxRetries: 5,
    retryDelay: 1000,
    backoff: 'exponential',
    maxDelay: 30000,
    statusCodes: [408, 429, 500, 502, 503, 504],
    retryOnTimeout: true,
    retryOnNetworkError: true
  }
});

Configuration Formats

The retry option accepts three formats:

Boolean

retry: true enables retries with all defaults: 3 attempts, 1 second delay, no backoff, standard status codes.

retry: false or retry: undefined disables retries entirely.

Number

retry: 5 retries up to 5 times with default settings for everything else.

Object

Full configuration with all available options:

interface RetryConfig {
  maxRetries?: number;       // Alias: limit
  retryDelay?: number;       // Alias: delay
  maxDelay?: number;
  backoff?: number | 'exponential' | 'linear' | ((attempt, baseDelay) => number);
  statusCodes?: number[];    // Alias: retryOn
  retryOnTimeout?: boolean;
  retryOnNetworkError?: boolean;
  methods?: HttpMethod[];
  condition?: (error, attempt) => boolean | Promise<boolean>;
  onRetry?: (error, attempt, delay) => boolean | void | Promise<boolean | void>;
  onRetryExhausted?: (error, totalAttempts) => void | Promise<void>;
}

Configuration Reference

maxRetries / limit

Maximum number of retry attempts. Both names are accepted; limit is an alias for maxRetries.

retry: { maxRetries: 5 }
// or equivalently:
retry: { limit: 5 }

Default: 3

retryDelay / delay

Base delay between retries in milliseconds. This is the starting delay before any backoff multiplier is applied. Both names are accepted.

retry: { retryDelay: 2000 }
// or equivalently:
retry: { delay: 2000 }

Default: 1000 (1 second)

maxDelay

Maximum delay between retries in milliseconds. Caps the computed delay even when using exponential backoff, preventing unreasonably long waits.

retry: {
  retryDelay: 1000,
  backoff: 'exponential',
  maxDelay: 30000 // Never wait more than 30 seconds
}

Default: 30000 (30 seconds)

backoff

Controls how the delay changes between retry attempts. Accepts several formats:

Exponential Backoff

Multiplies the delay by 2 on each attempt: 1s, 2s, 4s, 8s, 16s…

retry: { retryDelay: 1000, backoff: 'exponential' }
// 'exponential' is equivalent to backoff: 2

Linear Backoff

Adds the base delay on each attempt: 1s, 2s, 3s, 4s, 5s…

retry: { retryDelay: 1000, backoff: 'linear' }

Custom Multiplier

Any number acts as the exponential base. For example, backoff: 3 produces: 1s, 3s, 9s, 27s…

retry: { retryDelay: 1000, backoff: 3 }
// Attempt 1: 1000ms
// Attempt 2: 3000ms
// Attempt 3: 9000ms

Custom Function

Full control over delay calculation. Receives the attempt number (1-indexed) and the base delay.

retry: {
  retryDelay: 1000,
  backoff: (attempt, baseDelay) => {
    // Fibonacci-like: 1s, 1s, 2s, 3s, 5s...
    return Math.min(fib(attempt) * baseDelay, 30000);
  }
}

No Backoff

backoff: 1 (the default) means constant delay — every retry waits the same amount.

Default: 1 (no backoff, constant delay)

Jitter: All computed delays have +/-10% jitter applied automatically to prevent thundering herd problems when many clients retry simultaneously.

statusCodes / retryOn

HTTP status codes that trigger a retry. Both names are accepted.

retry: { statusCodes: [429, 500, 502, 503, 504] }
// or equivalently:
retry: { retryOn: [429, 500, 502, 503, 504] }

Default: [408, 425, 429, 500, 502, 503, 504, 520]

retryOnTimeout

Whether to retry on timeout errors (ETIMEDOUT, ECONNABORTED, UND_ERR_CONNECT_TIMEOUT, UND_ERR_HEADERS_TIMEOUT).

retry: { retryOnTimeout: true }

Default: true

retryOnNetworkError

Whether to retry on network errors (ECONNREFUSED, ECONNRESET, ENOTFOUND, EAI_AGAIN, ETIMEDOUT, ECONNABORTED, EPIPE, EHOSTUNREACH, ENETUNREACH).

retry: { retryOnNetworkError: true }

Default: true

methods

HTTP methods that are safe to retry. Non-idempotent methods like POST are excluded by default to prevent duplicate submissions.

retry: { methods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'] }

To also retry POST requests (be careful with side effects):

retry: { methods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'] }

Default: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']

condition

Custom predicate called after the built-in checks (status codes, error types) pass. Return true to allow the retry, false to stop retrying.

retry: {
  maxRetries: 3,
  condition: async (error, attempt) => {
    // Only retry if the service says it's recoverable
    if (error.response?.data?.recoverable === false) {
      return false;
    }
    // Don't retry if we've been rate limited too many times
    if (error.response?.status === 429 && attempt > 1) {
      return false;
    }
    return true;
  }
}

onRetry

Called before each retry attempt. Receives the error, attempt number (1-indexed), and the computed delay in milliseconds. Return false to cancel the retry.

retry: {
  maxRetries: 5,
  onRetry: (error, attempt, delay) => {
    console.log(`Retry ${attempt}/5 in ${delay}ms: ${error.message}`);
    // Return false to cancel this retry
    if (isUnrecoverable(error)) return false;
  }
}

onRetryExhausted

Called when all retry attempts are exhausted and the request is about to fail. Use for alerting, logging, or cleanup.

retry: {
  maxRetries: 3,
  onRetryExhausted: async (error, totalAttempts) => {
    await alerting.send({
      level: 'error',
      message: `Request failed after ${totalAttempts} attempts: ${error.message}`,
      url: error.config?.url
    });
  }
}

Instance-Level Defaults

Set retry configuration on the Rezo instance to apply to all requests. Per-request config overrides instance defaults.

const client = new Rezo({
  retry: {
    maxRetries: 3,
    retryDelay: 1000,
    backoff: 'exponential',
    maxDelay: 15000,
    retryOnTimeout: true,
    retryOnNetworkError: true
  }
});

// Uses instance defaults
await client.get('/api/users');

// Override for this request: 5 retries with linear backoff
await client.get('/api/critical', {
  retry: { maxRetries: 5, backoff: 'linear' }
});

// Disable retry for this request
await client.get('/api/idempotent-check', { retry: false });

When both instance and request retry configs are objects, the request-level values take priority for each field. Unspecified fields fall back to the instance defaults.

The beforeRetry Hook

The hooks system provides a beforeRetry hook that fires before each retry attempt, independent of the onRetry callback in the retry config. Use it for cross-cutting concerns like logging or token refresh.

const client = new Rezo({
  retry: { maxRetries: 3, backoff: 'exponential' },
  hooks: {
    beforeRetry: [
      async (config, error, retryCount) => {
        console.log(`[Hook] Retry ${retryCount}: ${error.code}`);

        // Refresh token before retrying a 401
        if (error.response?.status === 401) {
          const token = await getNewToken();
          config.headers.set('Authorization', `Bearer ${token}`);
        }
      }
    ]
  }
});

The beforeRetry hook runs after the delay and right before the retry request is dispatched. It receives:

  • config — the request config (mutable, so you can modify headers, etc.)
  • error — the error that caused the retry
  • retryCount — the current retry number (1 for first retry)

Retry Decision Flow

When a request fails, Rezo evaluates whether to retry in this order:

  1. Max retries check: Has the attempt count exceeded maxRetries? If yes, fail.
  2. Method check: Is the request method in the methods list? If not, fail.
  3. Status code check: Does the response status match any code in statusCodes? If yes, retry.
  4. Error code check: Is the error code a timeout error (when retryOnTimeout: true) or a network error (when retryOnNetworkError: true)? If yes, retry.
  5. Custom condition: If condition is provided, call it. Return true to retry, false to fail.
  6. Delay calculation: Compute delay using retryDelay, backoff, maxDelay, and jitter.
  7. onRetry callback: Call onRetry if provided. If it returns false, cancel the retry.
  8. Wait and retry: Sleep for the computed delay, then retry the request.

Practical Examples

API with Exponential Backoff

const api = new Rezo({
  baseURL: 'https://api.example.com',
  retry: {
    maxRetries: 5,
    retryDelay: 500,
    backoff: 'exponential',
    maxDelay: 30000,
    statusCodes: [429, 500, 502, 503, 504]
  }
});

// Delays: ~500ms, ~1000ms, ~2000ms, ~4000ms, ~8000ms (with jitter)
const { data } = await api.get('/users');

Scraper with Aggressive Retry

const scraper = new Rezo({
  retry: {
    maxRetries: 10,
    retryDelay: 2000,
    backoff: 'linear',
    maxDelay: 60000,
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'],
    retryOnTimeout: true,
    retryOnNetworkError: true,
    onRetry: (error, attempt, delay) => {
      console.log(`[${attempt}/10] Retrying in ${delay}ms: ${error.message}`);
    },
    onRetryExhausted: (error, totalAttempts) => {
      console.error(`Gave up after ${totalAttempts} attempts: ${error.config?.url}`);
    }
  }
});

Conditional Retry Based on Response Body

const client = new Rezo({
  retry: {
    maxRetries: 3,
    condition: async (error, attempt) => {
      // Some APIs return 200 with error in body
      const body = error.response?.data;
      if (body?.error?.retryable === true) return true;
      if (body?.error?.code === 'RATE_LIMITED') return true;
      return false;
    }
  }
});