Caching Is Portable. Invalidation Isn’t.
- Date
A couple months ago I wrote about why we should cache the HTML too. The basic argument was pretty simple: the fastest request is the one your server never sees. We cache images, CSS, and JavaScript without thinking twice, then render the exact same HTML again for every visitor.
Astro 7 just shipped with a bunch of performance work, but one feature stood out to me: route caching. It takes that argument and fixes the most annoying part of actually putting it into practice.
Fast enough, until it isn’t
I feel like caching has been largely ignored in the headless world because most sites are fast and cheap enough. A serverless function renders the page in a few hundred milliseconds, the hosting bill stays tiny, and nobody has a reason to care that the same work happens on every request.
Then the site gets a giant audience and both problems become urgent at the same time.
Serverless hosting can make this stranger. A platform might run your function near the visitor, which cuts down the round trip, but moving the server closer doesn’t remove any work. The function still runs. The CMS still gets queried. The HTML still gets rendered. Edge execution and edge caching sound similar, but they’re solving different problems.
In a lot of setups the CDN sits in front of the serverless function but doesn’t cache its HTML by default. Every request passes straight through to compute. You can add an application cache behind the CDN, but by then the request has already invoked your function, which is exactly the work I want to avoid.
Cloudflare just shipped Workers Cache to fix exactly this problem. Workers originally ran in front of an origin and its cache. Once frameworks started deploying the entire application as a Worker, the Worker became the origin, so there was nothing left behind it to cache. Workers Cache flips that architecture around: on a hit, the cache responds and the Worker never runs.
Headers only got us halfway there
To be fair, we already have a portable interface for telling a CDN how to cache a response: the Cache-Control header. Set s-maxage, maybe add stale-while-revalidate, and any CDN that understands standard HTTP caching has enough information to save the response.
That works great until the content changes.
Say a product is cached for an hour, then someone updates its price in the CMS. You could wait out the hour, but serving a knowingly wrong price for 59 minutes isn’t a particularly good caching strategy. You want the CMS to fire a webhook and purge that product immediately.
There isn’t a standard HTTP header for that. Cloudflare, Netlify, Vercel, and every other CDN have their own purge APIs, authentication, tag formats, and limitations. At that point your otherwise portable application grows a little bundle of infrastructure-specific code.
This is where Astro’s route caching gets interesting. A page can describe how it should be cached:
cache.set({
maxAge: 300,
swr: 60,
tags: [`products:${Astro.params.id}`],
});
Then a webhook can invalidate the same tag when the product changes:
await cache.invalidate({ tags: [`products:${slug}`] });
The application owns the caching intent. The adapter translates it into whatever the host actually needs. Astro 7 currently has experimental CDN cache providers for Cloudflare, Netlify, and Vercel, and the same Astro.cache and cache.invalidate() calls work across all three.
That’s the abstraction I want. Cache lifetimes were already mostly portable. Invalidation was the part gluing an application to its CDN.
You can also define cache behavior for groups of routes in astro.config.mjs, which is probably where I’d put the boring defaults:
routeRules: {
'/blog/[...path]': { maxAge: 300, swr: 60 },
}
Then route code only needs to get involved when it has useful tags or needs to opt out.
Next.js got close first
Next.js has had full-route caching and on-demand revalidation for a while. On Vercel, the whole thing is integrated: Next.js knows how the response was rendered, Vercel knows how it was cached, and revalidatePath() or revalidateTag() connects the two.
Self-hosted caching works too, but on-demand revalidation stops at the Next.js server cache. An arbitrary CDN will keep serving its copy until the TTL expires unless you also call its purge API. That’s the gap. Astro’s adapters make the host-specific purge part of the cache provider, while the application keeps the same interface.
Portable doesn’t mean automatic
There are tradeoffs here. The CDN providers are still experimental, and right now “CDN agnostic” really means the three supported platforms. If you host somewhere else, someone still has to write the provider.
You also still have to decide what is safe to cache. A personalized dashboard is not made cacheable just because the API is pleasant. Cache the public product page, not the logged-in account sitting next to it. Invalidation webhooks need authentication. Tags need enough thought that updating one product doesn’t purge the entire catalog (unless that’s actually what you want).
The API removes platform plumbing. It can’t remove the need to understand your own data.
Still, this feels like exactly the right direction. My hope is that the provider interface eventually escapes Astro and becomes something more framework agnostic, maybe as a Vite plugin or a small standard that Vite frameworks can share. Every server-rendered framework needs to express the same handful of ideas: cache this response, serve it stale while refreshing, and purge it by path or tag.
We already have standard language for the first two. Astro is taking a real swing at the third.