DEV Community

Cover image for One cookie silently turned my caching off for my best visitors
r f
r f

Posted on Edited on

One cookie silently turned my caching off for my best visitors

One cookie silently turned my caching off for my best visitors

I run an anonymous AI terminal — a web app with a 1979 mainframe look, a working chat, saved conversations, private generated apps, and no signup at all. It has real users, in the modest sense: about 1,300 page loads and 220 real tasks in the last 31 days.

Last week I found out that every one of those page loads was re-downloading the whole thing from the server.

Not "wasn't optimally cached". Re-downloaded: 83 KB of script, 31 KB of stylesheet, another 27 KB of UI code, and a 760 KB boot clip — the whole front end, every time, for every visitor who had ever used the site.

The signal I was sending

The app sets one cookie on arrival: a random anonymous identity token. That cookie is why the product works — it is how a thread stays yours without an account.

The edge in front of it had one rule: a request carrying a cookie is private, so mark the response no-store.

That rule is correct for an API. It is wrong for a document. My pages are byte-identical for everyone: the identity lives in the cookie and is hydrated by JavaScript, never rendered into the HTML. So the markup said "don't keep this", and Cloudflare obeyed:

  • GET /assets/console.js with the cookie: cf-cache-status: BYPASS, cache-control: no-store
  • the exact same URL without a cookie: REVALIDATED, public, max-age=14400

The zone's cache rule was working the entire time. My origin was opting out of it, and every visitor with a cookie — which is every returning visitor — fell off the cache. The most expensive visitors got the slowest, heaviest experience.

Why it happened at all

The cookie check sat in the forwarding path as a shortcut, not as policy. Somebody wrote "credential-bearing = don't cache" because it was true for the endpoints they had in mind, and it silently also covered static files and public pages.

The fix was to make the decision a function of the response, not of the request:

  • public documents (home, about, help, library, every guide) → public, max-age=0, s-maxage=300, stale-while-revalidate=600
  • published share pages → a short 60-second browser lifetime
  • anything that sets a cookie, says private, or varies on cookies → still no-store
  • assets keep a must-revalidate cache with an ETag, so a repeat load is a cheap 304

The request cookie stops being a veto. The response decides.

The whole server-side change was a policy function and one line in the proxy. What took longer was proving it: the acceptance run covers 725 tests, and the public checks re-request every asset twice, because the first request after a policy change is a legitimate MISS and a naive test scores a cold miss as a failure.

The part that surprised me

My host's cache rule matched file extensions, not text/html. Fixing the origin was not enough — pages stayed DYNAMIC. The obvious answer is "add a cache rule", except the API token I hold can list rulesets but not read or write the cache-ruleset entrypoint.

So the Worker did it instead. It already cached the homepage in front of the origin, the same way people have cached landing pages for twenty years. I widened it to public documents only, with the same guards: store a response only when it is HTML, 200, marked public by the origin, and carries no Set-Cookie. Now a repeat view is served at the edge with cf-cache-status: HIT and never touches the server.

One deliberate exception: published share pages stay off that cache. Each render has to reach the origin, or the view counter that tells me whether sharing works at all quietly under-counts.

What it changed

Same site, same features, same bytes:

  • repeat visits of a guide: MISS then HIT, identical HTML, zero origin work
  • identity and conversation JSON: still no-store, still private
  • asset requests: REVALIDATED even when signed in, served from cache with a 304
  • 41 public checks and 10 browser checks green, deployed with a rehearsed rollback

The same day's numbers are not comparable yet — the wave landed hours ago — but the mechanism is now visible instead of theoretical: repeat views are HIT at the edge with identical bytes, and the origin is not asked at all. That is the point. Caching is a traffic lever that costs nothing and needs no new visitors.

Honest limits

  • Cloudflare rewrites the browser lifetime of a hit to the zone default of four hours. A deploy does not purge it; I have no purge token, so the edge copy expires on its own after 300 seconds and a cache buster handles the rest.
  • The cache key ignores query strings. That is fine here because the only query the pages read is a client-side prefilled prompt.
  • Caching makes page weight cheaper, not inference cheaper. The model call is untouched.

Takeaway

If you cache by request, one cookie silently turns caching off for your best users. Decide from the response, prove it with two requests rather than one, and keep private surfaces explicitly out.


The app the post is about is mu7hur.com — mine, and where these headers serve. A 32-second screen recording of the same session exists if the interface matters more than the headers do.

Built and maintained entirely by myself. This post was drafted with my own agent, which also wrote the acceptance runs; every number in it comes from a check I ran and can re-run.

Top comments (0)