---
title: "Astro cache warming: prerendered, on-demand, and the Cloudflare Workers edge — warmup.rocks"
description: "Astro sites cache in three layers: prerendered pages, on-demand SSR routes with route caching, and the per-colo edge. Which layer goes cold when, what Astro 7 route caching changes, and how to keep it all warm — from a team running Astro on Workers in production."
canonical: https://warmup.rocks/blog/astro-cache-warming
---

Astro

# Astro cache warming: prerendered, on-demand, and the Cloudflare Workers edge

July 2026 · 9 min read · [← All posts](https://warmup.rocks/blog/)

We run a large content site built with **[Astro](https://astro.build/) on Cloudflare Workers** in production — content managed in [EmDash](https://github.com/emdash-cms/emdash), and it's the site whose cache problems [turned into warmup.rocks](https://warmup.rocks/blog/cloudflare-workers-cache-api). Astro is a great fit for edge caching because it makes the crucial decision explicit: every route is either **prerendered** at build time or rendered **on demand**. Those two kinds of routes go cold in completely different ways, and most Astro performance advice only covers the first. Here's the full map.

## Layer 1: prerendered routes — cold only in one, sneaky way

Pages with `prerender = true` (or everything, in `output: 'static'`) are HTML files. On Cloudflare they're served via Workers static assets, on Netlify/Vercel from their static layers. No render time, no origin — so nothing to warm, right? Almost. Static files are still delivered through a **per-location edge cache**: the first request in each data center fetches from the central store before that colo can serve it locally. That's a small penalty (tens of ms, not seconds), so for purely static Astro sites, warming is a nice-to-have. The real reasons to keep reading are layers 2 and 3.

## Layer 2: on-demand routes — this is where the seconds live

The moment a route renders on demand (SSR with the [Cloudflare adapter](https://docs.astro.build/en/guides/integrations-guide/cloudflare/), Node, Vercel or Netlify), every uncached request pays: Worker/function startup, data fetches (CMS, database, APIs), and render time. Our numbers before caching HTML: 600 ms to 2 s [TTFB](https://warmup.rocks/blog/time-to-first-byte) depending on the route — for content that changes a few times a day. The fix is caching the rendered HTML at the edge, and with Astro there are two generations of doing this:

### The classic way: headers you set yourself

```
Astro.response.headers.set(
  'Cache-Control', 'public, max-age=300, stale-while-revalidate=604800'
);
```

Works everywhere, but on Cloudflare it conflates two audiences: browsers _and_ the edge read `Cache-Control`. You usually want browsers on a short leash (they can't be purged) and the edge on a long one (it can). The split: `Cache-Control` for browsers, `Cloudflare-CDN-Cache-Control` (or `CDN-Cache-Control` on other CDNs) for the edge.

### The Astro 7 way: route caching

Astro 7 made this first-class: **route caching** lets you declare cache behavior per route pattern in `astro.config`, and a provider (e.g. the Cloudflare one) translates it into the right edge headers and cache tags:

```
// astro.config.mjs
routeRules: {
  '/blog/**': { cache: { maxAge: 86400, swr: 604800, tags: ['pages'] } },
}
```

Per-route overrides via `Astro.cache.set()` — including opting error responses _out_ so a transient 500 doesn't get cached for a day. The `Cache-Tag` part is the sleeper feature: your CMS publish hook can purge exactly the affected pages instead of the whole zone.

## Layer 3: the edge cache itself — per colo, and cold after every deploy

Whatever headers you send, the cache they fill is **per data center**. Cloudflare runs [330+ colos](https://warmup.rocks/cloudflare-data-centers); a page cached in Frankfurt is still uncached in Singapore. Three production lessons from running exactly this stack (measured, not read in docs):

-   **Avoid `s-maxage` if you want background revalidation.** In the Workers-fronted cache we measured that `s-maxage` forced _synchronous_ revalidation — [cf-cache-status](https://warmup.rocks/blog/cf-cache-status) `EXPIRED` (visitor waits) instead of `UPDATING` (visitor gets stale instantly). Declare SWR via route caching / `Cloudflare-CDN-Cache-Control` and leave `s-maxage` out.
-   **Deploys start the world cold.** On Workers, the cache namespace is tied to the deployed version — ship a new build and every colo is empty at once. Deploy several times a day and "edge-cached" quietly becomes a part-time truth.
-   **SWR only rescues pages that are _in_ the cache.** After a deploy or on long-tail pages there's no stale copy to serve — the visitor eats the full render. Stale-while-revalidate softens expiry, not cold starts.

## The warming playbook for Astro

1.  **Prerender everything that can be prerendered.** Astro makes hybrid trivial; static routes are warm by construction (almost).
2.  **Give on-demand HTML real edge TTLs + SWR** via route caching (Astro 7) or split cache headers — short for browsers, long for the edge.
3.  **Purge by Cache-Tag on publish**, not "purge everything" — your content changes page by page.
4.  **Warm after every deploy.** A deploy is your biggest cold-start event. One step in your deploy workflow — our [GitHub Action](https://github.com/marketplace/actions/warmup-rocks-cdn-cache-warmer) or a [curl to your deploy hook](https://warmup.rocks/docs/deploy-hooks) — re-warms every location before the first visitor notices.
5.  **Warm on a schedule** matched to your TTLs, so long-tail pages and low-traffic regions stay hot between deploys. [warmup.rocks](https://warmup.rocks/) reads your sitemap (Astro's `@astrojs/sitemap` output works as-is) and warms from 90+ edge locations, reporting [hit ratio](https://warmup.rocks/blog/cache-hit-ratio) per location and run.
6.  **Verify from outside.** Your own colo always looks warm. Run the [cache checker](https://warmup.rocks/cache-checker) for statuses per location, or the [global TTFB test](https://warmup.rocks/ttfb-test) for what it costs your visitors.
