Django cookbook · Intermediate

Cache expensive queries and invalidate on change

A dashboard query takes seconds, but caching it shows stale data after edits.

Create a free account to save this page, take notes and track your progress. Sign in

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

Was this page helpful?