javascript 95 lines · 3 tabs

Deduplicate Concurrent Async Calls With a Promise Cache in Node.js

Shared by codesnips Sep 2026
3 tabs
'use strict';

function dedupeAsync(producer, keyFn) {
  const pending = new Map();
  const resolveKey = keyFn || ((...args) => JSON.stringify(args));

  return function deduped(...args) {
    const key = resolveKey(...args);

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

    const promise = Promise.resolve()
      .then(() => producer(...args))
      .finally(() => {
        pending.delete(key);
      });

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

module.exports = { dedupeAsync };
3 files · javascript Explain with highlit

When several parts of a Node.js service ask for the same expensive resource at the same moment — a config fetch, a user lookup, a token refresh — each call independently fires off the underlying work. This is the thundering-herd problem: N concurrent callers cause N identical round trips, hammering a downstream API or database for a value that is identical across all of them. The fix is a promise cache: cache the in-flight Promise itself, keyed by the request, so concurrent callers share one execution and one result.

The dedupe helper implements this idea in a few lines. dedupeAsync wraps a producer function and holds a Map of key to pending promise. On the first call for a key it invokes the producer, stores the returned promise, and — crucially — attaches a .finally that deletes the entry once settled. Every subsequent call that arrives while the promise is still pending gets the same promise back instead of starting new work. Because the entry is cleared on settle, this is deduplication of concurrent calls, not a long-lived value cache; the next call after resolution starts fresh work. The keyFn lets callers derive a stable string key from arguments, and errors propagate to all sharers since a rejected promise is what gets cached and then evicted.

The UserLoader tab shows a realistic consumer. fetchUser performs a raw HTTP GET, and wrapping it with dedupeAsync keyed by user id means a burst of requests for the same id collapses into a single upstream call. The warm method demonstrates that even a Promise.all fan-out over duplicate ids issues each unique request only once.

The dedupe.test tab pins the behavior down: it counts producer invocations while firing many concurrent calls and asserts the producer ran once, that all callers resolve to the same value, and that a later call re-invokes the producer after the cache slot is freed. A key pitfall this design avoids is caching rejections forever — because eviction happens in finally, a transient failure does not poison future calls. It also stays memory-safe by never retaining settled promises. This pattern is worth reaching for whenever idempotent async work can be triggered redundantly under load.


Related snips

Share this code

Here's the card — post it anywhere.

Deduplicate Concurrent Async Calls With a Promise Cache in Node.js — share card
Link copied