Advanced Usage¶
TTL and Expiry¶
from datetime import datetime, timedelta
from django.core.cache import cache
cache.set("foo", "bar", timeout=25)
cache.ttl("foo") # 25
cache.pttl("foo") # 25000
cache.ttl("missing") # -2 (key doesn't exist)
cache.expire("foo", timeout=5) # ttl() returns 5
cache.pexpire("foo", timeout=5500) # pttl() returns 5500
cache.expireat("foo", datetime.now() + timedelta(hours=1)) # ttl() returns ~3600
cache.pexpireat("foo", datetime.now() + timedelta(milliseconds=900, hours=1)) # pttl() returns ~3600900
cache.persist("foo") # ttl() returns None (no expiration)
ttl() returns the seconds until expiry, None for a key without expiry (set with timeout=None), and -2 for a missing or expired key.
Atomic Operations and Locks¶
cache.set("key", "value1", nx=True) # True
cache.set("key", "value2", nx=True) # False, the key keeps "value1"
cache.set("counter", 0)
cache.incr("counter") # 1
cache.incr("counter", delta=5) # 6
cache.decr("counter") # 5
# lock() returns a distributed lock with the threading.Lock interface
with cache.lock("somekey"):
do_some_thing()
Lock Interface lists the lock options.
Bulk Operations¶
# Get all matching keys (not recommended for large datasets)
cache.keys("foo_*") # ["foo_1", "foo_2"]
# For large datasets, iterate with server-side cursors
for key in cache.iter_keys("foo_*"):
print(key)
cache.delete_pattern("foo_*")
# When many keys match, a larger itersize needs fewer round trips
cache.delete_pattern("foo_*", itersize=100_000)
The pattern is a case-sensitive Redis glob on every backend. "*" matches every key under the cache's prefix and version. See Key patterns.
Data Structures¶
Hashes¶
from django.core.cache import cache
# Set a single field
cache.hset("user:1", "name", "Alice")
# Set multiple fields at once
cache.hset("user:1", mapping={"email": "alice@example.com", "age": 30})
# Get a single field
name = cache.hget("user:1", "name") # "Alice"
# Get multiple fields
values = cache.hmget("user:1", "name", "email") # ["Alice", "alice@example.com"]
# Get all fields and values
user = cache.hgetall("user:1") # {"name": "Alice", "email": "...", "age": 30}
# Increment a numeric field
cache.hincrby("user:1", "age", 1) # 31
cache.hincrbyfloat("user:1", "score", 0.5) # For floating point
# Check if field exists
cache.hexists("user:1", "name") # True
# Delete fields
cache.hdel("user:1", "age")
# Get count of fields
cache.hlen("user:1") # 3
# Get all values
cache.hvals("user:1") # ["Alice", "alice@example.com", 0.5]
Hash Field Expiration¶
Hash fields can have their own TTL on Redis 7.4+ or Valkey 9.0+. hsetex and hgetex need Redis 8.0+ or Valkey 9.0+. An older server raises NotSupportedError.
from datetime import datetime, timedelta
cache.hset("session:42", mapping={"token": "abc", "csrf": "xyz", "theme": "dark"})
# Expire fields; one reply code per field
cache.hexpire("session:42", 300, "token", "csrf") # [1, 1]
cache.httl("session:42", "token", "theme", "nope") # [300, None, -2]
# Only lengthen an existing TTL (nx/xx/gt/lt mirror EXPIRE's options)
cache.hexpire("session:42", timedelta(hours=1), "token", gt=True) # [1]
# Absolute deadlines and millisecond precision
cache.hexpireat("session:42", datetime.now() + timedelta(days=1), "csrf")
cache.hpexpire("session:42", 1500, "theme")
cache.hpttl("session:42", "theme") # [1500]
# Set fields and their TTL in one round trip; fnx/fxx guard the write
cache.hsetex("session:42", "token", "def", timeout=300) # True
cache.hsetex("session:42", mapping={"a": 1, "b": 2}, timeout=60, fnx=True) # False if any exist
cache.hsetex("session:42", "token", "ghi", keepttl=True) # rewrite, keep its TTL
# Read fields and refresh (or drop) their TTL in one round trip
cache.hgetex("session:42", "token", timeout=600) # ["ghi"]
cache.hgetex("session:42", "token", persist=True) # ["ghi"], TTL removed
cache.hpersist("session:42", "csrf") # [1]
Rewriting a field with hset, or with hsetex without keepttl=True, clears that field's TTL. hsetex() treats timeout the way set() does. The default uses the backend's TIMEOUT, None means no expiry, and timeout=0 deletes the fields immediately.
Sorted Sets¶
from django.core.cache import cache
# Add members with scores
cache.zadd("leaderboard", {"alice": 100, "bob": 85, "charlie": 92})
# Get rank (0-indexed, ascending by score)
cache.zrank("leaderboard", "alice") # 2 (highest score = last)
cache.zrevrank("leaderboard", "alice") # 0 (highest score = first)
# Get score
cache.zscore("leaderboard", "bob") # 85.0
# Get multiple scores
cache.zmscore("leaderboard", "alice", "bob") # [100.0, 85.0]
# Increment score
cache.zincrby("leaderboard", 10, "bob") # 95.0
# Get range by rank (ascending)
cache.zrange("leaderboard", 0, -1) # All members sorted by score
# Get range by rank with scores
cache.zrange("leaderboard", 0, -1, withscores=True)
# Get range by score
cache.zrangebyscore("leaderboard", 80, 100)
# Count members in score range
cache.zcount("leaderboard", 80, 100) # 3
# Remove members
cache.zrem("leaderboard", "charlie")
# Remove by rank range
cache.zremrangebyrank("leaderboard", 0, 1) # Remove lowest 2
# Get total count
cache.zcard("leaderboard")
Lists¶
from django.core.cache import cache
# Push elements
cache.lpush("queue", "first") # Prepend (left)
cache.rpush("queue", "last") # Append (right)
# Pop elements
cache.lpop("queue") # Remove and return first
cache.rpop("queue") # Remove and return last
# Get element by index
cache.lindex("queue", 0) # First element
# Get range of elements
cache.lrange("queue", 0, -1) # All elements
# Set element at index
cache.lset("queue", 0, "new_first")
# Trim to range
cache.ltrim("queue", 0, 99) # Keep first 100 elements
# Get length
cache.llen("queue")
# Find element position
cache.lpos("queue", "target") # Returns index or None
# Move element between lists atomically
cache.lmove("source", "dest", "LEFT", "RIGHT") # LPOP source, RPUSH dest
Raw Client Access¶
get_client() returns the underlying client. Its type depends on the backend, as Raw Client Access lists.
Lua Scripts¶
eval_script() runs a Lua script and sends its keys and args as given. A pre_hook transforms the keys and args before the script runs, and a post_hook transforms the result. keys_only_pre, for example, adds the cache's key prefix and version. With post_hook=None, the default, the result comes back unchanged. aeval_script() is the async twin. Lua Script Methods covers EVALSHA and the hook signatures.
from django.core.cache import cache
from django_cachex import (
Encoded, # Marks an ARGV entry for encoded_pre
encoded_pre, # Prefix keys, encode only the args wrapped in Encoded(...)
keys_only_pre, # Prefix keys, leave args unchanged
full_encode_pre, # Prefix keys AND encode args (serialize values)
decode_single_post, # Decode a single returned value
decode_list_post, # Decode a list of returned values
)
# keys_only_pre applies the cache's key prefix and version
count = cache.eval_script(
"""
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return current
""",
keys=["user:123:requests"],
args=[60],
pre_hook=keys_only_pre,
)
# full_encode_pre serializes every arg, decode_single_post decodes the result
old_session = cache.eval_script(
"""
local old = redis.call('GET', KEYS[1])
redis.call('SET', KEYS[1], ARGV[1])
return old
""",
keys=["session:abc"],
args=[{"user_id": 123, "permissions": ["read", "write"]}],
pre_hook=full_encode_pre,
post_hook=decode_single_post,
)
Mixed Values and Scalars¶
Values that a later get() reads back must go through the serializer and compressor. Scalars that Lua consumes with tonumber or a string compare must not. Wrap the values in Encoded, at any ARGV position, and use encoded_pre:
from django_cachex import Encoded, encoded_pre
SETEXPIRE = """
local value = ARGV[1]
local ex = tonumber(ARGV[2])
local nx = ARGV[3] == "1"
if nx and redis.call('EXISTS', KEYS[1]) == 1 then
return 0
end
redis.call('SET', KEYS[1], value, 'EX', ex)
return 1
"""
cache.eval_script(
SETEXPIRE,
keys=["session:abc"],
args=[Encoded({"user_id": 123}), 300, "1"],
pre_hook=encoded_pre,
)
Custom Processing Hooks¶
Custom hooks receive a ScriptHelpers instance:
from django_cachex import ScriptHelpers
def my_pre(helpers: ScriptHelpers, keys, args):
# First arg is a secondary key, rest are values
processed_args = [helpers.make_key(args[0], helpers.version)]
processed_args.extend(helpers.encode_values(args[1:]))
return helpers.make_keys(keys), processed_args
def my_post(helpers: ScriptHelpers, result):
# Result is [count, list_of_values]
return {
"count": result[0],
"values": helpers.decode_values(result[1]) if result[1] else [],
}
result = cache.eval_script(
"...",
keys=["primary"],
args=["secondary", value_a, value_b],
pre_hook=my_pre,
post_hook=my_post,
)