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.