In episode 8, we attached a /health endpoint and metrics so we know whether the system is alive before users report it. Episode 9 answers the next question: how do we make the system respond faster as traffic grows? With Redis as a cache, the expense-tracking app can return the transaction list and the monthly report in a few milliseconds instead of querying PostgreSQL every time — but only if we cache the right things and invalidate at the right moment.
1. Install Redis and configure the cache
Install django-redis, then declare the default cache pointing at Redis. Keep Redis on the same machine or in its own container, set a password, and cap memory with maxmemory-policy allkeys-lru so Redis evicts the least-used keys when it fills up.
# settings/production.pyCACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": env("REDIS_URL"), # redis://127.0.0.1:6379/1 "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, "KEY_PREFIX": "expense", "TIMEOUT": 300, # 5 phút mặc định }}
2. Cache the transaction list per user
The first rule: the cache key must contain the user id, because spending data is private. Use cache.get, query the database only on a cache miss, and always set a timeout so stale data expires on its own.
# expenses/views.pyfrom django.core.cache import cachedef monthly_summary(request, year, month): key = f"summary:{request.user.id}:{year}-{month:02d}" data = cache.get(key) if data is None: data = compute_summary(request.user, year, month) cache.set(key, data, timeout=300) return JsonResponse(data)
3. Invalidate at the right moment: signals
A cache is only safe when changed data makes the old cache disappear immediately. Attach the post_save and post_delete signals of the Transaction model to delete the report key for exactly that month and that user, instead of clearing the whole cache. This keeps the cache hit rate high without ever serving stale figures.
# expenses/signals.pyfrom django.core.cache import cachefrom django.db.models.signals import post_save, post_deletefrom django.dispatch import receiverfrom .models import Transaction@receiver([post_save, post_delete], sender=Transaction)def invalidate_summary(sender, instance, **kwargs): key = f"summary:{instance.user_id}:{instance.date:%Y-%m}" cache.delete(key)
4. Common caching mistakes
- Sharing one cache key across all users: one person sees another person’s figures — always put the user id in the key.
- No timeout: a bad key sits in the cache until someone deletes it by hand — always set a default TIMEOUT.
- Caching a page that contains a CSRF token or sensitive data: only cache non-sensitive fragments, or skip caching.
- Clearing the cache with cache.clear() on every deploy: Redis goes empty and the database takes the full load — change KEY_PREFIX per deploy version instead of wiping everything.
Applying it to your project
Start from a single endpoint: the monthly report page of the expense-tracking app. Measure response time before and after enabling the cache using the /metrics endpoint from episode 8, then expand to the transaction list and the dashboard. If after a week the cache hit rate is below 50%, revisit the timeout and check whether the invalidation signal actually runs.
Related posts
- Django Running in Production #8: Monitoring and Health Checks — /health Endpoint, Uptime, Metrics with django-prometheus
- Django Production Operations #7: Centralized Logging and Error Capture with Sentry
- Django Production Operations #4: PostgreSQL in Production — Connections, CONN_MAX_AGE, PgBouncer Pooling, Basic Backups
FAQ
When should I use a cache, and when not?
Cache data that is read often and changes rarely: monthly reports, category lists. Do not cache data that must be visible the moment it is written, a balance right after a transaction, or a page holding session-sensitive information.
What if Redis loses its data on restart?
A cache is designed to be losable: when the cache is empty, the code queries the database again and refills the cache. As long as there is always a fallback path when cache.get returns None, the system still runs correctly — just slower for a while.
How long should the timeout be?
Start with 5 minutes for reports and 60 seconds for lists that change often, then adjust by the cache hit rate. A short timeout is safer than a long one, because stale data disappears on its own.
What is a cache stampede, and how do I avoid it?
It is when a hot key expires and hundreds of requests hit the database at once. The simple fix: use cache.get_or_set with a lock, or stagger timeouts so keys do not expire at the same time.
💬 Have you tried this in your own project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will fold good ideas into the post.
Sources
- Django documentation — Caching framework: https://docs.djangoproject.com/en/stable/topics/cache/
- django-redis: https://github.com/jazzband/django-redis
