SingleflightLoading saved progress…

Singleflight

Singleflight collapses concurrent calls that share a key into one execution: while a call for that key is in flight, every other caller receives the same pending promise instead of starting the work again. The moment that promise settles, the key is released, so the next call runs the work afresh.

The classic use is request de-duplication. Ten components mount at once and each asks for the current user; without singleflight that is ten identical network requests. Wrap the fetch in singleflightDedup keyed by 'user' and it fires once, with all ten awaiting the same response. The same guard fits any expensive, idempotent job — refreshing an auth token, warming a cache, rebuilding a report — where a burst of callers would otherwise repeat identical work.

Signature

function singleflightDedup(): SingleFlight;

interface SingleFlight {
  // Runs fn() for `key`. While that promise is pending, any other
  // do(key, ...) returns the SAME promise without calling fn again.
  // On settle (resolve or reject) the key is freed.
  do<T>(key: string, fn: () => Promise<T>): Promise<T>;
}

Examples

const sf = singleflightDedup();
const load = () => fetch('/api/user').then((r) => r.json());

const a = sf.do('user', load);
const b = sf.do('user', load); // 'user' is already in flight

a === b; // true — load() ran once; both callers await one request
const sf = singleflightDedup();

await sf.do('user', load); // runs load(), then frees 'user'
sf.do('user', load);       // 'user' free again → runs load() a second time
sf.do('posts', load);      // different key → its own independent flight

Notes

  • In-flight only — sharing lasts while the promise is pending. Once it settles the key is released; this is de-duplication, not a persistent cache.
  • Both outcomes free the key — the key is removed whether the promise resolves or rejects, so one failure never wedges that key forever.
  • Keys are independent — only same-key concurrent calls collapse; different keys never share a promise.
  • Same reference, not a copy — every deduped caller gets the exact same promise, so they all settle together with one value or one error.
  • fn is a thunk — it takes no arguments and returns a promise; pass any parameters to the work through a closure.
  • You choose the key — de-duplication happens only when callers pass the same key, so identical work must map to the same key string.

FAQ

How is singleflight different from memoization or caching?
Memoization keeps the result forever (or until eviction); singleflight only shares the promise while it is in flight and releases the key the moment it settles, so the next call re-runs the work with fresh data.
What happens if the shared call fails?
The one rejected promise is handed to every concurrent caller, and the key is removed on rejection just like on success, so a later call gets a clean retry instead of a cached failure.
Does singleflight debounce or cancel calls?
No. It never delays or drops a call. The first caller for a free key runs immediately; it only prevents duplicate concurrent executions of the same key while one is already running.
Loading editor…