Django caching cheatsheet.

Backends

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://localhost:6379/1",
    },
}

Other backends:

  • LocMemCache — in-memory per-process (dev only).
  • DatabaseCache — DB-backed (overkill).
  • FileBasedCache — filesystem.
  • MemcachedCache — Memcached.

Low-level API

from django.core.cache import cache

cache.set("key", value, timeout=60)
value = cache.get("key")
value = cache.get("key", "default")
cache.delete("key")
cache.delete_many(["k1", "k2"])
cache.clear()

cache.add("key", value, timeout=60)         # only if missing
cache.get_or_set("key", default, timeout)

# Multiple
cache.set_many({"a": 1, "b": 2})
cache.get_many(["a", "b"])

# Atomic counter
cache.incr("counter")
cache.decr("counter")
cache.incr_version("key")

Pattern: cache aside

def get_post(post_id):
    key = f"post:{post_id}"
    post = cache.get(key)
    if post is None:
        post = Post.objects.get(id=post_id)
        cache.set(key, post, timeout=300)
    return post

Invalidation

def update_post(post_id, **data):
    Post.objects.filter(id=post_id).update(**data)
    cache.delete(f"post:{post_id}")

Or via signal:

@receiver(post_save, sender=Post)
def invalidate(sender, instance, **kwargs):
    cache.delete(f"post:{instance.id}")

Versioning

cache.set("user:1", data, version=2)
cache.get("user:1", version=2)

Switch versions to invalidate ranges.

Per-view cache

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def view(request):
    ...

# CBV
from django.utils.decorators import method_decorator

@method_decorator(cache_page(60 * 15), name="dispatch")
class MyView(ListView):
    ...

Caches the entire response. Watch for per-user content.

from django.views.decorators.vary import vary_on_headers, vary_on_cookie

@vary_on_cookie
@cache_page(60 * 15)
def view(request):
    ...

@vary_on_headers("User-Agent")
@cache_page(60)
def view(request): ...

Per-site caching

MIDDLEWARE = [
    "django.middleware.cache.UpdateCacheMiddleware",
    ...,
    "django.middleware.common.CommonMiddleware",
    "django.middleware.cache.FetchFromCacheMiddleware",
]

CACHE_MIDDLEWARE_SECONDS = 600
CACHE_MIDDLEWARE_KEY_PREFIX = "myapp"

Caches every page. Aggressive; usually not what you want.

Template fragment caching

{% load cache %}

{% cache 600 sidebar request.user.id %}
  ... expensive sidebar ...
{% endcache %}

Cache key includes the timeout and any vars listed.

QuerySet caching

def get_top_posts():
    return cache.get_or_set(
        "top_posts",
        lambda: list(Post.objects.filter(featured=True)[:10]),
        timeout=300,
    )

Wrap in list() to materialize before caching (QuerySets are lazy).

Cached property

from django.utils.functional import cached_property

class Post:
    @cached_property
    def expensive(self):
        return compute()

Per-instance cache (memory, lasts as long as instance).

Locking (cache stampede)

When many requests hit a cold cache, all of them re-compute. Avoid via lock:

from django.core.cache import cache
import time

def get_with_lock(key, fn, timeout=300):
    val = cache.get(key)
    if val is not None: return val
    
    lock_key = f"{key}:lock"
    if cache.add(lock_key, "1", timeout=10):     # atomic
        try:
            val = fn()
            cache.set(key, val, timeout=timeout)
        finally:
            cache.delete(lock_key)
        return val
    
    # Someone else is computing; wait briefly and retry
    time.sleep(0.1)
    return cache.get(key) or fn()

Redis directly

For lists, sets, hashes — use Redis client directly:

import redis

r = redis.from_url(settings.REDIS_URL, decode_responses=True)

r.lpush("queue", "task1")
r.rpop("queue")
r.zadd("leaderboard", {"alice": 100, "bob": 80})
r.zrevrange("leaderboard", 0, 9)
r.hset("user:1", mapping={"name": "A", "email": "[email protected]"})
r.hgetall("user:1")

Django cache is just KV — for more, use Redis directly.

Sessions in cache

SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"

Or cached_db (cache + DB fallback). Fast and persistent.

Cache headers

from django.views.decorators.cache import cache_control

@cache_control(max_age=3600, public=True)
def view(request):
    ...

Sets Cache-Control: public, max-age=3600. Useful for CDN caching.

Cache key conventions

post:{id}                  # individual entity
posts:list:{q_hash}        # collection by query
user:{id}:permissions      # derived data
counter:posts:total        # counters

Make keys explicit and grep-able.

Common mistakes

  • cache.set with non-pickleable object → silent failure.
  • Caching per-user data without including user in key.
  • cache_page on a view that uses cookies → wrong content served.
  • Forgetting to invalidate on update.
  • Cache TTL too long → stale data; too short → useless cache.

Read this next

If you want my caching patterns, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .