Guide

Retry Logic

Automatic request retries with configurable backoff strategies.

The library includes built-in retry logic to handle transient failures automatically. This is particularly useful for network errors, rate limiting, and temporary server issues.

Enabling Retries

Enable retries at the client level or per-request:

import { createClient } from '@outloud/reqo'

// Enable with defaults
const client = createClient({
  retry: true
})

// Or per-request
const data = await reqo.$get('/users', {}, {
  retry: true
})

Options

Customize retry behavior with detailed options:

limit
number
Maximum number of retry attempts. Defaults to 2.
const client = createClient({
  retry: {
    limit: 3  // Retry up to 3 times
  }
})
methods
RequestMethod[]
HTTP methods that can be retried. Defaults to ['GET', 'HEAD', 'OPTIONS'].
const client = createClient({
  retry: {
    methods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']
  }
})
statusCodes
number[]
HTTP status codes that trigger retries. Defaults to [408, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 530].
const client = createClient({
  retry: {
    statusCodes: [408, 429, 500, 502, 503, 504]
  }
})
codes
string[]
Error codes that trigger retries. Defaults to ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED', 'UND_ERR_SOCKET']
const client = createClient({
  retry: {
    codes: ['ECONNRESET', 'ETIMEDOUT']
  }
})
delay
DelayFn
Delay function that returns milliseconds to wait before retry. Default: Exponential backoff 0.1 * (2 ** (retryCount - 1)) * 1000
const client = createClient({
  retry: {
    delay: (state) => {
      // Linear backoff: 1s, 2s, 3s
      return state.retryCount * 1000
    }
  }
})
validate
ValidateFn
Custom validation function to determine if retry should occur.

If the function returns true, the request will be retried. If it returns false, no retry will occur.
If the function returns undefined, the default validation logic is used as fallback.
const client = createClient({
  retry: {
    validate: (state, options) => {
      // Custom retry logic
      return state.retryCount <= options.limit && state.error.status >= 500
    }
  }
})

Default Behavior

By default, retries are configured for:

  • Methods: GET, HEAD, OPTIONS (safe, idempotent methods)
  • Status Codes: 408, 429, 500, 502, 503, 504, 520-526, 530
  • Network Errors: ECONNRESET, ETIMEDOUT, ENOTFOUND, ECONNREFUSED
  • Limit: 2 retries
  • Delay: Exponential backoff (100ms, 200ms, 400ms, ...)
// Default configuration
const defaultRetry = {
  limit: 2,
  methods: ['GET', 'HEAD', 'OPTIONS'],
  statusCodes: [408, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 530],
  codes: ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED', 'UND_ERR_SOCKET'],
  delay: (state) => 0.1 * (2 ** (state.retryCount - 1)) * 1000
}

Backoff Strategies

Exponential Backoff (Default)

Doubles the delay between each retry:

const client = createClient({
  retry: {
    limit: 4,
    delay: (state) => {
      // 100ms, 200ms, 400ms, 800ms
      return 0.1 * (2 ** (state.retryCount - 1)) * 1000
    }
  }
})

Linear Backoff

Increases delay by a constant amount:

const client = createClient({
  retry: {
    limit: 3,
    delay: (state) => {
      // 1s, 2s, 3s
      return state.retryCount * 1000
    }
  }
})

Fixed Delay

Same delay between all retries:

const client = createClient({
  retry: {
    limit: 3,
    delay: () => 2000  // Always 2 seconds
  }
})

Jittered Backoff

Add randomness to prevent thundering herd:

const client = createClient({
  retry: {
    limit: 3,
    delay: (state) => {
      const baseDelay = 0.1 * (2 ** (state.retryCount - 1)) * 1000
      const jitter = Math.random() * 1000
      return baseDelay + jitter
    }
  }
})

Custom Based on Error

Adjust delay based on the error:

const client = createClient({
  retry: {
    delay: (state) => {
      // Longer delay for rate limiting
      if (state.error.status === 429) {
        const retryAfter = state.error.response?.headers.get('Retry-After')
        if (retryAfter) {
          return parseInt(retryAfter) * 1000
        }
        return 60000  // 1 minute
      }
      
      // Standard exponential backoff
      return 0.1 * (2 ** (state.retryCount - 1)) * 1000
    }
  }
})

Retry-After Header

Handle the Retry-After response header:

const client = createClient({
  retry: {
    limit: 3,
    delay: (state) => {
      // Check for Retry-After header
      const retryAfter = state.error.response?.headers.get('Retry-After')
      
      if (retryAfter) {
        // Retry-After can be seconds or HTTP date
        const delay = parseInt(retryAfter)
        if (!isNaN(delay)) {
          return delay * 1000
        }
        
        // Parse as date
        const date = new Date(retryAfter)
        return date.getTime() - Date.now()
      }
      
      // Default backoff
      return state.retryCount * 1000
    }
  }
})

Retrying Non-Idempotent Methods

By default, only safe methods (GET, HEAD, OPTIONS) are retried. To retry POST, PUT, PATCH, DELETE:

const client = createClient({
  retry: {
    limit: 2,
    methods: ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
  }
})

// Or per-request
await reqo.$post('/users', userData, {
  retry: {
    limit: 2,
    methods: ['POST']
  }
})
Be careful when retrying non-idempotent methods (POST, PATCH) as they may cause duplicate operations.

Status Code Filtering

Retry only specific status codes:

const client = createClient({
  retry: {
    // Only retry server errors
    statusCodes: [500, 502, 503, 504]
  }
})

// Only retry rate limiting
const rateLimitClient = createClient({
  retry: {
    statusCodes: [429],
    delay: (retryCount, error) => {
      const retryAfter = error.response?.headers.get('Retry-After')
      return retryAfter ? parseInt(retryAfter) * 1000 : 60000
    }
  }
})

Network Error Handling

Retry on specific network errors:

const client = createClient({
  retry: {
    codes: [
      'ECONNRESET',      // Connection reset
      'ETIMEDOUT',       // Timeout
      'ENOTFOUND',       // DNS lookup failed
      'ECONNREFUSED',    // Connection refused
      'UND_ERR_SOCKET'   // Undici socket error
    ]
  }
})

Custom Validation

Implement complex retry logic with validation function:

const client = createClient({
  retry: {
    validate: (state, options) => {
      // Don't retry after limit
      if (state.retryCount > options.limit) {
        return false
      }
      
      // Don't retry client errors (4xx except 429)
      if (state.error.status >= 400 && state.error.status < 500 && state.error.status !== 429) {
        return false
      }
      
      // Don't retry on specific error messages
      if (state.error.message.includes('Invalid token')) {
        return false
      }
      
      // Retry server errors and network issues
      return state.error.status >= 500 || state.error.code !== ''
    }
  }
})

Per-Request Retry

Override client retry settings for specific requests:

const client = createClient({
  retry: {
    limit: 2,
    methods: ['GET']
  }
})

// This request has different retry config
const data = await client.$post('/users', userData, {
  retry: {
    limit: 5,
    methods: ['POST'],
    delay: (state) => state.retryCount * 2000
  }
})

// This request has no retry
const noRetry = await client.$get('/users', {}, {
  retry: false
})

Timeouts and Retries

When using both timeouts and retries, the total time includes all retry attempts:

const client = createClient({
  timeout: 10000,  // 10 second total timeout
  retry: {
    limit: 3,
    delay: (count) => count * 1000  // 1s, 2s, 3s
  }
})

// If the first attempt takes 8s and fails, the retry will be
// canceled after 2s to respect the 10s total timeout

Practical Examples

API with Rate Limiting

const api = createClient({
  url: 'https://api.example.com',
  retry: {
    limit: 5,
    statusCodes: [429, 503],
    delay: (state) => {
      if (state.error.status === 429) {
        const retryAfter = state.error.response?.headers.get('Retry-After')
        return retryAfter ? parseInt(retryAfter) * 1000 : 60000
      }
      return state.retryCount * 1000
    }
  }
})

Unreliable Network

const unreliableApi = createClient({
  url: 'https://unreliable-service.com',
  retry: {
    limit: 5,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    codes: ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'],
    delay: (state) => {
      // Aggressive exponential backoff with jitter
      const base = 0.5 * (2 ** (state.retryCount - 1)) * 1000
      const jitter = Math.random() * 500
      return base + jitter
    }
  }
})