export const metadata = { slug: "guides/caching", title: "Caching on Cloudflare", description: "Choose how vinext stores rendered responses and cached data on Cloudflare Workers.", section: "Guides", sectionOrder: 3, order: 1, navTitle: "Caching on Cloudflare", keywords: ["vinext caching", "Workers Response Store", "Next.js ISR Cloudflare"], }; # Caching Vinext supports several caching setups on Cloudflare. Caching is optional: if you do not enable it, your app still works without a shared response or data cache. **Workers Response Store, Workers Cache, KV, and Static Assets support both App Router and Pages Router.** The adapter configuration is the same for either router; the Next.js APIs that make a page cacheable differ. The [Static Assets response adapter](#static-assets-response-cache) serves immutable build-time snapshots instead of supporting runtime ISR. ## What Vinext caches There are two main kinds of cache: - **Response / CDN cache:** rendered HTML for both routers, App Router RSC payloads, and Pages Router JSON data used for client-side navigation (`/_next/data/...`). - **Data cache:** cached `fetch` calls, `unstable_cache`, and functions marked with `"use cache"`. App Router uses `revalidatePath()` and `revalidateTag()` to invalidate cached content. Pages Router exposes `res.revalidate()` from API routes for on-demand page revalidation, with a current Response Store limitation described below. See [Which Pages Router pages are cached](#which-pages-router-pages-are-cached) and [Which App Router pages are cached](#which-app-router-pages-are-cached) for each router's cacheability rules. Static files and browser caching are separate from these options. Cloudflare can cache built assets without enabling a vinext cache adapter. ## Opting a route out of response caching If an App Router page or Route Handler must render for every request and should not consult the shared response cache, prefer an explicit route segment config: ```ts export const dynamic = "force-dynamic"; ``` This matches Next.js behavior and lets vinext identify the route at build time, before a configured Response Store or Workers Cache lookup. Use it for an unconditional opt-out instead of relying on request APIs such as `cookies()` or `headers()` to make the route dynamic during rendering. A matching public cache policy from `next.config` still takes precedence when one is deliberately configured. For Pages Router, use `getServerSideProps()` for per-request rendering. Its responses are not shared by default; avoid setting a public cache policy if the response must remain request-specific. Pages Router does not use the App Router's `dynamic` route segment export. ## Options
Setup Response storage Data storage Best for Main trade-off
No persistent cache In-memory In-memory Dynamic apps and initial migrations Every request may need to render and fetch its data
Workers Response Store (recommended) Workers Cache backed by R2 Workers Response Store Durable responses, SWR, cache warming, and one cache system for responses and data Requires R2, a SQLite Durable Object, and either a separate cache Worker or extra bindings on the app Worker
Workers Cache + data cache Workers Cache Workers KV Fast edge responses using Cloudflare's native caches Cached responses have no durable backing store, and a hit in one region does not guarantee a hit elsewhere
Data cache Workers KV Workers KV A simple persistent cache without Workers Cache Requests still reach the Worker and KV is eventually consistent
Static Assets Workers Static Assets Separately configured Immutable sites that should render during the build but still run as a Worker Read-only until the next deployment; runtime writes and invalidations are ignored
When caching is enabled through `vinext init`, Workers Response Store is the default choice. The snippets below extend the generated config; keep your other Worker settings and bindings. `existingExports` represents your current Worker's `exports` object, or `{}` if it has none. ## Workers Response Store Workers Response Store is the most complete and best supported option. Workers Cache serves hot responses, R2 provides a durable backing store, and a SQLite Durable Object tracks metadata and invalidation state. An edge miss can read the stored response from R2 instead of immediately rendering the route again. It also handles the data cache, so it replaces both `workersCacheCdnAdapter()` and `kvDataAdapter()`: ```ts import { responseStoreAdapter } from "@vinext/cloudflare/cache/response-store-adapter"; vinext({ cache: responseStoreAdapter() }); ``` Metadata sharding is available as an explicit scaling option: ```ts vinext({ cache: responseStoreAdapter({ shards: 16 }) }); ``` Each cache key remains strongly coordinated by one SQLite Durable Object. Tag/path refreshes and purges fan out across all shards. The option is disabled by default, and changing the count starts a new cache layout for the deployed Worker version. ### Service-binding mode The default service-binding mode keeps the cache service in a separate Worker. `vinext init` defines both Workers in `cloudflare.config.ts` using the config helper: ```ts import { createWorkersResponseStoreServiceBindingConfig } from "@vinext/cloudflare/cache/config"; const responseStore = await createWorkersResponseStoreServiceBindingConfig({ worker: { name: "my-app-response-store", compatibilityDate: "2026-09-27", compatibilityFlags: ["nodejs_compat"], }, bucket: "my-app-response-store-cache-bodies", }); export const responseStoreServiceBinding = responseStore.serviceBindingWorker; ``` Merge the application settings into the existing Worker config, preserving your other bindings: ```ts defineWorker({ // ...existing Worker settings ...responseStore.applicationWorker, env: { // ...existing bindings ...responseStore.applicationWorker.env, }, }); ``` The helper owns the Response Store's R2 binding, SQLite Durable Object export and binding, cache settings, and application service/version-metadata bindings. It imports bindings and exports from your installed Cloudflare Vite plugin. Set `accountId` on the top-level `defineConfig` object if needed, not on a Worker. You can also pass it to the helper and use the returned `responseStore.accountId` there. Add the exported Worker to your existing `cloudflare()` options in `vite.config.ts`: ```ts import { responseStoreServiceBinding } from "./cloudflare.config"; cloudflare({ // ...existing plugin options auxiliaryWorkers: [{ config: responseStoreServiceBinding }], }); ``` Build both Workers and deploy the Response Store explicitly. `cf deploy` creates the named R2 bucket if it does not already exist: ```sh pnpm run build:vinext pnpm run deploy:response-store pnpm run deploy:vinext ``` Init generates `deploy:response-store` as `cf deploy --prebuilt --mode production --worker my-app-response-store` using your project's Worker name. `create-vinext-app` names the other scripts `build` and `deploy`. Both Workers build together, but application deployments do not deploy the Response Store. Redeploy it only when its package or config changes. ### Self-contained mode For a single-Worker deployment, select self-contained mode in the adapter: ```ts vinext({ cache: responseStoreAdapter({ mode: "self-contained" }) }); ``` This avoids a second Worker, but the application Worker owns the R2 bucket, Durable Object, and cache-enabled entrypoint, which may not be desired for some applications. Use the matching config helper. The adapter generates the required Worker exports during the build: ```ts import { createWorkersResponseStoreSelfContainedConfig } from "@vinext/cloudflare/cache/config"; const cache = await createWorkersResponseStoreSelfContainedConfig({ worker: "my-app", bucket: "my-app-response-store-cache-bodies", }); defineWorker({ // ...existing Worker settings ...cache, env: { /* existing bindings, */ ...cache.env }, exports: { ...existingExports, ...cache.exports }, }); ``` `cf deploy` creates the named R2 bucket if it does not already exist. No auxiliary Worker or separate Response Store deployment is needed in this mode. ## Workers Cache and KV The older split setup uses Workers Cache for rendered responses and Workers KV for cached data: ```ts import { workersCacheCdnAdapter } from "@vinext/cloudflare/cache/workers-cache-cdn-adapter"; import { kvDataAdapter } from "@vinext/cloudflare/cache/kv-data-adapter"; vinext({ cache: { cdn: workersCacheCdnAdapter(), data: kvDataAdapter(), }, }); ``` Workers Cache can serve a response without rerunning the render stage. Middleware and request routing still run before vinext selects the cached response entrypoint. Route Handler and Pages API responses keep their explicit `Cache-Control`, including policies set by middleware or `next.config`. As in Next.js, a pass-through middleware match does not remove browser freshness. The app owns this client policy; choose its lifetime with middleware, authentication and downstream caches in mind. The framework's backing-store policy is separate. An ordinary `Cache-Control` header cannot make a dynamic route cacheable, disable an otherwise static route's storage, or change its revalidation lifetime. For example, `revalidate = 60` still stores a response for 60 seconds with `Cache-Control: max-age=1`, `private`, or `no-store`; `revalidate = 2` still refreshes after two seconds with `max-age=3600`. Pages `getStaticProps` revalidation follows the same rule. `getServerSideProps` and Pages APIs remain uncached internally even when they return a public browser policy. Metadata routes use their framework static/dynamic classification and revalidation settings. Explicit provider headers remain a Cloudflare extension: `Cloudflare-CDN-Cache-Control` takes precedence over `CDN-Cache-Control` and can select the shared response-cache policy, subject to admission safeguards. Thus `Cache-Control: max-age=10` with `Cloudflare-CDN-Cache-Control: max-age=3600` keeps a 10-second browser lifetime and an hour-long response-cache lifetime. Without a provider override, vinext supplies the framework storage policy to the adapter; it does not infer it from the browser header. The gateway consumes provider headers and preserves the authored client policy. Framework-managed responses without an explicit client policy still require browser revalidation. This setup is fast when an entry is present at the edge, but Workers Cache is distributed rather than one globally shared cache. A response cached in one region may still miss in another. Tiered caching can reduce this duplication, but without a durable response store a regional miss or eviction can require another render. The KV data cache is also eventually consistent. `revalidatePath()` and `revalidateTag()` purge the edge by tag. [Workers Cache purges](https://developers.cloudflare.com/workers/cache/purge/) use Cloudflare's Free-tier purge limits, which every Worker in the account shares. As in Next.js, vinext sends one batched purge per cacheLife profile at the end of a Server Action or Route Handler, not one per call. If a purge fails, for example because the rate limit is reached, vinext logs the error and the action still succeeds: the data cache is already invalidated, but edge-cached responses for those tags stay stale until they expire. A route that only needs the client to refetch dynamic data can call `refresh()`, which doesn't purge. `revalidatePath()` skips the edge purge when every App route that could serve the path sets `dynamic = "force-dynamic"`, because those responses are never cached at the edge. Marking a signed-in area's layout `force-dynamic` therefore keeps its revalidations off the purge budget. The data cache is still invalidated. vinext purges as usual when the app also has a Pages Router or when a metadata route could serve the path. Use `createWorkersCacheConfig()` to configure the response entrypoints and version metadata, and add the KV binding for the data adapter: ```ts import { bindings } from "cf/config"; import { createWorkersCacheConfig } from "@vinext/cloudflare/cache/config"; const cache = await createWorkersCacheConfig(); defineWorker({ // ...existing Worker settings ...cache, env: { // ...existing bindings ...cache.env, VINEXT_KV_CACHE: bindings.kv(), }, exports: { ...existingExports, ...cache.exports }, }); ``` The helper disables caching on the application's default entrypoint and enables it on `VinextCachedResponse`. The source config owns these policies; vinext does not rewrite them in the Build Output. ## KV data cache You can use Workers KV without Workers Cache: ```ts import { kvDataAdapter } from "@vinext/cloudflare/cache/kv-data-adapter"; vinext({ cache: { data: kvDataAdapter(), }, }); ``` Add the matching namespace to the Worker's `env` in `cloudflare.config.ts`: ```ts import { bindings } from "cf/config"; env: { // ...existing bindings VINEXT_KV_CACHE: bindings.kv(), }, ``` Cloudflare automatically provisions the namespace on deployment. To reuse an existing namespace, pass its ID to `bindings.kv({ id: "" })`. This is the smallest persistent setup. The same data cache can hold ISR responses and nested cached data, but every lookup goes through the application Worker and KV's eventual consistency may briefly expose older values after an update. Each cached page or data entry in KV expires after the adapter's `ttlSeconds` (30 days by default, set with `kvDataAdapter({ ttlSeconds })` and raised to KV's 60-second minimum if it's between 0 and 60). This includes App Router pages with `revalidate = false` and Pages Router pages whose `getStaticProps()` omits `revalidate` or returns `revalidate: false`. They are rendered again on the next request once their entry expires. ## Static Assets response cache For immutable routes, `staticAssetsAdapter()` packages locally prerendered responses from both routers into the configured client assets output. It stores App Router HTML, RSC payloads, and metadata responses, plus Pages Router HTML, the props used by `/_next/data/...` navigation requests, and build-time redirect and not-found results. The Worker reads those artifacts through its `ASSETS` binding: ```ts import { staticAssetsAdapter } from "@vinext/cloudflare/cache/static-assets-adapter"; vinext({ prerender: true, cache: { cdn: staticAssetsAdapter(), }, }); ``` For Pages Router, prerendering includes automatically static pages and `getStaticProps()` pages, including the paths returned by `getStaticPaths()`. HTML and navigation JSON use the same build-time props. SSR pages and fallback paths that were not prerendered continue to render at runtime. Serving ordinary JavaScript, CSS, images, and other static files through Workers Static Assets works with either router and does not require this adapter. This is deliberately read-only. Cache writes, `revalidatePath()`, `revalidateTag()`, and Pages Router's `res.revalidate()` do not change the packaged responses; deploying a new build replaces them. A prerendered page is served for every query string, as Next.js serves its static output. The App Router prerender skips a page that reads `searchParams` or another dynamic API. Routes that were not prerendered continue to render normally, but this adapter does not persist their responses. Configure a separate `cache.data` adapter if the app also uses cached data. Finite `revalidate` intervals do not refresh packaged responses either. Use a writable response cache if those routes need runtime ISR; Static Assets always serves the build-time snapshot until the next deployment. Pages Router locale-prefixed routes use their own prerendered snapshots. i18n domain routing currently renders at runtime. The prerenderer does not produce separate snapshots for each domain, and those domains can change `defaultLocale`, page props, and generated links even for the same locale. Static Assets keeps those cache identities separate rather than reusing the locale-only snapshot. Use a runtime cache adapter for those domain routes. The private cache directory must be routed through the Worker so clients cannot fetch raw cache files directly. `vinext init --platform=cloudflare --cdn-cache=static-assets` configures this automatically. For manual setup, add the following to your existing Worker in `cloudflare.config.ts`: ```ts import { bindings } from "cf/config"; defineWorker({ // ...existing Worker settings assets: { // ...existing asset settings runWorkerFirst: ["/_vinext/static-cache/*"], }, env: { // ...existing bindings ASSETS: bindings.assets(), }, }); ``` Avoid `runWorkerFirst: true`; it also routes the build output through the Worker, which returns 404 for it (see [Middleware and static files](/docs/deploying/cloudflare#middleware-and-static-files)). The `/*` pattern that `vinext init` generates already covers the private directory. Otherwise, preserve any other Worker-first routes when adding the private directory pattern. Exclusion patterns must not bypass the private directory; init rejects exclusion patterns other than its build-output exclusions when configuring this cache. See Cloudflare's [`run_worker_first` reference](https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first) for the pattern syntax. Existing typed configs require manual Static Assets setup; init stops before modifying the project so it cannot overwrite a custom binding or leave private files exposed. ## Cache warming Cache warming is optional during Cloudflare deploys and supports both App Router and Pages Router. With a configured cache adapter, run: ```sh npx @vinext/cloudflare deploy --warm-cache ``` For an existing deployment serving 100% of traffic from one version, vinext uploads the new Worker, stages it at 0%, discovers routes through the staged Worker with its real bindings, checks readiness, and requests eligible responses to fill the configured cache before promoting the version. Unlike a local prerender and KV bulk upload, rendering happens in the deployed Worker. Static exports (`output: "export"`) still need local prerendered files. The default warmup makes one fill request per admitted cache identity. Add `--warm-cache-certify` to require a second, header-only request that proves warmed entries are reusable before promotion. If staging or required warmup fails, the command stops rather than promoting an unverified version; inspect the deployment state before retrying. A first deployment cannot use this staged flow: deploy normally once, then enable warming on subsequent deploys. The command also needs a reachable production route or custom domain and a Worker name for staged version overrides; use `--warm-cache-target https://example.com` if the target origin cannot be inferred. Only discoverable, cacheable route identities are warmed; dynamic parameters and query variants are not all enumerated automatically. HTML, RSC, and Pages Router data use separate requests where applicable. For Workers Cache, a warm request does not guarantee a cache hit in every region. See [Cloudflare's Workers Caching documentation](https://developers.cloudflare.com/workers/cache/). ### Traffic-aware warming For apps with many routes, use traffic-aware warming to prioritize pages people actually visit: ```sh npx @vinext/cloudflare deploy --traffic-aware-warm-cache ``` vinext queries Cloudflare zone analytics, ranks page paths by request count, and resolves them against the app's routes. It selects paths up to the traffic coverage target or route limit, then uses the same staged warming flow and cacheability checks described above. Traffic can supply concrete dynamic paths that were not enumerated by `generateStaticParams()` or `getStaticPaths()`; pages that cannot be cached still render on demand.
Flag Purpose Default
--traffic-aware-coverage <pct> Target percentage of traffic to cover 90
--traffic-aware-limit <count> Maximum number of selected routes 1000
--traffic-aware-window <hours> Analytics lookback window 24
--warm-cache-target <origin> HTTPS origin for analytics zone selection, discovery, probing, and warming Inferred from Cloudflare configuration and deploy output
For example, target 95% of traffic from the last 48 hours, with a maximum of 500 routes: ```sh npx @vinext/cloudflare deploy --traffic-aware-warm-cache \ --traffic-aware-coverage 95 --traffic-aware-limit 500 --traffic-aware-window 48 ``` This requires a custom domain, a cache adapter that exposes build identity, and `CLOUDFLARE_API_TOKEN` with **Zone > Analytics > Read** and **Zone > Zone > Read** permissions for that zone. See [Cloudflare API token permissions](https://developers.cloudflare.com/fundamentals/api/reference/permissions/). Zone analytics are unavailable for `*.workers.dev` hosts. For typed config projects, declare `domains: ["example.com"]` on the Worker in `cloudflare.config.ts`. The first domain in the generated Build Output is used for both analytics and staged warming. Wrangler projects use their configured routes for analytics and deployed triggers for the warming origin. Use `--warm-cache-target` to provide the production HTTPS origin manually; its hostname selects the analytics zone, and the same origin is used for staged discovery, probing, and warming: ```sh npx @vinext/cloudflare deploy --traffic-aware-warm-cache \ --warm-cache-target https://example.com ``` Use `--traffic-aware-warm-cache` on its own to select routes by traffic. Adding `--warm-cache` uses the full discovered warm plan instead of limiting it to the traffic-selected routes. Traffic-aware warming skips selection when all-route prerendering is configured, and normally skips warming when analytics or a safely stageable existing deployment are unavailable. Add `--warm-cache-certify` to require reusable cache hits for every selected entry before promotion. Certification fails if warming is skipped or no entries can be certified. Use `--no-promote` to leave the warmed version at 0% traffic for verification. ## Which option should I choose? - Choose **no persistent cache** while migrating an app or when every response is intentionally dynamic. - Choose **Workers Response Store (recommended)** for the most complete Cloudflare caching setup and durable response storage. - Choose **Workers Cache + KV** when you specifically want the existing native Workers Cache architecture and accept that responses have no backing store. - Choose **KV only** when you want the simplest persistent cache and do not need Workers Cache to serve responses. - Choose **Static Assets** for response content that is immutable for the lifetime of a deployment and should be produced during the build, with either router. You can run `vinext init --platform=cloudflare` to configure these choices. The generated Vite and Cloudflare config files are normal source files and can be adjusted later. ## Which Pages Router pages are cached Pages Router uses the page's data-fetching methods to determine its caching behavior: - **`getStaticProps()`:** caches the rendered page and its JSON data. Omitting `revalidate` or returning `revalidate: false` keeps the entry fresh until it is invalidated or removed by the cache backend. Return a positive `revalidate` interval in seconds to enable ISR. - **`getStaticPaths()`:** supplies concrete paths for a dynamic `getStaticProps()` page. Its `fallback` setting determines whether paths outside that list can be generated on demand. In production, Next.js only calls `getStaticPaths()` at build time. vinext calls it at request time in the deployed server or Worker when a path has no fresh or stale cached entry, including paths outside the list that return 404 under `fallback: false`, expired entries, and on-demand revalidation. Cache hits, stale responses, and draft-mode requests do not call it. - **Pages without `getServerSideProps()` or `getInitialProps()`:** qualify for automatic static optimization and response caching through Workers Response Store, Workers Cache, or prerendering with Static Assets. A custom `_app.getInitialProps` disables this for pages without `getStaticProps()`. - **`getServerSideProps()` or page `getInitialProps()`:** render per request by default. An explicitly authored public cache policy can opt a response into Workers Response Store or Workers Cache; use this only when the response can be shared between visitors. The KV-only setup persists `getStaticProps()` ISR entries; it does not persist automatically static pages or SSR responses just because they have public cache headers. Preview and draft-mode requests bypass shared page caching. For example, this Pages Router page can refresh its cached HTML and navigation data after 60 seconds: ```tsx // pages/index.tsx export async function getStaticProps() { return { props: { generatedAt: new Date().toISOString() }, revalidate: 60, }; } export default function Page({ generatedAt }: { generatedAt: string }) { return

Generated at {generatedAt}

; } ``` The interval belongs in the `getStaticProps()` result. Pages Router does not use the App Router's `export const revalidate` convention. See the [Next.js Pages Router ISR guide](https://nextjs.org/docs/pages/guides/incremental-static-regeneration). For on-demand revalidation, call `await res.revalidate("/")` in a Pages Router API route. With KV or the default in-memory cache, this regenerates the stored page. With Workers Cache, it renders the page and purges the tagged edge responses so subsequent requests refill the cache. **Current Response Store limitation:** `res.revalidate()` does not invalidate stored Response Store page responses. The call can complete without refreshing the response that visitors receive. Time-based ISR is supported, but on-demand Pages Router invalidation needs additional adapter support. ## Which App Router pages are cached vinext stores a rendered App Router page (ISR) when Next.js would treat its route as static or SSG: - a route without dynamic segments, or a dynamic route whose `generateStaticParams` covers its last dynamic segment, is static unless its render uses a dynamic API. Returning `[]` from `generateStaticParams` opts every path into on-demand ISR, unless `dynamicParams = false`; - `dynamic = "force-dynamic"`, `revalidate = 0` or an edge `runtime` makes a route dynamic. Otherwise, `dynamic = "force-static"` or `dynamic = "error"` makes it static. Other routes render on every request. Their `"use cache"` functions and cached `fetch` calls are still reused. Routes built with `cacheComponents` follow their own rules, and [Differences from Next.js](/docs/reference/differences#caching) lists the cases where vinext classifies a route differently. A static page defaults to `revalidate = false`, as in Next.js, unless a `revalidate` export, a `fetch` with a numeric `next.revalidate`, `cacheLife()` or a `"use cache"` function in its render sets a lifetime. A cached `fetch` without one, such as `cache: "force-cache"`, doesn't shorten the page's lifetime. A `revalidate = false` page is rendered again only after `revalidatePath()` or `revalidateTag()` invalidates it, or once the cache drops it. Dynamic APIs include `cookies()`, `headers()`, `connection()`, a `fetch` with `cache: "no-store"` outside a cache scope, and reading `searchParams`. Under `dynamic = "force-static"` they return empty values or don't count, so the page is still stored. `useSearchParams()` in a client component doesn't make a page dynamic. Under `dynamic = "force-static"` it returns empty search params in the HTML and the browser URL's after hydration, as in Next.js. Otherwise, in production, a cacheable page's HTML gets the nearest `` fallback and the browser renders the real value after hydration, as in Next.js. With no `` boundary above the call, the HTML request fails with Next.js's `useSearchParams() should be wrapped in a suspense boundary` error, where Next.js fails the build. ## Query strings Next.js keeps one cache entry per static page, whatever the query string, and vinext does the same for App Router pages: `/blog/a?ref=x` and `/blog/a?utm_source=y` share the `/blog/a` entry. A render that reads the query is dynamic and isn't stored, so one visitor's query can't reach the shared entry. - **KV and in-memory:** page keys leave out the query, for App Router and Pages Router pages. - **Workers Response Store:** a miss renders from the full request, and the response is stored under the path without the query. - **Workers Cache:** a hit skips rendering (middleware and routing still run), so the key is chosen before rendering, from the cacheability manifest that a staged deploy writes (`--warm-cache`, see [Cache warming](#cache-warming)). Paths the manifest marks static share one entry across queries, and a request for one that turns dynamic at runtime can fail with a 500, like Next.js's `Page changed from static to dynamic at runtime` error. Other paths, and every path without the manifest, keep the full query in their key. With Workers Response Store and Workers Cache, some responses aren't stored under the query-free key, for example interception RSC requests and responses that an explicit provider policy in a `next.config` `headers()` rule makes cacheable. Pages Router pages keep the query in their keys on these two backends. In production, most App page responses that vinext knows will not be stored send Next.js's `Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate`, unless middleware or a `next.config` `headers()` rule sets `Cache-Control`. ## Freshness and revalidation vinext follows Next.js-style cache semantics: - A **fresh** entry is returned immediately. - A **stale** entry may be returned while stale-while-revalidate refreshes it in the background. - An **expired** entry is not returned and must be regenerated. - `revalidatePath()` invalidates content associated with a path. - `revalidateTag()` invalidates content associated with a cache tag. - Pages Router's `res.revalidate()` requests on-demand page regeneration from an API route, subject to the [Response Store limitation above](#which-pages-router-pages-are-cached). With the KV or in-memory cache, when a background regeneration of an App Router page fails, the previous entry is kept and regeneration is retried later, as in Next.js. The adapter changes where entries live and how they are served, but it should not change the caching API used by application code. ## How `"use cache"` arguments are keyed As in Next.js, a `"use cache"` function's arguments are serialized with React's Server Function argument encoder, and that serialized form is the cache key. Files and Blobs are keyed by their bytes alone, so files with the same contents share an entry even if their names, types, or timestamps differ. Values the encoder can't serialize, such as JSX, functions, and class instances, are passed through without affecting the key; the function can render or return them but can't read their properties. ## How cached `fetch` request bodies are keyed A cached `fetch` call's request body is part of its cache key, so requests that differ only in their body get separate entries. Strings, binary data, `ReadableStream`s, `URLSearchParams`, `FormData` and `Blob`s are keyed by their contents. Any other body, such as an async iterable or an object the runtime converts to a string, is built into a `Request` the way `fetch` itself does, and keyed by that request's bytes. Next.js doesn't key these other bodies, so it gives requests that differ only in such a body the same entry. A stream's chunks, whether from a `ReadableStream`, an async iterable or a `Request`, are keyed by the bytes sent for them. Workers sends the bytes of `ArrayBuffer`, `DataView` and other typed array chunks as well as `Uint8Array`s, so those are keyed too. Some requests with a body are fetched without the cache: - **Large bodies.** A body over 1 MiB isn't keyed. - **Other chunk types.** A body isn't keyed if it streams anything other than binary chunks (or, in a `ReadableStream` body, strings). Node.js converts an async iterable's chunks to `Uint8Array`s, but Workers passes them through, so on Workers an async iterable that yields strings is fetched uncached. A cached `fetch` reads the body, up to 1 MiB, before it sends the request. A body that waits for the response, or for anything else that happens after the request starts, never finishes, so the fetch doesn't either. Aborting the request takes effect only once that read ends. Next.js also reads a stream body before it sends the request. Like `fetch` itself, a cached `fetch` copies its URL, headers, `cf` and body when it is called. It keys and sends that copy, so changing those objects after the call changes neither. A stream body, from a `ReadableStream` or a `Request`, can't be copied then: as with `fetch`, its chunks are read after the call, so a chunk changed before it is read is sent changed. It is keyed by the bytes read, and sent as those same bytes, as in Next.js. Next.js sends the caller's other objects, though, so a request changed after its key is built is sent, and its response stored, under the key of the request it was changed from. ## Where generated metadata goes `generateMetadata()` can resolve after the rest of the page. Next.js streams a dynamic page, so its generated tags (``, canonical, robots and so on) arrive in `<body>` once they resolve. It renders a static or ISR page whole before serving it, so that page has them in `<head>`. Browsers and most crawlers read metadata in either place. Bots that match `htmlLimitedBots` wait for the metadata and get it in `<head>`, in both. vinext streams every render that a visitor waits on, including the miss that stores a page, so the first byte doesn't wait for the whole page. Renders that no visitor waits on are rendered whole, and store generated metadata in `<head>`: - **Static Assets and the opt-in prerender:** the build renders every page whole. - **KV and in-memory:** background regenerations. - **Workers Response Store:** regenerations and [cache-warming](#cache-warming) fills. - **Workers Cache:** cache-warming fills and revalidations. vinext adds `Last-Modified` to a cacheable App Router page that has no validator of its own, so Workers Cache revalidates it with a conditional request (`If-Modified-Since`). vinext renders any conditional request for an App Router page whole. This differs from Next.js in these cases: - **A page stored from a miss has generated metadata in `<body>`.** Its first regeneration replaces it with a `<head>` document. A `revalidate = false` page keeps it until it's invalidated or the cache drops it. With Workers Response Store or Workers Cache, deploying with `--warm-cache` stores the warmed pages with metadata in `<head>`. - **A miss streams its first byte sooner.** Next.js waits for the whole page. - **On Workers Cache, a browser's own conditional request that misses the cache waits for the whole page,** like a revalidation.