From 64979c468cd2eb7e9dd596fa4b48c1dcfb2f9c5c Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Fri, 20 Feb 2026 13:32:54 +0000 Subject: [PATCH 1/8] Add docs for experimental route caching --- astro.sidebar.ts | 1 + .../en/reference/content-loader-reference.mdx | 30 +- .../experimental-flags/route-caching.mdx | 561 ++++++++++++++++++ .../en/reference/modules/astro-content.mdx | 39 +- 4 files changed, 625 insertions(+), 6 deletions(-) create mode 100644 src/content/docs/en/reference/experimental-flags/route-caching.mdx diff --git a/astro.sidebar.ts b/astro.sidebar.ts index db3cc4b1829ea..9172318b242b1 100644 --- a/astro.sidebar.ts +++ b/astro.sidebar.ts @@ -147,6 +147,7 @@ export const sidebar = [ group('reference.experimental', { items: [ 'reference/experimental-flags', + 'reference/experimental-flags/route-caching', 'reference/experimental-flags/client-prerender', 'reference/experimental-flags/content-intellisense', 'reference/experimental-flags/chrome-devtools-workspace', diff --git a/src/content/docs/en/reference/content-loader-reference.mdx b/src/content/docs/en/reference/content-loader-reference.mdx index 1d7ae3747a58f..de19dd4188bd7 100644 --- a/src/content/docs/en/reference/content-loader-reference.mdx +++ b/src/content/docs/en/reference/content-loader-reference.mdx @@ -610,7 +610,32 @@ export function myLoader(config): LiveLoader{entry.data.name} +

{entry.data.description}

+``` + +Without route caching enabled, you can use cache hints to set response headers manually for your own caching strategy: ```astro title="src/pages/store/[id].astro" --- @@ -624,7 +649,6 @@ if (error) { return Astro.redirect('/404'); } -// Apply cache hints to response headers if (cacheHint?.tags) { Astro.response.headers.set('Cache-Tag', cacheHint.tags.join(',')); } @@ -639,7 +663,7 @@ if (cacheHint?.lastModified) { ``` :::note -Cache hints only provide values that can be used in other parts of your project and do not automatically cause the response to be cached by Astro. You can use them to create your own caching strategy, such as setting HTTP headers or using a CDN. +Cache hints do not automatically cause the response to be cached by Astro. They provide values you can pass to [route caching](/en/reference/experimental-flags/route-caching/) or use to implement your own caching strategy. ::: ## Distributing your loader diff --git a/src/content/docs/en/reference/experimental-flags/route-caching.mdx b/src/content/docs/en/reference/experimental-flags/route-caching.mdx new file mode 100644 index 0000000000000..d4cb46bd9e55d --- /dev/null +++ b/src/content/docs/en/reference/experimental-flags/route-caching.mdx @@ -0,0 +1,561 @@ +--- +title: Experimental Route Caching +sidebar: + label: Route Caching +i18nReady: false +tableOfContents: + minHeadingLevel: 2 + maxHeadingLevel: 6 +--- + +import Since from '~/components/Since.astro'; +import ReadMore from '~/components/ReadMore.astro'; + +

+ +**Type:** `object`
+**Default:** `undefined`
+ + +

+ +Enables a platform-agnostic API for caching responses from [on-demand rendered](/en/guides/on-demand-rendering/) pages and endpoints. Cache directives set in your routes are translated into the appropriate headers or runtime behavior depending on your configured cache provider. + +Route caching builds on standard [HTTP caching semantics](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching) including `max-age` and [`stale-while-revalidate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-while-revalidate), with support for tag-based and path-based invalidation, config-level route rules, and pluggable cache providers that adapters can set automatically. + +This feature requires [on-demand rendering](/en/guides/on-demand-rendering/). Prerendered pages are already static and do not use route caching. + +To enable this feature, configure `experimental.cache` with a cache provider in your Astro config: + +```js title="astro.config.mjs" ins={2} ins="memoryCache" +import { defineConfig, memoryCache } from 'astro/config'; +import node from '@astrojs/node'; + +export default defineConfig({ + adapter: node({ mode: 'standalone' }), + experimental: { + cache: { + provider: memoryCache(), + }, + }, +}); +``` + +## Using route caching + +Use `Astro.cache` in `.astro` pages or `context.cache` in API routes and middleware to control caching per request. Cache defaults for groups of routes can also be defined declaratively in your config using [`experimental.routeRules`](#route-rules). + +### Setting cache options + +Call `cache.set()` with an options object to enable caching for the current response. + +The following example caches a page for 2 minutes, serves stale content for 1 minute while revalidating, and tags the response for targeted invalidation: + +```astro title="src/pages/index.astro" ins={3-7} +--- +export const prerender = false; // Not needed in 'server' mode + +Astro.cache.set({ + maxAge: 120, + swr: 60, + tags: ['home'], +}); +--- + +Cached page +``` + +In API routes and middleware, use `context.cache`: + +```ts title="src/pages/api/data.ts" ins={2-5} +export function GET(context) { + context.cache.set({ + maxAge: 300, + tags: ['api', 'data'], + }); + return Response.json({ ok: true }); +} +``` + +### Opting out of caching + +Call `cache.set(false)` to explicitly opt a request out of caching. This is useful when a matched [route rule](#route-rules) would otherwise cache the response: + +```astro title="src/pages/dashboard.astro" +--- +if (isPersonalized) { + Astro.cache.set(false); +} +--- +``` + +### Reading cache state + +Access the current accumulated cache options via `cache.options`: + +```ts title="src/pages/api/debug.ts" +const { maxAge, swr, tags } = context.cache.options; +``` + +### Merge behavior + +Multiple calls to `cache.set()` within a single request are merged: + +- **Scalar values** (`maxAge`, `swr`, `etag`): last-write-wins +- **`lastModified`**: most recent date wins +- **`tags`**: accumulate across all calls + +Middleware, layouts, content loaders, and page code can each contribute cache directives independently. + +## Using with live content collections + +Route caching integrates directly with [live content collections](/en/guides/content-collections/#live-content-collections). `cache.set()` accepts `CacheHint` and `LiveDataEntry` objects natively, allowing cache hints from loaders to be passed through without manually setting headers. + +A [live loader](/en/reference/content-loader-reference/) can return a `cacheHint` on individual entries or on the collection as a whole. These hints include `tags` (for targeted invalidation) and `lastModified` (for freshness). When passed to `cache.set()`, they merge with any other cache options already set on the page. + +### Passing cache hints from entries + +Pass the `cacheHint` returned by `getLiveEntry()` or `getLiveCollection()` directly to `cache.set()`. + +The following example passes the loader's cache hint and adds a `maxAge` to control how long the response stays fresh: + +```astro title="src/pages/products/[id].astro" +--- +import { getLiveEntry } from 'astro:content'; + +const { entry, error, cacheHint } = await getLiveEntry('products', Astro.params.id); + +if (error) { + return Astro.redirect('/404'); +} + +if (cacheHint) { + Astro.cache.set(cacheHint); +} +Astro.cache.set({ maxAge: 300 }); +--- + +

{entry.data.name}

+``` + +A `LiveDataEntry` can also be passed directly. Astro extracts its `cacheHint` automatically: + +```astro title="src/pages/products/[id].astro" +--- +import { getLiveEntry } from 'astro:content'; + +const { entry, error } = await getLiveEntry('products', Astro.params.id); + +if (error) { + return Astro.redirect('/404'); +} + +Astro.cache.set(entry); +Astro.cache.set({ maxAge: 300, swr: 60 }); +--- + +

{entry.data.name}

+``` + +### Invalidating by entry + +`cache.invalidate()` also accepts a `LiveDataEntry`, invalidating all cached responses tagged with that entry's cache tags. + +The following example invalidates the cached response for a specific product entry: + +```ts title="src/pages/api/revalidate.ts" +import { getLiveEntry } from 'astro:content'; + +export async function POST(context) { + const { entry } = await getLiveEntry('products', 'featured'); + if (entry) { + await context.cache.invalidate(entry); + } + return Response.json({ ok: true }); +} +``` + +### Collection-level cache hints + +When fetching a full collection with `getLiveCollection()`, Astro merges cache hints from the collection response and all individual entries: tags are accumulated and the most recent `lastModified` wins. + +The following example passes the merged cache hint from a collection and sets a 10-minute freshness window: + +```astro title="src/pages/products/index.astro" +--- +import { getLiveCollection } from 'astro:content'; + +const { entries, error, cacheHint } = await getLiveCollection('products'); + +if (error) { + return new Response('Error loading products', { status: 500 }); +} + +if (cacheHint) { + Astro.cache.set(cacheHint); +} +Astro.cache.set({ maxAge: 600 }); +--- + + +``` + + + See the [Content Loader Reference](/en/reference/content-loader-reference/) for more about + implementing cache hints in your live loaders. + + +## Invalidation + +Purge cached entries by tag or path using `cache.invalidate()`. + +The following example creates an API route that invalidates by tag and by path: + +```ts title="src/pages/api/revalidate.ts" +export async function POST(context) { + // Invalidate all entries tagged 'data' + await context.cache.invalidate({ tags: ['data'] }); + + // Invalidate a specific path + await context.cache.invalidate({ path: '/api/data' }); + + return Response.json({ purged: true }); +} +``` + +Tag-based invalidation removes all cached entries whose tags include any of the provided tags. Path-based invalidation is exact-match only (no glob or wildcard patterns). + +## Route rules + +

+ +

+ +`experimental.routeRules` sets default cache options for routes declaratively in your config, without modifying route code. This is useful for applying caching to large groups of routes at once. + +The following example caches all API routes with stale-while-revalidate, product pages with a 1-hour freshness window, and blog posts for 5 minutes: + +```js title="astro.config.mjs" ins={8-12} +import { defineConfig, memoryCache } from 'astro/config'; + +export default defineConfig({ + experimental: { + cache: { + provider: memoryCache(), + }, + routeRules: { + '/api/*': { swr: 600 }, + '/products/*': { cache: { maxAge: 3600, tags: ['products'] } }, + '/blog/[...slug]': { maxAge: 300, swr: 60 }, + }, + }, +}); +``` + +### Pattern syntax + +Route patterns support: + +- **Static paths**: `/about`, `/api/health` +- **Dynamic parameters**: `/products/[id]`, `/blog/[slug]` +- **Rest parameters**: `/docs/[...path]` +- **Glob wildcards**: `/api/*` + +Patterns use the same matching and priority rules as Astro's [file-based routing](/en/guides/routing/), so more specific patterns take precedence. + +### Shortcut and full forms + +Route rules support two equivalent forms: + +```js title="astro.config.mjs" +routeRules: { + // Flat cache options at the rule level + '/api/*': { swr: 600 }, + + // Full form: nested under a `cache` key + '/api/*': { cache: { swr: 600 } }, +} +``` + +When both forms are present, the nested `cache` values take precedence over flat values. + +### Merging with per-route `cache.set()` + +Per-route `cache.set()` calls merge with config-level route rules. Route code can override or extend the defaults set in config. + +## Cache providers + +Cache behavior is determined by the configured **cache provider**. Providers fall into two categories: + +### CDN providers + +CDN providers translate cache directives into response headers (e.g. `CDN-Cache-Control`, `Cache-Tag`) and rely on a CDN or reverse proxy to handle caching. These internal headers are stripped before the response reaches the client. + +A CDN provider implements `setHeaders()` to produce the appropriate headers. + +### Runtime providers + +Runtime providers implement `onRequest()` to intercept requests and cache responses in-process. They add an `X-Astro-Cache` response header for observability: + +- **`HIT`**: response served from cache +- **`MISS`**: response rendered fresh and stored in cache +- **`STALE`**: stale response served while revalidating in the background + +### Built-in memory cache provider + +Astro includes a built-in in-memory LRU cache provider suitable for single-instance deployments. Import `memoryCache` from `astro/config`: + +```js title="astro.config.mjs" ins="memoryCache" +import { defineConfig, memoryCache } from 'astro/config'; + +export default defineConfig({ + experimental: { + cache: { + provider: memoryCache({ max: 500 }), + }, + }, +}); +``` + +#### `memoryCache()` options + +

+ +**Type:** `{ max?: number }`
+**Default:** `{ max: 1000 }` + +

+ +##### `max` + +

+ +**Type:** `number`
+**Default:** `1000` + +

+ +Maximum number of entries to keep in cache. When the cache exceeds this limit, the least recently used entry is evicted. + +## Writing a custom cache provider + +A cache provider has two parts: + +1. **The runtime module** — A file that **default-exports** a `CacheProviderFactory` function. This module is bundled into your SSR output, so it must be runtime-agnostic: avoid Node.js built-in modules (e.g. `node:fs`, `node:path`) unless your target runtime supports them. + +2. **The config helper** — A function exported for users to call in `astro.config.mjs`. It returns a `CacheProviderConfig` object (`{ entrypoint, config }`) that tells Astro where to find the runtime module and what options to pass it. This is the same pattern used by `memoryCache()` from `astro/config`. + +The following example shows a config helper that accepts typed options and points to a runtime module: + +```ts title="my-provider/config.ts" +import type { CacheProviderConfig } from 'astro'; + +interface MyProviderOptions { + apiKey: string; + region?: string; +} + +export function myCache(options: MyProviderOptions): CacheProviderConfig { + return { + entrypoint: 'my-provider/runtime', // resolved from the project root + config: options, // passed to the factory at runtime + }; +} +``` + +The config helper is then called in the Astro config: + +```js title="astro.config.mjs" +import { defineConfig } from 'astro/config'; +import { myCache } from 'my-provider/config'; + +export default defineConfig({ + experimental: { + cache: { + provider: myCache({ apiKey: '...' }), + }, + }, +}); +``` + +The runtime module default-exports a factory that receives the serialized `config` and returns a `CacheProvider`: + +```ts title="my-provider/runtime.ts" +import type { CacheProviderFactory } from 'astro'; + +const factory: CacheProviderFactory = (config) => { + return { + name: 'my-cache-provider', + + // CDN-style: translate cache options into response headers + setHeaders(options) { + const headers = new Headers(); + if (options.maxAge !== undefined) { + let value = `max-age=${options.maxAge}`; + if (options.swr !== undefined) { + value += `, stale-while-revalidate=${options.swr}`; + } + headers.set('CDN-Cache-Control', value); + } + if (options.tags?.length) { + headers.set('Cache-Tag', options.tags.join(',')); + } + return headers; + }, + + // Runtime-style: intercept requests (optional) + async onRequest(context, next) { + // Check cache, call next(), store response... + return next(); + }, + + // Handle invalidation requests + async invalidate(options) { + // Purge by tags or path... + }, + }; +}; + +export default factory; +``` + +### `CacheProvider` interface + +#### `name` + +

+ +**Type:** `string` + +

+ +A unique name for the provider, used in logs and for identification. + +#### `setHeaders()` + +

+ +**Type:** `(options: CacheOptions) => Headers` + +

+ +Optional. Translates cache options into response headers. Called after the response is rendered but before it is sent to the client. These headers are stripped from the final response. + +#### `onRequest()` + +

+ +**Type:** `(context: { request: Request; url: URL; waitUntil?: (promise: Promise) => void }, next: MiddlewareNext) => Promise` + +

+ +Optional. Intercepts requests to implement runtime caching. The `context` includes a `waitUntil` function (when available in the runtime) for background work such as stale-while-revalidate. + +#### `invalidate()` + +

+ +**Type:** `(options: InvalidateOptions) => Promise` + +

+ +Required. Handles purge requests by tag or path. + +### `CacheProviderFactory` + +

+ +**Type:** `(config: Record | undefined) => CacheProvider` + +

+ +The factory function type. Receives the provider's serializable config object from the Astro config. + +## API reference + +### `cache.set()` + +

+ +**Type:** `(options: CacheOptions | false) => void` + +

+ +Sets cache options for the current request. Pass `false` to opt out of caching. + +#### `CacheOptions` + +| Property | Type | Description | +| -------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------ | +| `maxAge` | `number` | Time in seconds the response is considered fresh. | +| `swr` | `number` | Stale-while-revalidate window in seconds. Stale content is served while a fresh response is generated in the background. | +| `tags` | `string[]` | Cache tags for targeted invalidation. Tags accumulate across multiple `set()` calls. | +| `lastModified` | `Date` | When multiple `set()` calls provide `lastModified`, the most recent date wins. | +| `etag` | `string` | Entity tag for conditional requests. | + +### `cache.options` + +

+ +**Type:** `Readonly` + +

+ +Read-only snapshot of the current accumulated cache options, including all merged `maxAge`, `swr`, `etag`, `lastModified`, and `tags` values. + +### `cache.tags` + +

+ +**Type:** `string[]` + +

+ +Read-only array of all accumulated cache tags. + +### `cache.invalidate()` + +

+ +**Type:** `(options: InvalidateOptions) => Promise` + +

+ +Purges cached entries. Requires a configured cache provider. + +#### `InvalidateOptions` + +| Property | Type | Description | +| -------- | -------------------- | --------------------------------------------------------------------- | +| `path` | `string` | Exact path to invalidate. No glob or wildcard support. | +| `tags` | `string \| string[]` | Tag or tags to invalidate. All entries with matching tags are purged. | + +### `CacheProviderConfig` + +

+ +**Type:** `{ entrypoint: string \| URL; config?: Record }` + +

+ +The configuration object passed to `experimental.cache.provider`. Use a helper function (e.g. `memoryCache()`) for type-safe configuration. + +## Error handling + +### `CacheNotEnabled` + +Thrown when `Astro.cache` or `context.cache` is used but `experimental.cache` is not configured. The error message explains how to enable the feature. + +### `CacheProviderNotFound` + +Thrown at build time when the configured cache provider cannot be resolved. This typically means the package is not installed or the import path is incorrect. + +## Dev mode behavior + +In dev mode, the cache API is available so route code does not need conditional checks, but no actual caching occurs. `cache.set()` calls are accepted silently, and `cache.invalidate()` is a no-op. + +## Further reading + +For full details and to give feedback on this experimental API, see the [Route Caching RFC](https://github.com/withastro/roadmap/pull/1245). diff --git a/src/content/docs/en/reference/modules/astro-content.mdx b/src/content/docs/en/reference/modules/astro-content.mdx index 7bd116be14023..2a21a9b14e72d 100644 --- a/src/content/docs/en/reference/modules/astro-content.mdx +++ b/src/content/docs/en/reference/modules/astro-content.mdx @@ -543,7 +543,23 @@ const { error } = await getLiveCollection('products'); An object providing guidance on how to cache this collection. -The following example accesses the cache hint for entries in a live collection named `products` and uses it to set the response headers: +If you have [experimental route caching](/en/reference/experimental-flags/route-caching/) enabled, pass the cache hint directly to `Astro.cache.set()`: + +```astro title="src/pages/shop/index.astro" +--- +import { getLiveCollection } from 'astro:content'; +export const prerender = false; // Not needed in 'server' mode + +const { cacheHint } = await getLiveCollection('products'); + +if (cacheHint) { + Astro.cache.set(cacheHint); +} +Astro.cache.set({ maxAge: 600 }); +--- +``` + +You can also use cache hints to set response headers manually: ```astro title="src/pages/shop/index.astro" --- @@ -624,9 +640,26 @@ if (error) { **Type:** CacheHint | undefined

-An object providing data that can be used to set response headers to inform a caching strategy. +An object providing data that can be used to inform a caching strategy. + +If you have [experimental route caching](/en/reference/experimental-flags/route-caching/) enabled, pass the cache hint directly to `Astro.cache.set()`: + +```astro title="src/pages/shop/[id].astro" +--- +import { getLiveEntry } from 'astro:content'; + +export const prerender = false; // Not needed in 'server' mode + +const { cacheHint } = await getLiveEntry('products', Astro.params.id); + +if (cacheHint) { + Astro.cache.set(cacheHint); +} +Astro.cache.set({ maxAge: 300 }); +--- +``` -The following example accesses the cache hint for an entry in a live collection named `products` and uses it to set the response headers: +You can also use cache hints to set response headers manually: ```astro title="src/pages/shop/[id].astro" --- From eadf6d6afbfd94a97ad581850574a3dbb49dcd89 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 24 Feb 2026 12:23:17 +0000 Subject: [PATCH 2/8] Apply suggestions from code review Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> Co-authored-by: Armand Philippot Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .../experimental-flags/route-caching.mdx | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/content/docs/en/reference/experimental-flags/route-caching.mdx b/src/content/docs/en/reference/experimental-flags/route-caching.mdx index d4cb46bd9e55d..ff050ac936e2c 100644 --- a/src/content/docs/en/reference/experimental-flags/route-caching.mdx +++ b/src/content/docs/en/reference/experimental-flags/route-caching.mdx @@ -21,13 +21,13 @@ import ReadMore from '~/components/ReadMore.astro'; Enables a platform-agnostic API for caching responses from [on-demand rendered](/en/guides/on-demand-rendering/) pages and endpoints. Cache directives set in your routes are translated into the appropriate headers or runtime behavior depending on your configured cache provider. -Route caching builds on standard [HTTP caching semantics](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching) including `max-age` and [`stale-while-revalidate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-while-revalidate), with support for tag-based and path-based invalidation, config-level route rules, and pluggable cache providers that adapters can set automatically. +Route caching builds on standard [HTTP caching semantics](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching), including `max-age` and [`stale-while-revalidate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-while-revalidate), with support for tag-based and path-based invalidation, config-level route rules, and pluggable cache providers that adapters can set automatically. This feature requires [on-demand rendering](/en/guides/on-demand-rendering/). Prerendered pages are already static and do not use route caching. To enable this feature, configure `experimental.cache` with a cache provider in your Astro config: -```js title="astro.config.mjs" ins={2} ins="memoryCache" +```js title="astro.config.mjs" {2,5} ins="memoryCache" import { defineConfig, memoryCache } from 'astro/config'; import node from '@astrojs/node'; @@ -51,7 +51,7 @@ Call `cache.set()` with an options object to enable caching for the current resp The following example caches a page for 2 minutes, serves stale content for 1 minute while revalidating, and tags the response for targeted invalidation: -```astro title="src/pages/index.astro" ins={3-7} +```astro title="src/pages/index.astro" ins={4-8} --- export const prerender = false; // Not needed in 'server' mode @@ -111,7 +111,7 @@ Middleware, layouts, content loaders, and page code can each contribute cache di Route caching integrates directly with [live content collections](/en/guides/content-collections/#live-content-collections). `cache.set()` accepts `CacheHint` and `LiveDataEntry` objects natively, allowing cache hints from loaders to be passed through without manually setting headers. -A [live loader](/en/reference/content-loader-reference/) can return a `cacheHint` on individual entries or on the collection as a whole. These hints include `tags` (for targeted invalidation) and `lastModified` (for freshness). When passed to `cache.set()`, they merge with any other cache options already set on the page. +A [live loader](/en/reference/content-loader-reference/#live-loaders) can return a `cacheHint` on individual entries or on the collection as a whole. These hints include `tags` (for targeted invalidation) and `lastModified` (for freshness). When passed to `cache.set()`, they merge with any other cache options already set on the page. ### Passing cache hints from entries @@ -138,7 +138,7 @@ Astro.cache.set({ maxAge: 300 });

{entry.data.name}

``` -A `LiveDataEntry` can also be passed directly. Astro extracts its `cacheHint` automatically: +A [`LiveDataEntry`](/en/reference/content-loader-reference/#livedataentry) can also be passed directly. Astro extracts its `cacheHint` automatically: ```astro title="src/pages/products/[id].astro" --- @@ -177,7 +177,7 @@ export async function POST(context) { ### Collection-level cache hints -When fetching a full collection with `getLiveCollection()`, Astro merges cache hints from the collection response and all individual entries: tags are accumulated and the most recent `lastModified` wins. +When fetching a full collection with `getLiveCollection()`, Astro merges cache hints from the collection response and all individual entries: tags are accumulated, and the most recent `lastModified` wins. The following example passes the merged cache hint from a collection and sets a 10-minute freshness window: @@ -225,7 +225,7 @@ export async function POST(context) { } ``` -Tag-based invalidation removes all cached entries whose tags include any of the provided tags. Path-based invalidation is exact-match only (no glob or wildcard patterns). +Tag-based invalidation removes all cached entries whose tags include any of the provided tags. Path-based invalidation is exact-match only (no [glob](/en/guides/imports/#glob-patterns) or wildcard patterns). ## Route rules @@ -263,7 +263,7 @@ Route patterns support: - **Rest parameters**: `/docs/[...path]` - **Glob wildcards**: `/api/*` -Patterns use the same matching and priority rules as Astro's [file-based routing](/en/guides/routing/), so more specific patterns take precedence. +Patterns use the same matching and priority rules as Astro's [file-based routing](/en/guides/routing/#route-priority-order), so more specific patterns take precedence. ### Shortcut and full forms @@ -328,6 +328,8 @@ export default defineConfig({

+To further control how entries are cached, you can use the following options. + ##### `max`

@@ -345,7 +347,7 @@ A cache provider has two parts: 1. **The runtime module** — A file that **default-exports** a `CacheProviderFactory` function. This module is bundled into your SSR output, so it must be runtime-agnostic: avoid Node.js built-in modules (e.g. `node:fs`, `node:path`) unless your target runtime supports them. -2. **The config helper** — A function exported for users to call in `astro.config.mjs`. It returns a `CacheProviderConfig` object (`{ entrypoint, config }`) that tells Astro where to find the runtime module and what options to pass it. This is the same pattern used by `memoryCache()` from `astro/config`. +2. **The config helper** — A function exported for users to call in `astro.config.mjs`. It returns a [`CacheProviderConfig` object](#cacheproviderconfig) that tells Astro where to find the runtime module and what options to pass it. This is the same pattern used by `memoryCache()` from `astro/config`. The following example shows a config helper that accepts typed options and points to a runtime module: @@ -380,7 +382,7 @@ export default defineConfig({ }); ``` -The runtime module default-exports a factory that receives the serialized `config` and returns a `CacheProvider`: +The runtime module default-exports a factory that receives the serialized `config` and returns a [`CacheProvider`](#cacheprovider-interface): ```ts title="my-provider/runtime.ts" import type { CacheProviderFactory } from 'astro'; @@ -423,6 +425,8 @@ export default factory; ### `CacheProvider` interface +Describes a provider used for caching. This requires the `name` and `invalidate()` properties and accepts optional properties. + #### `name`

@@ -499,7 +503,7 @@ Sets cache options for the current request. Pass `false` to opt out of caching.

-**Type:** `Readonly` +**Type:** Readonly\<CacheOptions\> +**Type:** Readonly\<CacheOptions\>

From b93732a297114a9cf785db2770e88a44849a625e Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 24 Feb 2026 14:19:33 +0000 Subject: [PATCH 4/8] Changes from review --- .../experimental-flags/route-caching.mdx | 85 ++++++++++++++++--- 1 file changed, 71 insertions(+), 14 deletions(-) diff --git a/src/content/docs/en/reference/experimental-flags/route-caching.mdx b/src/content/docs/en/reference/experimental-flags/route-caching.mdx index f85ad19a06613..aeff4d4c307b3 100644 --- a/src/content/docs/en/reference/experimental-flags/route-caching.mdx +++ b/src/content/docs/en/reference/experimental-flags/route-caching.mdx @@ -445,7 +445,7 @@ A unique name for the provider, used in logs and for identification.

-Optional. Translates cache options into response headers. Called after the response is rendered but before it is sent to the client. These headers are stripped from the final response. +Translates cache options into response headers. Called after the response is rendered but before it is sent to the client. These headers are stripped from the final response. #### `onRequest()` @@ -455,7 +455,7 @@ Optional. Translates cache options into response headers. Called after the respo

-Optional. Intercepts requests to implement runtime caching. The `context` includes a `waitUntil` function (when available in the runtime) for background work such as stale-while-revalidate. +Intercepts requests to implement runtime caching. The `context` includes a `waitUntil` function (when available in the runtime) for background work such as stale-while-revalidate. #### `invalidate()` @@ -465,7 +465,7 @@ Optional. Intercepts requests to implement runtime caching. The `context` includ

-Required. Handles purge requests by tag or path. +Handles purge requests by tag or path. ### `CacheProviderFactory` @@ -491,13 +491,55 @@ Sets cache options for the current request. Pass `false` to opt out of caching. #### `CacheOptions` -| Property | Type | Description | -| -------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------ | -| `maxAge` | `number` | Time in seconds the response is considered fresh. | -| `swr` | `number` | Stale-while-revalidate window in seconds. Stale content is served while a fresh response is generated in the background. | -| `tags` | `string[]` | Cache tags for targeted invalidation. Tags accumulate across multiple `set()` calls. | -| `lastModified` | `Date` | When multiple `set()` calls provide `lastModified`, the most recent date wins. | -| `etag` | `string` | Entity tag for conditional requests. | +##### `maxAge` + +

+ +**Type:** `number` + +

+ +Time in seconds the response is considered fresh. + +##### `swr` + +

+ +**Type:** `number` + +

+ +Stale-while-revalidate window in seconds. Stale content is served while a fresh response is generated in the background. + +##### `tags` + +

+ +**Type:** `string[]` + +

+ +Cache tags for targeted invalidation. Tags accumulate across multiple `set()` calls. + +##### `lastModified` + +

+ +**Type:** `Date` + +

+ +When multiple `set()` calls provide `lastModified`, the most recent date wins. + +##### `etag` + +

+ +**Type:** `string` + +

+ +Entity tag for conditional requests. ### `cache.options` @@ -531,10 +573,25 @@ Purges cached entries. Requires a configured cache provider. #### `InvalidateOptions` -| Property | Type | Description | -| -------- | -------------------- | --------------------------------------------------------------------- | -| `path` | `string` | Exact path to invalidate. No glob or wildcard support. | -| `tags` | `string \| string[]` | Tag or tags to invalidate. All entries with matching tags are purged. | +##### `path` + +

+ +**Type:** `string` + +

+ +Exact path to invalidate. No glob or wildcard support. + +##### `tags` + +

+ +**Type:** `string \| string[]` + +

+ +Tag or tags to invalidate. All entries with matching tags are purged. ### `CacheProviderConfig` From 62be5275687d23e030a5cc5f10bca77a52d1f59a Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 24 Feb 2026 14:21:24 +0000 Subject: [PATCH 5/8] Fix escaping --- .../docs/en/reference/experimental-flags/route-caching.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/docs/en/reference/experimental-flags/route-caching.mdx b/src/content/docs/en/reference/experimental-flags/route-caching.mdx index aeff4d4c307b3..f352293f8ffc4 100644 --- a/src/content/docs/en/reference/experimental-flags/route-caching.mdx +++ b/src/content/docs/en/reference/experimental-flags/route-caching.mdx @@ -587,7 +587,7 @@ Exact path to invalidate. No glob or wildcard support.

-**Type:** `string \| string[]` +**Type:** `string | string[]`

@@ -597,7 +597,7 @@ Tag or tags to invalidate. All entries with matching tags are purged.

-**Type:** `{ entrypoint: string \| URL; config?: Record }` +**Type:** `{ entrypoint: string | URL; config?: Record }`

From 6ab3ba757614ccba39d256e3e2e7245eb4c7fe42 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 24 Feb 2026 15:32:05 +0000 Subject: [PATCH 6/8] Remove nested syntax --- .../experimental-flags/route-caching.mdx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/content/docs/en/reference/experimental-flags/route-caching.mdx b/src/content/docs/en/reference/experimental-flags/route-caching.mdx index f352293f8ffc4..70ab03e701282 100644 --- a/src/content/docs/en/reference/experimental-flags/route-caching.mdx +++ b/src/content/docs/en/reference/experimental-flags/route-caching.mdx @@ -247,7 +247,7 @@ export default defineConfig({ }, routeRules: { '/api/*': { swr: 600 }, - '/products/*': { cache: { maxAge: 3600, tags: ['products'] } }, + '/products/*': { maxAge: 3600, tags: ['products'] }, '/blog/[...slug]': { maxAge: 300, swr: 60 }, }, }, @@ -265,22 +265,6 @@ Route patterns support: Patterns use the same matching and priority rules as Astro's [file-based routing](/en/guides/routing/#route-priority-order), so more specific patterns take precedence. -### Shortcut and full forms - -Route rules support two equivalent forms: - -```js title="astro.config.mjs" -routeRules: { - // Flat cache options at the rule level - '/api/*': { swr: 600 }, - - // Full form: nested under a `cache` key - '/api/*': { cache: { swr: 600 } }, -} -``` - -When both forms are present, the nested `cache` values take precedence over flat values. - ### Merging with per-route `cache.set()` Per-route `cache.set()` calls merge with config-level route rules. Route code can override or extend the defaults set in config. From 60cde86444190752414e3cf0d9ce4509de0deadf Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Fri, 27 Feb 2026 13:52:49 +0000 Subject: [PATCH 7/8] Update src/content/docs/en/reference/experimental-flags/route-caching.mdx Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com> --- .../docs/en/reference/experimental-flags/route-caching.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/docs/en/reference/experimental-flags/route-caching.mdx b/src/content/docs/en/reference/experimental-flags/route-caching.mdx index 70ab03e701282..c384ab5f3fa3d 100644 --- a/src/content/docs/en/reference/experimental-flags/route-caching.mdx +++ b/src/content/docs/en/reference/experimental-flags/route-caching.mdx @@ -1,7 +1,7 @@ --- -title: Experimental Route Caching +title: Experimental route caching sidebar: - label: Route Caching + label: Route caching i18nReady: false tableOfContents: minHeadingLevel: 2 From b0c21a02e814aab9d48975a3f31446ea0efa4c7a Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Mon, 2 Mar 2026 11:39:51 +0000 Subject: [PATCH 8/8] Add new memory cache options --- .../experimental-flags/route-caching.mdx | 70 +++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/src/content/docs/en/reference/experimental-flags/route-caching.mdx b/src/content/docs/en/reference/experimental-flags/route-caching.mdx index c384ab5f3fa3d..86ff98c52bbd9 100644 --- a/src/content/docs/en/reference/experimental-flags/route-caching.mdx +++ b/src/content/docs/en/reference/experimental-flags/route-caching.mdx @@ -289,7 +289,7 @@ Runtime providers implement `onRequest()` to intercept requests and cache respon ### Built-in memory cache provider -Astro includes a built-in in-memory LRU cache provider suitable for single-instance deployments. Import `memoryCache` from `astro/config`: +Astro includes a built-in in-memory LRU runtime cache provider suitable for single-instance deployments. Import `memoryCache` from `astro/config`: ```js title="astro.config.mjs" ins="memoryCache" import { defineConfig, memoryCache } from 'astro/config'; @@ -303,17 +303,27 @@ export default defineConfig({ }); ``` +#### Cache key behavior + +The memory provider automatically normalizes cache keys for better hit rates: + +- **Query parameter sorting**: Parameters are sorted alphabetically, so `/page?b=2&a=1` and `/page?a=1&b=2` resolve to the same cache entry. +- **Tracking parameter exclusion**: Common analytics and tracking parameters (e.g. `utm_source`, `fbclid`, `gclid`) are excluded from the cache key by default. This prevents cache fragmentation from marketing links without affecting your page content. +- **Vary header support**: When a response includes a [`Vary`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) header, the memory provider uses the specified request header values to create separate cache entries for each variant. For example, a response with `Vary: Accept-Language` will cache different versions for different languages. + +:::note +The `Cookie` header is always ignored for Vary-based cache keying because it has extremely high cardinality (every user typically has different cookies), making it effectively uncacheable. +::: + #### `memoryCache()` options

-**Type:** `{ max?: number }`
+**Type:** `{ max?: number; query?: object }`
**Default:** `{ max: 1000 }`

-To further control how entries are cached, you can use the following options. - ##### `max`

@@ -325,6 +335,58 @@ To further control how entries are cached, you can use the following options. Maximum number of entries to keep in cache. When the cache exceeds this limit, the least recently used entry is evicted. +##### `query` + +Controls how query parameters are handled in cache keys. + +###### `query.sort` + +

+ +**Type:** `boolean`
+**Default:** `true` + +

+ +Sort query parameters alphabetically so that parameter order does not affect the cache key. Set to `false` to disable sorting, and cache URLs with different query parameter order separately. This is useful when parameter order is significant. + +###### `query.exclude` + +

+ +**Type:** `string[]`
+**Default:** `['utm_*', 'fbclid', 'gclid', 'gbraid', 'wbraid', 'dclid', 'msclkid', 'twclid', 'li_fat_id', 'mc_cid', 'mc_eid', '_ga', '_gl', '_hsenc', '_hsmi', '_ke', 'oly_anon_id', 'oly_enc_id', 'rb_clickid', 's_cid', 'vero_id', 'wickedid', 'yclid', '__s', 'ref']` + +

+ +Exclude query parameters whose names match these patterns from the cache key. Supports glob wildcards (e.g. `"utm_*"`). Cannot be used together with `include`. + +By default, common tracking and analytics parameters are excluded. Set to `[]` to include all query parameters in the cache key: + +```js title="astro.config.mjs" +memoryCache({ + query: { exclude: [] }, +}); +``` + +###### `query.include` + +

+ +**Type:** `string[]` + +

+ +Only include these query parameter names in the cache key. All other parameters are ignored, including the default tracking parameter exclusions. Cannot be used together with `exclude`. + +The following example only uses `page` and `sort` parameters in the cache key, ignoring all others: + +```js title="astro.config.mjs" +memoryCache({ + query: { include: ['page', 'sort'] }, +}); +``` + ## Writing a custom cache provider A cache provider has two parts: