How to Implement Request Deduplication in Axios

Request deduplication is a technique used to prevent duplicate, simultaneous HTTP requests from hitting the server by sharing a single in-flight response among identical callers. This article explains the step-by-step process of implementing request deduplication in the Axios HTTP client using a cache of pending promises and Axios interceptors.

1. Generating a Unique Request Key

To identify duplicate requests, you must create a unique identifier based on the request configuration. This key typically combines the HTTP method, the URL, and serialized versions of the query parameters and request body.

function generateRequestKey(config) {
  const { method, url, params, data } = config;
  return [
    method?.toLowerCase(),
    url,
    JSON.stringify(params || {}),
    JSON.stringify(data || {})
  ].join('&');
}

2. Maintaining a Pending Requests Map

Create a Map instance to store currently in-flight requests. This map associates the unique request key with the pending Axios promise.

const pendingRequests = new Map();

3. Implementing Request and Response Interceptors

You can intercept outgoing requests to check if an identical request is already pending. If it exists, return the stored promise instead of sending a new network request. When the request resolves or rejects, remove it from the map.

import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
});

// Custom adapter to return cached in-flight promises
const defaultAdapter = apiClient.defaults.adapter;

apiClient.interceptors.request.use((config) => {
  const requestKey = generateRequestKey(config);

  if (pendingRequests.has(requestKey)) {
    // Attach the existing promise to the config to bypass the network call
    config.adapter = () => pendingRequests.get(requestKey);
  } else {
    // Create the network request promise using the default adapter
    const requestPromise = defaultAdapter(config);
    pendingRequests.set(requestKey, requestPromise);
  }

  return config;
});

apiClient.interceptors.response.use(
  (response) => {
    const requestKey = generateRequestKey(response.config);
    pendingRequests.delete(requestKey);
    return response;
  },
  (error) => {
    if (error.config) {
      const requestKey = generateRequestKey(error.config);
      pendingRequests.delete(requestKey);
    }
    return Promise.reject(error);
  }
);

4. Alternative Wrapper Approach

Instead of overriding the adapter, you can wrap Axios calls in a helper function. This approach is often simpler to maintain in modular codebases:

export function deduplicatedRequest(config) {
  const key = generateRequestKey(config);

  if (pendingRequests.has(key)) {
    return pendingRequests.get(key);
  }

  const promise = apiClient(config).finally(() => {
    pendingRequests.delete(key);
  });

  pendingRequests.set(key, promise);
  return promise;
}

5. Handling Edge Cases