Route Caching
Astro’s route caching stores the rendered response of an on-demand page, so a cache hit skips rendering. EmDash supplies cache tags for the content a page renders and invalidates those tags when the content changes. Pages can stay cached for days while a publish usually becomes visible within seconds.
The object cache stores query results and still renders every page. With a cache provider configured, anonymous renders skip the object cache and read from the database, so a render that refills an invalidated page doesn’t come from an older snapshot (see What gets cached). The route cache then serves anonymous visitors, and the object cache mainly speeds up renders for signed-in editors.
Choose a cache provider
Section titled “Choose a cache provider”Route caching needs a cache provider. Use the one that matches your deployment:
| Deployment | Provider | Import from |
|---|---|---|
| Node.js | memoryCache() |
astro/config |
| Cloudflare | cacheCloudflare() |
@astrojs/cloudflare/cache |
memoryCache() keeps entries in the memory of the server process. cacheCloudflare() is a CDN provider: it stores responses in the Cloudflare Workers Cache, and a cache hit never runs the Worker. Astro’s Netlify and Vercel adapters ship CDN providers as well; see Adapter cache providers. EmDash invalidates through Astro’s provider-independent cache.invalidate() API, so its tags work with any provider. Provider differences covers the behavior that depends on the provider. astro dev never caches, so test caching against a production build.
Configure route caching
Section titled “Configure route caching”Register the provider and declare which routes may be cached, for how long, and under which tags. This example uses the Cloudflare provider; on Node, use cache: { provider: memoryCache() } with memoryCache imported from astro/config:
import { defineConfig } from "astro/config";import { cacheCloudflare } from "@astrojs/cloudflare/cache";
export default defineConfig({ // ...your existing output, adapter, and integrations cache: { provider: cacheCloudflare(), }, routeRules: { "/": { maxAge: 86_400, swr: 604_800, tags: ["posts", "pages", "all"] }, "/blog": { maxAge: 86_400, swr: 604_800, tags: ["posts", "all"] }, "/blog/[slug]": { maxAge: 86_400, swr: 604_800, tags: ["posts", "all"] }, "/sitemap.xml": { maxAge: 86_400, swr: 604_800, tags: ["posts", "pages", "all"] }, "/sitemap-[collection].xml": { maxAge: 86_400, swr: 604_800, tags: ["posts", "pages", "all"] }, },});maxAgeis how long the cache serves the entry before the route renders again. EmDash invalidates stale entries on publish (below), but an invalidation can fail, somaxAgealso bounds how long a page can stay stale, plus one stale response whileswrrevalidates it. Check Provider differences before choosing a long value.swrlets the cache keep serving a stale entry while it revalidates in the background, so visitors rarely wait on a render once a page is cached.tagsname the entry for invalidation. Tag each route with the collections it renders, using the collection names: EmDash’s built-in invalidation uses collection names, so matching tags are invalidated automatically. A broad extra tag on every cached route (hereall, which is not a collection name, so EmDash never invalidates it on its own) works as an emergency switch: invalidating it, for example withcontext.cache.invalidate({ tags: ["all"] })in an endpoint you protect yourself, clears everything at once.
The query helpers also return a cacheHint you can pass to Astro.cache.set() inside the page. Its tags name the exact entry (getEmDashEntry), the collection (getEmDashCollection), or the chrome resource (getMenuWithCacheHint, getSiteSettingsWithCacheHint, getTaxonomyTermsWithCacheHint, getWidgetAreaWithCacheHint). These are the same tags EmDash invalidates on writes, so a page that passes its hints is invalidated when an entry or chrome resource it renders is written, including ones its route rule doesn’t name:
---import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;if (!slug) return Astro.rewrite("/404");
const { entry, cacheHint } = await getEmDashEntry("posts", slug);if (Astro.cache?.enabled) Astro.cache.set(cacheHint);---Astro.cache.set() merges with the route rule, and tags accumulate across calls. The cache hints carry tags but no maxAge, so the route rule decides how long a page is cached.
Give every public route an explicit rule. Rules match the request path, so list your public routes individually: a rest parameter at the site root, such as /[...slug], also matches the admin and API routes under /_emdash/.
Invalidation on publish is built-in
Section titled “Invalidation on publish is built-in”EmDash’s content API routes invalidate the route cache whenever live content changes: publishing, unpublishing, deleting, or restoring an entry from the trash, and saving changes that reach the live entry, invalidate the tags [collection, id]. Creating or duplicating an entry invalidates nothing, because the new entry is a draft. Any route rule tagged with the collection name (as above) is therefore usually refreshed within seconds of a publish, even with a long maxAge.
Site chrome is covered too: saving site settings invalidates emdash:settings, menu edits invalidate emdash:menu:{name}, and taxonomy and widget-area writes invalidate their matching emdash:taxonomy:{name} / emdash:widget-area:{name} tags. Pages pick these tags up automatically when they use the cacheHint from getMenuWithCacheHint(), getSiteSettingsWithCacheHint(), getTaxonomyTermsWithCacheHint(), or getWidgetAreaWithCacheHint().
Creating, editing, or deleting a redirect invalidates the cached page at the redirect’s source path, with and without a trailing slash, so the redirect applies on the next request.
Restoring an older revision, and changes to bylines, the media library, and comments, invalidate nothing. Cached pages affected by them update once they expire or the entry that renders them is saved again. Scheduled publishing invalidates the published entries’ tags on Cloudflare only; see Provider differences.
This is also why the tag convention matters: EmDash invalidates collection names, entry ids, and the emdash:* chrome tags. If your route rules use other tag names, the built-in invalidation will not match them and stale entries survive until maxAge expires.
Keep responses out of the cache
Section titled “Keep responses out of the cache”Route rules match the request path, so they also apply to responses you do not want cached under a long TTL. EmDash keeps responses rendered for a signed-in user, and responses sent with Cache-Control: private or no-store, out of the route cache automatically, and 404 responses too. Other error responses and your own middleware redirects are the template’s job, opted out per request with Astro.cache.set(false).
Astro.cache.set(false) only holds until the next Astro.cache.set() call. A layout renders after the page frontmatter, so a layout that sets a cache hint turns caching back on for a page that opted out. Pass the opt-out to the layout (for example as a prop) and skip its Astro.cache.set() calls when it is set.
Preview and edit links are handled automatically
Section titled “Preview and edit links are handled automatically”EmDash’s middleware opts these responses out of the route cache itself, so no template code is needed: requests carrying a ?_preview= token (valid or not, because those URLs are per-token and a cached draft would otherwise stay reachable after the token expires), and responses with the editor toolbar injected. With toolbar: "client", the same applies to ?_edit URLs. Valid preview responses and toolbar-injected responses are additionally marked Cache-Control: private, no-store for browsers and downstream CDNs.
Error responses
Section titled “Error responses”EmDash keeps every 404 out of the route cache, so publishing a missing entry later shows it right away. Other error responses, such as a 500 rendered on a cached route, inherit the rule’s TTL. Opt them out with Astro.cache.set(false).
Redirects in middleware
Section titled “Redirects in middleware”Middleware that short-circuits with a redirect (for example, host canonicalization) must opt out with context.cache?.set(false). Otherwise the redirect is cached under the route rule’s TTL, and on a CDN provider that doesn’t separate cache entries by hostname it can be served to requests for the canonical hostname too, causing an instant redirect loop. Use optional chaining: context.cache is undefined on some responses, such as the 404 page for a URL that matches no route.
Editors and cached pages
Section titled “Editors and cached pages”The route cache stores one shared variant per URL, and its cache key ignores cookies, so a cache hit never reflects an editor’s session. With the default server-rendered toolbar, an editor browsing the public site is usually served the cached anonymous variant, without the toolbar, and the emdash-edit-mode cookie has no effect on a cached URL. A page rendered for a signed-in editor is never stored in the shared cache; the next anonymous visitor fills it.
Set toolbar: "client" for sites that use route caching. A small script then shows the edit pill to signed-in editors on cached pages, and the edit view loads from an ?_edit URL that is always rendered fresh. Preview links bypass the cache in either mode.
Provider differences
Section titled “Provider differences”Node.js with memoryCache()
Section titled “Node.js with memoryCache()”- Each server process keeps its own cache, and an invalidation only clears the process that handled the write. If you run several processes or instances, the others keep serving their cached pages until
maxAgeexpires. - A restart empties the cache.
memoryCache()only stores a response that has amaxAge, so a route without a rule is never cached, even if the page callsAstro.cache.set()with a cache hint.- Scheduled publishing does not invalidate the route cache on Node. A page that lists a scheduled entry shows it once its cached copy expires, so keep
maxAgeshort on routes that list scheduled content.
Cloudflare with cacheCloudflare()
Section titled “Cloudflare with cacheCloudflare()”- The provider writes
Cloudflare-CDN-Cache-ControlandCache-Tagresponse headers. The Workers Cache consumes both; clients never see them, and anyCache-Controlheader your pages set for browsers is unaffected. - When a cache provider is configured, the
@astrojs/cloudflareadapter enables the Workers Cache automatically: the build writes"cache": { "enabled": true }into the generated Wrangler configuration thatwrangler deploypicks up. - A route without a rule is marked
no-store, but only if nothing callsAstro.cache.set()while it renders. A cache hint withoutmaxAge, like the ones fromgetSiteSettingsWithCacheHint()andgetMenuWithCacheHint(), producesCloudflare-CDN-Cache-Control: publicwithout a lifetime, so the Workers Cache falls back to its default cache lifetimes (2 hours for a200response). If your layout sets these hints, every page that uses the layout becomes cacheable, with or without a rule. - By default the Worker version is part of the cache key, so every deploy starts with an empty cache and the first requests after it render in the Worker. To keep entries across deploys, set
"cache": { "enabled": true, "cross_version_cache": true }in your Wrangler configuration (see Cross-version caching). Template changes then only show once the affected entries expire or are invalidated. - Scheduled publishing invalidates the tags of the entries it publishes from the Cron Trigger.
Purge rate limits
Section titled “Purge rate limits”The Cloudflare provider invalidates by purging the Workers Cache. Workers Cache purges always use Cloudflare’s Free plan purge limits, whatever plan your account is on: 5 requests per minute with a burst of 25, per account (see Purge rate limits). Most content writes cost one purge request each: saves that change live content (including autosaves in collections that save directly to the live entry), publishing, unpublishing, deleting, restoring, scheduling, and assigning taxonomy terms to an entry, plus every settings, menu, taxonomy, or widget edit. A redirect write costs one purge request per source path variant, so up to four when a redirect’s source changes.
A rate-limited purge fails without an error in the admin, and the affected pages keep their cached version until maxAge expires. During bulk edits, or with several editors working at once, choose a maxAge you can accept as the worst case for stale content.
Browser caching is separate
Section titled “Browser caching is separate”The route cache can afford long TTLs because EmDash invalidates it; browser caches are not invalidated. When a page passes a cache hint with a last-modified date (as entry and collection hints do) and sets no Cache-Control itself, Astro sends Cache-Control: no-cache, so browsers revalidate on every visit. If you set your own header, for example with Astro.response.headers.set("Cache-Control", ...), keep it short (minutes, not days), so a publish also reaches returning visitors quickly. stale-while-revalidate in that header gives returning visitors instant loads while their browser revalidates in the background.