· via dev.to (home feed)
Stale Cloudflare Pages deploys traced to s-maxage, plus the Workers products that inflate bills
Two dev.to write-ups unpack Cloudflare's quiet failure modes: a copied s-maxage header that keeps Pages serving old HTML for up to a week, and per-operation billing that inflates Workers bills.

Two recent write-ups on dev.to approach Cloudflare from opposite directions: one traces a Cloudflare Pages deployment that reported success but kept serving yesterday's HTML, while the other maps exactly where money leaks out of the platform's $5 Workers plan. Both land on the same lesson: the defaults are quiet, and the overrides are quieter.
A deploy that succeeded and changed nothing
According to the first dev.to post, a new Pages deployment was marked Production and showed Success, yet both the custom domain and the project's pages.dev hostname served the previous index.html for more than a day. The response headers explained it: cf-cache-status: HIT, an age of roughly 30 hours, and cache-control: public, s-maxage=604800. Two further redeploys with different content hashes changed nothing, because the edge had been told it could hold that HTML for a week.
Pages' default for cacheable assets, the author notes, is public, max-age=0, must-revalidate, and Pages never adds s-maxage on its own. A week-long shared cache lifetime therefore has to come from the project itself: a _headers rule, a Pages Function, or an advanced-mode worker.
Where the header hides
The usual source is a catch-all rule in a _headers file, the kind copied out of performance guides, which matches every path including HTML. The post recommends searching the build output as well as the source, since frameworks copy public/ into it. It also carries a caveat: _headers rules don't apply to responses generated by Pages Functions, so if an SSR framework or a worker file renders the page, the header is being set in that code's own Response.
Purging is mostly not an option
This is the uncomfortable part. The documented purge path runs through a zone's caching settings, and a project on pages.dev whose custom domain lives on another DNS provider has no zone to purge. Per the Cloudflare documentation the post cites, assets are cached per data center with a one-week TTL, and old assets can linger up to a week after a deploy.
The fix has two steps in a strict order: remove whatever sets s-maxage on HTML and deploy, otherwise each deploy restarts a fresh seven-day clock, then wait out the copies already cached. Purge Everything clears a custom domain that sits on your own zone, but the pages.dev hostname can stay stale until retention expires. In the meantime, the author suggests checking the new build on its hash-based deployment URL, which pins to that exact build and should not share the production cache, though they have not verified that last part.
The same symptom can appear with clean headers if a Cache Rule on the custom domain sets an Edge TTL on everything or on the root path. Long cache lifetimes are safe only for fingerprinted files, the post argues; bundlers like Vite and Astro already put hashes in asset names, so the one rule to avoid is a catch-all that covers HTML.
Where the $5 plan leaks money
The second dev.to post breaks down the $5/month Workers Paid plan, which includes 10 million requests and 30 million CPU milliseconds, with overage at $0.30 per million requests and $0.02 per million CPU ms. Bandwidth isn't charged. The bills that surprise people, the author writes, come from a few products doing far more work than their owners realized.
Queues bill three operations per message (one write, one read, one delete), so batching improves efficiency but not the invoice, and every retry adds a read. KV writes cost roughly ten times reads ($5.00 versus $0.50 per million beyond the included million writes), so a counter updated on every request at 5 million writes a month is about $20 in writes alone. D1 bills rows scanned rather than rows returned, meaning a filter on an unindexed column can scan a whole table to return three rows. Durable Objects are billed at 128 MB each whether or not they use that memory, WebSocket connections without the Hibernation API bill for the entire connection, and pending I/O can keep an object billably in memory for up to 15 minutes.
Elsewhere, the biggest Workers AI waste is calling a model on the same input every time instead of caching results; Vectorize's query cost scales with the number of stored vectors, not the results returned; and Browser Rendering includes 10 hours a month at $0.09 per hour after that, so unclosed sessions add up. R2 stays cheap unless code calls HeadObject or ListObjects on every request, in which case the metadata belongs in D1 or KV.
Why it matters
Both write-ups describe the same failure mode: infrastructure behaving correctly while an invisible setting quietly redefines what correct means. On Pages, one copied header turns every deploy into a week-long rollout with no cancel button. On Workers, generous allowances conceal products whose billing unit (messages, writes, rows scanned, GB-seconds, stored vectors) is not the unit developers reason about. The practical defences overlap: read the response headers before redeploying a third time, count operations per request before launch, and switch on billing alerts before anything else.
- #cloudflare
- #caching
- #cloudflare-pages
- #workers
- #billing