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

  1. Understanding Axios Basics
  2. Why Performance Matters
  3. Profiling Axios Requests
  4. Optimizing Request Configuration
  5. Connection Reuse & HTTP/2
  6. Caching Strategies
  7. Request Batching & Parallelism
  8. Interceptors for Efficiency
  9. Error Handling without Overhead
  10. Axios in Browser vs Node.js
  11. Advanced Techniques: Custom Adapters & Streaming
  12. Testing and Benchmarking
  13. Best‑Practice Checklist
  14. Conclusion
  15. Resources

Understanding Axios Basics

Before we can optimize, we need a clear mental model of how Axios works under the hood.

Core Request Flow

  1. Configuration Merge – The axios(config) call merges the user‑provided config with defaults (axios.defaults).
  2. 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/https modules).
  3. Interceptor Chain – Request interceptors run first, followed by the adapter, then response interceptors.
  4. TransformationtransformRequest and transformResponse functions (default JSON handling).
  5. 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:

EnvironmentToolWhat It Shows
BrowserChrome DevTools (Network tab)Timing breakdown (DNS, SSL, Request/Response)
Node.jsclinic doctor or node --profCPU usage, async call stacks
Bothaxios-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-age for 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

ConcernTechnique
CORS pre‑flightConsolidate custom headers; use simple request methods (GET, POST) where possible.
CompressionEnable gzip/brotli on the server; browsers automatically decompress.
Service‑Worker CacheSee earlier section; reduces network round‑trips for repeat calls.
AbortControllerUse 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

ConcernTechnique
Keep‑AliveUse httpAgent/httpsAgent with keepAlive:true.
HTTP/2Adopt a custom adapter (see earlier).
DNS CachingUse third‑party DNS cache like node-dns-cache.
Cluster/Worker ThreadsDistribute 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

Feel free to explore these links for deeper dives, sample projects, and community discussions. Happy coding!