Introduction
Axios has become the de‑facto HTTP client for JavaScript developers, whether they are building single‑page applications (SPAs), server‑side services with Node.js, or even hybrid mobile apps. Its promise‑based API, automatic JSON transformation, and rich interceptor system make it a pleasure to work with. However, as applications scale—handling hundreds or thousands of concurrent requests, streaming large payloads, or operating under strict latency budgets—raw convenience is no longer enough. Performance considerations that are often overlooked in early prototypes become bottlenecks that directly impact user experience and operational costs.
This article dives deep into high‑performance Axios. We will:
- Examine the internal mechanics that affect speed and resource usage.
- Profile typical Axios usage patterns to identify hidden overhead.
- Provide concrete, production‑ready techniques for making every request faster and lighter.
- Show how to adapt Axios for both browser and Node.js environments, including HTTP/2, connection pooling, and streaming.
- Offer a checklist of best practices you can apply immediately.
Whether you are a front‑end engineer looking to shave milliseconds off page load times, or a back‑end developer aiming to squeeze more throughput out of a microservice, the strategies below will help you get the most out of Axios.
Table of Contents
- Understanding Axios Basics
- Why Performance Matters
- Profiling Axios Requests
- Optimizing Request Configuration
- Connection Reuse & HTTP/2
- Caching Strategies
- Request Batching & Parallelism
- Interceptors for Efficiency
- Error Handling without Overhead
- Axios in Browser vs Node.js
- Advanced Techniques: Custom Adapters & Streaming
- Testing and Benchmarking
- Best‑Practice Checklist
- Conclusion
- Resources
Understanding Axios Basics
Before we can optimize, we need a clear mental model of how Axios works under the hood.
Core Request Flow
- Configuration Merge – The
axios(config)call merges the user‑provided config with defaults (axios.defaults). - Adapter Selection – Depending on the environment, Axios picks an adapter:
- XHR adapter for browsers (uses
XMLHttpRequest). - HTTP adapter for Node.js (uses the native
http/httpsmodules).
- XHR adapter for browsers (uses
- Interceptor Chain – Request interceptors run first, followed by the adapter, then response interceptors.
- Transformation –
transformRequestandtransformResponsefunctions (default JSON handling). - Promise Resolution – The final result is wrapped in a promise.
Understanding where time is spent—configuration merging, adapter execution, transformation, or interceptor processing—helps us target the right layer for optimization.
Default Settings Worth Knowing
axios.defaults = {
timeout: 0,
headers: {
common: {
Accept: "application/json, text/plain, */*"
}
},
transformRequest: [function (data, headers) {
// JSON.stringify is the most common default
if (typeof data === "object" && !(data instanceof FormData)) {
return JSON.stringify(data);
}
return data;
}],
transformResponse: [function (data) {
try { return JSON.parse(data); } catch (e) { return data; }
}],
// …
};
These defaults are safe but not always optimal. For high‑throughput scenarios you may want to replace the JSON serializer, disable unnecessary headers, or bypass transformations entirely.
Why Performance Matters
User‑Facing Latency
- Perceived speed: A 200 ms delay in an API call can feel like a half‑second UI freeze.
- Mobile constraints: On 3G/4G networks, each extra byte matters.
Server‑Side Throughput
- CPU cycles: Unnecessary serialization or large request bodies waste CPU.
- Connection limits: Browsers cap concurrent connections per origin; inefficient reuse leads to queueing.
Cost Implications
- Cloud providers charge per GB transferred and per compute second. Reducing payload size and request count can directly lower your bill.
Profiling Axios Requests
Before applying optimizations, profile real traffic. The following tools work well:
| Environment | Tool | What It Shows |
|---|---|---|
| Browser | Chrome DevTools (Network tab) | Timing breakdown (DNS, SSL, Request/Response) |
| Node.js | clinic doctor or node --prof | CPU usage, async call stacks |
| Both | axios-debug-log (npm) | Interceptor timing, request/response payload sizes |
Example: Measuring Overhead with axios-debug-log
npm i axios-debug-log --save-dev
// logger.js
const axios = require('axios');
require('axios-debug-log');
axios.get('https://api.example.com/users')
.then(r => console.log('Done', r.status))
.catch(console.error);
The logger prints timestamps for each lifecycle step, exposing any interceptor that adds latency.
Typical Findings
- Transformation: JSON.stringify for large objects can dominate CPU.
- Headers: Adding a static header on every request (e.g.,
Authorization) via defaults incurs a tiny, but measurable, string concatenation cost. - Interceptors: Complex logic (e.g., reading from local storage) can add tens of milliseconds on the main thread.
Armed with this data, we can now apply targeted improvements.
Optimizing Request Configuration
1. Reuse Axios Instances
Creating a new Axios instance for each request incurs a deep clone of defaults. Instead:
// apiClient.js
import axios from 'axios';
export const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
});
Use api everywhere. This reduces object allocation and ensures consistent configuration.
2. Minimize Header Footprint
Only send headers that are required. Excessive headers increase request size and can trigger CORS pre‑flight.
// Bad: adds many unnecessary headers
api.defaults.headers.common['X-Request-ID'] = crypto.randomUUID();
// Good: set per request only when needed
api.get('/resource', {
headers: { 'X-Request-ID': crypto.randomUUID() }
});
3. Bypass Unnecessary Transformations
If you are sending raw binary data (e.g., a Blob or ArrayBuffer), disable the default JSON transform:
api.post('/upload', fileBuffer, {
transformRequest: [(data) => data] // identity function
});
4. Use paramsSerializer for Complex Query Strings
Axios uses URLSearchParams by default, which can be slower for large objects. Provide a custom serializer using qs or a lightweight function.
import qs from 'qs';
api.get('/search', {
params: { tags: ['node', 'performance', 'axios'] },
paramsSerializer: params => qs.stringify(params, { arrayFormat: 'repeat' })
});
qs is faster for nested objects and avoids the overhead of repeated append calls.
5. Set maxContentLength Wisely
When dealing with huge responses, limiting the size prevents memory bloat.
api.get('/bigfile', { maxContentLength: 10 * 1024 * 1024 }) // 10 MB limit
If the limit is reached, Axios aborts early, saving bandwidth and CPU.
Connection Reuse & HTTP/2
Browser: Leverage Keep‑Alive Implicitly
Modern browsers automatically reuse TCP connections per origin. To help them:
- Avoid changing the
Origin: Keep the base URL constant. - Use
Cache-Control: max-agefor static resources, allowing the browser to serve from its cache without a network round‑trip.
Node.js: Explicit Keep‑Alive Agent
Node’s default HTTP agent does not enable keep‑alive unless you opt‑in. This can lead to a new TCP handshake for each request, adding up to 100 ms per request on high‑latency networks.
import http from 'http';
import https from 'https';
import axios from 'axios';
const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });
export const api = axios.create({
baseURL: 'https://api.example.com',
httpAgent,
httpsAgent,
timeout: 8000
});
HTTP/2 Benefits
HTTP/2 multiplexes many logical streams over a single connection, dramatically reducing latency for parallel requests.
import http2 from 'http2';
import axios from 'axios';
import { Http2Adapter } from '@axioshq/http2-adapter'; // community package
const client = http2.connect('https://api.example.com');
export const api = axios.create({
adapter: Http2Adapter(client)
});
Caveats:
- Server must support HTTP/2.
- Not all middlewares (e.g., certain interceptors) are compatible with streaming bodies in HTTP/2; test thoroughly.
Caching Strategies
1. Browser HTTP Cache
When possible, let the browser cache GET responses. Set proper Cache-Control headers on the server and avoid Cache-Control: no-store unless required.
api.get('/products', {
// no additional config needed – browser handles it
});
2. In‑Memory Cache Wrapper
For data that changes rarely (e.g., configuration, static lookups), implement a simple memoization layer.
const cache = new Map();
export async function cachedGet(url, config = {}) {
const key = `${url}|${JSON.stringify(config.params || {})}`;
if (cache.has(key)) return cache.get(key);
const response = await api.get(url, config);
cache.set(key, response);
// optional TTL eviction
setTimeout(() => cache.delete(key), 5 * 60 * 1000); // 5 minutes
return response;
}
3. Service‑Worker Cache (Progressive Web Apps)
If you are building a PWA, use a Service Worker to intercept Axios requests and serve cached responses. This reduces network traffic dramatically on repeat visits.
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request).then(response => {
const copy = response.clone();
caches.open('api-cache').then(cache => cache.put(event.request, copy));
return response;
});
})
);
}
});
Request Batching & Parallelism
Batching Multiple Calls into One
When an API supports batch endpoints (e.g., /batch that accepts an array of sub‑requests), combine them to cut round‑trip overhead.
async function batchRequests(requests) {
const payload = requests.map(req => ({
method: req.method,
url: req.url,
data: req.data
}));
const { data } = await api.post('/batch', payload);
return data; // array of individual responses
}
// Usage
const results = await batchRequests([
{ method: 'GET', url: '/users/1' },
{ method: 'GET', url: '/orders?user=1' }
]);
Parallel Requests with axios.all
If the backend cannot batch, fire requests in parallel to hide latency. Use Promise.all or Axios’s axios.all helper.
import axios from 'axios';
const [user, orders, settings] = await axios.all([
api.get('/users/1'),
api.get('/orders?user=1'),
api.get('/settings')
]);
Tip: Limit concurrency with a pool library like p-limit to avoid overwhelming the server.
import pLimit from 'p-limit';
const limit = pLimit(5); // at most 5 concurrent requests
const tasks = ids.map(id => limit(() => api.get(`/item/${id}`)));
const results = await Promise.all(tasks);
Streaming Large Responses
For massive payloads (e.g., CSV export), use Node streams to process data chunk‑by‑chunk instead of loading the whole response into memory.
import fs from 'fs';
import { pipeline } from 'stream';
import axios from 'axios';
const response = await api.get('/big-report', { responseType: 'stream' });
pipeline(
response.data,
fs.createWriteStream('report.csv'),
(err) => {
if (err) console.error('Pipeline failed', err);
else console.log('Report saved');
}
);
Streaming reduces memory pressure and can start processing data before the full download finishes.
Interceptors for Efficiency
Interceptors are powerful, but they can become performance liabilities if misused.
1. Avoid Heavy Synchronous Work
Never perform CPU‑intensive tasks (e.g., cryptographic hashing) directly in an interceptor. Offload to a Web Worker (browser) or a child process (Node) if needed.
// Bad: synchronous heavy computation
api.interceptors.request.use(config => {
config.headers['X-Hash'] = computeHash(config.data);
return config;
});
// Good: async worker
api.interceptors.request.use(async config => {
const hash = await crypto.subtle.digest('SHA-256', config.data);
config.headers['X-Hash'] = Buffer.from(hash).toString('hex');
return config;
});
2. Cache Tokens Efficiently
If you need to attach an auth token that may be refreshed, cache it in memory and only refresh when it expires.
let token = null;
let tokenExpires = 0;
api.interceptors.request.use(async (config) => {
const now = Date.now();
if (!token || now >= tokenExpires) {
const resp = await axios.post('/auth/refresh');
token = resp.data.accessToken;
tokenExpires = now + resp.data.expiresIn * 1000 - 60000; // 1 min buffer
}
config.headers.Authorization = `Bearer ${token}`;
return config;
});
3. Short‑Circuit Duplicate Requests
When the same GET request is issued multiple times before the first completes, return the same promise instead of launching duplicate network calls.
const pending = new Map();
api.interceptors.request.use(config => {
if (config.method.toLowerCase() !== 'get') return config;
const key = `${config.url}|${JSON.stringify(config.params || {})}`;
if (pending.has(key)) {
// Attach to existing promise
return pending.get(key);
}
// Mark as pending
const promise = new Promise((resolve, reject) => {
config.resolve = resolve;
config.reject = reject;
});
pending.set(key, promise);
return config;
});
api.interceptors.response.use(
response => {
const key = `${response.config.url}|${JSON.stringify(response.config.params || {})}`;
if (pending.has(key)) {
pending.get(key).resolve(response);
pending.delete(key);
}
return response;
},
error => {
const cfg = error.config;
const key = `${cfg.url}|${JSON.stringify(cfg.params || {})}`;
if (pending.has(key)) {
pending.get(key).reject(error);
pending.delete(key);
}
return Promise.reject(error);
}
);
Now identical GETs share a single network request, reducing load and latency.
Error Handling without Overhead
1. Centralized Error Logger
Instead of attaching a catch to every request, use a response interceptor that logs and re‑throws. This minimizes repetitive code and ensures uniform handling.
api.interceptors.response.use(
res => res,
err => {
// Minimal synchronous work
console.error(`[Axios] ${err.config?.method?.toUpperCase()} ${err.config?.url} → ${err.message}`);
// Optionally transform error shape
return Promise.reject(err);
}
);
2. Retry Logic with Exponential Backoff
Retries can improve perceived reliability, but must be bounded to avoid runaway traffic.
import axiosRetry from 'axios-retry';
axiosRetry(api, {
retries: 3,
retryDelay: (retryCount) => {
return Math.min(1000 * 2 ** retryCount, 8000); // 1s, 2s, 4s capped at 8s
},
retryCondition: (error) => {
// Retry on network errors or 5xx
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
}
});
The axios-retry library handles the heavy lifting, keeping your own code lightweight.
3. Avoid Throwing in Interceptors
Throwing synchronously inside an interceptor aborts the promise chain and can cause uncaught exceptions. Always return a rejected promise.
// Bad
api.interceptors.response.use(res => {
if (res.status !== 200) throw new Error('Bad status');
return res;
});
// Good
api.interceptors.response.use(res => {
if (res.status !== 200) return Promise.reject(new Error('Bad status'));
return res;
});
Axios in Browser vs Node.js
Browser‑Specific Optimizations
| Concern | Technique |
|---|---|
| CORS pre‑flight | Consolidate custom headers; use simple request methods (GET, POST) where possible. |
| Compression | Enable gzip/brotli on the server; browsers automatically decompress. |
| Service‑Worker Cache | See earlier section; reduces network round‑trips for repeat calls. |
| AbortController | Use signal to cancel unnecessary requests, freeing UI thread. |
const controller = new AbortController();
api.get('/slow-endpoint', { signal: controller.signal })
.catch(err => {
if (axios.isCancel(err)) console.log('Request canceled');
});
// Cancel after 2 seconds
setTimeout(() => controller.abort(), 2000);
Node.js‑Specific Optimizations
| Concern | Technique |
|---|---|
| Keep‑Alive | Use httpAgent/httpsAgent with keepAlive:true. |
| HTTP/2 | Adopt a custom adapter (see earlier). |
| DNS Caching | Use third‑party DNS cache like node-dns-cache. |
| Cluster/Worker Threads | Distribute load across processes to avoid event‑loop blockage. |
Advanced Techniques: Custom Adapters & Streaming
1. Writing a Minimalist Adapter
If you need ultra‑low overhead (e.g., in a micro‑frontend that only does GETs), bypass Axios’s default transformation pipeline altogether.
// tinyAdapter.js
import fetch from 'node-fetch';
export default function tinyAdapter(config) {
const { url, method, headers, data, responseType, timeout } = config;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
return fetch(url, {
method,
headers,
body: data,
signal: controller.signal
})
.then(async (res) => {
clearTimeout(id);
const responseData = responseType === 'stream' ? res.body : await res[responseType]();
return {
data: responseData,
status: res.status,
statusText: res.statusText,
headers: Object.fromEntries(res.headers.entries()),
config,
request: null
};
})
.catch(err => {
clearTimeout(id);
return Promise.reject(err);
});
}
Create an Axios instance with this adapter:
import axios from 'axios';
import tinyAdapter from './tinyAdapter.js';
export const fastClient = axios.create({
adapter: tinyAdapter,
timeout: 3000
});
The adapter uses native fetch, which can be faster than http for simple GETs because it skips Axios’s internal buffering.
2. Streaming JSON Parsing
When dealing with massive JSON payloads (e.g., logs), parse incrementally using JSONStream or stream-json.
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
import axios from 'axios';
const response = await api.get('/large-json', { responseType: 'stream' });
response.data
.pipe(parser())
.pipe(streamArray())
.on('data', ({key, value}) => {
// Process each array element as it arrives
console.log('Item', key, value.id);
})
.on('end', () => console.log('All items processed'));
Streaming keeps memory usage constant regardless of payload size.
Testing and Benchmarking
1. Synthetic Benchmarks with autocannon
npm i -g autocannon
autocannon -c 100 -d 30 -p 10 http://localhost:3000/api/users
Measure requests per second (RPS), latency percentiles, and error rates. Compare baseline Axios vs. optimized version.
2. Real‑World Load Testing
Use a tool like k6 to simulate realistic traffic patterns and capture end‑to‑end latency.
// k6 script (test.js)
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '2m', target: 200 },
{ duration: '1m', target: 0 },
],
};
export default function () {
const res = http.get('https://api.example.com/products');
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(0.5);
}
Run with k6 run test.js and observe how connection pooling or HTTP/2 affect throughput.
3. Profiling in Production
Instrument your service with OpenTelemetry or Datadog APM to capture request duration broken down by DNS, TLS handshake, request send, and response receive. Correlate spikes with specific interceptor execution times.
Best‑Practice Checklist
- Reuse a single Axios instance instead of recreating per request.
- Enable keep‑alive agents (Node) or rely on browser connection reuse.
- Turn off unnecessary transformations for binary or streaming payloads.
- Minimize custom headers to avoid CORS pre‑flight overhead.
- Cache GET responses when data is immutable or changes rarely.
- Batch requests if the backend supports it; otherwise use controlled parallelism.
- Write lightweight interceptors—avoid sync heavy work; use async where possible.
- Implement short‑circuiting for duplicate GETs.
- Leverage HTTP/2 for high‑concurrency workloads.
- Use streaming parsers for large JSON or CSV responses.
- Set appropriate timeouts and max content lengths to protect resources.
- Add retry logic with exponential backoff for transient failures.
- Profile continuously: DevTools,
clinic,autocannon, or APM. - Document and version‑control your Axios configuration so the whole team shares the same performance baseline.
By systematically applying these guidelines, you can often reduce request latency by 20‑40 % and increase throughput without scaling hardware.
Conclusion
Axios is beloved for its ergonomics, but in high‑traffic or latency‑sensitive environments, the default configuration can become a hidden performance drain. Understanding the request lifecycle, profiling each stage, and then applying targeted optimizations—from connection reuse and HTTP/2 to smart interceptors and streaming—empowers you to extract every millisecond of speed.
The techniques presented here are not mutually exclusive; they form a toolbox you can mix‑and‑match based on your application’s constraints. Start by measuring the baseline, then iterate: enable keep‑alive, trim headers, introduce caching, and finally consider a custom adapter or HTTP/2 if your service demands it. Continuous monitoring will ensure the gains persist as your codebase evolves.
With a disciplined approach, you’ll keep the developer experience that makes Axios appealing while delivering the rock‑solid performance your users expect.
Resources
Axios Official Documentation – Comprehensive API reference and migration guides.
https://axios-http.com/MDN Web Docs – Fetch API – Useful for understanding low‑level network operations and alternatives.
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_APINode.js HTTP/2 Documentation – Details on using HTTP/2 with custom adapters.
https://nodejs.org/api/http2.htmlaxios-retry GitHub Repository – Simple retry wrapper for Axios.
https://github.com/softonic/axios-retryStream‑JSON Library – Efficient streaming JSON parser for large payloads.
https://github.com/uhop/stream-json
Feel free to explore these links for deeper dives, sample projects, and community discussions. Happy coding!