How to Retry Requests in Axios
Build bounded Axios retries with exponential backoff, Retry-After handling, cancellation, mutation safety, and production troubleshooting.

Axios does not automatically retry failed requests. Add a response interceptor or use the axios-retry package, then define which failures are transient, how many attempts are allowed, how long to wait, and whether the operation can safely be repeated. A retry must return the new Axios promise so the original caller receives the eventual result.
By default, Axios rejects responses outside the 2xx range. Its response interceptors are therefore the extension point for retrying HTTP failures; your policy can also inspect network errors, cancellation, request methods, status codes, and response headers. See the Axios interceptor and error-handling documentation.
1. Decide what is safe to retry
Retrying is useful for short outages, connection resets, overloaded upstreams, and rate limits. It is dangerous when the first request may have reached the server and only its response was lost. A timeout or missing response does not prove that the server did nothing.
| Condition | Typical policy | Reason |
|---|---|---|
| Network error with no response | Retry only for safe or idempotent operations | The request may have been sent before the connection failed. |
| 408 Request Timeout | Usually retry safe methods | The server did not finish the request in time. |
| 429 Too Many Requests | Honor Retry-After, with a maximum wait |
The server is explicitly asking the client to slow down. |
| 500, 502, 503, 504 | Retry safe or documented idempotent operations | These often represent temporary server or gateway failures. |
| 400, 401, 403, 404, 422 | Do not retry automatically | Repeating an invalid request does not fix it. |
| POST payment or order creation | Do not retry unless the API supports idempotency keys | A successful first attempt could be duplicated. |
The HTTP verb alone is not a complete safety guarantee. GET, HEAD, and OPTIONS are normally safe. PUT and DELETE are defined as idempotent by HTTP semantics, but the actual API must implement those semantics correctly. Treat POST as non-retryable unless the API documents an idempotency mechanism.
2. A bounded Axios response interceptor
This complete example retries network failures and selected 5xx responses for GET, HEAD, and OPTIONS. It stores the counter on the request configuration, applies exponential backoff, honors a bounded Retry-After value, supports AbortController, and allows an individual request to opt out.

import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10_000
});
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 250;
const MAX_DELAY_MS = 10_000;
const SAFE_METHODS = new Set(['get', 'head', 'options']);
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
function retryAfterMs(value) {
if (!value) return null;
const seconds = Number(value);
if (Number.isFinite(seconds) && seconds >= 0) {
return Math.min(seconds * 1000, MAX_DELAY_MS);
}
const date = Date.parse(value);
if (!Number.isNaN(date)) {
return Math.min(Math.max(0, date - Date.now()), MAX_DELAY_MS);
}
return null;
}
function wait(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(new axios.CanceledError());
const timer = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(timer);
reject(new axios.CanceledError());
}, { once: true });
});
}
api.interceptors.response.use(
response => response,
async error => {
const config = error.config;
if (!config || config.noRetry || config.signal?.aborted) {
return Promise.reject(error);
}
const method = String(config.method || 'get').toLowerCase();
const status = error.response?.status;
const networkFailure = !error.response && !error.code?.startsWith('ERR_CANCELED');
const retryableStatus = status != null && RETRYABLE_STATUS.has(status);
if ((!networkFailure && !retryableStatus) || !SAFE_METHODS.has(method)) {
return Promise.reject(error);
}
config.retryCount = config.retryCount || 0;
if (config.retryCount >= MAX_RETRIES) {
return Promise.reject(error);
}
config.retryCount += 1;
const retryAfter = retryAfterMs(error.response?.headers?.['retry-after']);
const exponential = BASE_DELAY_MS * (2 ** (config.retryCount - 1));
const delay = retryAfter ?? Math.min(exponential, MAX_DELAY_MS);
await wait(delay, config.signal);
return api(config);
}
);
const response = await api.get('/status');
console.log(response.data);
There are at most three replays after the initial request. The final failure is returned to the caller. The marker remains on config when the interceptor calls api(config), so the replay cannot start an unbounded loop.
Jitter and shared services
Many clients failing at the same time can synchronize their exponential delays. Add bounded random jitter when your service has a large client population:
const jittered = Math.min(
MAX_DELAY_MS,
Math.round((BASE_DELAY_MS * 2 ** (config.retryCount - 1)) * (0.5 + Math.random()))
);
Use one consistent policy per client. Log the attempt number, status, delay, request identifier, and final outcome, but avoid logging authorization headers or response bodies that contain secrets.
3. Configure axios-retry instead
axios-retry supplies named hooks for retry count, condition, delay, timeout behavior, and retry callbacks. Its documented default condition is a network error or a 5xx response on an idempotent method. Its documented default delay is zero, so configure a delay when you need backoff. Read the axios-retry README for the version installed in your project.
import axios from 'axios';
import axiosRetry from 'axios-retry';
const api = axios.create({ timeout: 10_000 });
axiosRetry(api, {
retries: 3,
retryCondition: error => {
const method = String(error.config?.method || 'get').toLowerCase();
const status = error.response?.status;
const safe = ['get', 'head', 'options'].includes(method);
return safe && (!error.response || (status >= 500 && status < 600));
},
retryDelay: (retryCount, error) => {
const header = error.response?.headers?.['retry-after'];
const seconds = Number(header);
if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1000, 10_000);
return Math.min(250 * 2 ** (retryCount - 1), 10_000);
},
shouldResetTimeout: false,
onRetry: (retryCount, error, requestConfig) => {
console.warn({ retryCount, status: error.response?.status, url: requestConfig.url });
}
});
const result = await api.get('https://api.example.com/status');
retries controls the replay count. retryCondition must match the API’s actual semantics. retryDelay can be exponential, linear, or custom. shouldResetTimeout determines whether the timeout budget is reset between attempts; document this choice because a reset can make a request take much longer overall.
4. Axios status handling and validateStatus
Axios normally sends non-2xx responses to the rejected interceptor. If you configure validateStatus to return true for a status, that response reaches the fulfilled handler instead. A retry policy that only lives in the rejection handler will then miss it.
const api = axios.create({
validateStatus: status => status < 500
});
api.interceptors.response.use(response => {
if (response.status === 429) {
// Handle or convert this response here because validateStatus fulfilled it.
}
return response;
}, error => Promise.reject(error));
Inspect Axios errors by category: error.response means the server returned a response; error.request means a request was made but no response arrived; neither property usually means setup or configuration failed. A missing response is not proof that a mutation is safe to repeat.
5. Cancellation, timeouts, and mutation safety
Axios supports cancellation through AbortController. Your backoff wait must observe the same signal, otherwise a caller can cancel the HTTP request while a delayed retry is still scheduled. Distinguish cancellation from a network failure and never retry an aborted request.

const controller = new AbortController();
setTimeout(() => controller.abort(), 2_000);
await api.get('/slow-endpoint', {
signal: controller.signal,
noRetry: true
});
For a mutation, send an idempotency key when the server supports one and retry only according to that API’s documentation:
await api.post('/payments', payload, {
headers: { 'Idempotency-Key': paymentAttemptId },
noRetry: true
});
Keep a total deadline in addition to a per-attempt timeout. Three attempts with a ten-second timeout can otherwise occupy a worker for roughly thirty seconds plus backoff. If the API has a request deadline header, propagate it and stop when the remaining budget is exhausted.
6. Equivalent retry loops outside Axios
cURL
for attempt in 1 2 3 4; do
status=$(curl -sS -o response.json -w '%{http_code}' https://api.example.com/status) &&
case "$status" in
200|201|204) cat response.json; break ;;
408|429|500|502|503|504) sleep $((2 ** (attempt - 1))); ;;
*) cat response.json; exit 1 ;;
esac
done
Python requests
import time
import requests
retryable = {408, 429, 500, 502, 503, 504}
for attempt in range(4):
try:
response = requests.get('https://api.example.com/status', timeout=10)
if response.status_code not in retryable or attempt == 3:
response.raise_for_status()
print(response.json())
break
retry_after = response.headers.get('Retry-After')
delay = float(retry_after) if retry_after and retry_after.isdigit() else 2 ** attempt
time.sleep(min(delay, 10))
except requests.RequestException:
if attempt == 3:
raise
time.sleep(min(2 ** attempt, 10))
Node.js fetch
for (let attempt = 0; attempt < 4; attempt++) {
try {
const res = await fetch('https://api.example.com/status');
if (![408, 429, 500, 502, 503, 504].includes(res.status) || attempt === 3) {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
console.log(await res.json());
break;
}
} catch (error) {
if (attempt === 3) throw error;
}
await new Promise(resolve => setTimeout(resolve, Math.min(250 * 2 ** attempt, 10_000)));
}
7. Troubleshooting checklist
| Symptom | Cause | Fix |
|---|---|---|
| Retries never happen | validateStatus fulfills the response, or the condition excludes the method. |
Inspect status routing and method filters. |
| Requests loop forever | The attempt counter is not preserved on replay. | Store it on config or use the plugin’s retries option. |
| 429s make the outage worse | Immediate retries ignore rate limits. | Parse and cap Retry-After; add exponential backoff and jitter. |
| Duplicate orders or charges | A mutation was replayed after an ambiguous timeout. | Disable automatic retry or use server idempotency keys. |
| Cancellation still triggers a retry | The delay promise does not observe the abort signal. | Clear the timer and reject the wait on AbortSignal. |
| Overall latency is excessive | Every attempt gets a fresh timeout. | Use a total deadline and decide deliberately whether timeout resets. |
| Only some 5xx responses retry | The status allowlist is narrower than expected. | Document the list and include only statuses your upstream treats as transient. |
8. Performance, reliability, and cost notes
- Retries add latency by design. Measure first-attempt latency, retry rate, final success rate, and time spent waiting.
- Bound attempts, delay, and total deadline. Unbounded retries consume connection pools and can amplify an outage.
- Retry at one layer when possible. If a browser, service client, queue, and gateway all retry, the effective request count can multiply.
- Cache safe GET responses when freshness allows. Caching reduces load more predictably than repeated retries.
- Follow server rate-limit guidance and preserve correlation IDs across attempts.
- Axios and axios-retry do not make a non-idempotent operation safe. Reliability comes from an explicit contract with the server.
9. Or skip the browser setup
If your workflow needs screenshots of a page after a retrying job succeeds, ScreenshotNeo provides a single GET request that returns PNG, JPEG, WebP, or PDF. It accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Each cleanup step can be turned off.
Only clean shots are billed. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for options such as full-page lazy-image loading, CSS selector capture, device presets, dark mode, custom CSS and JavaScript, waits, blocked resources, headers, cookies, geolocation, resizing, caching, signed links, async webhooks, bulk capture, and PDF settings. There are 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
10. FAQ
Does Axios have a retry option built in?
Axios exposes interceptors and request cancellation, but retry policy is application code or a package such as axios-retry.
How many retries should I use?
Choose a small bounded number based on your latency budget and upstream guidance. Three replays is a common starting point, not a universal rule.
Should every network error be retried?
No. A network error can occur after the server processed a mutation. Restrict retries to safe operations or use an idempotency key.
What does Retry-After contain?
It may be a number of seconds or an HTTP date. Parse both forms, cap the wait, and fall back to your backoff only when the value is absent or invalid.
Why does my interceptor see a 429 in the success handler?
Your validateStatus function probably treats 429 as fulfilled. Handle it there or change the status policy.


