Monitor Memory Leaks in Axios Interceptors
Axios interceptors are powerful tools for transforming requests and handling responses globally, but failing to remove them when they are no longer needed creates memory leaks. This guide explains how lingering interceptor closures retain references to large objects, demonstrates how to monitor and detect these leaks using browser and Node.js profiling tools, and outlines programmatic patterns to track and prevent runaway interceptor registration.
How Axios Interceptors Cause Memory Leaks
Axios stores interceptors in an internal array called
handlers inside an InterceptorManager
instance. When you register an interceptor using
axios.interceptors.request.use() or
axios.interceptors.response.use(), Axios pushes your
callback functions into this array.
If you register interceptors inside short-lived components, dynamic
service factories, or repeated lifecycle events without cleaning them
up, the handlers array grows indefinitely. Because
interceptors often form closures over surrounding scopes—such as
component state, authentication tokens, or entire UI tree references—the
garbage collector cannot reclaim that memory.
Programmatic Monitoring: Inspecting Handler Counts
The fastest way to detect a leak programmatically during testing or
runtime is to inspect the internal handlers array length
directly on the Axios instance:
import axios from 'axios';
const api = axios.create();
function checkInterceptorCount(instance) {
const requestCount = instance.interceptors.request.handlers.filter(Boolean).length;
const responseCount = instance.interceptors.response.handlers.filter(Boolean).length;
console.log(`Active Request Interceptors: ${requestCount}`);
console.log(`Active Response Interceptors: ${responseCount}`);
if (requestCount > 10 || responseCount > 10) {
console.warn('Potential memory leak detected: High interceptor count.');
}
}Note: When an interceptor is ejected via eject(),
Axios sets its slot in the array to null. Filtering by
Boolean ensures you only count active, non-ejected
handlers.
Monitoring Memory Leaks in the Browser (Chrome DevTools)
To visually confirm and trace interceptor memory leaks in frontend applications:
- Open DevTools: Navigate to the Memory panel in Chrome DevTools.
- Take a Baseline Snapshot: Select Heap snapshot and click Take snapshot.
- Trigger the Suspected Workflow: Perform the UI action that repeatedly mounts components or issues requests (e.g., navigating back and forth between routes 10 times).
- Take a Second Snapshot: Capture another heap snapshot.
- Compare Snapshots:
- Change the view dropdown from Summary to Comparison.
- Filter by
InterceptorManagerorAxios. - Look at the # Delta column. If the number of
handlersor functions attached to Axios is positive and increasing with every iteration, you have a leak.
- Inspect the Retainer Tree: Click on the retained object to inspect its retainers. Check the bottom pane to see which closure or component holds the reference.
Monitoring Memory Leaks in Node.js
For backend services, use heap analysis and metrics tracking to catch accumulating interceptors.
1. Tracking Heap Growth Over Time
Monitor baseline memory metrics using
process.memoryUsage() to identify upward trends in heap
usage:
setInterval(() => {
const usage = process.memoryUsage();
console.log({
heapUsedMB: (usage.heapUsed / 1024 / 1024).toFixed(2),
heapTotalMB: (usage.heapTotal / 1024 / 1024).toFixed(2),
});
}, 10000);2. Taking Heap
Snapshots via the v8 Module
Generate a snapshot when memory thresholds are breached:
import v8 from 'v8';
import fs from 'fs';
function takeSnapshot(filename) {
const stream = v8.getHeapSnapshot();
const fileStream = fs.createWriteStream(filename);
stream.pipe(fileStream);
}Load the resulting .heapsnapshot file into Chrome
DevTools (Memory panel -> Load) and search for
InterceptorManager to verify retained closures.
How to Fix Lingering Interceptors
To stop memory leaks, always store the interceptor ID returned by
.use() and pass it to .eject() when the
consuming lifecycle ends.
Example: React Cleanup
with useEffect
import { useEffect } from 'react';
import axiosInstance from './apiClient';
function UserProfile({ token }) {
useEffect(() => {
// 1. Attach interceptor and store the ID
const interceptorId = axiosInstance.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${token}`;
return config;
});
// 2. Clean up on unmount or token change
return () => {
axiosInstance.interceptors.request.eject(interceptorId);
};
}, [token]);
return <div>Profile Component</div>;
}Best Practices
- Define Interceptors Globally Once: Attach global authentication and logging interceptors at application startup rather than inside component lifecycles or request handlers.
- Use Dedicated Instances: If a specific module
requires unique headers, use
axios.create()to create a local instance rather than mutating a shared global instance.