The Workers Cache API is per-colo — the lesson that became warmup.rocks
warmup.rocks didn't start as a product idea. It started as a performance problem on one of our own sites: a content platform built with Astro on Cloudflare Workers, content managed in EmDash (the open-source, Git-based CMS we also contribute to). Server-rendered at the edge, cached at the edge — on paper, the fastest architecture there is. In practice, we kept seeing 600 ms+ TTFBs from regions we cared about. This article is the field notes: three things the docs technically tell you about Workers caching, but whose consequences you only feel on a real site.
Lesson 1: caches.default is colo-local. Really local.
The Workers Cache API looks like a global key-value store for responses:
const cache = caches.default;
let res = await cache.match(request);
if (!res) {
res = await render(request);
ctx.waitUntil(cache.put(request, res.clone()));
}
But cache.put() writes to the cache of the data center your
Worker is running in — and nowhere else. No replication, no tiered cache
(Tiered Cache applies to fetch()-proxied traffic, not the Cache API). With
330+ Cloudflare data centers, a
cache.put() in Zurich warms exactly 1 of them. Every colo your visitors
hit re-renders the page once per TTL — which is fine for your top pages in your top
markets and quietly terrible for everything else. We could see it directly: the same
URL, TTFB 45 ms in Frankfurt, 700 ms
in Singapore. (Run the global TTFB test against your own
Worker and you'll see your version of that table.)
Lesson 2: the Cache API ignores stale-while-revalidate
Our Cache-Control strategy assumed SWR semantics: serve stale instantly, refresh in
the background. The edge cache (Cache Rules / Cloudflare-CDN-Cache-Control)
honors that. The Cache API doesn't — cache.match() simply
returns nothing once max-age is up, and your Worker does a full synchronous
re-render. We also measured a subtler variant at the edge layer: with
s-maxage set, entries revalidated synchronously (status
EXPIRED) instead of in the background (UPDATING). The
practical rule we landed on: keep s-maxage out of responses you want SWR
behavior for, and treat the Cache API as a plain TTL cache with zero grace — anything
needing SWR belongs in the zone-level edge cache with
Cloudflare-CDN-Cache-Control: max-age=86400, stale-while-revalidate=604800.
You can verify which behavior you're getting from outside with the
cache checker: HIT vs UPDATING
vs EXPIRED in cf-cache-status tells
the whole story.
Lesson 3: every deploy starts the world cold
The one that actually hurt: Workers Cache API entries are namespaced per deployed version (unless you opt into cross-version caching where available). Deploy a new version and your Worker cache is empty — in every colo, at once. Combined with lesson 1, a Tuesday-afternoon deploy meant: the next visitor in each of 330+ locations got a full SSR render. For a site that deploys several times a day, "edge-cached" was true maybe 70% of the time in big markets and almost never in small ones.
What we built: a warmer. Then a product.
The first fix was a cron Worker that fetched our sitemap every hour and requested
every URL — which warmed exactly one colo (see lesson 1) and taught us the real
requirement: warming has to happen from where the visitors are, not
from where your script runs. So we built a warmer that requests every sitemap URL from
dozens of countries, so the warm copies land in the actual edge caches that serve real
traffic — and triggered it after every deploy, not just on a schedule. Hit ratios per
location made the effect measurable: the Singapore row went from
MISS / 700 ms to HIT / 60 ms, permanently.
That tool is what you're reading the blog of. The deploy trigger became the deploy hook + GitHub Action; the multi-country crawler became warmup.rocks.
Recommendations if you're on Workers today
- Prefer the zone edge cache over the Cache API for HTML. Set
Cloudflare-CDN-Cache-Control(with SWR) from your framework or a Cache Rule — it gets tiered cache, SWR and purge-by-tag. Use the Cache API only for colo-local memoization where its limits don't bite. - Purge by Cache-Tag on publish instead of purging everything — your CMS knows which pages changed. (EmDash setups can do this from a publish hook.)
- Assume deploys mean cold. Warm right after each deploy — a one-line GitHub Action in the deploy workflow closes the gap before the first visitor finds it.
- Measure from outside, per location. Your own colo always looks warm to you. Six-location checks (cache / TTFB) show what the rest of the world gets.
Running on Workers? Deploys are your cold-start events.
warmup.rocks warms your site from 90+ edge locations on a schedule and on every deploy — built on Workers lessons we learned the slow way, so you don't have to.
Start your 7-day free trial