Caching & CDN setup
Vary: Accept is necessary but not always sufficient. Cloudflare, Fastly, and Vercel each have quirks you need to know before production.
Setting Vary: Accept is the baseline for content negotiation through
caches. Most CDNs honor it — but “honor” varies, and some behaviors
need explicit configuration. Here’s what to check.
Cloudflare
Cloudflare’s default edge cache respects Vary: Accept on cacheable
responses. But a few gotchas:
- Varying on more than a small set of values is discouraged. If
your
Acceptvalues are bounded (justtext/markdownandtext/html), you’re fine. If you accept wild variety, Cloudflare may decline to cache. - Transform Rules can override. If you’ve enabled HTML minification,
compression tweaks, or email obfuscation, verify they don’t strip
Vary. - Tiered Cache and Smart Tiered Cache honor
Varycorrectly, but cache-warming tools should be sent with the rightAcceptheader or they’ll populate the wrong representation. - Cache Rules let you explicitly set the cache key. You can add
Acceptto the cache key manually:- Cache Rules → Custom cache key → Include header: Accept
- Markdown for Agents (Cloudflare’s managed feature) handles all of this automatically. See the guide.
Fastly
Fastly’s edge cache honors Vary: Accept per HTTP spec. If your origin
returns the header, Fastly keys the cache on Accept automatically —
no custom VCL required. See
Fastly’s Vary documentation.
Custom VCL is only needed if you want the edge to add a Vary header the origin forgot, or to change the cache-key shape beyond what Vary gives you. For append-at-edge:
sub vcl_fetch {
if (beresp.http.Vary) {
set beresp.http.Vary = beresp.http.Vary ", Accept";
} else {
set beresp.http.Vary = "Accept";
}
}
Vercel
Vercel’s edge cache respects Vary: Accept on Cache-Control: s-maxage=...
responses. For Next.js App Router:
export const revalidate = Nin a route — respects Vary.- Route handlers that return
Cache-Control: s-maxage=60+Vary: Accept— cached per-variant at the edge. - ISR (
getStaticProps+revalidate) — Vary works, but you need to trigger regeneration for both variants.
Vercel’s dashboard shows cache hits per variant; check that both
Markdown and HTML requests get x-vercel-cache: HIT after warmup.
Netlify
Netlify Edge respects Vary: Accept via the standard Cache-Control
semantics. The Netlify-specific quirk: if you’re using Netlify Functions
or Edge Functions, ensure the function itself returns the Vary header —
don’t rely on _headers file rules for dynamic responses.
Origin-level caching
If your own reverse proxy is doing the caching (not a CDN in front of it), add Accept to the cache key. For Nginx:
proxy_cache_key "$scheme$request_method$host$request_uri$http_accept";
Testing your cache
# Prime the cache with Markdown
curl -sI -H "Accept: text/markdown" https://yoursite.com/article
# Then with HTML — should get a distinct response (new cache entry)
curl -sI -H "Accept: text/html" https://yoursite.com/article
# Then request Markdown again — should be a cache HIT now
curl -sI -H "Accept: text/markdown" https://yoursite.com/article
Check for age:, x-cache:, or cf-cache-status: headers to confirm
which request was served from cache. If all three come from origin,
your cache isn’t using Vary correctly.
The “forgot Vary entirely” failure mode
No Vary: Accept on a negotiated response is the single most common
production bug. Symptoms:
- Agents report they’re getting HTML with
Content-Type: text/html, butcurlshows the Markdown is being served correctly - Users in a browser see raw Markdown on a page that should be HTML
- Cache hit rates look normal; the cache is just serving the first-seen representation to everyone
Fix: add Vary: Accept to every negotiated response, and purge your
cache. Until you purge, cached entries are still keyed on URL alone and
will keep serving the wrong thing.