How Your App's Headers Control Caching
Overview
On the Assets Only profile, the edge network decides whether to cache a
page from the Cache-Control header your app sends with it. (The Full
Site profile ignores these headers. See Caching Profiles.)
Before you added the WAF, your app was likely served straight to browsers with no shared cache in between, so some of these headers had no visible effect. Once the edge network sits in front of your app, they do. If responses that should be live start coming back stale after setup, the headers below are the usual reason.
What gets cached
| Your app sends | Edge network |
|---|---|
Cache-Control: no-store |
Never caches |
Cache-Control: private |
Never caches |
Cache-Control: public, max-age=N |
Caches for N seconds |
Cache-Control: s-maxage=N |
Caches for N seconds |
No Cache-Control header |
Caches briefly (about a minute; redirects up to 10 minutes) |
URLs ending in an asset file extension (images, CSS, JavaScript, PDFs) are handled by the asset caching rules instead, whatever the header says. See Asset File Caching.
Responses with no header
A response with no Cache-Control header gets a short default cache
life: about a minute for pages, and up to 10 minutes for redirects. API
endpoints, JSON responses and redirects often send no header, so two
users can briefly see the same response, or a client polling for changes
can see an old one.
Set a header on every response that must stay live:
Cache-Control: private, no-cache
The simplest way is a default set early in your app (middleware or a before-filter) that individual routes can override.
Framework defaults
Some frameworks send a long shared-cache lifetime by default. Next.js, for example, sends this on pages it prerenders at build time:
Cache-Control: s-maxage=31536000, stale-while-revalidate
s-maxage applies only to shared caches like the edge network, and
browsers ignore it, so it had no effect before. With the WAF in place
those pages are cached for up to a year. That’s usually fine for pages
that only change when you deploy, as long as the cache is cleared on
each release. See Automatic Cache Clearing with Heroku App
Webhooks.
One URL, more than one response
If one URL returns different content depending on a request header (for
example, an HTML page for browsers and JSON for API clients, picked by
the Accept header), a cached copy of one can be served to a request
for the other.
Tell the edge network which request header the response depends on with
Vary. Send it on the cacheable variant:
Vary: Accept
With Vary: Accept, the edge network stores a separate copy per
Accept value, so a request for JSON never gets a cached HTML page.
Vary can be sent as its own header line next to one your framework
already sends.
Checking a URL
Request the URL twice and look at the cache status header in the response:
curl -I https://www.example.com/your/path
HIT means the response came from the edge cache. MISS means it
came from your app and may be stored for the next request. BYPASS
means a Non-Cached Path rule sent it straight to your app.
If a URL still needs to skip the cache and you can’t change its headers, add it as a Non-Cached Path, then clear the cache.