Handling Concurrent Axios GET Requests with Caching

When multiple UI components mount simultaneously, they often trigger identical GET requests to the same endpoint, leading to redundant network traffic and server strain. This article explores the most effective strategy for resolving this issue in Axios: combining in-flight request deduplication with standard response caching. By sharing a single active promise among concurrent calls and persisting the resolved response, you ensure that duplicate network operations are eliminated entirely.

The Core Strategy: In-Flight Promise Deduplication

Standard HTTP caching (like storing responses in localStorage or memory) only works after a request has finished. If three identical GET requests fire within milliseconds of each other, all three hit the network before any response can be cached.

To fix this, you must track in-flight promises. When a GET request is initiated:

  1. Generate a unique key based on the request URL and query parameters.
  2. Check if a request with that key is already pending.
  3. If pending, return the existing promise instead of making a new network call.
  4. If not pending, initiate the request, store the promise in a Map, and remove it from the Map once settled.

Implementation: Custom Axios In-Flight Cache

You can implement this pattern natively using Axios interceptors and a simple Map.

import axios from 'axios';

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

// Store pending requests
const pendingRequests = new Map();

// Helper to generate a unique cache key
const generateRequestKey = (config) => {
  const { method, url, params } = config;
  return [method, url, JSON.stringify(params || {})].join('&');
};

apiClient.interceptors.request.use((config) => {
  // Only deduplicate GET requests
  if (config.method?.toLowerCase() === 'get') {
    const requestKey = generateRequestKey(config);

    if (pendingRequests.has(requestKey)) {
      // Return the in-flight promise to prevent a duplicate network call
      config.adapter = () => pendingRequests.get(requestKey);
    }
  }
  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);
  }
);

// Wrapper to track the actual promise
export const getCachedData = (url, config = {}) => {
  const requestConfig = { ...config, method: 'get', url };
  const requestKey = generateRequestKey(requestConfig);

  if (!pendingRequests.has(requestKey)) {
    const requestPromise = apiClient(requestConfig);
    pendingRequests.set(requestKey, requestPromise);
    return requestPromise;
  }

  return pendingRequests.get(requestKey);
};

Using Pre-Built Libraries

If you prefer a production-tested library that handles in-flight deduplication alongside standard TTL-based caching, axios-cache-interceptor is the recommended solution.

It natively merges identical concurrent requests without extra configuration:

import axios from 'axios';
import { setupCache } from 'axios-cache-interceptor';

const instance = axios.create();
const cachedAxios = setupCache(instance, {
  // Automatically handles concurrent requests and caches resolved responses
  ttl: 1000 * 60 * 5, // 5 minutes
});

// Triggering these simultaneously results in only one network request:
Promise.all([
  cachedAxios.get('/users/profile'),
  cachedAxios.get('/users/profile'),
  cachedAxios.get('/users/profile'),
]);

Key Considerations