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:
- Generate a unique key based on the request URL and query parameters.
- Check if a request with that key is already pending.
- If pending, return the existing promise instead of making a new network call.
- 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
- Error Handling: Always ensure that failed requests are purged from your pending cache immediately so that subsequent retries are not blocked by a rejected promise.
- Cache Key Granularity: Include request headers
(like
Authorization) in your cache key if different users or authentication states share the same runtime. - Non-GET Requests: Never apply in-flight
deduplication to
POST,PUT,PATCH, orDELETErequests, as these methods represent non-idempotent mutations.