skip to content

In an async Django view, what do cache.aget() and cache.aset() actually do with the built-in cache backends?

level: middleimportance: nice to knowfreq 22%

answer

  1. a-prefixed twins of every method
  2. no native async backend yet
  3. sync_to_async under the hood
  4. DatabaseCache in async code

basics

~20 s

Django's aget() and aset() are async wrappers: with every built-in backend they run the synchronous method through sync_to_async in a worker thread. They keep the event loop unblocked, but the cache I/O itself is not asynchronous.

solid answer

~40 s

`BaseCache` defines an `a`-prefixed twin for each method (`aget`, `aset`, `aadd`, `aget_or_set`, `adelete_many`, `aincr`, `atouch` and so on) with the same arguments. None of the built-in backends overrides them: each twin calls the synchronous method via `asgiref`'s `sync_to_async(..., thread_sensitive=True)`, and the docs state that Django does not yet support asynchronous caching. In an async view you should still `await cache.aget(key)` rather than call `cache.get()`: the sync call would block the event loop for the network round trip, and with `DatabaseCache` it touches the ORM connection, which raises `SynchronousOnlyOperation` in an async context. Because thread-sensitive calls share one thread, many concurrent cache calls are serialised rather than parallel.

go deeper

for a junior

Recall that every cache method has an a-prefixed twin to await in async views.

for a middle

Explain that built-in backends implement the twins with sync_to_async and why the sync calls must not be used in async code.

for a senior

Account for serialised thread-sensitive calls and synchronous callables inside aget_or_set when tuning async views.

for a principal

Weigh whether async views bring real gains when their I/O, including the cache, still runs in threads.

## The async twins Every method of Django's cache API has an async counterpart with an `a` prefix and the same arguments: | Sync | Async | |---|---| | `get`, `set`, `add` | `aget`, `aset`, `aadd` | | `get_or_set`, `get_many`, `set_many` | `aget_or_set`, `aget_many`, `aset_many` | | `delete`, `delete_many`, `clear` | `adelete`, `adelete_many`, `aclear` | | `incr`, `decr`, `touch` | `aincr`, `adecr`, `atouch` | | `incr_version`, `has_key` | `aincr_version`, `ahas_key` | You `await` them inside `async def` views, middleware and other async code: ```python from django.core.cache import cache async def leaderboard(request): rows = await cache.aget("leaderboard:weekly") ... ``` ## What they do under the hood The twins are defined once, in `django.core.cache.backends.base.BaseCache`. For the simple methods, the implementation is: ```python async def aget(self, key, default=None, version=None): return await sync_to_async(self.get, thread_sensitive=True)(key, default, version) ``` No built-in backend, not `RedisCache`, not the Memcached backends, not `LocMemCache`, provides a native async implementation. Django's documentation says it has "developing support for asynchronous cache backends, but does not yet support asynchronous caching". The composite methods (`aget_or_set`, `aget_many`, `aincr` and similar) either call the backend's own sync method through `sync_to_async` when the backend overrides it, or compose the other async twins, for example `aget_or_set()` awaits `aget()`, then `aadd()`, then `aget()`. ## Why use them anyway - **The event loop stays free**: a sync `cache.get()` in an async view blocks the whole loop for the network round trip, stalling every other request on that worker. - **`DatabaseCache` requires it**: the database backend runs queries through the ORM connection, whose `cursor()` is marked async-unsafe. Calling it directly from an async context raises `SynchronousOnlyOperation`; the `a` twins move the call into a thread where it is allowed. - **Future-proofing**: if a backend gains native async methods, code already written with `await cache.aget()` benefits without changes. ## The cost - `thread_sensitive=True` runs the call in a single shared thread, so concurrent cache calls from many coroutines are **serialised** rather than overlapped. - Each call pays the overhead of a thread hand-off. For a very fast local cache this can exceed the cache access itself. - An async view that makes several cache calls should batch them with `aget_many()` / `aset_many()` instead of awaiting many single calls. ## Example: a leaderboard in an async view ```python from django.core.cache import cache from django.http import JsonResponse async def leaderboard(request, region): keys = [f"leaderboard:{region}:page:1", f"leaderboard:{region}:updated"] found = await cache.aget_many(keys) rows = found.get(keys[0]) if rows is None: rows = await build_leaderboard(region) # an async-safe builder await cache.aset(keys[0], rows, 300) return JsonResponse({"rows": rows, "updated": found.get(keys[1])}) ``` - One `aget_many()` replaces two separate awaits, halving the thread hand-offs. - `build_leaderboard()` must itself be async-safe, for example using the ORM's async query methods, because it runs on the event loop. - Swapping in `cache.get_many()` here would block the event loop on Redis or Memcached, and raise `SynchronousOnlyOperation` on `DatabaseCache`. ## Practical guidance 1. In `async def` code, always use the `a` twins; never call the sync methods directly. 2. Do not expect a speed-up from switching a sync view to async just for cache access; the work still happens in a thread. 3. Prefer batching and fewer round trips over many small awaits. 4. In sync views keep using the sync methods; there is nothing to gain from wrapping them.

  • Why does calling cache.get() directly in an async view with DatabaseCache raise an error?
    `DatabaseCache` reads through the ORM's database connection, and `BaseDatabaseWrapper.cursor()` is decorated as async-unsafe. Called from a thread running an event loop, it raises `SynchronousOnlyOperation`. `await cache.aget()` runs the same code through `sync_to_async`, in a thread where synchronous ORM access is allowed.
  • Does await cache.aget_or_set(key, compute) run compute() in a thread?
    Not necessarily. For backends that do not override `get_or_set`, `aget_or_set()` awaits `aget()`, then calls `compute()` directly in the coroutine, then awaits `aadd()` and `aget()`. A slow synchronous `compute` therefore blocks the event loop; wrap it yourself or make it async-safe.

saying these in an interview costs you the question

  • RedisCache talks to Redis asynchronously when you call aget().
  • Async cache methods are only available on custom backends.
  • Calling cache.get() in an async view is harmless with any backend.
  • Using aget() makes many concurrent cache reads run in parallel.
  • The async twins take different arguments from the sync methods.