Tabeer.ai's Redis instance was already doing real work in production — Celery's broker and result backend for background email tasks. It sat unused for anything else, which meant a public, frequently-hit endpoint like the platform stats view was recomputing the same aggregate query on every single request. Redis was right there. Using it as Django's cache backend too, rather than adding a second caching layer, was the obvious move.
Django 5's Built-In Redis Backend, No Extra Package
Older Django projects reach for django-redis as a third-party dependency. Django 5.x ships its
own django.core.cache.backends.redis.RedisCache — no separate package needed, one less
dependency to track for CVEs and version compatibility:
if DEBUG:
CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
else:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": env("REDIS_CACHE_URL", default="redis://localhost:6379/1"),
}
}
Same Redis instance Celery already talks to, but logical database 1 instead of 0 — Redis
supports up to 16 numbered databases within one instance by default, so cache keys and Celery's
broker/result data never collide even though they share the same server process. This mirrors the
DEBUG-driven fallback already in place for Celery's own eager-mode setting: local development
runs against in-memory caching with zero external dependencies, production runs against real
Redis, and it's the same if DEBUG branch pattern a contributor would already recognize from the
Celery config right above it.
Where It Actually Gets Used
PlatformStatsView — the endpoint the homepage calls for question counts, student counts, and
mock-test totals — was doing a handful of .count() and .aggregate() queries on every request,
for numbers that only meaningfully change a few times a day:
class PlatformStatsView(APIView):
CACHE_KEY = "content:platform_stats"
CACHE_TTL_SECONDS = 300
def get(self, request):
cached = cache.get(self.CACHE_KEY)
if cached is not None:
return Response(cached)
data = { ... } # the existing aggregate queries, unchanged
cache.set(self.CACHE_KEY, data, self.CACHE_TTL_SECONDS)
return Response(data)
A 5-minute TTL is short enough that a genuinely stale number (say, right after a new question is added) self-corrects quickly, and long enough to absorb the actual request volume this endpoint sees without recomputing on every hit.
Testing a Cache, Not Just Code That Calls .cache.set()
The easy version of this test just checks that cache.set gets called and calls it done — which
doesn't actually prove caching works, only that the code compiles. I wanted a test that proves a
real cache hit:
def test_response_is_cached_across_requests(self):
Question.objects.create(...)
first = self.client.get(url)
self.assertEqual(first.data["total_questions"], 1)
Question.objects.create(...) # a second question now exists in the DB
second = self.client.get(url)
self.assertEqual(second.data["total_questions"], 1) # still 1 — proves the cache, not the DB, answered this
cache.clear()
third = self.client.get(url)
self.assertEqual(third.data["total_questions"], 2) # recompute works once the cache is empty
The middle assertion is the actual test — if caching weren't working, that request would
correctly return 2, matching the database's true state. Getting 1 back is only possible if the
first response actually got served from cache instead of recomputed, which is the specific thing
worth verifying rather than assuming.
setUp(self): cache.clear() was also necessary — Django's test runner doesn't isolate the cache
between tests the way it isolates the database in a transaction, so a stale cached value from an
earlier test can silently make a later one pass or fail for the wrong reason.
Why Not django-redis
django-redis adds connection pooling controls and a few extra features (like native support for
Redis Sentinel) that this project doesn't need. Django's own RedisCache backend, added in
Django 5.0, covers the actual requirement — get/set/delete with a TTL, backed by a real
Redis connection — without a third-party dependency whose maintenance and security posture would
need separate tracking going forward.
If a hot endpoint in your Django app is doing real database work on every single request and you already have Redis running for something else, this is usually a same-day change. Get in touch.
By Shahid Malik