Django's Four Levels of Caching, and How to Choose
Per-site, per-view, template fragment, low-level: most developers only know one, and it's usually the wrong one for the job. Caching is the highest-leverage performance tool in Django. A cached response can be hundreds of times faster than one that hits the database and renders a template, because you're handing back a stored answer instead of computing it again. But here's what trips people up: Django doesn't have a cache. It has four levels of caching, arranged from "cache the entire website" down to "cache this one number," and choosing the right level is the whole skill. Most developers learn exactly one (@cache_page), reach for it everywhere, and either cache too much (and serve stale or wrong data) or too little (and leave performance on the table). Let's fix that by walking the full ladder, from the widest, bluntest tool to the finest, most surgical one, so you know exactly which to reach for. Everything here is current for Django 6.x. And the good news up front: the Redis backend is now built into Django (django.core.cache.backends.redis.RedisCache), so you no longer need a third-party package to use the best production cache. First, the Setup: Pick a Backend (Once) Before any caching happens, you tell Django where to store cached data, in the CACHES setting. For production, that's almost always Redis: an in-memory store that's fast, shared across all your app servers, and now natively supported. # settings.py CACHES = { "default": { "BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", "KEY_PREFIX": "myapp", # namespace your keys to avoid collisions "TIMEOUT": 300, # default: entries expire after 5 minutes } } One principle worth internalizing from the Django docs before we go further: No cache backend should ever be used for permanent storage. Caches are for temporary copies of data you can always recompute. A cache can evict anything at any time, so never store something there you can't afford to lose. Cache is a speed layer, never a database. With a backend chosen, the four levels are just different scopes at which you apply it. Here's the ladder, widest to narrowest. Level 1: Per-Site Caching (the Sledgehammer) The broadest option: cache every page on your entire site. You add two middleware (UpdateCacheMiddleware and FetchFromCacheMiddleware) and Django caches the full response of every page for a set time. MIDDLEWARE = [ "django.middleware.cache.UpdateCacheMiddleware", # MUST be first "django.middleware.common.CommonMiddleware", "django.middleware.cache.FetchFromCacheMiddleware", # MUST be last ] CACHE_MIDDLEWARE_SECONDS = 600 # cache every page for 10 minutes The ordering isn't arbitrary. UpdateCacheMiddleware goes first and FetchFromCacheMiddleware last so that "fetch from cache on the way in, save to cache on the way out" wraps everything: the onion, from the request-lifecycle article. When it fits: a mostly-static, content-heavy site where nearly every page is the same for every visitor, like a blog, a documentation site, or a marketing site. When it's a disaster: any site with logged-in users or personalized pages, because per-site caching will happily cache one user's logged-in page and serve it to everyone. It's the bluntest tool on the ladder: powerful for the narrow case, dangerous outside it. Most real apps skip straight past it. Level 2: Per-View Caching (the One Everybody Knows) Down one notch in granularity: instead of the whole site, cache the output of specific views. This is the @cache_page decorator, and it's the level the docs (and everyone else) recommend you start with: from django.views.decorators.cache import cache_page @cache_page(60 * 15) # cache THIS view's response for 15 minutes def product_list(request): products = Product.objects.all() # expensive query - now runs once per 15 min return render(request, "products.html", {"products": products}) The first request runs the view, hits the database, renders the template, and stores the whole response. For the next 15 minutes, every request to that URL gets the stored response instantly: no query, no rendering. For a read-heavy page that's expensive to build and the same for everyone, this is a massive, one-line win. But here's the critical thing to understand about it, straight from the docs: The per-view cache is keyed off the URL. It stores "the response for /products/" and serves that to anyone who requests /products/. That's perfect for a public product list, and a data leak for anything personalized. A dashboard at /dashboard/ would get cached for the first user and served to the next. cache_page has no idea who is asking; it only knows the URL. Which brings us to the fix that makes per-view caching safe for varying content. The Vary Fix: Caching That Respects Who's Asking The URL-only key is the source of most cache_page bugs, and Django's solution is the Vary header and its decorators. Vary tells the cache: "the response depends on this request header too, so key the cache on it as well." from django.views.decorators.cache import cache_page from django.views.decorators.vary import vary_on_headers, vary_on_cookie # Cache SEPARATELY per language - English and French get their own cache entries @cache_page(60 * 15) @vary_on_headers("Accept-Language") def product_list(request): ... # Cache separately per user session - each cookie gets its own entry @cache_page(60 * 15) @vary_on_cookie def user_dashboard(request): ... vary_on_headers("Accept-Language") makes the cache key include the language header, so English and French visitors get different cached entries instead of one being served the other's page. vary_on_cookie keys on the session cookie, so each user gets their own cache entry. That's how you'd (carefully) cache a per-user page without leaking one user's data to another. The rule: the moment a cached view's output depends on anything beyond the URL (language, cookies, user), you must Vary on it, or the cache will serve the wrong version. There's also never_cache to explicitly mark a view as uncacheable, and cache_control to tune the headers. Both are useful escape hatches. Level 3: Template Fragment Caching (Cache Part of a Page) Sometimes a page is mostly dynamic but has one expensive, rarely-changing chunk: a sidebar of "top categories," a footer of popular posts, a rendered navigation menu. Caching the whole view is wrong (the rest is personalized), but recomputing that chunk every request is wasteful. Template fragment caching caches just that piece of the template: {% load cache %} {# cache this block for 30 minutes under the name "sidebar" #} {% cache 1800 sidebar %} {% for category in expensive_category_list %} {{ category.name }} {% endfor %} {% endcache %} Everything outside the {% cache %} block renders fresh on every request; everything inside is stored and reused. You can also make a fragment vary per-something by adding a key variable: {% cache 1800 sidebar request.user.id %} gives each user their own cached fragment. This is the level people forget exists, and it's exactly right for "the page is dynamic but this part is expensive and stable." Level 4: The Low-Level Cache API (Surgical Control) At the finest grain, you forget views and templates entirely and cache arbitrary Python data (the result of one expensive query, an API call, a computed value) with a direct get/set interface: from django.core.cache import cache def get_dashboard_stats(user): key = f"dashboard_stats:{user.id}" # you control the exact key stats = cache.get(key) # try the cache first if stats is None: # cache miss - compute it stats = expensive_stats_calculation(user) cache.set(key, stats, 60 * 10) # store for 10 minutes return stats This is the most powerful and most flexible level, because you decide exactly what to cache, under exactly what key, for exactly how long, and crucially, exactly when to invalidate it (cache.delete(key) when the underlying data changes). When per-view caching is too coarse (you only want to cache the data, not the whole response, and you want the response itself to stay per-request), this is the tool. It's the level you graduate to when the decorators aren't precise enough, which in a real personalized app is often. Choosing the Right Level (the Actual Decision) Here's the ladder as a decision, from what you should reach for first to the escape hatches: Start with per-view (@cache_page) on read-heavy pages that are the same for everyone: public listings, article pages, landing pages. Biggest win for least effort. Add Vary the instant the output depends on language, cookies or user. Use template fragment caching when a page is mostly dynamic but has an expensive, stable chunk: sidebars, menus, "popular" widgets. Drop to the low-level API when you need to cache specific data under your own keys with your own invalidation: per-user stats, an expensive query's result, a third-party API response. This is where serious apps spend most of their caching effort. Consider per-site caching only for genuinely static, no-login sites, and be very sure there's no personalized content, or you'll leak pages between users. The mental model: you're choosing a scope. What's expensive and stable Caching level The whole site Per-site A whole page Per-view Part of a page Template fragment A specific piece of data Low-level API Match the scope of the cache to the scope of what's actually expensive-and-stable, and you get the speed without caching things that shouldn't be shared. The Two Rules That Keep Caching from Biting You Whatever level you pick, two principles prevent the classic caching disasters. 1. Never cache anything personalized under a key that doesn't include the user. This is the cache_page-keys-by-URL trap in general form. If the data differs per user (or per tenant, per language), the cache key must differ too: via Vary, a fragment key variable, or a user ID in your low-level key. Otherwise you serve one person's data to another. (This is a whole class of bug worth its own deep-dive.) 2. Have an invalidation plan, don't just rely on TTL. A timeout means "serve stale data for up to N seconds." Fine for a product list; dangerous for a permission or a balance. For data that must be fresh on change, actively cache.delete() the key when the underlying data updates, and keep the TTL only as a safety net. Why Understanding This Makes You Better at Django Most developers' entire caching knowledge is "slap @cache_page on it," which means they can only cache things that are identical for every user for a fixed time. That's a small slice of what actually needs caching in a real app. The developer who knows the full ladder looks at a slow page and asks the right question: is the whole thing cacheable, or just a fragment, or just the data behind it? Then they pick the scope that captures the expensive-and-stable part without accidentally sharing personalized content. That single distinction, match the cache scope to what's actually expensive and stable, is the difference between caching that speeds things up safely and caching that either does nothing useful or leaks data between users. And the framework itself is elegant once you see it as a ladder rather than a single feature: one backend (Redis, now built in), applied at four scopes from whole-site to single-value, each with a way to vary by user when needed. Reach for per-view first, fragment when only a chunk is expensive, and the low-level API when you need real control. Cache is transformed from a mysterious speed trick into a deliberate tool you aim precisely at the slow, stable parts of your app. The 30-Second Takeaway Django has four caching levels, from widest to narrowest scope, and choosing the right scope is the whole skill. Configure a backend once (Redis is now built in: django.core.cache.backends.redis.RedisCache). Never use a cache for permanent storage. Per-site (cache middleware): caches every page. Only for static, no-login sites, or it serves one user's page to everyone. Per-view (@cache_page): caches a specific view's response. Start here for read-heavy public pages. It's keyed by URL only, so add Vary (vary_on_headers, vary_on_cookie) the moment output depends on language, cookies or user, or you'll serve the wrong version. Template fragment ({% cache %}): caches just an expensive, stable chunk of an otherwise-dynamic page (sidebars, menus). Add a key variable to vary per user. Low-level API (cache.get/set/delete): cache arbitrary data under your own keys with your own invalidation: per-user stats, query results, API responses. Where serious apps do most caching. Choose by scope: whole site → per-site; whole page → per-view; part of a page → fragment; a piece of data → low-level. Two safety rules: never cache personalized data under a key that doesn't include the user (or you leak data between users); and actively invalidate on change for data that must be fresh, keeping TTL as a backstop. Your Turn Look at your slowest page and ask one question: is the whole thing the same for every user, or just part of it, or just the data behind it? Your answer picks your caching level, and picking the right one is most of the battle. Which caching level does your app lean on most? Tell me in the comments 👇 If this was useful, follow me here for more stories from the backend trenches, and subscribe to The Backend Tech Lead on Substack for weekly blueprints.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to