Cache Django Pages at Cloudflare's Edge
Cache Django HTML at Cloudflare’s edge safely: Cache Rules for anonymous pages, bypassing cache when session or CSRF cookies exist, respecting origin Cache-Control, purging on deploy or content change, and avoiding cached private pages.
Problem statement
Cloudflare caches static files by default but not HTML. For content-heavy Django sites (blogs, documentation, marketing pages, product catalogues), most page views are anonymous and identical, so caching HTML at the edge can cut origin load dramatically and make pages load from a nearby data centre. The risk is caching something personal: a page rendered for a logged-in user, a form with someone's CSRF token, or a cart. A safe setup caches only anonymous requests and lets Django's headers decide lifetimes.
- Make cacheable views send
Cache-Control: public, s-maxage=...and avoid touching the session on them. - Create a Cloudflare Cache Rule: eligible for cache when the request is for your HTML paths and has no
sessionidorcsrftokencookie. - Set Edge TTL to "Use cache-control header if present" so Django controls lifetimes; bypass when
privateorno-store. - Exclude
/admin/,/accounts/,/api/, checkout, and any POST endpoints. - Purge by URL or prefix when content changes (from Django signals or your CMS), and after deploys that change templates.
- Verify with the
cf-cache-statusheader.
Step-by-step solution
1. Prepare Django responses
from django.views.decorators.cache import cache_control
@cache_control(public=True, max_age=60, s_maxage=3600, stale_while_revalidate=60)
def article_detail(request, slug):
article = get_object_or_404(Article, slug=slug, published=True)
return render(request, "articles/detail.html", {"article": article})
The template must not use request.user, {% csrf_token %}, or messages for anonymous visitors, or Django adds Vary: Cookie and sets cookies. Load personal bits (login state in the header) with a small JavaScript fetch to an uncached endpoint.
2. Create the Cache Rule
In Cloudflare → Caching → Cache Rules, add a rule with an expression like:
(http.request.method eq "GET")
and starts_with(http.request.uri.path, "/articles/")
and not http.cookie contains "sessionid"
and not http.cookie contains "csrftoken"
Action: Eligible for cache, Edge TTL Use cache-control header if present, bypass cache if not, and Browser TTL Respect origin. Add a second rule with higher priority that bypasses cache for /admin/, /accounts/, /api/, /cart/, and /checkout/.
3. Keep forms working
Pages with forms need CSRF tokens tied to the visitor. Either exclude them from caching, or render the form page without a token and fetch one via JavaScript before submission (/csrf/ endpoint returning a token and setting the cookie). Never cache a page that contains a CSRF token for everyone.
4. Purge on change
When an editor publishes or updates content, purge the affected URLs through the Cloudflare API:
import requests
from django.conf import settings
def purge_urls(urls):
requests.post(
f"https://api.cloudflare.com/client/v4/zones/{settings.CF_ZONE_ID}/purge_cache",
headers={"Authorization": f"Bearer {settings.CF_API_TOKEN}"},
json={"files": urls}, timeout=10,
).raise_for_status()
Call it from a post_save handler via a Celery task. After deploys that change shared templates (header, footer), purge everything or use cache tags (Enterprise) or prefix purges.
5. Measure
cf-cache-status: HIT means served from the edge; MISS fetched from origin; BYPASS or DYNAMIC not cached. Cloudflare's analytics show the cache hit ratio for HTML; aim for most anonymous page views to be hits.
Verification check: in a private window, request an article twice and see cf-cache-status: HIT; log in and request it again and see BYPASS with your personal header; publish an edit and confirm the purge makes the change visible within seconds.
Explanation
A CDN can cache any response, but it cannot know which HTML is personal. Django signals intent through Cache-Control and Vary, while Cloudflare's Cache Rules decide eligibility based on request properties such as path and cookies. Combining both, with cookies as the gate, gives a simple invariant: requests from anonymous visitors may be served from the edge, requests carrying a session never are. Purging on content change keeps cached pages fresh without short TTLs.
Edge cases / notes
- Analytics cookies set by JavaScript do not reach Django but are sent to Cloudflare; only check for Django's own cookies in rules.
- Messages framework adds a cookie after redirects; ensure flash messages appear only on uncached pages.
- Language selection by cookie or
Accept-Languageneeds separate cache keys (custom cache key on language, or language in the URL). - APO/automatic platform optimisation products are built for WordPress; use Cache Rules for Django.
Next steps
- Get headers right first with Set Cache-Control Headers in Django for CDNs and Browsers.
- Full Cloudflare setup: Deploy Django Behind Cloudflare.
FAQ
Does Cloudflare cache Django HTML by default?
No. Cloudflare caches static file types by default; HTML needs a Cache Rule.
How do I avoid caching logged-in pages on Cloudflare?
Make the cache rule require that sessionid and csrftoken cookies are absent, and bypass admin and account paths.
How do I clear cached pages after an update?
Purge the affected URLs through the Cloudflare API, triggered from Django when content changes.
Can pages with forms be cached?
Only if the CSRF token is fetched separately; never cache a page that embeds a per-visitor token.