Django cookbook · Intermediate
Cache expensive queries and invalidate on change
A dashboard query takes seconds, but caching it shows stale data after edits.
Approach
Cache with cache.get_or_set under a versioned key, and delete the key after the transaction that changes the
data commits (transaction.on_commit), so readers never re-cache data from a rolled-back write.
Solution
from django.core.cache import cache
from django.db import transaction
from django.db.models import Sum
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from shop.models import Order
STATS_KEY = "dashboard:stats:v2"
def dashboard_stats() -> dict:
def compute():
totals = Order.objects.filter(status="paid").aggregate(revenue=Sum("total"))
return {"revenue": totals["revenue"] or 0, "orders": Order.objects.filter(status="paid").count()}
return cache.get_or_set(STATS_KEY, compute, timeout=300)
@receiver([post_save, post_delete], sender=Order)
def invalidate_stats(sender, **kwargs):
transaction.on_commit(lambda: cache.delete(STATS_KEY))
Pitfalls
- Deleting the key before the transaction commits, so a concurrent request caches old data again.
- Caching per-user data under a shared key.
- No timeout, so a missed invalidation leaves data stale forever.
Variations
@cache_page(60)caches whole public views; vary on headers with@vary_on_headers.- Bump the key version (
v2) when the cached data's shape changes.
Further reading: Django's cache framework
Reference: https://docs.djangoproject.com/en/6.0/topics/cache/
More Django recipes
- Add a required column without downtime
- Add full-text search with PostgreSQL
- Add health checks and structured logging
- Avoid N+1 queries with select_related and prefetch_related
- Call external APIs concurrently in async views
- Configure settings from environment variables
- Enforce rules with database constraints
- Harden security settings, including a Content Security Policy