Speed Up Django With Template Fragment Caching

Use Django template fragment caching in production: the cache tag with vary-on keys, per-user and per-language fragments, cached template loaders, invalidation with make_template_fragment_key, and avoiding stale or leaked content.

Problem statement

A page is slow, but caching the whole response is impossible because part of it is personal: the user's name, cart count, or permissions. Profiling shows most time goes into a few expensive sections, such as a category menu built from dozens of queries, a "popular products" sidebar, or a rendered list of comments. Django's {% cache %} template tag caches just those fragments, keyed by whatever makes them vary, while the rest of the page stays dynamic.

Caching part of a pagePage requestpersonal headerFragment cachemenu, sidebarMissrender once, storeHitskip queriesCache the expensive, shared fragments and keep personal parts rendered per request.
  1. Configure a shared cache backend (Redis or Memcached), not LocMemCache.
  2. Wrap expensive template sections: {% load cache %}{% cache 600 sidebar %}...{% endcache %}.
  3. Add vary-on arguments for anything that changes the output: {% cache 600 product_card product.pk product.updated_at %}.
  4. Include the language for translated content and the user only when the fragment is personal.
  5. Invalidate explicitly with make_template_fragment_key when data changes, or let updated_at in the key do it.
  6. Make sure querysets used only inside the fragment are lazy so a cache hit skips the queries.

Step-by-step solution

1. Cache a shared fragment

{% load cache %}
{% cache 900 category_menu request.LANGUAGE_CODE %}
  <nav>
    {% for cat in categories %}
      <a href="{{ cat.get_absolute_url }}">{{ cat.name }} ({{ cat.product_count }})</a>
    {% endfor %}
  </nav>
{% endcache %}

Pass categories as a lazy queryset (not a list) from the view or a context processor; on a cache hit the template never iterates it, so the queries never run.

2. Vary on what changes the output

Every value that changes the rendered HTML must be part of the key:

{% cache 3600 product_card product.pk product.updated_at request.LANGUAGE_CODE %}
  {% include "shop/_product_card.html" %}
{% endcache %}

Including updated_at creates a new key whenever the product changes, so the old fragment simply expires unused, which avoids explicit invalidation.

what each part of the cache tag does{% load cache %}{% cache3600product_cardproduct.pkproduct.updated_atLANGUAGE_CODE %}{% include "shop/_product_card.html" %}{% endcache %}1231Timeout in seconds; None means never expire.2Fragment name: unique per template block.3Changes the key when data changes, so no manual invalidation.Every value that changes the HTML must appear in the keydjango-deployment.com
Missing a vary-on value shows one user's or language's fragment to others.

3. Personal fragments

If a fragment depends on the user, include request.user.pk in the key, or better, keep personal parts outside cached fragments. Caching per user multiplies entries by the number of active users and helps only when each user loads the page repeatedly.

Never cache a fragment that contains {% csrf_token %} without varying on the user's session; the cached token would be served to other users and their forms would fail.

4. Invalidate explicitly when needed

When a key cannot include a version field (for example a sidebar of "latest posts"), delete it on change:

from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=Post)
def clear_latest_posts(sender, **kwargs):
    for lang in ("en", "de"):
        cache.delete(make_template_fragment_key("latest_posts", [lang]))

The vary-on list must match the tag's arguments exactly.

5. Enable the cached template loader

Separate from fragment caching, Django's cached template loader keeps compiled templates in memory. It is enabled automatically when DEBUG = False and loaders is not set explicitly; if you configure loaders yourself, wrap them in django.template.loaders.cached.Loader.

what should be cached?Is the whole page the same for every visitor?yescache_page or CDNwhole responsemostly, small personal bitsFragment cacheexpensive shared partsdata reused across pagesLow-level cachecache.get_or_setFragment caching fits pages that are mostly shared with a personal headerdjango-deployment.com
Combine levels: a CDN for anonymous pages, fragments for logged-in ones.

Verification check: with django-debug-toolbar locally or query logging on staging, load the page twice: the second load should run far fewer queries. In production, compare response times before and after, and check Redis keyspace_hits rising.

Explanation

The {% cache %} tag builds a key from the fragment name and the vary-on values, looks it up in the default cache, and either outputs the stored HTML or renders the block and stores it. Because Django templates evaluate querysets lazily, skipping the block on a hit also skips the database queries inside it. The pattern works best for fragments that are expensive to build and shared across many requests, with keys that capture every input that changes the output.

Edge cases / notes

  • Cache alias. Use {% cache 600 name using="fragments" %} to store fragments in a separate cache with its own memory and eviction.
  • Template changes after a deploy do not change keys; bump a version in the fragment name or clear the cache on deploy.
  • Stampedes on very hot fragments can happen when many requests miss at once; a slightly randomised timeout spreads expiry.
  • HTML size. Large fragments consume cache memory; cache the expensive inner parts instead.

Next steps

FAQ

What is template fragment caching in Django?

Caching the rendered HTML of part of a template with the {% cache %} tag, keyed by a name and vary-on values.

How do I invalidate a cached fragment?

Include a version or updated_at in the key, or delete it with make_template_fragment_key.

Can I cache fragments that show user data?

Only by varying on the user, which multiplies entries. Usually keep personal parts outside cached fragments.

Why does fragment caching not reduce queries?

The data was evaluated in the view (for example list(queryset)). Pass lazy querysets so a cache hit skips them.

Keep going