aiodrf-async-cache
Native async Redis and Valkey backends for Django's cache framework, built on the async clients of redis-py and valkey-py. It requires Python 3.12+ and Django 5.2+, and depends on neither Django REST framework nor django-aiodrf.
pip install "aiodrf-async-cache[redis]" # redis-py
pip install "aiodrf-async-cache[valkey]" # valkey-py
Usage
# settings.py
CACHES = {
"default": {
"BACKEND": "aiodrf_async_cache.redis.AsyncRedisCache",
"LOCATION": "redis://localhost:6379/1",
"TIMEOUT": 300,
"KEY_PREFIX": "myapp",
"VERSION": 1,
},
}
For Valkey, use aiodrf_async_cache.valkey.AsyncValkeyCache with a
valkey:// URL; it does not need django-valkey.
from django.core.cache import caches
async def cached_total():
cache = caches["default"]
await cache.aset("total", 42)
return await cache.aget("total")
The backends subclass Django's BaseCache and implement its
asynchronous API:
aadd, aget, aset, atouch, adelete, aget_many, aset_many,
adelete_many, ahas_key, aget_or_set, aincr, adecr,
aincr_version, adecr_version, aclear and aclose, with Django's
parameter names, defaults, and key, version and timeout rules. A missing
value differs from a cached None, and aset_many returns the keys it could
not set. Without a timeout the entry uses TIMEOUT; None keeps it, and
zero or a negative timeout expires it at once. KEY_FUNCTION and a
subclass's key validation apply as in Django.
Configuration and backend lookup stay in Django, as its
cache design
expects: there is no service registry and no patch. The default codec is
Django's RedisSerializer, so integers stay usable by the server's atomic
increments. Codec work for a batch runs in one worker call, and callbacks
keep their order.
The backends are async only. Django's synchronous cache middleware, template
fragment caching and other synchronous callers need a separate synchronous
cache alias. Django 6.1's cache middleware is not asynchronous; the
middleware in aiodrf_async_cache.middleware applies Django's page-cache
policy with these backends.
Connection ownership
A backend's client belongs to the first event loop that uses it: close it on
that loop with await cache.aclose() once its users are done, and do not
share a backend between event loops. cache_lifespan() builds a backend of
its own from a Django cache alias and closes it on exit, for resources that a
worker owns:
from contextlib import asynccontextmanager
from aiodrf_async_cache.lifespan import cache_lifespan
@asynccontextmanager
async def resources():
async with cache_lifespan("default") as cache:
yield cache
With cache_lifespan("default", ping=True) the backend sends PING before
it is yielded, so a server that cannot reach Redis fails at startup with the
driver's connection error, not on the first request. The default, False,
opens nothing until the first command.
The context manager works with any lifespan owner. AsyncCacheMiddleware
reads the cache from the lifespan of
aiodrf-asgi-lifespan
(the lifespan extra) by default: set
DJANGO_LIFESPAN = "project.lifecycle.resources" and serve its ASGI
application. When the lifespan holds several clients, subclass the
middleware and return the cache from get_cache(request); such a subclass
does not need the lifespan package.
Options and limits
OPTIONS accepts:
callback_mode:"thread"(the default) runs synchronous callbacks in a worker thread;"inline"runs them on the event loop, for callbacks that never block.serializer: a dotted path, a class or an object withdumpsandloads.max_connections(20),async_pool_classandasync_pool_kwargs.
Other options go to the driver's async client. Async callbacks are awaited,
and so is an awaitable that a synchronous callback returns. Decoded text
responses (decode_responses) are refused: the codecs need bytes.
topology="sentinel" needs sentinels=[(host, port), ...] and takes the
service name as LOCATION; sentinel_kwargs configures the discovery
connections. topology="cluster" takes a seed URL as LOCATION and sends
batches as separate commands per key, so their keys may be in different
slots; such batches are not atomic. aclear() empties the whole selected
database, or every primary in a cluster, whatever the KEY_PREFIX: see
Security.
aincr and adecr need a codec that stores integers as Redis integers:
Django's RedisSerializer, or a codec whose supports_integer_operations
attribute is true.
Codecs
The default codec, Django's RedisSerializer, uses pickle: only connect to a
cache that only trusted code can write to.
aiodrf_async_cache.codecs.MsgspecCodec (the msgspec extra) stores
MessagePack, optionally decoded into a msgspec type:
from aiodrf_async_cache.codecs import MsgspecCodec
CACHES["default"]["OPTIONS"] = {"serializer": MsgspecCodec()}
It stores integers as Redis integers, so aincr and adecr work with it.
Its values are not readable by Django's RedisCache, and it does not suit
the page-cache middleware, which caches response objects.
CompressedCodec wraps another codec and compresses its values of at least
min_size bytes:
from django.core.cache.backends.redis import RedisSerializer
from aiodrf_async_cache.codecs import CompressedCodec
CACHES["default"]["OPTIONS"] = {
"serializer": CompressedCodec(RedisSerializer(), compressor="zlib", min_size=1024)
}
- A compressed value starts with the byte
0xC1, which MessagePack never uses and pickle and Redis integers never start with, and a format byte (1 for zlib, 2 for zstd). Smaller values are stored as the inner codec wrote them, so values written before compression was enabled stay readable. - Integers are never compressed, so
aincrworks when the inner codec supports it. compressor="zstd"needs the standard library'scompression.zstd(CPython 3.14 or later); elsewhere it raisesImproperlyConfigured.- Django's
RedisCachecannot read compressed values: do not enable it on entries a synchronous alias reads.
Single-flight aget_or_set
When a popular key expires, every concurrent request computes it again.
aiodrf_async_cache.singleflight.aget_or_set has the signature of Django's
aget_or_set and lets concurrent misses for the same key share one
computation:
from aiodrf_async_cache.singleflight import aget_or_set
async def load_user():
return await User.objects.aget(pk=42)
user = await aget_or_set(cache, "user:42", load_user, timeout=300)
defaultis a value or a callable; what a coroutine function returns is awaited.- Calls share a computation when they use the same cache object and the same final key (prefix, version and key). Callers that share one receive the same object.
- A cancelled caller does not cancel the computation, which still stores its value. If the computation raises, every waiting caller gets the exception, nothing is cached, and the next call computes again.
- Deduplication is per event loop, so each worker process computes a missing value once at a time. It works with any cache that implements Django's async API.
The django-valkey extra provides
aiodrf_async_cache.django_valkey.LifespanConnectionFactory, which gives
django-valkey's own backend pools owned by one lifespan. It is independent of
the native backends.
Performance
The network I/O is native, but synchronous codec calls run in a worker
thread by default, and that hop costs more than the codec on small values.
Performance explains when
callback_mode="inline" is safe. tools/benchmark.py measures both modes
with both codecs.
Further documentation
- Security: pickle, TLS and ACL users, credentials,
aclear(). - Servers, connections and operations: test coverage, timeouts and pools, retries, closing, health checks, OpenTelemetry, locks.
- Migrating from Django's RedisCache and django-redis.
- Performance: execution context and
callback_mode. - Recipes: namespace invalidation with
version=, hashed keys.
Contributing
See CONTRIBUTING.md.
Metadata
Release files for aiodrf-async-cache 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| aiodrf_async_cache-0.2.0.tar.gz | 53.0 kB | With provenance | Details for aiodrf_async_cache-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| aiodrf_async_cache-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for aiodrf_async_cache-0.2.0-py3-none-any.whl |
Total release size: 74.1 kB
Release files / aiodrf_async_cache-0.2.0.tar.gz
| Download URL | aiodrf_async_cache-0.2.0.tar.gz |
|---|---|
| Size | 53.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e0bb40b3338a7bb2a64b43c099743426608222290e9a8ca9ec0d173b715049e6
|
|
BLAKE2b-256 checksum How to use checksums |
65a38e84cf1688ea838bae6dd53a93e3d49a08710e221f518f8d11d81766c96e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / aiodrf_async_cache-0.2.0-py3-none-any.whl
| Download URL | aiodrf_async_cache-0.2.0-py3-none-any.whl |
|---|---|
| Size | 21.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ef3b6f07d1b2379c8aa27e043c252810367f040a5efc25339e3676e6199d5c34
|
|
BLAKE2b-256 checksum How to use checksums |
a6e3d01f27ad65ee2532b0ca9f02f0f0f2d7f6bb7dd30810bb66484fd9e271b7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logSecurity for aiodrf-async-cache 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for aiodrf_async_cache-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.