Async, Concurrency and Startup
async functions
Decorate an async def function as you would any other. Its result is
cached, not its coroutine:
@cache_results(timeout=300)
async def get_forecast(city: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(FORECAST_URL, params={"city": city})
return response.json()
forecast = await get_forecast("Lisbon")
The cache is read and written without blocking the event loop. If your cache
backend implements Django’s aget() and aset() itself, they are used.
Otherwise, the ordinary get() and set() run in a worker thread. Django’s
default aget() and aset() aren’t used, because they run every call on
one shared thread. django-redis has no async methods of its own, so with Redis
the worker threads are used.
invalidate() is synchronous for async functions too. From async code, call
it through asgiref.sync.sync_to_async().
Concurrent misses
When several threads of one process call a function with the same arguments while the result isn’t cached, the first one computes it, and the others wait for it and read it from the cache, instead of all computing it.
This doesn’t extend to other processes or servers, which compute the result independently, or to async callers, which don’t wait.
A function that calls itself with the same arguments doesn’t deadlock: the lock is re-entrant.
During startup
Decorators are applied when their module is imported, which often happens
while Django is still starting. Touching Django’s cache framework that early
can leave it on the wrong backend, so cache-results imports
django.core.cache lazily, and uses the cache only once Django’s apps are
ready. A decorated function called before then simply runs uncached.