Caching and Performance (Optional)
Introduction
This optional chapter introduces Django’s cache framework, per-view caching with @cache_page, low-level cache.get/set, and storing sessions in Redis—patterns that reduce database load as traffic grows. It links to the Redis track for infrastructure; you can skip this chapter until you deploy a site with performance requirements.
Prerequisites
- Settings and Multi-Environment
- QuerySets and Relationships —
select_relatedbaseline - Redis running locally or via Docker (optional hands-on)
When to Cache
| Cache | Skip cache |
|---|---|
| Expensive read-heavy pages | Personalized per-user HTML |
| Rarely changing lists | Strong consistency required on every read |
| Session/token lookups at scale | Data that must never be stale without invalidation |
Optimize queries first (select_related, prefetch_related, indexes)—caching is not a fix for N+1 queries.
Cache Backends
settings/base.py — local memory (single process, dev only):
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
}
}Production Redis (requires django-redis or Redis backend):
pip install django-redisCACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}Database cache and file-based backends exist—Redis is the common production choice. See Redis introduction.
Verify:
python manage.py shellfrom django.core.cache import cache
cache.set("ping", "pong", timeout=60)
cache.get("ping") # 'pong'Low-Level cache API
blog/views.py:
Invalidate on content change (signal or view after save):
cache.delete("post_list_html_v1")Prefer caching query results or fragment templates over full HttpResponse bytes when pages include user-specific nav.
Per-View Cache Decorator
from django.views.decorators.cache import cache_page
@cache_page(60 * 5) # 5 minutes
def post_list(request):
posts = Post.objects.filter(published=True)
return render(request, "blog/post_list.html", {"posts": posts})URL and query string participate in cache key—different ?page=2 gets separate entries.
Vary by header (e.g. language):
from django.views.decorators.vary import vary_on_headers
@vary_on_headers("Accept-Language")
@cache_page(3600)
def home(request):
...Template Fragment Caching
blog/templates/blog/post_list.html:
{% load cache %}
{% cache 500 post_list_sidebar %}
<aside>...</aside>
{% endcache %}Cache expensive partials independently of the full page.
Cache QuerySet Results
def get_popular_posts():
key = "popular_posts_ids"
ids = cache.get(key)
if ids is None:
ids = list(
Post.objects.filter(published=True)
.order_by("-created_at")[:10]
.values_list("id", flat=True)
)
cache.set(key, ids, timeout=600)
return Post.objects.filter(id__in=ids)Store primary keys, not model instances—pickle/version issues across deploys.
Sessions in Redis
Default session engine uses the database. Switch to cache backend:
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"Sessions then live in Redis when CACHES["default"] is Redis—reduces django_session table writes on busy sites.
Signed cookie sessions (signed_cookies) avoid server storage but limit session size and complicate revocation.
Database Connection conn_max_age
Reuse DB connections across requests (with Gunicorn workers):
DATABASES = {
"default": {
...
"CONN_MAX_AGE": 60,
}
}Not Redis—simple win alongside caching.
Middleware Cache (Awareness)
UpdateCacheMiddleware / FetchFromCacheMiddleware cache entire site for anonymous users—rare; CDN/Nginx caching often simpler.
Monitoring Cache Hit Rate
Log cache misses in dev; in production use Redis INFO stats, APM tools, or custom metrics. See Redis performance.
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Cache never invalidated | Stale content after edit | delete keys in post_save |
LocMemCache in multi-worker Gunicorn | Inconsistent cache per worker | Redis backend |
| Caching authenticated pages | User A sees User B data | vary_on_cookie or do not cache |
| Caching without query optimization | Still slow on miss | Fix ORM first |
| Huge values in Redis | Memory pressure | Shorter TTL, smaller payloads |
Post-Chapter Checklist
- You configured
CACHES(locmem or Redis) - You used
cache.get/setor@cache_page - You know when to invalidate cache keys
- You understand sessions-on-cache option
- You tried Redis
cache.setin shell (optional)
FAQ## FAQ
django-redis vs redis-py directly?
Use django-redis for Django cache API integration; raw redis-py for custom logic.
Cache template whole site?
Possible with middleware; CDN static + API caching often clearer for SPAs.
Memcached?
django.core.cache.backends.memcached.PyMemcacheCache—Redis more common today.
Per-user API rate limit?
DRF throttling classes or Nginx limit_req—related to performance/abuse.
This chapter required?
No—marked optional; return when scaling read traffic.