Handling Axios Rate Limits with Retry-After Header

When interacting with external APIs, reaching server-enforced rate limits typically results in an HTTP 429 Too Many Requests response containing a Retry-After header. This article covers how to automatically handle these rate limits in Axios using response interceptors, parse the Retry-After header accurately, and pause execution before retrying the failed request without manual intervention.


Understanding the Retry-After Header

A 429 Too Many Requests response usually provides a Retry-After header indicating how long your client must wait before making another request. The value comes in one of two formats:

  1. Seconds: An integer indicating the number of seconds to wait (e.g., Retry-After: 30).
  2. HTTP Date: A specific UTC date and time after which the request may be retried (e.g., Retry-After: Wed, 21 Oct 2026 07:28:00 GMT).

Parsing the Retry-After Value

To handle both formats, write a utility function that converts the header value into milliseconds:

function getRetryDelay(headerValue) {
  if (!headerValue) return 1000; // Default fallback delay (1 second)

  // Check if the value is an integer (seconds)
  const seconds = Number(headerValue);
  if (!isNaN(seconds)) {
    return seconds * 1000;
  }

  // Otherwise, attempt to parse as an HTTP date
  const retryDate = Date.parse(headerValue);
  if (!isNaN(retryDate)) {
    const delay = retryDate - Date.now();
    return Math.max(delay, 0);
  }

  return 1000;
}

Implementing the Axios Response Interceptor

Axios response interceptors let you intercept incoming errors, inspect the status code, and re-run the original request configuration.

Use a helper delay function and track the number of retries directly on the Axios request configuration object to prevent infinite retry loops.

import axios from 'axios';

// Create a custom Axios instance
const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

// Utility function to pause execution
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Attach the response interceptor
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const { config, response } = error;

    // Check if the error is due to rate limiting
    if (response && response.status === 429) {
      config._retryCount = config._retryCount || 0;
      const MAX_RETRIES = 3;

      if (config._retryCount < MAX_RETRIES) {
        config._retryCount += 1;

        const retryAfterHeader = response.headers['retry-after'];
        const delayMs = getRetryDelay(retryAfterHeader);

        // Wait for the duration specified by the server
        await sleep(delayMs);

        // Re-execute the request with the same configuration
        return apiClient(config);
      }
    }

    return Promise.reject(error);
  }
);

export default apiClient;

Best Practices