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

When to Cache

CacheSkip cache
Expensive read-heavy pagesPersonalized per-user HTML
Rarely changing listsStrong consistency required on every read
Session/token lookups at scaleData 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):

python
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "unique-snowflake",
    }
}

Production Redis (requires django-redis or Redis backend):

bash
pip install django-redis
python
CACHES = {
    "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:

bash
python manage.py shell
python
from 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):

python
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

python
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):

python
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:

django
{% load cache %}
{% cache 500 post_list_sidebar %}
  <aside>...</aside>
{% endcache %}

Cache expensive partials independently of the full page.

Cache QuerySet Results

python
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:

python
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):

python
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

MistakeSymptomFix
Cache never invalidatedStale content after editdelete keys in post_save
LocMemCache in multi-worker GunicornInconsistent cache per workerRedis backend
Caching authenticated pagesUser A sees User B datavary_on_cookie or do not cache
Caching without query optimizationStill slow on missFix ORM first
Huge values in RedisMemory pressureShorter TTL, smaller payloads

Post-Chapter Checklist

  • You configured CACHES (locmem or Redis)
  • You used cache.get/set or @cache_page
  • You know when to invalidate cache keys
  • You understand sessions-on-cache option
  • You tried Redis cache.set in 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.