Skip to content

axios

attachResilience(instance) runs every request of an axios instance through the same pipeline as the fetch integration: a total timeout, retries with backoff and jitter that honour Retry-After, and a circuit breaker per host. It works by wrapping the instance’s adapter, so your interceptors, transforms and call sites stay as they are.

Terminal window
npm i resilience-pipeline axios # axios >= 1 (optional peer dependency)
import axios from 'axios';
import { attachResilience } from 'resilience-pipeline/axios';
const api = axios.create({ baseURL: 'https://api.example.com' });
const resilience = attachResilience(api, { name: 'orders-api', attemptTimeout: '3s' });
const { data } = await api.get('/orders/42'); // retried on 5xx / network errors
await api.post('/orders', order, { headers: { 'Idempotency-Key': crypto.randomUUID() } });
await api.get('/report', { resilience: { timeout: '2m', retries: 0 } }); // per-request override
resilience.eject(); // e.g. on shutdown: restores the previous adapter

You can also attach it to the default export: attachResilience(axios).

total timeout (30 s) → retry (3×, exponential from 200 ms, full jitter, Retry-After)
→ per host: circuit breaker → concurrency limiter (optional) → attempt timeout (10 s) → axios adapter

The defaults, options and per-request fields are the same as for fetch. See the fetch options table. Only the client-specific parts differ:

Option Type Default Description
onBeforeAttempt (config, context) => config | void none Called before every attempt with a copy of the request config; return a replacement (e.g. a refreshed Authorization header).
getAdapter axios.getAdapter taken from the instance, or import('axios') Resolves adapter names (['xhr', 'http', 'fetch']). Pass it in bundled browser code.

All shared options: name, totalTimeout, attemptTimeout, retry, retryOn, circuitBreaker, concurrency, pipelineKey, maxPipelines, retryUnsafeMethods, timeProvider.

Importing resilience-pipeline/axios adds a typed resilience field to AxiosRequestConfig:

await api.get('/health', { resilience: { retries: 0, timeout: '1s' } });
await api.put('/profile', body, { resilience: { attemptTimeout: '20s' } });
await api.post('/orders', order, { resilience: { idempotent: true } }); // the server deduplicates

Fields: retries (number | false), retryOn, timeout, attemptTimeout, idempotent. Same meaning as for fetch.

axios rejects non-2xx responses with an AxiosError that carries error.response. The integration reads that response, so status-based rules work the same as with fetch:

Failure Retried by default Counted by the breaker
Response 408 429 500 502 503 504 yes yes
Other 4xx / 5xx response no no
Error without a response, code ECONNRESET, ECONNREFUSED, ECONNABORTED, ETIMEDOUT, EPIPE, EAI_AGAIN, ENETUNREACH, EHOSTUNREACH, ERR_NETWORK, UND_ERR_SOCKET yes yes
Attempt timeout (TimeoutRejectedError) yes yes
Any other error without a response (ERR_CANCELED, a bad config…) no no

After the last retry, the caller gets the original AxiosError, so error.response.status and axios.isAxiosError(error) keep working. Rejections from the pipeline itself (BrokenCircuitError, TimeoutRejectedError, BulkheadRejectedError) come through unchanged; check them with isBrokenCircuitError() and friends from resilience-pipeline.

Only idempotent requests are retried: GET HEAD OPTIONS TRACE PUT DELETE, or any method with an Idempotency-Key header or resilience: { idempotent: true }. The rules are explained on the fetch page.

A response discarded for a retry with responseType: 'stream' has its stream destroyed, so the socket is released.

Request interceptors run once per request, before the adapter. Retries happen inside the adapter, so they reuse the headers from the first attempt. To change something on every attempt, use onBeforeAttempt:

import { AxiosHeaders } from 'axios';
attachResilience(api, {
name: 'crm',
retryOn: [401, 429, 503],
onBeforeAttempt: async (config) => ({
...config,
headers: new AxiosHeaders(config.headers as AxiosHeaders).set('Authorization', `Bearer ${await tokens.get()}`),
}),
});

Each attempt receives the attempt’s own signal, so the attempt timeout and the caller’s signal abort the underlying request.

axios’ default adapter is a list of names (['xhr', 'http', 'fetch']) that only axios can resolve. The integration gets getAdapter from the default export, or loads axios with a dynamic import() on first use. Bundlers may not resolve that import; pass it explicitly:

import axios from 'axios';
attachResilience(api, { getAdapter: axios.getAdapter });

If the instance already has a function as its adapter (a mock adapter, a custom one), that function is wrapped directly and getAdapter isn’t used.