Caching is often the cheapest way to make a slow page fast, if you can keep it correct.
Configure backends in CACHES. Options include LocMemCache (default, per process, development only), RedisCache and PyMemcacheCache (shared across processes and servers), DatabaseCache and FileBasedCache.
Caching levels, from finest to coarsest:
- Low-level API:
from django.core.cache import cache, thencache.get(key),cache.set(key, value, timeout=300),cache.get_or_set(key, callable, timeout),cache.delete(key),cache.incr(key). Cache expensive computed values, such as API results or aggregates. - Template fragments:
{% load cache %}{% cache 600 sidebar request.user.pk %}...{% endcache %}caches part of a page, varying by the extra arguments. - Per view:
@cache_page(60 * 15)caches whole responses, keyed by URL. Responses that vary by cookie or language need@vary_on_cookieor similar. Avoid it for personalized pages. - Per site:
UpdateCacheMiddlewareandFetchFromCacheMiddleware, rarely the right choice beyond simple public sites.
Invalidation is the hard part. Strategies:
- short timeouts when staleness is acceptable
- delete or update keys when data changes (in the code that writes, or via signals)
- versioned keys: include
updated_ator a version number in the key so new data naturally uses a new key
HTTP caching complements server caching: ETag/Last-Modified via @condition or ConditionalGetMiddleware, and Cache-Control headers for browsers and CDNs.
functools.cached_property on models caches per instance, not across requests.
Example
# settings.py
CACHES = {"default": {"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1"}}
# blog/views.py
from django.core.cache import cache
from django.db.models import Count
from django.shortcuts import render
from django.views.decorators.cache import cache_page
from .models import Tag
def popular_tags():
return cache.get_or_set(
"popular-tags:v1",
lambda: list(Tag.objects.annotate(n=Count("posts")).order_by("-n").values("name", "n")[:10]),
timeout=600)
@cache_page(60 * 5) # whole response, keyed by URL
def archive(request, year):
return render(request, "blog/archive.html", {"year": year, "tags": popular_tags()})
def on_post_saved(): # call when posts change
cache.delete("popular-tags:v1")
# template fragment:
# {% load cache %}{% cache 600 sidebar request.user.pk %}...expensive sidebar...{% endcache %}Remember
- Use a shared backend like Redis in production; LocMemCache is per process
- Low-level cache API for data, {% cache %} for fragments, @cache_page for whole views
- Plan invalidation: timeouts, explicit deletes or versioned keys
Watch out for
- Using @cache_page on a page with user-specific content, serving one user's page to others.