Serve a Django Site Through Fastly
Put Fastly in front of Django: configure the origin and host header, cache anonymous pages with Surrogate-Control and surrogate keys, bypass logged-in users, purge by key from Django signals, and set shielding, TLS, and compression for static files.
Problem statement
A high-traffic Django site, such as a publisher or a large catalog, needs edge caching of HTML, not just static files, with instant invalidation when editors publish. Fastly is a programmable CDN built on Varnish ideas, with fast global purges and surrogate keys that tag cached responses so you can purge every page that shows a given article or product. Django needs to send the right headers and call the purge API when content changes.
- Create a Fastly service with your Django origin, and set the host header override to your site's domain.
- Send
Surrogate-Control: max-age=600for edge TTL andCache-Controlfor browsers, separately. - Add
Surrogate-Keyheaders listing the objects on each page (article-42 category-7). - Bypass the cache for requests with Django session cookies and for non-GET methods (Fastly's defaults pass cookies' responses; check your VCL or Compute config).
- Call Fastly's purge-by-key API from a Celery task after saves; use soft purges to serve stale content while refetching.
Step-by-step solution
1. Configure the service
In Fastly, create a service with the domain www.example.com and an origin host (your load balancer or server). Enable TLS to the origin and set the override host to www.example.com so Django's ALLOWED_HOSTS matches. Turn on shielding: one Fastly POP near your origin fetches from Django, and other POPs fetch from it, which reduces origin traffic.
In Django:
SECURE_PROXY_SSL_HEADER = ("HTTP_FASTLY_SSL", "1") # Fastly sets Fastly-SSL on TLS requests
USE_X_FORWARDED_HOST = False
Check which headers your Fastly configuration sends; Fastly-SSL and X-Forwarded-Proto are common choices.
2. Separate edge and browser caching
from django.utils.cache import patch_cache_control
def article_detail(request, slug):
article = get_object_or_404(Article, slug=slug, published=True)
response = render(request, "articles/detail.html", {"article": article})
if not request.user.is_authenticated:
response["Surrogate-Control"] = "max-age=600, stale-while-revalidate=60, stale-if-error=86400"
response["Surrogate-Key"] = f"article-{article.pk} category-{article.category_id} articles"
patch_cache_control(response, public=True, max_age=60)
return response
Fastly uses Surrogate-Control for its TTL and strips it before sending to browsers; browsers follow Cache-Control. A long edge TTL with purging plus a short browser TTL keeps pages fresh after edits.
3. Bypass personalized requests
Pages for logged-in users must not be shared. In Fastly's VCL (vcl_recv), pass requests that carry Django's sessionid cookie, and strip unrelated cookies so anonymous requests share cache entries. Django's own Vary: Cookie on session-using responses is another safeguard, but varying on the whole Cookie header destroys hit rates, so prefer explicit passes.
4. Purge by surrogate key
import requests
from celery import shared_task
@shared_task(autoretry_for=(requests.RequestException,), retry_backoff=True)
def fastly_purge_keys(keys):
requests.post(
f"https://api.fastly.com/service/{settings.FASTLY_SERVICE_ID}/purge",
headers={"Fastly-Key": settings.FASTLY_API_TOKEN, "Fastly-Soft-Purge": "1",
"Surrogate-Key": " ".join(keys)},
timeout=10,
).raise_for_status()
@receiver(post_save, sender=Article)
def article_saved(sender, instance, **kwargs):
keys = [f"article-{instance.pk}", f"category-{instance.category_id}", "articles"]
transaction.on_commit(lambda: fastly_purge_keys.delay(keys))
A soft purge marks content stale rather than deleting it, so with stale-while-revalidate users keep getting a fast response while Fastly fetches the new version.
5. Static files and compression
Hashed static files from ManifestStaticFilesStorage can be cached for a year at the edge and in browsers. Enable Brotli or gzip compression for text types in Fastly, or serve WhiteNoise's precompressed files and make sure Accept-Encoding is normalized.
Explanation
Fastly caches responses at edge locations according to surrogate headers and can invalidate them globally in a few seconds. Surrogate keys solve the hard part of HTML caching: a single article appears on its own page, category pages, the home page, and feeds, and you rarely know all those URLs. Tagging each response with the objects it contains lets one purge call update every page that showed the changed object.
Edge cases / notes
- CSRF tokens. Cached pages with forms embed a token from the request that filled the cache; load forms dynamically or exclude those pages.
- Header size. Many keys per page can exceed header limits; group keys (for example
articles) for lists. - Compute vs VCL. Fastly offers VCL services and Compute (Wasm); the caching concepts are the same.
Next steps
- Get headers right with Django Cache-Control Headers for CDNs.
- Self-hosted alternative: Put Varnish Cache in Front of Django.
- Another CDN option: Cache Django Pages With Cloudflare.
FAQ
What is a surrogate key?
A tag in the Surrogate-Key response header that lets you purge all cached responses with that tag at once.
Can Fastly cache pages for logged-in users?
Not shared copies. Pass requests with session cookies to Django.
Why use Surrogate-Control instead of Cache-Control?
It sets the edge TTL independently from the browser TTL, so you can cache long at the edge and short in browsers.
How fast are Fastly purges?
Typically a few seconds globally, which makes long edge TTLs practical.