python 123 lines · 3 tabs

In-Memory TTL Cache for FastAPI Endpoints With a Cache-Key Builder Dependency

Shared by codesnips Sep 2026
3 tabs
import asyncio
import time
from dataclasses import dataclass
from typing import Any, Dict


class _Miss:
    def __repr__(self) -> str:
        return "<MISS>"


MISS = _Miss()


@dataclass
class _Entry:
    value: Any
    expires_at: float


class TTLCache:
    def __init__(self, purge_interval: float = 60.0) -> None:
        self._data: Dict[str, _Entry] = {}
        self._lock = asyncio.Lock()
        self._purge_interval = purge_interval
        self._last_purge = time.monotonic()

    async def get(self, key: str) -> Any:
        async with self._lock:
            entry = self._data.get(key)
            if entry is None:
                return MISS
            if entry.expires_at <= time.monotonic():
                self._data.pop(key, None)
                return MISS
            return entry.value

    async def set(self, key: str, value: Any, ttl: float) -> None:
        async with self._lock:
            self._data[key] = _Entry(value, time.monotonic() + ttl)
            self._maybe_purge()

    async def clear(self) -> None:
        async with self._lock:
            self._data.clear()

    def _maybe_purge(self) -> None:
        now = time.monotonic()
        if now - self._last_purge < self._purge_interval:
            return
        self._last_purge = now
        expired = [k for k, e in self._data.items() if e.expires_at <= now]
        for k in expired:
            self._data.pop(k, None)
3 files · python Explain with highlit

This snippet shows how to add a small in-memory TTL cache to expensive FastAPI endpoints without pulling in Redis or another external store. It is useful when an endpoint recomputes the same result for many callers within a short window — for example an aggregated report or a slow upstream call — and the data can tolerate being a few seconds stale.

The TTLCache tab implements the store itself. Entries are kept in a plain dict keyed by a string, each holding the value and a monotonic expires_at timestamp computed from time.monotonic() so wall-clock jumps never corrupt expiry. Access is guarded by an asyncio.Lock because FastAPI serves coroutines on a single event loop and concurrent requests can interleave; the lock keeps get, set, and the opportunistic _purge sweep consistent. get returns a sentinel MISS object rather than None so that a legitimately cached None value is distinguishable from a miss.

The cache dependency tab is where the FastAPI idiom lives. cache_key_builder is a dependency that reads the request path and sorted query parameters and returns a stable string key — sorting matters so ?a=1&b=2 and ?b=2&a=1 collapse to one entry. cached is a decorator factory that wraps an endpoint coroutine: it resolves the injected key, checks the shared TTLCache, and either returns the hit or awaits the real handler and stores the result. The single-flight asyncio.Lock per key prevents a thundering herd where many simultaneous misses all recompute the same value; only the first computes while the rest await and read the freshly cached result.

The reports router tab wires it together on a realistic endpoint. expensive_report simulates a slow aggregation and is decorated with @cached(ttl=30), and the key dependency is declared with Depends, so the framework injects it exactly as it would any other dependency. A companion DELETE route calls cache.clear() to support manual invalidation.

The trade-offs are worth noting: an in-process cache is not shared across workers, so with several Uvicorn workers each holds its own copy and hit rates drop. It also grows unbounded unless purged, which the periodic sweep and TTL bound in practice. For a single-process service or a best-effort speedup it is a lightweight, dependency-free win; for coherence across a fleet a shared cache is the right tool.


Related snips

Share this code

Here's the card — post it anywhere.

In-Memory TTL Cache for FastAPI Endpoints With a Cache-Key Builder Dependency — share card
Link copied