Set Cache-Control Headers in Django for CDNs and Browsers

Control how browsers and CDNs cache Django responses: cache_control and never_cache decorators, public versus private, max-age versus s-maxage, Vary headers from sessions and cookies, ETags, and safe defaults for logged-in pages.

Problem statement

Every response from Django can tell browsers and CDNs whether and how long to cache it. Without explicit headers, behaviour depends on defaults: CDNs often do not cache HTML at all, browsers revalidate unpredictably, and a misconfigured rule can cache a logged-in page and show it to the next visitor. Setting Cache-Control, Vary, and validators deliberately makes public pages fast and keeps private pages private.

Who may cache a response?Django viewsets headersCache-Controlpublic, s-maxageVaryCookie, languageCDN + browserobey or skipHeaders from Django decide where a response may be stored and for how long.
  1. Public, anonymous pages: @cache_control(public=True, max_age=60, s_maxage=600).
  2. Personal pages: @cache_control(private=True, no_cache=True) or @never_cache.
  3. Understand Vary: Cookie: any view that touches the session adds it, which makes shared caches skip or fragment the response.
  4. Static files with hashed names: public, max-age=31536000, immutable (WhiteNoise sets this).
  5. Use ConditionalGetMiddleware or @condition for ETags and Last-Modified so revalidation returns 304.
  6. Verify with curl -I through the CDN and check its cache status header.

Step-by-step solution

1. Decorate views explicitly

from django.views.decorators.cache import cache_control, never_cache

@cache_control(public=True, max_age=60, s_maxage=600)
def pricing(request):
    return render(request, "pricing.html")

@never_cache
def dashboard(request):
    return render(request, "dashboard.html", {"orders": request.user.orders.all()})

max_age applies to browsers and shared caches; s_maxage applies only to shared caches such as CDNs and overrides max_age there. A short browser lifetime with a longer CDN lifetime lets you purge the CDN on change while browsers recheck soon.

2. Know the directives

  • public: any cache may store it. private: only the user's browser.
  • no-cache: may be stored but must be revalidated before use. no-store: never stored.
  • max-age=N and s-maxage=N: freshness lifetimes in seconds.
  • stale-while-revalidate=N: serve stale content while fetching a fresh copy (supported by many CDNs).
  • immutable: the content will never change at this URL.
headers by response typeCache-ControlHashed static files✓public, max-age=31536000, immutablePublic marketing pagepublic, max-age=60, s-maxage=600API list, public datapublic, max-age=30, stale-while-revalidate=60Logged-in page!private, no-cacheForms with CSRF!private, no-storePublic only when the response is identical for everyonedjango-deployment.com
When unsure, choose private: a missed cache is slower, a leaked page is a breach.

Django adds Vary: Cookie whenever a view accesses request.session or request.user, including indirectly through templates and context processors ({{ user }}, {% csrf_token %}). Shared caches then key responses by the whole Cookie header, which is unique per visitor, so nothing is shared. For truly public pages, avoid touching the session: use a separate base template without the user menu, or load personal parts with JavaScript.

4. Add validators for cheap revalidation

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.middleware.http.ConditionalGetMiddleware",
    # ...
]

ConditionalGetMiddleware adds an ETag based on the content and returns 304 Not Modified when the client's If-None-Match matches. For expensive pages, compute the validator before rendering with @condition(etag_func=..., last_modified_func=...) so unchanged pages skip rendering entirely.

5. Set defaults centrally

A small middleware can apply a safe default to any response that has no Cache-Control, for example private, no-cache for authenticated users, so forgetting a decorator never makes a personal page cacheable.

cache headers you can trustExplicit decorators on cacheable viewspublic, s-maxagenever_cache on personal pagesdashboards, accountPublic templates avoid the sessionno Vary: CookieETags or Last-Modified304 on revalidationCDN cache status checkedHIT, MISS, BYPASSVerify headers through the CDN, not only against Gunicorndjango-deployment.com
Look for cf-cache-status, x-cache, or age headers in responses.

Verification check:

curl -sI https://example.com/pricing/ | grep -iE "cache-control|vary|age|cf-cache-status|x-cache"
curl -sI https://example.com/dashboard/ -b "sessionid=..." | grep -i cache-control

The pricing page should show public and, on the second request, a CDN HIT; the dashboard should show private.

Explanation

HTTP caching is controlled by response headers that every cache in the path interprets. Cache-Control sets who may store a response and for how long, Vary lists request headers that produce different responses (so caches must key on them), and validators (ETag, Last-Modified) allow cheap revalidation. Django provides decorators and middleware for each, but leaves defaults conservative. Being explicit per view turns caching from accidental into designed, which is both faster and safer.

Edge cases / notes

  • CDN overrides. Some CDNs ignore origin headers for HTML unless configured to respect them; check your CDN's cache rules.
  • Redirects and errors can be cached too; avoid long lifetimes on 301s you might change, and never cache 5xx responses.
  • Authorization header requests are not stored by shared caches unless public is set explicitly.
  • Per-site cache middleware sets these headers automatically based on CACHE_MIDDLEWARE_SECONDS, but only for responses it considers cacheable.

Next steps

FAQ

How do I set Cache-Control headers in Django?

Use the cache_control and never_cache decorators from django.views.decorators.cache, or set response["Cache-Control"].

What is the difference between max-age and s-maxage?

max-age applies to all caches; s-maxage applies only to shared caches like CDNs and takes precedence there.

Why does my CDN not cache Django pages?

Often Vary: Cookie from session access, missing public, or CDN rules that bypass HTML.

How do I stop logged-in pages being cached?

Use @never_cache or private, no-cache, and set a safe default in middleware.

Keep going