erb python 88 lines · 3 tabs

Cache an Expensive Dashboard Fragment with Django's Template Cache Tag and Signal-Based Invalidation

Shared by codesnips Sep 2026
3 tabs
{% load cache %}
<section class="dashboard">
  <h1>{{ team.name }} — Overview</h1>

  {% cache 3600 team_order_stats team.id stats_version %}
    <div class="stat-grid">
      <div class="stat">
        <span class="label">Orders</span>
        <span class="value">{{ stats.total_orders }}</span>
      </div>
      <div class="stat">
        <span class="label">Revenue</span>
        <span class="value">${{ stats.total_revenue|floatformat:2 }}</span>
      </div>
      <div class="stat">
        <span class="label">Avg. Order</span>
        <span class="value">${{ stats.avg_order|floatformat:2 }}</span>
      </div>
    </div>
  {% endcache %}
</section>
3 files · erb, python Explain with highlit

This snippet shows how to cache an expensive dashboard fragment using Django's {% cache %} template tag, and then keep that cache correct with signal-based invalidation rather than relying on time-to-live expiry alone.

In dashboard.html, the fragment that aggregates order statistics is wrapped in {% cache %}. The tag takes a timeout, a fragment name, and a set of vary-on keys — here the team.id and a version token from the context. The version token is the trick that makes precise invalidation possible: {% cache %} builds its key by hashing all vary-on arguments, so changing the token effectively points the template at a fresh cache slot without needing to compute and delete the exact key.

In views.py, TeamDashboardView computes stats_version by reading a small integer from the cache under a per-team key. This read is cheap compared to the aggregate query it guards. The heavy annotate/aggregate work happens inside the cached block, so on a warm cache the ORM query never runs. make_naive timeouts and the vary_on list keep tenants isolated so one team never sees another's numbers.

In signals.py, a post_save and post_delete handler on Order calls bump_stats_version, which uses cache.incr (falling back to cache.set when the key is absent) to advance the per-team token. Because the template's vary-on now includes a new value, the next render misses the cache and recomputes, while every other team's fragment stays warm. This is far cheaper and safer than trying to reconstruct the exact make_template_fragment_key and delete it.

The approach trades a tiny extra cache read per request for correctness: data-changing events, not clock time, drive invalidation. A generous timeout acts only as a safety net. Pitfalls to watch include ensuring the signal fires for bulk operations — QuerySet.update() and bulk_create() bypass post_save, so those paths must bump the version explicitly. Reaching for this pattern makes sense when a fragment is expensive to build, changes infrequently relative to reads, and must reflect writes promptly.


Related snips

ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
python
class Product(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    cost = models.DecimalField(max_digits=10, decimal_places=2)
    margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)

Django model signals vs overriding save

django python models
by Priya Sharma 2 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
ruby
Rails.application.configure do
  config.after_initialize do
    Bullet.enable = true
    Bullet.alert = false
    Bullet.bullet_logger = true
    Bullet.console = true

N+1 query detection with Bullet gem

rails performance activerecord
by Alex Kumar 2 tabs
python
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [

Django URL namespacing and reverse lookups

django python urls
by Priya Sharma 3 tabs
ruby
json.array! @posts do |post|
  json.cache! ['v1', post], expires_in: 1.hour do
    json.id post.id
    json.title post.title
    json.excerpt post.excerpt
    json.published_at post.published_at

Fragment caching for expensive JSON serialization

rails caching performance
by Alex Kumar 1 tab

Share this code

Here's the card — post it anywhere.

Cache an Expensive Dashboard Fragment with Django's Template Cache Tag and Signal-Based Invalidation — share card
Link copied