Caching an Astro site on Cloudflare Pages goes wrong mostly because of wrong assumptions about what Pages does already. This guide sticks to what Cloudflare documents. (Cloudflare now also recommends Workers with static assets for new projects; the caching ideas below are similar there, but check its docs.)
Three kinds of response
An Astro site on Pages serves three different things:
- Hashed build assets (
/_astro/*.js,*.css, images). The file name changes whenever the content changes. - Prerendered HTML, including every locale’s static pages (
/en/about/,/de/about/). These are static files. - On-demand routes (pages with
export const prerender = false, API endpoints). These run in a Pages Function.
What Pages does by default
From Cloudflare’s documentation:
- Every deployed asset stays cached on the Cloudflare CDN until your next deployment (served from Tiered Cache).
- Cacheable HTML responses get
Cache-Control: public, max-age=0, must-revalidateby default, so browsers revalidate every time but the CDN still serves them quickly.
That’s already the right behaviour for HTML. Don’t add long max-age values to HTML: that’s what makes browsers show old pages after a deploy.
_headers: make hashed assets immutable
The one header worth adding is a long cache for hashed assets, since their names change on every content change:
# public/_headers
/_astro/*
Cache-Control: public, max-age=31536000, immutable
Leave HTML, including localized routes, on the default.
_routes.json: decide what hits the Function
When your project has on-demand routes, the Astro Cloudflare adapter generates a _routes.json that tells Pages which paths run the Function and which are served as static files. The adapter already excludes prerendered pages and build assets from the Function, and it has options to extend the include and exclude lists (see the @astrojs/cloudflare docs).
Be careful with broad manual exclusions such as /en/*: anything excluded is never sent to the Function, so on-demand routes, endpoints or i18n middleware behaviour under that prefix stop working. Exclude only what you know is purely static.
On-demand routes
Responses from a Pages Function or Worker are not cached by Cloudflare’s CDN just because they have a Cache-Control header. That header still matters for browsers and other caches. If expensive on-demand pages need edge caching, do it explicitly: use the Workers Cache API in your code, or Astro’s route caching (stable since Astro 7) with a CDN cache provider.
Set headers for on-demand responses from Astro:
---
export const prerender = false;
Astro.response.headers.set('Cache-Control', 'private, no-store'); // e.g. per-user pages
---
Checking what happens
curl -sI https://your-project.pages.dev/en/about/ | grep -iE 'cache-control|cf-cache-status|age'
For static pages you’d expect the default HTML Cache-Control and a CDN cache status. For on-demand routes, cf-cache-status typically shows DYNAMIC, which is normal for Function responses unless you added caching yourself.
[discussion]
Comments are powered by Giscus — backed by GitHub Discussions. Sign in with GitHub to join the conversation.