Skip to content

Async Support

Django's cache methods and the django-cachex extensions have async twins with an a prefix, such as aget(), attl() and ahset(). The exceptions are get_client(), info(), slowlog_get() and slowlog_len(). cache.get() and await cache.aget() use the same alias, with no separate configuration. The API reference covers the twins under Async Methods.

from django.core.cache import cache
from django.http import JsonResponse


async def user_profile(request, user_id):
    key = f"user:{user_id}:profile"
    profile = await cache.aget(key)
    if profile is None:
        profile = await get_user_profile_from_db(user_id)
        await cache.aset(key, profile, timeout=3600)
    return JsonResponse(profile)


async def leaderboard(request):
    # Top 10, highest score first
    top_players = await cache.azrevrange("game:leaderboard", 0, 9, withscores=True)
    return JsonResponse({"leaderboard": top_players})

Backend Support

Backend The a* methods
redis-py and valkey-py backends Run natively on redis.asyncio or valkey.asyncio
valkey-glide backends Run natively on glide's async glide.GlideClient
LocMemCache Call the sync twin directly, without a thread. It does no I/O, so awaiting it from an event loop is harmless
DatabaseCache Run the sync twin through asgiref.sync.sync_to_async, as Django's BaseCache.aget() does
Django's own backends Django's methods only, without the django-cachex extensions

The native clients do not go through the asgiref thread pool. The Cluster and Sentinel variants run their async methods the same way.

Async Pipelines

async with await cache.apipeline() as pipe:
    pipe.set("a", 1)
    pipe.hset("h", "field", "value")
    results = await pipe.execute()

Queueing methods such as set() and hset() are synchronous. Await only apipeline() and execute(), on every backend. The async pipeline otherwise behaves like the sync pipeline().

Event Loops and Connection Pools

On the redis-py and valkey-py backends, sync and async calls use separate connection pools. Sync calls use one pool per server, which every thread shares. Async calls use one pool per server and event loop, because connections belong to the loop that opened them. Each loop reuses its pools for every call.

Short-lived event loops

Each asyncio.run() starts a new event loop, so it opens a new pool and a new TCP connection. Use the sync methods where loops are short-lived.

Context Recommendation
ASGI views (uvicorn, daphne) Use async methods (aget, aset)
WSGI views (gunicorn, uwsgi) Use sync methods (get, set)
Management commands Use sync methods
Celery tasks Use sync methods
Background tasks with persistent loop Use async methods

Custom Async Pool Class

async_pool_class sets the async connection pool class, as an import path or a class:

CACHES = {
    "default": {
        "BACKEND": "django_cachex.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "async_pool_class": "myapp.pools.CustomAsyncConnectionPool",
        },
    }
}

Closing Async Connections

await cache.aclose() disconnects the async pools and clients the alias opened on the running loop. The next await opens new ones, so call aclose() when you are done with a loop, not between requests. Aliases with the same configuration share pools, so aclose() disconnects them too, including connections in use. They reconnect on their next command.

close() leaves the sync pools connected, because Django calls it on every request_finished signal. Calling either method is optional. django-cachex drops the pools of closed loops on every pool lookup and on close().