API Reference¶
Cache Methods¶
A method that the backend or the connected server does not support raises NotSupportedError. Local backends lists what LocMemCache and DatabaseCache lack.
Standard Django Cache Methods¶
| Method | Description |
|---|---|
get(key, default=None) |
Get a value |
set(key, value, timeout=DEFAULT) |
Set a value |
add(key, value, timeout=DEFAULT) |
Set only if key doesn't exist |
delete(key) |
Delete a key |
touch(key, timeout=DEFAULT) |
Update timeout on a key |
get_many(keys) |
Get multiple values |
set_many(data, timeout=DEFAULT) |
Set multiple values |
delete_many(keys) |
Delete multiple keys |
get_or_set(key, default, timeout=DEFAULT) |
Get value or set default |
clear() |
Clear the cache, see Clearing keys |
has_key(key) |
Check if key exists |
incr(key, delta=1) |
Increment a value |
decr(key, delta=1) |
Decrement a value |
incr_version(key, delta=1) |
Increment key version |
decr_version(key, delta=1) |
Decrement key version |
close() |
Close connections |
Set Method Options¶
| Parameter | Description |
|---|---|
timeout |
Expiration in seconds (None = never, 0 = immediate) |
nx |
Only set if key doesn't exist (SETNX) |
xx |
Only set if key exists |
get |
Return the previous value (atomic get-and-set) |
The Valkey/Redis backends and LocMemCache take all three flags, and TrackingCache passes them to its transport. DatabaseCache takes only nx. nx together with get needs Redis 7.0+.
Extended Methods¶
| Method | Description |
|---|---|
ttl(key) |
Get TTL in seconds (None = no expiry, -2 = not found) |
pttl(key) |
Same as ttl in milliseconds |
expire(key, timeout) |
Set expiration in seconds |
pexpire(key, timeout) |
Set expiration in milliseconds |
expireat(key, when) |
Set expiration at datetime |
pexpireat(key, when) |
Set expiration at datetime (ms precision) |
expiretime(key) |
Unix timestamp (seconds) when the key expires. Needs Redis 7.0+ (any supported Valkey) |
persist(key) |
Remove expiration |
type(key) |
Get the data type of a key |
memory_usage(key, version=None, *, samples=None) |
Bytes the key and its value take on the server, or None for a missing key. samples sets the MEMORY USAGE sample count for containers (0 = all) |
largest_keys(pattern="*", count=10, version=None, *, samples=None, itersize=None) |
The count largest keys matching pattern as (key, bytes) pairs, largest first. Runs MEMORY USAGE on every matching key |
info(section=None) |
Server statistics from INFO as a dict. On the Valkey/Redis backends, section, such as "memory", returns one section |
slowlog_get(count=10) |
Up to count of the newest slow log entries |
slowlog_len() |
Number of entries in the slow log |
lock(key, ...) |
Get a distributed lock, see Lock Interface |
keys(pattern) |
Get keys matching pattern |
iter_keys(pattern) |
Iterate keys matching pattern |
scan(cursor=0, pattern="*", count=None, version=None, key_type=None) |
Single SCAN iteration. key_type keeps only keys of that type |
delete_pattern(pattern) |
Delete keys matching pattern |
rename(src, dst, version=None, version_src=None, version_dst=None) |
Rename a key; raises KeyNotFoundError when src does not exist. version_src and version_dst override version for one side |
renamenx(src, dst, version=None, version_src=None, version_dst=None) |
Rename a key only if dst does not exist; False when dst exists or src does not |
Key patterns¶
keys(), iter_keys(), scan() and delete_pattern() take a case-sensitive Redis glob on every backend. The glob supports *, ?, [abc], [a-z], [^abc], and \ to escape any of them. The empty pattern matches only the empty key, so delete_pattern("") deletes at most one key. Use "*" to match every key.
keys(), iter_keys() and scan() return a key name that is not valid UTF-8 decoded with the surrogateescape error handler, as os.listdir() does, and the key commands take that name back as is.
Data-structure calls with no arguments¶
sadd, srem, hdel, hset, lpush, rpush, zrem, xdel and xack return 0 when called with no members, fields, values or entry ids. So does zadd with an empty mapping. smismember, zmscore, hgetex and the field-TTL methods from hexpire to hpersist return []. These calls send no command and create no key. In a pipeline, such a call still adds its 0 or [] to the execute() result, so the results line up with the calls.
Hash Methods¶
| Method | Description |
|---|---|
hset(key, field=None, value=None, version=None, mapping=None, items=None) |
Set hash field(s); pass field/value, a mapping dict, or a flat items list |
hdel(key, *fields) |
Delete hash field(s) |
hexists(key, field) |
Check if hash field exists |
hget(key, field) |
Get a hash field value |
hgetall(key) |
Get all fields and values in a hash |
hkeys(key) |
Get all field names in a hash |
hincrby(key, field, amount=1) |
Increment hash field by integer |
hincrbyfloat(key, field, amount=1.0) |
Increment hash field by float |
hlen(key) |
Get number of fields in hash |
hmget(key, *fields) |
Get multiple hash field values |
hsetnx(key, field, value) |
Set hash field only if it doesn't exist |
hvals(key) |
Get all values in a hash |
hexpire(key, timeout, *fields, nx=False, xx=False, gt=False, lt=False) |
Set a TTL in seconds (or a timedelta) on hash fields. Returns the HEXPIRE reply code per field |
hpexpire(key, timeout, *fields, ...) |
Same as hexpire with millisecond precision |
hexpireat(key, when, *fields, ...) |
Expire hash fields at a Unix timestamp or datetime |
hpexpireat(key, when, *fields, ...) |
Same as hexpireat with millisecond precision |
httl(key, *fields) |
Get TTL in seconds per field (None = no expiry, -2 = not found) |
hpttl(key, *fields) |
Same as httl in milliseconds |
hexpiretime(key, *fields) |
Absolute Unix timestamp (seconds) per field (None = no expiry, -2 = not found) |
hpersist(key, *fields) |
Remove field expirations. Returns the HPERSIST reply code per field |
hsetex(key, field=None, value=None, timeout=DEFAULT_TIMEOUT, version=None, mapping=None, items=None, *, fnx=False, fxx=False, keepttl=False) |
Set hash field(s) and their TTL in one command; False when fnx/fxx blocks the write |
hgetex(key, *fields, timeout=None, persist=False) |
Get hash field(s) and set (timeout) or remove (persist) their TTL in the same command |
Field expiration needs Redis 7.4+ or Valkey 9.0+, and hsetex and hgetex need Redis 8.0+ or Valkey 9.0+.
Set Methods¶
| Method | Description |
|---|---|
sadd(key, *members) |
Add member(s) to set |
srem(key, *members) |
Remove member(s) from set |
smembers(key) |
Get all members of set |
sismember(key, member) |
Check if member exists in set |
smismember(key, *members) |
Check if multiple members exist |
scard(key) |
Get number of members |
spop(key, count=None) |
Remove and return random member(s) |
srandmember(key, count=None) |
Get random member(s) without removing |
smove(src, dst, member) |
Move member between sets |
sdiff(keys) |
Get difference of sets |
sdiffstore(dest, keys) |
Store difference of sets |
sinter(keys) |
Get intersection of sets |
sinterstore(dest, keys) |
Store intersection of sets |
sunion(keys) |
Get union of sets |
sunionstore(dest, keys) |
Store union of sets |
sscan(key, cursor=0, ...) |
Incrementally iterate set members |
sscan_iter(key, ...) |
Iterate over set members using SSCAN |
sdiff, sinter, sunion and their *store forms take keys as one key or a sequence. Write sdiff(["a", "b"]), because sdiff("a", "b") passes "b" as version.
Members must stay hashable after a round trip through the serializer. The JSON and MessagePack serializers return a tuple as a list, so sadd rejects tuples there. smembers, sdiff, sinter, sunion, spop and sscan return a Python set. 1, True and 1.0 are one entry in it but three members on the server, and scard counts three. LocMemCache and DatabaseCache store one member, see Local backends.
Sorted Set Methods¶
| Method | Description |
|---|---|
zadd(key, mapping, *, nx, xx, ch, gt, lt) |
Add member(s) with scores |
zcard(key) |
Get number of members |
zcount(key, min_score, max_score) |
Count members with scores in range |
zincrby(key, amount, member) |
Increment member's score |
zrange(key, start, end, ...) |
Get members by index range |
zrevrange(key, start, end, ...) |
Get members by index range (descending) |
zrangebyscore(key, min_score, max_score, ...) |
Get members by score range |
zrevrangebyscore(key, max_score, min_score, ...) |
Get members by score range (descending) |
zrank(key, member) |
Get member's rank (ascending) |
zrevrank(key, member) |
Get member's rank (descending) |
zrem(key, *members) |
Remove member(s) |
zremrangebyrank(key, start, end) |
Remove members by rank range |
zremrangebyscore(key, min_score, max_score) |
Remove members by score range |
zscore(key, member) |
Get member's score |
zmscore(key, *members) |
Get multiple members' scores |
zpopmin(key, count=None) |
Remove and return members with lowest scores |
zpopmax(key, count=None) |
Remove and return members with highest scores |
List Methods¶
| Method | Description |
|---|---|
llen(key) |
Get list length |
lpush(key, *values) |
Prepend value(s) to list |
rpush(key, *values) |
Append value(s) to list |
lpop(key, count=None) |
Remove and return the first element, or a list of the first count elements |
rpop(key, count=None) |
Remove and return the last element, or a list of the last count elements |
lindex(key, index) |
Get element by index |
lrange(key, start, end) |
Get elements in range |
lset(key, index, value) |
Set element at index |
ltrim(key, start, end) |
Trim list to range |
lrem(key, count, value) |
Remove elements equal to value |
lpos(key, value, ...) |
Find element position in list |
linsert(key, where, pivot, value) |
Insert value before or after pivot |
lmove(src, dst, wherefrom, whereto) |
Atomically move element between lists |
blpop(keys, timeout=0) |
Blocking pop from head of list; keys is one key or a sequence |
brpop(keys, timeout=0) |
Blocking pop from tail of list; keys is one key or a sequence |
blmove(src, dst, timeout, ...) |
Blocking move between lists |
Stream Methods¶
| Method | Description |
|---|---|
xadd(key, fields, entry_id="*", maxlen=None, ..., nomkstream=False) |
Append an entry, returning its ID; None when nomkstream=True and the stream does not exist |
xlen(key) |
Number of entries in the stream |
xrange(key, start="-", end="+", count=None) |
Range of entries (forward) |
xrevrange(key, end="+", start="-", count=None) |
Range of entries (reverse) |
xread(streams, count=None, block=None) |
Read new entries from one or more streams |
xtrim(key, maxlen=None, approximate=True, minid=None, ...) |
Cap stream length |
xdel(key, *entry_ids) |
Delete entries by ID |
xinfo_stream(key, full=False) |
Stream metadata |
xinfo_groups(key) |
Consumer group metadata |
xinfo_consumers(key, group) |
Per-consumer metadata for a group |
xgroup_create(key, group, entry_id="$", mkstream=False) |
Create a consumer group |
xgroup_destroy(key, group) |
Drop a consumer group |
xgroup_setid(key, group, entry_id) |
Re-anchor a consumer group's read position |
xgroup_delconsumer(key, group, consumer) |
Drop a consumer from a group |
xreadgroup(group, consumer, streams, count=None, block=None) |
Read entries as a group consumer |
xack(key, group, *entry_ids) |
Acknowledge processed entries |
xpending(key, group, ...) |
Inspect pending (unacked) entries |
xclaim(key, group, consumer, min_idle_time, entry_ids, ...) |
Claim pending entries |
xautoclaim(key, group, consumer, min_idle_time, ...) |
Auto-claim entries idle longer than threshold |
Lua Script Methods¶
eval_script() sends EVALSHA and, after a NOSCRIPT reply, loads the script and retries. A pipeline sends queued scripts as plain EVAL. django_cachex.script_sha(script) returns the SHA-1 digest. Lua Scripts has examples.
result = cache.eval_script(
script, # Lua source
keys=(), # KEYS
args=(), # ARGV
pre_hook=None, # (helpers, keys, args) -> (keys, args)
post_hook=None, # (helpers, result) -> result; None returns the result unchanged
version=None, # key version for prefixing
)
Pre-built Hooks¶
| Hook | Description |
|---|---|
encoded_pre |
Prefix keys, encode the args wrapped in Encoded(...), pass the rest through |
keys_only_pre |
Prefix keys, leave args unchanged |
full_encode_pre |
Prefix keys and encode all args |
decode_single_post |
Decode a single returned value |
decode_list_post |
Decode a list of returned values |
Encoded(value) marks one ARGV entry for encoded_pre to encode.
ScriptHelpers¶
The helpers argument of both hooks:
| Attribute/Method | Description |
|---|---|
make_key(key, version) |
Apply cache key prefix |
make_keys(keys) |
Prefix multiple keys |
encode(value) |
Encode a value (serialize + compress) |
encode_values(values) |
Encode multiple values |
decode(value) |
Decode a value |
decode_values(values) |
Decode multiple values |
version |
Current key version |
Async Methods¶
Every cache method on this page except get_client(), info(), slowlog_get() and slowlog_len() has an async twin with an a prefix, such as aget() or ahset(). alock(), asemaphore() and apipeline() are async def, so async with needs an extra await, as in async with await cache.alock("k"):. See Async Support.
Raw Client Access¶
| Parameter | Description |
|---|---|
key |
Ignored. Accepted for compatibility with django-redis |
write |
True returns a client for the primary. With a multi-URL LOCATION or Sentinel, False (the default) picks a replica pool when one exists. Cluster and valkey-glide ignore it |
get_client() bypasses the key prefix, version, serializer and compressor. The client type depends on the backend:
| Backend | get_client() returns |
|---|---|
ValkeyCache (valkey-py) |
valkey.Valkey |
RedisCache (redis-py) |
redis.Redis |
ValkeyGlideCache (valkey-glide) |
a proxy for glide_sync.GlideClient that forwards every attribute and translates server errors; isinstance(client, GlideClient) is False |
Use the cache API for portable code. cache.adapter also skips prefixing and serialization, as in await cache.adapter.aget(prefixed_key). For the async client, call await cache.adapter.get_async_client().
Lock Interface¶
lock = cache.lock(key, version=None, lease=None, sleep=0.1, *, blocking=True, timeout=None, thread_local=True)
| Parameter | Description |
|---|---|
key |
Lock name |
version |
Cache version of the lock key |
lease |
Seconds until a held lock expires and releases itself. None means no expiry |
sleep |
Seconds between acquire attempts |
blocking |
Wait if the lock is held |
timeout |
Longest time acquire() waits before it returns False. None means no limit |
thread_local |
Keep the ownership token in thread-local storage, so one thread cannot release another's hold |
Pass lease by keyword, because cache.lock("k", 30) sets version=30 and leaves the lock without a TTL. alock() takes the same parameters. Locks work on the standalone and Sentinel Valkey/Redis backends. The cluster backends raise NotSupportedError, because the driver locks are not cluster-aware. Use semaphore() there.
On redis-py and valkey-py, the lock wraps the driver's lock and forwards every attribute. Its signature is acquire(sleep=None, blocking=None, blocking_timeout=None, token=None), and the async one has no sleep. The valkey-glide lock has acquire(*, blocking=None, timeout=None). Portable code passes timeout to cache.lock().
Lock failures raise django_cachex.lock.LockError or its subclass LockNotOwnedError:
from django_cachex.lock import LockError, LockNotOwnedError
def guarded_work():
lock = cache.lock("mylock", lease=30, timeout=5)
try:
if not lock.acquire():
return # another holder kept it for 5 seconds
do_work()
lock.release()
except LockNotOwnedError:
... # the lease ran out during do_work()
except LockError:
... # released twice or by another owner
A lock also works in a with block, and alock() returns the async form:
with cache.lock("mylock"):
do_work()
async with await cache.alock("mylock"):
await do_work()
# Manual acquire/release, async
lock = await cache.alock("mylock")
if await lock.acquire():
try:
await do_work()
finally:
await lock.release()
Semaphore Interface¶
semaphore() returns a weighted semaphore. Use it as a context manager:
with cache.semaphore("image-convert", capacity=4, lease=60):
# Up to 4 callers can hold this semaphore at once.
convert(...)
# Weighted: claim 100 of a 500 budget.
with cache.semaphore("memory-heavy", weight=100, capacity=500, lease=300):
convert_huge(...)
| Parameter | Description |
|---|---|
key |
Semaphore name. Sync and async callers with the same name share the budget. |
capacity |
Total budget, set by the first caller. A later caller with a different value replaces it, and LocMemCache also emits a RuntimeWarning. |
weight |
How much of the capacity this caller claims. The default 1 makes a counting semaphore. |
version |
Cache version of the semaphore keys. |
lease |
Seconds until a held claim expires, which frees the claim of a crashed holder. Required on the Valkey/Redis backends, ignored on LocMemCache. |
timeout |
Default wait for acquire() before it raises SemaphoreTimeoutError. None means no limit. |
Semaphores work on every Valkey/Redis backend, cluster included, and on LocMemCache. LocMemCache serves waiters in strict FIFO order within the process. The Valkey/Redis backends poll the server, and their FIFO order across processes is best-effort.
acquire() and aacquire() take keyword-only blocking=True and timeout. A blocking call returns True or raises SemaphoreTimeoutError, and only blocking=False can return False. Without a timeout argument, the value from cache.semaphore() applies. An explicit timeout=None waits without limit.
sem = cache.semaphore("mysem", capacity=4, lease=30)
sem.acquire(timeout=5) # raises SemaphoreTimeoutError after 5 seconds
try:
do_work()
finally:
sem.release()
release() frees the claim. extend(additional_seconds) adds to the remaining lease and returns False when the claim was already released or reaped. LocMemCache has no lease, so there extend() returns whether the claim is held.
On the Valkey/Redis backends, another caller can reclaim the weight of a claim whose lease expired while its holder still works. The holder's release() then finds no claim and logs a warning to the django_cachex.semaphore logger instead of raising.
The Valkey/Redis backends keep a semaphore in keys that start with {<key>}:, where <key> carries the KEY_PREFIX and VERSION. The braces make <key> a hash tag, so on a cluster all of them share a slot. clear(), clear_all_versions(), keys() and the admin key browser skip these keys. They go away when the last claim is released with nobody waiting, and a guard TTL expires them when the last holder dies without releasing.
Async code awaits aacquire(), arelease() and aextend():
# Context manager
async with await cache.asemaphore("mysem", capacity=4, lease=30):
await do_work()
# Manual acquire/release
sem = await cache.asemaphore("mysem", capacity=4, lease=30)
await sem.aacquire()
try:
await do_work()
finally:
await sem.arelease()
await sem.acquire() runs the sync acquire, then raises TypeError on the returned bool and leaves the claim held until its lease expires.
Pipelines¶
pipe = cache.pipeline(*, transaction=True, version=None)
pipe = await cache.apipeline(*, transaction=True, version=None)
transaction=True, the default on the standalone and Sentinel backends, wraps the batch in MULTI/EXEC. The cluster backends default to transaction=False and raise NotSupportedError for transaction=True. version overrides the cache's VERSION for every key queued on the pipeline.
pipeline() returns a django_cachex.Pipeline. apipeline() returns its subclass django_cachex.AsyncPipeline, which takes async with and an awaited execute(). The queueing methods are synchronous in both, and only execute() sends the commands. Leaving the with block discards the commands still queued.
with cache.pipeline() as pipe:
pipe.set("key1", "value1")
pipe.hset("hash", "field", "value")
results = pipe.execute()
async with await cache.apipeline() as pipe:
pipe.set("key1", "value1")
pipe.hset("hash", "field", "value")
results = await pipe.execute()
A pipeline queues get, set, delete, exists, incr, decr, type, rename, renamenx, memory_usage, eval_script and the TTL methods from ttl to persist. It also queues the hash, list, set, sorted-set and stream methods, except sscan, sscan_iter and the blocking blpop, brpop and blmove. Other methods, such as set_many, add or delete_pattern, have no pipeline form, so queue their underlying commands. execute() returns the results as a list in queue order.
The queueing methods take the parameters of the cache methods. Their flags decide what a step adds to the results:
set()addsTrue, orFalsewhennxorxxblocks the write. Withget=Trueit adds the previous value orNoneinstead.zadd(key, mapping, *, ..., incr=False)takesincr=Trueon the pipeline only. It sendsZADD ... INCRfor the single pair inmappingand adds the new score, orNonewhennx,xx,gtorltblocked it.zrange(key, start, end, *, withscores=False, desc=False)takesdesc=Trueon the pipeline only, which gives the result ofzrevrange.xautoclaim(..., justid=True)raisesNotSupportedErrorwhen queued. Usejustid=False, or callcache.xautoclaim().
The redis-py and valkey-py cluster pipelines refuse some multi-key commands, as Cluster lists.
Clearing keys¶
| Method | Description |
|---|---|
clear() |
Delete only this cache's keys (KEY_PREFIX + VERSION), with delete_pattern("*"). |
clear_all_versions(itersize=None) |
Delete this cache's keys (KEY_PREFIX) in every version and return the number deleted. |
flush_db() |
Run FLUSHDB, which deletes every key in the Redis/Valkey database, whatever its prefix. |
The table describes the Valkey/Redis backends. Only they support clear_all_versions() and flush_db(), and the other backends raise NotSupportedError. Neither clear() nor clear_all_versions() deletes semaphore state.
On a shared database, clear() spares the keys of other caches only when each cache has its own KEY_PREFIX. Two caches on the default empty prefix both write :1:* keys, so clear() on either deletes the keys of both. Use flush_db() only to empty the whole database.
Settings Reference¶
OPTIONS Reference lists the OPTIONS keys and the backends that honor them.
StampedeConfig¶
django_cachex.StampedeConfig(buffer=60, beta=1.0, delta=1.0) is a frozen dataclass that tunes stampede prevention on the Valkey/Redis backends. Pass it to OPTIONS["stampede_prevention"], or to the stampede_prevention= keyword of one call. The keyword also takes True, False or None, and the dict form works in OPTIONS only.
| Field | Default | Description |
|---|---|---|
buffer |
60 |
Seconds added to TTL on writes; defines the early-recompute window. |
beta |
1.0 |
Multiplier on the recompute probability; higher = recompute earlier. |
delta |
1.0 |
Recompute-cost estimate (seconds); larger = recompute earlier. |
Exceptions¶
All exceptions below are importable from django_cachex and subclass CachexError.
| Exception | Description |
|---|---|
CachexError |
Base class of every django-cachex exception. |
WrongTypeError |
A command hit a key of another type, as Redis WRONGTYPE reports. Subclass of TypeError, raised by every backend. |
KeyNotFoundError |
rename() found no source key. The missing key is in key. Subclass of ValueError. |
CompressorError |
Compression or decompression failed. Triggers the configured compressor fallback chain. |
SerializerError |
Serialization or deserialization failed. Triggers the serializer fallback chain. |
NotSupportedError |
The backend or the connected server does not support the operation. It carries operation, backend (None when the server rejected the command) and detail. |
LockError |
A lock operation failed, such as a with block that could not acquire the lock or a release of an unlocked lock. A driver lock error is kept as __cause__. Subclass of ValueError. |
LockNotOwnedError |
Releasing or extending a lock the caller no longer owns (expired or stolen). Subclass of LockError. |
SemaphoreError |
A semaphore operation failed, such as re-acquiring before release. |
SemaphoreTimeoutError |
timeout elapsed before the semaphore could be acquired. Subclass of SemaphoreError. |
The ORM cache raises django_cachex.orm.exceptions.InvalidationError when a write or invalidate() cannot invalidate the cache. It subclasses CachexError and Django's DatabaseError, so atomic() rolls the transaction back.