anishfyi.com Projects All writings

March 16, 20268 min readRedis, Performance, Django

Caching High-Traffic Endpoints with Redis

A practical caching layer for read-heavy endpoints, covering cache keys, invalidation, and stampede protection.

Some endpoints are read often and change rarely. A vendor dashboard summary, a product catalogue, a config blob. Recomputing those on every request is wasted work. A Redis cache in front of them is one of the cheapest performance wins available.

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. 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))

Cache the result, not the whole response

Caching at the function level, around the value itself, beats caching a full HTTP response. The same cached summary can then feed an API endpoint, a server-rendered page, and a background report without being computed three times.

Working on something in this territory? Book thirty minutes.