Coalescing duplicate in-flight requests: one shared result for concurrent callers

Runs identical concurrent requests through a single shared call, caching the result while it's in flight and returning the same value to all callers.

Hard Python 3.9+ Aug 9, 2026 Caching & Redis 14 views 0 copies

Python code

56 lines
Python 3.9+
import time
import threading
from collections import defaultdict


class CoalescingExecutor:
    def __init__(self):
        self._locks = defaultdict(threading.Lock)
        self._in_flight = {}

    def execute(self, key, func):
        with self._locks[key]:
            if key in self._in_flight:
                return self._in_flight[key]

        future = threading.Event()
        result_holder = {}

        def run():
            try:
                result_holder["value"] = func()
            finally:
                future.set()
                with self._locks[key]:
                    self._in_flight.pop(key, None)

        with self._locks[key]:
            if key in self._in_flight:
                return self._in_flight[key]
            self._in_flight[key] = future
            threading.Thread(target=run, daemon=True).start()

        future.wait()
        return result_holder["value"]


if __name__ == "__main__":
    executor = CoalescingExecutor()
    calls = []

    def slow_operation():
        calls.append("start")
        time.sleep(0.1)
        calls.append("end")
        return 42

    results = []
    threads = [threading.Thread(target=lambda: results.append(executor.execute("key", slow_operation))) for _ in range(5)]

    for t in threads:
        t.start()
    for t in threads:
        t.join()

    print("results:", sorted(results))
    print("calls:", calls)

Output

stdout
results: [42, 42, 42, 42, 42]
calls: ['start', 'end']

How it works

The executor uses a defaultdict of locks per key to serialize access to the in-flight state. A first caller checks whether a future already exists and if not, starts a background thread that runs the function. Other threads block on the same future and receive the same result_holder value once it's set. The finally block cleans up the in-flight entry so future calls re-execute. This pattern collapses many identical simultaneous requests into one expensive operation, reducing load on downstream services.

Common mistakes

  • Not using a per-key lock, causing race conditions on the in-flight dict
  • Storing the result directly instead of a shared mutable holder that all waiters can read
  • Forgetting to remove the in-flight entry on error or exception, leaking stale futures
  • Creating the lock with a lambda so each key gets its own lock

Variations

  1. Use asyncio.Lock and asyncio.Event for single-threaded async coalescing
  2. Replace threading.Lock with a Redis distributed lock for multi-process coalescing
  3. Add a TTL to the cached result to also serve recently completed calls

Real-world use cases

  • Frontend servers receiving the same cache miss simultaneously all share one upstream database query.
  • A microservice deduplicates identical requests from many replicas into a single downstream API call.
  • Background workers coalesce redundant file processing tasks triggered concurrently by multiple events.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Caching & Redis

Related tutorials and quizzes for this topic.