Django Caching with Redis: Per-View, Template Fragment, and Low-Level
Speed up Django with Redis caching — per-view cache, template fragment cache, low-level cache API, and cache invalidation strategies.
What you'll learn
- ✓How to configure Django with Redis as the cache backend
- ✓How per-view caching eliminates repeated database queries
- ✓How template fragment caching speeds up expensive partials
- ✓How the low-level cache API gives you fine-grained control
- ✓Cache invalidation strategies that actually work
Prerequisites
- •A working Django project with views and templates
- •Redis installed locally or available as a service
A database query that runs in 50ms seems fast — until it runs 10,000 times per minute. Caching stores computed results so subsequent requests skip the expensive work entirely. Django’s cache framework is backend-agnostic, but Redis is the standard choice for production: it’s fast, supports expiration, and handles concurrent access well.
This guide covers all three caching layers Django provides, from coarse-grained view caching to fine-grained key-value operations.
Setting Up Redis as the Cache Backend
Install the Redis client:
pip install django-redis
Configure it in settings:
# settings.py
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
"KEY_PREFIX": "myapp",
"TIMEOUT": 300, # Default TTL: 5 minutes
}
}
The KEY_PREFIX namespaces your keys to avoid collisions if multiple apps share the same Redis instance. TIMEOUT sets the default expiration in seconds.
Verify the connection:
python manage.py shell
>>> from django.core.cache import cache
>>> cache.set("test", "working", 30)
>>> cache.get("test")
'working'
Per-View Caching
The simplest caching strategy. Wrap an entire view so the response is cached for a set duration.
# views.py
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # Cache for 15 minutes
def product_list(request):
products = Product.objects.select_related("category").all()
return render(request, "products/list.html", {"products": products})
Every request to this view hits Redis first. If a cached response exists and hasn’t expired, Django returns it immediately — no database query, no template rendering.
For class-based views, apply the decorator in urls.py:
# urls.py
from django.views.decorators.cache import cache_page
from .views import ProductListView
urlpatterns = [
path("products/", cache_page(60 * 15)(ProductListView.as_view()), name="product-list"),
]
Varying the Cache by Request Properties
If different users see different content, the cache key must vary:
from django.views.decorators.vary import vary_on_headers, vary_on_cookie
@cache_page(60 * 15)
@vary_on_cookie # Different cache entry per session cookie
def dashboard(request):
# User-specific data
...
# Or vary by specific headers
@cache_page(60 * 15)
@vary_on_headers("Authorization")
def api_view(request):
...
Template Fragment Caching
When a full view cache is too coarse — part of the page is dynamic but an expensive sidebar or widget can be cached — use fragment caching.
{% load cache %}
<div class="page">
<main>
{# This is dynamic — different for every request #}
<h1>{{ article.title }}</h1>
<p>{{ article.body }}</p>
</main>
<aside>
{# This is expensive but changes rarely — cache it for 10 minutes #}
{% cache 600 sidebar_popular_posts %}
<h2>Popular Posts</h2>
<ul>
{% for post in popular_posts %}
<li><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></li>
{% endfor %}
</ul>
{% endcache %}
</aside>
</div>
The {% cache %} tag takes a timeout (seconds), a fragment name, and optional vary arguments:
{# Cache per user — append user.id to the cache key #}
{% cache 600 user_notifications request.user.id %}
<div class="notifications">
{% for n in notifications %}
<p>{{ n.message }}</p>
{% endfor %}
</div>
{% endcache %}
Low-Level Cache API
For maximum control, use the cache API directly. This is ideal for caching query results, computed values, or API responses.
Basic Operations
from django.core.cache import cache
# Set a value with a 5-minute TTL
cache.set("homepage_stats", {"users": 1234, "posts": 567}, timeout=300)
# Get a value (returns None if missing)
stats = cache.get("homepage_stats")
# Get with a default
stats = cache.get("homepage_stats", default={"users": 0, "posts": 0})
# Delete a key
cache.delete("homepage_stats")
# Set only if the key doesn't exist
cache.add("lock:report_generation", True, timeout=60)
# Increment / decrement
cache.set("page_views", 0)
cache.incr("page_views")
cache.incr("page_views", delta=5)
The get-or-set Pattern
The most common pattern — check cache, compute if missing, store for next time:
from django.core.cache import cache
def get_trending_posts():
cache_key = "trending_posts_v1"
posts = cache.get(cache_key)
if posts is None:
# Cache miss — run the expensive query
posts = list(
Post.objects
.filter(status="published")
.annotate(score=F("views") + F("likes") * 3)
.order_by("-score")[:20]
.values("id", "title", "slug", "score")
)
cache.set(cache_key, posts, timeout=600) # 10 minutes
return posts
Django also provides cache.get_or_set() as a shortcut:
def get_trending_posts():
return cache.get_or_set(
"trending_posts_v1",
lambda: list(
Post.objects.filter(status="published")
.annotate(score=F("views") + F("likes") * 3)
.order_by("-score")[:20]
.values("id", "title", "slug", "score")
),
timeout=600,
)
Caching in Views
def product_detail(request, slug):
cache_key = f"product_detail:{slug}"
product = cache.get(cache_key)
if product is None:
product = get_object_or_404(
Product.objects.select_related("category").prefetch_related("images"),
slug=slug,
)
cache.set(cache_key, product, timeout=300)
return render(request, "products/detail.html", {"product": product})
Cache Invalidation
The hard part. Stale caches show users outdated data. Here are strategies that work.
Signal-Based Invalidation
Clear cache keys when the underlying data changes:
# blog/signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
from .models import Post
@receiver([post_save, post_delete], sender=Post)
def invalidate_post_caches(sender, instance, **kwargs):
cache.delete(f"post_detail:{instance.slug}")
cache.delete("trending_posts_v1")
cache.delete("homepage_stats")
Versioned Cache Keys
Instead of deleting keys, increment a version so old keys expire naturally:
def get_cache_version():
version = cache.get("post_cache_version")
if version is None:
version = 1
cache.set("post_cache_version", version, timeout=None)
return version
def get_trending_posts():
version = get_cache_version()
cache_key = f"trending_posts_v{version}"
return cache.get_or_set(cache_key, compute_trending, timeout=600)
def bump_post_cache():
"""Call this when posts change."""
cache.incr("post_cache_version")
Key Pattern Deletion
django-redis supports wildcard deletion:
from django_redis import get_redis_connection
conn = get_redis_connection("default")
keys = conn.keys("myapp:product_detail:*")
if keys:
conn.delete(*keys)
Use this sparingly — KEYS scans the entire keyspace and can block Redis on large datasets. Prefer explicit key tracking.
Caching Best Practices
Start with per-view caching for read-heavy pages. It’s the lowest effort for the biggest gain.
Use short TTLs over manual invalidation when stale data is acceptable for a few minutes. A 60-second TTL on a dashboard eliminates 99% of queries with minimal staleness.
Never cache authenticated or user-specific data with cache_page unless you add vary_on_cookie. Otherwise, user A might see user B’s dashboard.
Cache serializable values. Store dicts, lists, and primitives — not QuerySets or model instances. QuerySets are lazy and can’t be serialized meaningfully. Use .values() or convert to lists first.
Monitor cache hit rates. If your hit rate is below 80%, your TTLs are too short or your keys are too specific. Check with redis-cli INFO stats.
Summary
Django’s cache framework gives you three levels of caching: per-view for entire pages, template fragments for expensive partials, and the low-level API for precise control. Use Redis as the backend, start with coarse caching, and add signal-based invalidation as your needs grow. Short TTLs solve most staleness problems without complex invalidation logic.
Related articles
- Django Django Caching Strategies
Compare per-view, template fragment, low-level, and per-site caching in Django and learn when each pays off.
- Backend Caching Strategies: Write-Through, Write-Back, and TTL
Understand the major caching strategies for backend systems. Compare write-through, write-back, write-around, and cache-aside with real-world trade-offs.
- Django Django Database Optimization: select_related, prefetch_related, and Beyond
Eliminate N+1 queries in Django with select_related, prefetch_related, Prefetch objects, annotations, and query profiling tools.
- Django Django Celery Task Queue Tutorial
A practical guide to wiring Celery into Django for background work, scheduled jobs, and reliable task processing.