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.
Python code
56 linesimport 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
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
- Use asyncio.Lock and asyncio.Event for single-threaded async coalescing
- Replace threading.Lock with a Redis distributed lock for multi-process coalescing
- 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
More from Caching & Redis
- Cache Asides in Python with a Read-Through Loader easy
- Cache Data in Redis with Python easy
- Cache Penetration Null Object Mock in Python medium
- Cache Stampede Prevention with SingleFlight in Python medium
- Cache Warming with Python: Preload Hot Keys easy
- Consistent Hashing Cache Shard in Python medium
Keep learning
Related tutorials and quizzes for this topic.