· 2 min read
Cloudflare was revalidating every hashed asset on every page load
Workers Assets ships a default cache header that is correct for hand-written files and wrong for every file a bundler emits.

I deployed this site to Cloudflare Workers, then checked what the edge was actually sending back. One header was wrong in a way that costs every returning visitor a round trip per file.
$ curl -I https://syedtasavour.com/_next/static/chunks/209t93ay3-wpb.css
cf-cache-status: HIT
cache-control: public, max-age=0, must-revalidate
cf-cache-status: HIT looks reassuring. It is not the number that matters. That header describes Cloudflare's edge cache. cache-control describes the visitor's browser, and max-age=0, must-revalidate tells it to check with the server before reusing anything.
Why that default exists, and why it is wrong here
Workers Assets applies that header to everything it serves, and for a hand-written file it is the right call. If you upload about.html and later change it, you want browsers to notice.
Bundler output is the opposite case. That filename — 209t93ay3-wpb.css — is a content hash. The file cannot change without becoming a different filename. Asking whether it has changed is asking a question whose answer is knowable in advance, forever.
The cost is not the bytes, since a 304 has none. It is the round trip. Every script and stylesheet on the page, revalidated before first paint, on a connection where a round trip to the nearest edge is a hundred milliseconds or more. On mobile data that was the single largest avoidable delay on the site.
The fix
A _headers file at the root of the assets directory. Next puts its output in public/'s sibling, and the adapter copies public/ into the assets directory, so public/_headers ends up where the asset layer looks.
/_next/static/*
Cache-Control: public, max-age=31536000, immutable
/_next/image*
Cache-Control: public, max-age=31536000, immutable
immutable is the part that earns its place. Without it, a browser still revalidates on an explicit reload. With it, it does not — which is exactly right for a file whose name is a hash of its contents.
Do not do this to everything
Files you put in public/ are not content-hashed. portrait.webp keeps its name when you replace it. Give that a year and you have stranded a stale copy in every browser that ever loaded the site, with no way to reach them.
/portrait.webp
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
A day of cache, a week of serving the old one while fetching the new one in the background.
Verify, do not assume
The reason I found this is that I curled the deployed URL instead of trusting the config. Do that after every deploy that touches caching:
curl -sI https://your-site/_next/static/chunks/<some>.css | grep -i cache
Cloudflare does not serve the _headers file itself, so a 404 on /_headers is the expected result and worth checking too.