Astro Cloudflare Pages Caching: HTML & i18n Routes

How Cloudflare Pages caches an Astro site: prerendered HTML, hashed assets and on-demand routes, what _headers and _routes.json really do, and how to check it.

A browser viewport frame with hydrating component islands lifting off the page, floating in empty space with no ground plane, pure abstraction

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:

  1. Hashed build assets (/_astro/*.js, *.css, images). The file name changes whenever the content changes.
  2. Prerendered HTML, including every locale’s static pages (/en/about/, /de/about/). These are static files.
  3. 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-revalidate by 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.

What’s Next

Frequently Asked Questions

Why do visitors see an old page after I deploy?
Usually not because of Pages: by default it serves HTML with Cache-Control public, max-age=0, must-revalidate, so browsers revalidate. Check for headers you added yourself, a service worker, or another cache in front. Cloudflare's docs suggest purging the zone cache if stale assets persist after a deploy.
Does Cloudflare cache my prerendered Astro pages?
Yes. Pages serves prerendered HTML as static assets, and according to Cloudflare's docs an asset stays cached on the CDN until your next deployment. You don't need s-maxage for that.
Are on-demand (server-rendered) responses cached?
Not by default. Responses from Pages Functions and Workers are not stored in Cloudflare's CDN cache just because they carry a Cache-Control header; caching them needs the Cache API in your code or a caching layer built for it.

Get notified when new articles and designs land:

No spam. Unsubscribe any time.

Sergej Voronko
Sergej Voronko
SAP Basis · Senior Operations Manager · Linux infrastructure engineer
About the author →

[discussion]

Comments are powered by Giscus — backed by GitHub Discussions. Sign in with GitHub to join the conversation.