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:
- Seconds: An integer indicating the number of
seconds to wait (e.g.,
Retry-After: 30). - 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
- Enforce a Maximum Retry Count: Always define a limit (e.g., 3 to 5 attempts) to prevent exhausting resources if the server consistently fails or sends invalid headers.
- Normalize Header Names: HTTP header names in Axios
are lowercased by default
(
response.headers['retry-after']). Ensure lookups match this format. - Combine with Exponential Backoff: If the
Retry-Afterheader is missing on a429response, fall back to an exponential backoff algorithm with jitter rather than a fixed delay.