python 98 lines · 3 tabs

Coalesce Duplicate In-Flight Requests with an Asyncio Single-Flight Cache

Shared by codesnips Aug 2026
3 tabs
import asyncio
from typing import Awaitable, Callable, Dict, TypeVar

T = TypeVar("T")


class SingleFlight:
    def __init__(self) -> None:
        self._inflight: Dict[str, asyncio.Future] = {}

    async def do(self, key: str, factory: Callable[[], Awaitable[T]]) -> T:
        existing = self._inflight.get(key)
        if existing is not None:
            return await existing

        loop = asyncio.get_event_loop()
        future: asyncio.Future = loop.create_future()
        self._inflight[key] = future

        try:
            result = await factory()
        except BaseException as exc:
            if not future.done():
                future.set_exception(exc)
            raise
        else:
            if not future.done():
                future.set_result(result)
            return result
        finally:
            self._inflight.pop(key, None)

    def inflight_count(self) -> int:
        return len(self._inflight)
3 files · python Explain with highlit

This snippet implements the single-flight pattern in Python's asyncio: when many callers ask for the same key at the same time, only one underlying computation runs and everyone else waits on and shares its result. It solves the thundering-herd problem, where a cache miss on a hot key would otherwise fire N identical database or upstream calls simultaneously.

In singleflight.py, the core idea is that the first caller for a key creates a shared asyncio.Future and stores it in self._inflight under that key, then runs the coroutine factory. Later callers that arrive while the computation is still running find the existing future and simply await it — they never invoke the factory. Because asyncio runs on a single thread and there are no await points between the dictionary lookup and the insertion, the check-and-insert in do is atomic without an explicit lock; a Lock is only needed if the code path yields between those steps. The finally block removes the key from _inflight so the next request after completion starts fresh, and both success and failure are propagated to every waiter via set_result and set_exception. Sharing the exception matters: all coalesced callers see the same failure rather than one succeeding while the rest hang.

The key trade-off is that a shared future means a shared fate — one slow or failing computation blocks all waiters, so this pattern coalesces identical work but does not add retries or per-caller timeouts. It also deduplicates only concurrent requests, not sequential ones; pairing it with a TTL cache handles that.

In price_service.py, SingleFlight guards an expensive upstream fetch. get_price wraps the real call in a factory closure passed to flight.do, so a burst of requests for the same symbol collapses into one HTTP round trip. warm demonstrates that concurrent get_price calls for one symbol trigger exactly one _fetch_upstream.

In routes.py, a FastAPI endpoint calls the shared service instance, meaning a spike of clients hitting /price/{symbol} naturally coalesces at the service layer. This is where the pattern shines: read-heavy endpoints with hot keys, cache stampede protection, and any idempotent-but-expensive lookup where duplicate concurrent work is pure waste.


Related snips

Share this code

Here's the card — post it anywhere.

Coalesce Duplicate In-Flight Requests with an Asyncio Single-Flight Cache — share card
Link copied