Cache Stampede Prevention with SingleFlight in Python
Implements a SingleFlight pattern in Python to deduplicate concurrent cache-miss computations and prevent cache stampede.
Python code
59 linesimport threading
import time
from functools import wraps
class SingleFlight:
def __init__(self):
self._lock = threading.Lock()
self._inflight = None
def do(self, key, fn):
with self._lock:
if self._inflight is not None:
return self._inflight[1]
result_holder = []
done = threading.Event()
def runner():
try:
result_holder.append(fn())
finally:
done.set()
with self._lock:
self._inflight = None
self._inflight = (key, done)
t = threading.Thread(target=runner)
t.start()
done.wait()
return result_holder[0]
def mock_cache_and_stampede():
cache = {"value": None}
sf = SingleFlight()
def expensive_compute():
time.sleep(0.2)
return 42
def get_value():
if cache["value"] is None:
print("Cache miss, computing...")
value = sf.do("data", expensive_compute)
cache["value"] = value
return cache["value"]
threads = [threading.Thread(target=lambda: get_value()) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Final cached value: {cache['value']}")
print(f"Only one expensive computation occurred (singleflight).")
if __name__ == "__main__":
mock_cache_and_stampede()
Output
Cache miss, computing...
Final cached value: 42
Only one expensive computation occurred (singleflight).
How it works
The SingleFlight.do method serializes concurrent calls for the same key so only one thread runs the expensive function. A shared lock guards the in-flight state, and a threading.Event signals when the computation completes. This ensures all waiting threads receive the same result without redundant work. Once the function returns, the in-flight entry is cleared, allowing future calls to proceed normally. This pattern is essential for protecting caches and databases from traffic spikes.
Common mistakes
- Forgetting to clear the in-flight state in a finally block, leaving the lock stuck.
- Using a separate lock per key, which can lock up under heavy key variety.
- Not handling exceptions inside the runner, which can leave threads hanging indefinitely.
- Assuming the lock only protects the in-flight check and missing the reset step.
Variations
- Use asyncio with asyncio.Lock and Futures for async single-flight patterns.
- Integrate with Redis SETNX or Lua scripts for distributed single-flight across processes.
Real-world use cases
- Preventing simultaneous expensive database queries when multiple users request the same uncached data.
- Deduplicating identical API requests in web backends to reduce upstream load.
- Stopping concurrent recomputation of shared configuration or heavy ML model calls.
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 Warming with Python: Preload Hot Keys easy
- Coalescing duplicate in-flight requests: one shared result for concurrent callers hard
- Consistent Hashing Cache Shard in Python medium
Keep learning
Related tutorials and quizzes for this topic.