Request Deduplication: Prevent Duplicate Calls
Learn: Request Deduplication: Prevent Duplicate Calls
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Request Deduplication: Prevent Duplicate Calls with Caching
Problem
When multiple requests for the same resource are made simultaneously or in quick succession, your application may:
- Execute the same expensive operation multiple times
- Hit rate limits on external APIs
- Waste database queries and computational resources
- Create race conditions and inconsistent state
- Degrade performance and increase latency
Example: Three users request user profile data simultaneously → three identical database queries execute instead of one.
Solution
Implement request deduplication by caching pending requests. When a request is already in-flight, subsequent identical requests wait for the same result rather than triggering new operations.
Key Principles:
- Identify duplicate requests using a cache key (URL, parameters, etc.)
- Store pending promises instead of results
- Share the same promise across duplicate requests
- Resolve once when the operation completes
- Clean up after resolution to prevent memory leaks
Code
Basic Implementation
class RequestDeduplicator {
constructor() {
this.pendingRequests = new Map();
}
async execute(key, requestFn) {
// Return existing pending request if available
if (this.pendingRequests.has(key)) {
return this.pendingRequests.get(key);
}
// Create new promise for this request
const promise = requestFn()
.then(result => {
this.pendingRequests.delete(key);
return result;
})
.catch(error => {
this.pendingRequests.delete(key);
throw error;
});
// Store pending promise
this.pendingRequests.set(key, promise);
return promise;
}
}
// Usage
const deduplicator = new RequestDeduplicator();
async function fetchUserProfile(userId) {
return deduplicator.execute(`user:${userId}`, async () => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
});
}
// All three calls share the same request
Promise.all([
fetchUserProfile(1),
fetchUserProfile(1),
fetchUserProfile(1)
]);
Advanced Implementation with TTL
class SmartRequestDeduplicator {
constructor(options = {}) {
this.pendingRequests = new Map();
this.cache = new Map();
this.ttl = options.ttl || 0; // milliseconds, 0 = no caching
this.maxRetries = options.maxRetries || 0;
}
async execute(key, requestFn, options = {}) {
const cacheKey = key;
const now = Date.now();
// Check if result is cached and still valid
if (this.cache.has(cacheKey)) {
const { result, timestamp } = this.cache.get(cacheKey);
if (now - timestamp < this.ttl) {
return result;
}
this.cache.delete(cacheKey);
}
// Return pending request if exists
if (this.pendingRequests.has(cacheKey)) {
return this.pendingRequests.get(cacheKey);
}
// Execute with retry logic
const promise = this._executeWithRetry(
requestFn,
options.retries ?? this.maxRetries
)
.then(result => {
this.pendingRequests.delete(cacheKey);
// Cache result if TTL is set
if (this.ttl > 0) {
this.cache.set(cacheKey, { result, timestamp: Date.now() });
}
return result;
})
.catch(error => {
this.pendingRequests.delete(cacheKey);
throw error;
});
this.pendingRequests.set(cacheKey, promise);
return promise;
}
async _executeWithRetry(requestFn, retries) {
let lastError;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await requestFn();
} catch (error) {
lastError = error;
if (attempt < retries) {
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 100)
);
}
}
}
throw lastError;
}
invalidate(key) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
this.pendingRequests.clear();
}
getStats() {
return {
pendingRequests: this.pendingRequests.size,
cachedResults: this.cache.size
};
}
}
// Usage
const deduplicator = new SmartRequestDeduplicator({
ttl: 5000, // Cache for 5 seconds
maxRetries: 2
});
async function fetchData(id) {
return deduplicator.execute(`data:${id}`, async () => {
console.log(`Fetching data for ${id}`);
const response = await fetch(`/api/data/${id}`);
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
});
}
// Concurrent requests - only one actual fetch
await Promise.all([
fetchData(1),
fetchData(1),
fetchData(1)
]); // Logs "Fetching data for 1" once
// Subsequent request within TTL uses cache
await fetchData(1); // No log - uses cache
// After TTL expires
setTimeout(() => fetchData(1), 6000); // Logs again
React Hook Implementation
function useDeduplicatedFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const deduplicatorRef = useRef(new SmartRequestDeduplicator({
ttl: options.cacheTtl || 0
}));
useEffect(() => {
setLoading(true);
deduplicatorRef.current
.execute(url, async () => {
const response = await fetch(url);
if (!response.ok) throw new Error('Fetch failed');
return response.json();
})
.then(result => {
setData(result);
setError(null);
})
.catch(err => {
setError(err);
setData(null);
})
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useDeduplicatedFetch(
`/api/users/${userId}`,
{ cacheTtl: 5000 }
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
Database Query Deduplication
class DatabaseQueryDeduplicator {
constructor(db) {
this.db = db;
this.pendingQueries = new Map();
}
async query(sql, params = []) {
const key = this._generateKey(sql, params);
if (this.pendingQueries.has(key)) {
return this.pendingQueries.get(key);
}
const promise = this.db.query(sql, params)
.then(result => {
this.pendingQueries.delete(key);
return result;
})
.catch(error => {
this.pendingQueries.delete(key);
throw error;
});
this.pendingQueries.set(key, promise);
return promise;
}
_generateKey(sql, params) {
return `${sql}:${JSON.stringify(params)}`;
}
}
// Usage
const dbDedup = new DatabaseQueryDeduplicator(database);
// Multiple concurrent requests
const results = await Promise.all([
dbDedup.query('SELECT * FROM users WHERE id = ?', [1]),
dbDedup.query('SELECT * FROM users WHERE id = ?', [1]),
dbDedup.query('SELECT * FROM users WHERE id = ?', [1])
]); // Only one actual database query
Tips
1. Choose Appropriate Cache Keys
// Good: Includes all relevant parameters
const key = `${method}:${url}:${JSON.stringify(params)}`;
// Bad: Too generic
const key = 'user-data';
2. Handle Errors Carefully
// Errors should not be cached; allow retry
.catch(error => {
this.pendingRequests.delete(key);
throw error; // Don't cache failures
});
3. Implement Timeout Protection
async execute(key, requestFn, timeout = 30000) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
);
return Promise.race([
this._executeRequest(key, requestFn),
timeoutPromise
]);
}
4. Monitor Memory Usage
// Implement size limits
if (this.cache.size > this.maxCacheSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey); // LRU eviction
}
5. Provide Invalidation Mechanism
// Allow manual cache invalidation
invalidate(pattern) {
for (const [key] of this.cache) {
if (key.match(pattern)) {
this.cache.delete(key);
}
}
}
6. Use for External APIs
// Prevent rate limiting
const apiDedup = new SmartRequestDeduplicator({ ttl: 1000 });
async function callExternalAPI(endpoint) {
return apiDedup.execute(endpoint, async () => {
return fetch(`https://api.example.com${endpoint}`).then(r => r.json());
});
}
7. Combine with Circuit Breaker
// Prevent cascading failures
if (failureCount > threshold) {
this.pendingRequests.delete(key);
throw new Error('Circuit breaker open');
}
Key Takeaway: Request deduplication dramatically reduces redundant operations by sharing pending requests. Combine with TTL-based caching for optimal performance in high-concurrency scenarios.