Cache Stampede Prevention with SingleFlight in Python

Implements a SingleFlight pattern in Python to deduplicate concurrent cache-miss computations and prevent cache stampede.

Medium Python 3.9+ Aug 9, 2026 Caching & Redis 15 views 0 copies

Python code

59 lines
Python 3.9+
import 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

stdout
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

  1. Use asyncio with asyncio.Lock and Futures for async single-flight patterns.
  2. 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

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.