Caching
Some things TaskFlow computes are expensive relative to how often they actually change — a dashboard count that scans thousands of tasks, a sidebar widget that queries several related tables just to render the same numbers it rendered a minute ago. Caching means storing the result of that work the first time it's computed, then reusing the stored result for a while instead of recomputing it on every single request. Django has a full caching framework built in for exactly this, and this lesson covers it end to end.
Choosing a Cache Backend
Django's CACHES setting decides where cached values actually live. Which backend
makes sense depends entirely on how the project is deployed:
| Backend | When it makes sense |
|---|---|
LocMemCache | Stores cached values in your Django process's own memory. Simple, no extra service to run, fine for local development or a small single-server app — but each server process has its own separate cache, so it doesn't scale across multiple processes or machines. |
| Redis | The typical production choice. Runs as its own separate service that every Django process (and every worker, across every server) can share, so all of them see the same cached values consistently. |
| Memcached | Similar role to Redis — a shared, external cache store built specifically for this purpose, and still common in older or simpler production setups. |
# settings.py
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
}
}
The Low-Level Cache API
The most direct way to use the cache is the low-level API — read and write individual values by key, anywhere in your code, with an explicit expiration time:
from django.core.cache import cache
def get_dashboard_stats():
stats = cache.get('dashboard_stats')
if stats is None:
# Expensive: touches every Project, annotated with a task count
# exactly like django-aggregation-annotation.php's Count() example
stats = list(
Project.objects.annotate(task_count=Count('tasks')).values('name', 'task_count')
)
cache.set('dashboard_stats', stats, timeout=300)
return stats
cache.get('dashboard_stats') returns None the first time — nothing's been
stored under that key yet — so the expensive annotated query runs once and the result is saved with
cache.set(), alongside a timeout of 300 seconds. Every call within the next
five minutes gets the stored list back instantly, with no query at all; after that, the key expires and
the next call recomputes it fresh.
Per-View Caching with cache_page
Rather than caching one value manually, @cache_page caches an entire view's rendered
output for a set number of seconds. It's a natural fit for a view that's read heavily but changes
rarely — a public project list, say, that most visitors just browse without editing:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15)
def public_project_list(request):
projects = Project.objects.all()
return render(request, 'taskflow/public_project_list.html', {'projects': projects})
60 * 15 is just seconds written as an obvious expression — 15 minutes. The first request
after the cache expires runs the view normally, including its database queries; every request in the
15 minutes after that gets back the exact same rendered HTML with no view code running at all.
Template Fragment Caching
Sometimes caching the whole view is too blunt — the page has one slow piece (say, a sidebar
widget listing each project's busiest tags) sitting alongside content that genuinely does need to be
fresh every request. The {% cache %} template tag caches just that one fragment:
{% load cache %}
{% cache 600 project_tag_widget %}
{% endcache %}
600 is the fragment's timeout in seconds, and project_tag_widget is just a
name identifying this particular cached fragment — everything between {% cache %} and
{% endcache %} is rendered once, stored, and reused directly for every request within
that window, while the rest of the template around it still renders normally every single time.
The Real Caveat: Stale Data
There's an old joke that there are only two hard things in computer science: cache invalidation, naming things, and off-by-one errors. The practical version of that problem, stripped of the joke: a cached value can go stale — wrong, out of date — the moment the underlying data changes, right up until the cache's timeout finally expires. A dashboard stat cached for five minutes might show a task count that's already three tasks behind reality.
One common, practical mitigation is deleting the specific cache key the instant the data behind it
changes, rather than waiting out the timeout. Signals covered
post_save as a hook that fires right after a model instance is saved — a natural place to
clear a now-outdated cache entry:
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.cache import cache
@receiver(post_save, sender=Task)
def clear_dashboard_cache(sender, instance, **kwargs):
cache.delete('dashboard_stats')
This isn't a complete solution to cache invalidation in general — just one common, honest pattern: when in doubt about whether a cached value is still accurate, clear it proactively the moment the data it depends on changes, rather than hoping the timeout is short enough not to matter.