The cache you thought you had
We shipped s-maxage=600 months ago and told ourselves pages were cached at the edge. They were not. Not one of them, not once.
The measurement
Two URLs, same worker:
/ozer cache-control: public, s-maxage=600 · vary: cookie · no cf-cache-status / cache-control: public, s-maxage=600 · no vary · no cf-cache-status
The first diagnosis was Vary: Cookie — a known way to defeat shared caching. The landing page killed that theory: no Vary there either, and still no cf-cache-status.
What was actually happening
Cloudflare does not cache a Worker's generated response on its own. The Worker is the origin. There is no cache layer in front of it reading your headers and deciding to store the body. s-maxage was a string we sent to nobody.
The fix is to use the Cache API explicitly. Three rules:
- A request carrying a session cookie never touches the cache — not read, not written.
- The key is the URL alone. Cookie-bearing requests are already excluded, so
Vary: Cookieis stripped before storing. - Only responses that carry
s-maxageand set no cookie are stored. The page decides whether it is cacheable; there is no path list to go stale.
After:
request 1 x-cache: MISS request 2 x-cache: HIT request 3 x-cache: HIT
The second trap
What comes back from cache is not what you sent. We send max-age=0 so browsers always revalidate. The cached response came back with max-age=14400 — Cloudflare applies the zone's four-hour browser TTL on the way out.
Unnoticed, every visitor's browser would have frozen the page for four hours. In a product whose entire promise is that pages keep themselves current.
The page's own Cache-Control is now copied on write and restored on read.
The part worth stealing
There is a third lesson hiding here. We later needed a CSP source expression for our inline JSON-LD, and a nonce looked like the obvious answer.
It is the wrong answer for a cached page. Pages sit at the edge for ten minutes and every copy carries the same nonce. An attacker who downloads the page once learns it and can put it on their injected script. A hash is bound to content: it matches what we generate and nothing anyone injects.
Cache changes the security properties of things that look unrelated to caching.