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:

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.