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.
- Public, anonymous pages:
@cache_control(public=True, max_age=60, s_maxage=600). - Personal pages:
@cache_control(private=True, no_cache=True)or@never_cache. - Understand
Vary: Cookie: any view that touches the session adds it, which makes shared caches skip or fragment the response. - Static files with hashed names:
public, max-age=31536000, immutable(WhiteNoise sets this). - Use
ConditionalGetMiddlewareor@conditionfor ETags andLast-Modifiedso revalidation returns 304. - Verify with
curl -Ithrough 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=Nands-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.
3. Watch Vary: Cookie
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.
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
publicis set explicitly. - Per-site cache middleware sets these headers automatically based on
CACHE_MIDDLEWARE_SECONDS, but only for responses it considers cacheable.
Next steps
- Cache HTML at the edge with Cache Django Pages at Cloudflare's Edge.
- Or at your own proxy with Nginx proxy_cache for Django.
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.