Skip to main content

Command Palette

Search for a command to run...

Request Deduplication: Prevent Duplicate Calls

Learn: Request Deduplication: Prevent Duplicate Calls

Updated
5 min readView as Markdown
T

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:

  1. Identify duplicate requests using a cache key (URL, parameters, etc.)
  2. Store pending promises instead of results
  3. Share the same promise across duplicate requests
  4. Resolve once when the operation completes
  5. 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.