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:

  1. Open DevTools: Navigate to the Memory panel in Chrome DevTools.
  2. Take a Baseline Snapshot: Select Heap snapshot and click Take snapshot.
  3. 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).
  4. Take a Second Snapshot: Capture another heap snapshot.
  5. Compare Snapshots:
    • Change the view dropdown from Summary to Comparison.
    • Filter by InterceptorManager or Axios.
    • Look at the # Delta column. If the number of handlers or functions attached to Axios is positive and increasing with every iteration, you have a leak.
  6. 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