Some endpoints are read often and change rarely: a vendor dashboard summary, a product catalogue, a configuration blob. Recomputing those on every request is wasted work. A Redis cache in front of them is one of the cheapest performance wins available.
First principles
A cache is a second copy of a value, kept where it is faster to read. That one fact explains most of this post.
- A copy can be wrong. Once the original changes, the copy is stale until it expires or someone deletes it. Every cache design is a decision about how long that is allowed to last.
- A copy has to be found again. The key is the only link between a request and its copy, so keys need a scheme, not improvised strings.
- A missing copy has to be rebuilt. When a copy expires, the next reader pays the full cost of building it. If many readers arrive at once, they all pay it together.
Decide what is worth caching
Good cache candidates share three traits: they are expensive to build, they are read far more than they are written, and a few seconds of staleness causes no harm. If a value must always be exact to the millisecond, it does not belong in a cache.
Configure Redis as the cache backend
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": os.environ["REDIS_URL"],
"TIMEOUT": 300,
}
}
Cache keys you can reason about
Keys are an interface. Build them deliberately, and include a version segment so you can invalidate every key at once by bumping a number.
def vendor_summary_key(vendor_id):
return f"vendor:summary:v2:{vendor_id}"
The get-or-set pattern
Every cached read follows the same shape: look in the cache, build the value on a miss, store it, return it.
from django.core.cache import cache
def get_vendor_summary(vendor_id):
key = vendor_summary_key(vendor_id)
summary = cache.get(key)
if summary is None:
summary = build_vendor_summary(vendor_id)
cache.set(key, summary, timeout=600)
return summary
Invalidation is the hard part
There are two honest strategies. A short timeout accepts brief staleness and never needs explicit cleanup. Active invalidation deletes the key the moment the underlying data changes, usually from a model signal.
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Order)
def drop_vendor_summary(sender, instance, **kwargs):
cache.delete(vendor_summary_key(instance.vendor_id))
Defend against the stampede
When a hot key expires, every request that arrives in the next moment misses the cache and rebuilds the value at once. Keys that were set together also expire together, so the misses pile up. Adding a small random offset to the timeout spreads those expiries out so they do not all land together.
import random
cache.set(key, summary, timeout=600 + random.randint(0, 120))
The offset only helps with keys that expire together. It does not stop many requests from rebuilding one very hot key at the same moment. For that, let one request rebuild the value while the others keep serving the old copy or wait briefly.
Cache the result, not the whole response
Caching at the function level, around the value itself, beats caching a full Hypertext Transfer Protocol (HTTP) response. The same cached summary can then feed an application programming interface (API) endpoint, a server-rendered page, and a background report without being computed three times.