Skip to content

Recipes

Session Storage

Store Django sessions on a dedicated Valkey or Redis alias:

# settings.py
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "sessions"

CACHES = {
    "default": {
        "BACKEND": "django_cachex.cache.ValkeyCache",
        "LOCATION": "valkey://127.0.0.1:6379/0",
    },
    "sessions": {
        "BACKEND": "django_cachex.cache.ValkeyCache",
        "LOCATION": "valkey://127.0.0.1:6379/1",
    },
}

Rate Limiting

A rate limiter on a sorted set:

import time
from django.core.cache import cache


def is_rate_limited(user_id: str, limit: int = 100, window: int = 60) -> bool:
    """Record one request and return True if the user is over the limit."""
    key = f"ratelimit:{user_id}"
    now = time.time()
    window_start = now - window

    with cache.pipeline() as pipe:
        # Remove old entries
        pipe.zremrangebyscore(key, 0, window_start)
        # Add current request
        pipe.zadd(key, {str(now): now})
        # Count requests in window
        pipe.zcard(key)
        # Set expiry
        pipe.expire(key, window)
        results = pipe.execute()

    count = results[2]
    return count > limit

Cache Invalidation Patterns

To delete every key that matches a pattern, call cache.delete_pattern("user:*").

To invalidate a group of keys, increment a version counter in their names:

from django.core.cache import cache


def version_key(user_id: int) -> str:
    return f"user:{user_id}:version"


def get_user_cache_version(user_id: int) -> int:
    """Get current cache version for a user."""
    return cache.get(version_key(user_id), 1)


def invalidate_user_cache(user_id: int) -> None:
    """Invalidate all cached data for a user."""
    key = version_key(user_id)
    cache.add(key, 1, timeout=None)
    cache.incr(key)


def get_user_data(user_id: int) -> dict:
    """Get user data with versioned caching."""
    version = get_user_cache_version(user_id)
    key = f"user:{user_id}:data:v{version}"

    data = cache.get(key)
    if data is None:
        data = fetch_user_data_from_db(user_id)
        cache.set(key, data, timeout=3600)
    return data

The counter has timeout=None, so it cannot expire while its data keys are alive. add() creates it at 1 before incr(). Without add(), incr() on the Valkey and Redis backends creates the missing counter at delta. The version then stays 1, and the stale v1 data stays live. LocMemCache and DatabaseCache raise ValueError instead.

Distributed Locking

A lock keeps a critical section from running concurrently. lease is the TTL of the held lock, so a crashed holder's lock expires. timeout is the longest time acquire() waits:

from django.core.cache import cache

lock = cache.lock("process-payments", lease=30, timeout=5)
if lock.acquire():
    try:
        process_pending_payments()
    finally:
        lock.release()

The lock also works as a context manager. Lock Interface covers the backend differences in acquire() and the lock errors.

Gate Memory-Heavy Work With a Weighted Semaphore

A weighted semaphore shares a budget, such as a worker's memory, between tasks of different sizes. Each caller declares its weight and blocks while the budget has no room for it. Admission is FIFO, so a waiting large task holds back the smaller tasks queued behind it, even when they would fit.

from django.core.cache import cache
from django_cachex import SemaphoreTimeoutError

try:
    # 500 MB budget across all callers. This task uses ~100 MB.
    with cache.semaphore(
        "memory-pool",
        weight=100,
        capacity=500,
        lease=300,
        timeout=10,  # longest wait in acquire()
    ) as sem:
        convert_huge_image(...)
except SemaphoreTimeoutError:
    # Defer to a retry or fall back to a smaller pipeline.
    ...

On the Valkey and Redis backends, lease is required. It is the TTL of the held claim, so a crashed worker's weight returns to the budget when the lease expires. Call sem.extend(seconds) for a task that can run longer than its lease. Async tasks use async with await cache.asemaphore(...).

Development Without a Server

Use django_cachex.cache.LocMemCache:

# settings_dev.py
CACHES = {
    "default": {
        "BACKEND": "django_cachex.cache.LocMemCache",
        "LOCATION": "dev",
    }
}

It extends Django's LocMemCache with the hash, list, set and sorted-set commands, the TTL helpers and admin support. It has no streams, locks, pipelines or Lua. Local backends lists what it supports.

For testing

Use testcontainers in your tests for accurate server behavior.