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.

Django behind FastlyOriginDjango + GunicornSurrogate headersTTL and keysFastly edgecache, shield POPPurge by keyon model saveDjango tags responses; Fastly caches them and purges by tag in seconds.
  1. Create a Fastly service with your Django origin, and set the host header override to your site's domain.
  2. Send Surrogate-Control: max-age=600 for edge TTL and Cache-Control for browsers, separately.
  3. Add Surrogate-Key headers listing the objects on each page (article-42 category-7).
  4. 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).
  5. 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.

header and who reads itRead byExampleSurrogate-ControlFastly onlymax-age=600Surrogate-KeyFastly onlyarticle-42 articlesCache-Controlbrowserspublic, max-age=60VarybothAccept-EncodingEdge TTL and browser TTL are set independentlydjango-deployment.com
Fastly strips the surrogate headers before responses reach browsers.

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.

from save to fresh pages1Editor saves articletransaction commits2Task purges keysarticle-42, category-73Fastly marks pages stalesoft purge, global4Next request refetchesstale served meanwhile5Fresh page cacheduntil next purgeKeys purge every page that shows the article, not just its URLdjango-deployment.com
Listing pages, feeds, and the home page all carry the shared keys.

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

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.

Keep going