Caching: the cache framework and where it actually helps
Caching is storing the result of expensive work so you can reuse it instead of redoing it. Done well, it turns a slow page fast; done carelessly, it serves stale or wrong data and becomes the source of bugs that are maddening to track down ("it's showing the old price — but only sometimes"). Django has a full cache framework, and this lesson is how to use it, where it actually helps, and the discipline that keeps caching from creating more problems than it solves.
What caching is, and the one hard problem
Caching trades freshness for speed: you keep a computed result around and serve it again rather than recomputing. The speed-up can be enormous — a report that takes two seconds to build, served from cache in a millisecond. But it introduces the field's famous hard problem: invalidation — knowing when the cached value is stale and must be thrown away. Serve a cached value too long and users see old data; the entire skill of caching is deciding what to cache and for how long so that the staleness is acceptable. Cache the wrong thing and you get subtle "why is it showing yesterday's number?" bugs.
The cache framework: set, get, and a backend
Django's low-level cache API is simple:
from django.core.cache import cache
cache.set("todays_patient_count", 42, timeout=300) # store for 300 seconds
value = cache.get("todays_patient_count") # returns 42, or None if absent/expired
Verified: cache.set("k", "v", 30) followed by cache.get("k") returns "v". The timeout (seconds) is
how long the value lives before expiring — the primary invalidation tool. You configure a backend in
settings; the choice matters:
LocMemCache(default) — in-process memory. Fine for development, but not shared across processes, so with multiple web workers each has its own cache — useless for anything that must be consistent. Never rely on it in production.- Redis / Memcached — a shared, out-of-process cache all your workers hit. This is the real production choice — one cache, consistent across every worker.
# production
CACHES = {"default": {"BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": "redis://..."}}
The LocMemCache-in-production trap is real: it appears to work in development (one process) and behaves
inconsistently in production (many processes, many caches). Use a shared backend for anything that matters.
The cache_page decorator and template fragment caching
Two higher-level tools cover common cases. cache_page caches an entire view's rendered response:
from django.views.decorators.cache import cache_page
@cache_page(60 * 5) # cache this page for 5 minutes
def clinic_stats(request):
... # an expensive dashboard
Every request within five minutes gets the cached response without running the view. This is ideal for a page that is expensive to build and the same for everyone (a public stats page), and wrong for a page that is per-user or must be live (a patient's own records — you would serve one patient another's cached page, a serious bug). Template fragment caching caches part of a template:
{% load cache %}
{% cache 300 sidebar %}
... expensive sidebar ...
{% endcache %}
Use fragment caching when most of a page is cheap but one piece is expensive — cache just that piece.
Where caching actually helps — and where it does not
The judgement, plainly:
Cache when: the work is genuinely expensive (a heavy aggregation, an external API call, a slow report), the result is reused often, and some staleness is acceptable. A dashboard of clinic-wide statistics that changes slowly and is viewed constantly is the textbook case.
Do not cache when:
- The data must be live and correct — a patient's current balance, whether a slot is still free. Serving a stale answer here is a bug, not a performance win.
- The result is per-user and rarely reused — caching a page only one person sees once saves nothing and risks leaking it to the wrong user.
- The underlying query is already fast — caching a cheap query adds complexity and a staleness risk for
no real gain. Fix the query first (an index,
select_related) before reaching for a cache; caching a slow query is often papering over a missing index.
That last point is the most important discipline: caching is not the first response to a slow page. First make the query efficient (the indexes and N+1 lessons); cache only what is still expensive after that, and only where staleness is acceptable. A cache layered over unoptimised queries hides the real problem and adds an invalidation problem on top.
Invalidate deliberately
Because staleness is the danger, be deliberate about it:
- Prefer a short timeout you can reason about ("5 minutes stale is fine here") over trying to invalidate perfectly on every change — time-based expiry is simple and usually enough.
- When you must be fresher, invalidate on change: delete the cache key when the underlying data updates
(
cache.delete("key")in the save path). This is more precise but more code, and easy to get wrong (forget one update path and the cache goes stale). - Never cache without a timeout unless you have a clear invalidation strategy — an entry that never expires and is never deleted is stale data forever.
The rule that ties the lesson together: cache the expensive, reused, staleness-tolerant thing, on a shared backend, with a timeout you have chosen deliberately — and fix slow queries before caching, not instead of it.
Check your work
What caching trades. Freshness for speed — reuse a computed result instead of recomputing; the hard part is invalidation (knowing when it is stale).
The low-level API and backends. cache.set(key, value, timeout) / cache.get(key) (verified set/get);
LocMemCache is per-process (dev only — inconsistent across workers), Redis/Memcached is the shared
production choice.
Higher-level tools. cache_page caches a whole view's response (for expensive pages the same for
everyone, never per-user/live); {% cache %} caches a template fragment.
When to cache. Expensive work, reused often, staleness acceptable (a slow-changing dashboard). Not for live/correct data, per-user rarely-reused results, or already-fast queries.
Fix queries first. Caching is not the first response to a slow page — add the index / fix the N+1 first, cache only what is still expensive.
Invalidate deliberately. Prefer a chosen short timeout; delete keys on change when you need fresher; never cache without a timeout or a clear invalidation plan.
Practice
- Use
cache.set/cache.getin the shell with a short timeout; confirm the value returns, then expires. - Configure
LocMemCache, then reason about why it is wrong with multiple production workers; note what Redis changes. - Add
@cache_page(60)to an expensive public view; confirm repeated requests are fast, and reason about why you must not put it on a per-user view. - Cache an expensive template fragment with
{% cache %}; confirm the rest of the page still renders live. - Take a slow page, add an index / fix an N+1 to speed it up without caching, and reflect on why that is the right first step.
- Cache a value, then update the underlying data and observe the stale cache; add
cache.delete(key)to the update path and confirm it refreshes.
Official documentation
- Django — Django's cache framework — Backends, the API,
cache_page, fragment caching. - Django — Low-level cache API —
set/get/deleteand timeouts. - Django — Redis cache backend — The production backend.
Next: what to test in a Django app, and what not to.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship