Skip to main content
ofetch includes built-in retry logic for failed requests, automatically retrying requests that fail with specific status codes.

Default Retry Behavior

By default, ofetch retries failed requests once for safe methods (GET, HEAD, OPTIONS, TRACE):
import { ofetch } from 'ofetch'

// Automatically retried once if it fails with a retryable status code
const data = await ofetch('/api/data')
Methods that may have side effects (POST, PUT, PATCH, DELETE) are not retried by default to avoid unintended duplicate operations.
Retries only occur when the response status code matches one of the retryable status codes. Network errors and timeouts are also retried.

Retry Status Codes

The following HTTP status codes trigger automatic retries:
  • 408 - Request Timeout
  • 409 - Conflict
  • 425 - Too Early (Experimental)
  • 429 - Too Many Requests
  • 500 - Internal Server Error
  • 502 - Bad Gateway
  • 503 - Service Unavailable
  • 504 - Gateway Timeout
These status codes typically indicate temporary issues that may succeed on retry.

Configuring Retry Count

You can customize the number of retry attempts using the retry option:
retry
number | false
default:"1 for safe methods, 0 for unsafe methods"
The number of times to retry the request. Set to false to disable retries completely.
import { ofetch } from 'ofetch'

// Retry up to 3 times
const data = await ofetch('/api/unreliable', {
  retry: 3
})

// Disable retries
const data = await ofetch('/api/data', {
  retry: false
})

// Enable retries for POST requests
const result = await ofetch('/api/create', {
  method: 'POST',
  body: { name: 'test' },
  retry: 2
})

Retry Delay

You can add a delay between retry attempts:
retryDelay
number | ((context: FetchContext) => number)
default:"0"
Delay in milliseconds between retry attempts. Can be a fixed number or a function that returns the delay based on the fetch context.

Fixed Delay

import { ofetch } from 'ofetch'

// Wait 1 second between retries
const data = await ofetch('/api/data', {
  retry: 3,
  retryDelay: 1000
})

Dynamic Delay (Exponential Backoff)

import { ofetch } from 'ofetch'

// Exponential backoff: 500ms, 1000ms, 2000ms
const data = await ofetch('/api/data', {
  retry: 3,
  retryDelay: (context) => {
    const retryCount = (context.options.retry as number) || 0
    const maxRetries = 3
    const attempt = maxRetries - retryCount
    return Math.min(1000 * Math.pow(2, attempt), 10000)
  }
})

Retry with Jitter

import { ofetch } from 'ofetch'

// Add random jitter to prevent thundering herd
const data = await ofetch('/api/data', {
  retry: 3,
  retryDelay: (context) => {
    const baseDelay = 1000
    const jitter = Math.random() * 500
    return baseDelay + jitter
  }
})

Custom Retry Status Codes

You can override the default retry status codes:
retryStatusCodes
number[]
default:"[408, 409, 425, 429, 500, 502, 503, 504]"
Array of HTTP status codes that should trigger a retry.
import { ofetch } from 'ofetch'

// Only retry on 429 (Too Many Requests) and 503 (Service Unavailable)
const data = await ofetch('/api/data', {
  retry: 3,
  retryStatusCodes: [429, 503]
})

// Retry on custom status codes
const data = await ofetch('/api/data', {
  retry: 2,
  retryStatusCodes: [408, 500, 502, 503, 504]
})

Retry with Rate Limiting

Handle rate limiting with exponential backoff:
import { ofetch } from 'ofetch'

const data = await ofetch('/api/rate-limited', {
  retry: 5,
  retryStatusCodes: [429], // Only retry on rate limit
  retryDelay: (context) => {
    // Check for Retry-After header
    const retryAfter = context.response?.headers.get('Retry-After')
    if (retryAfter) {
      const delay = parseInt(retryAfter, 10)
      return isNaN(delay) ? 1000 : delay * 1000
    }
    
    // Fallback to exponential backoff
    const retryCount = (context.options.retry as number) || 0
    const maxRetries = 5
    const attempt = maxRetries - retryCount
    return Math.min(1000 * Math.pow(2, attempt), 30000)
  }
})

Abort Signal and Retries

Abort signals prevent retries from happening:
import { ofetch } from 'ofetch'

const controller = new AbortController()

// This will NOT retry if aborted
try {
  const data = await ofetch('/api/data', {
    retry: 3,
    signal: controller.signal
  })
} catch (error) {
  console.error(error) // AbortError
}

// Abort the request
controller.abort()
When a request is aborted (via AbortController or timeout), ofetch will not retry the request, even if retry is configured.

Retry Logic Flow

  1. Request is made
  2. If request fails or returns a retryable status code:
    • Check if retry count is greater than 0
    • Check if status code is in retryStatusCodes (or default list)
    • Check if request was not aborted
  3. If all checks pass:
    • Wait for retryDelay milliseconds
    • Retry the request with retry count decremented by 1
  4. If retry count reaches 0, throw FetchError

Complete Example

import { ofetch } from 'ofetch'

async function fetchWithRetry() {
  try {
    const data = await ofetch('/api/unstable-endpoint', {
      // Retry up to 3 times
      retry: 3,
      
      // Only retry on specific status codes
      retryStatusCodes: [408, 429, 500, 502, 503, 504],
      
      // Exponential backoff with max delay of 10 seconds
      retryDelay: (context) => {
        const retryCount = (context.options.retry as number) || 0
        const maxRetries = 3
        const attempt = maxRetries - retryCount
        const delay = Math.min(1000 * Math.pow(2, attempt), 10000)
        
        console.log(`Retry attempt ${attempt + 1}, waiting ${delay}ms`)
        return delay
      }
    })
    
    return data
  } catch (error) {
    console.error('All retry attempts failed:', error)
    throw error
  }
}

Build docs developers (and LLMs) love