import { Callout } from "nextra/components"; import { Tabs } from "nextra/components";
Next.js offers multiple ways to improve an application's performance by caching routes and network requests. An application will try to pre-render and cache as much data as possible during build-time to reduce the amount of work required when serving a response to a user.
The cache data are updated using revalidation, either peridiocally or on-demand:
- "Time-based revalidation" updates the cache data after the revalidation delay specified by the applications expires
- "On-demand revalidation" allows to invalid cache entries with a specific tag (via
revalidateTag) or at a given path (viarevalidatePath). You can also useres.revalidatein Pages router API route.
The @opennextjs/cloudflare caching supports rely on 3 components:
- An Incremental Cache to store the cache data
- A Queue to synchronize and deduplicate time-based revalidations
- A Tag Cache for On-demand revalidations via
revalidateTagandrevalidatePath.
You can also enable cache interception, to avoid calling the NextServer and thus loading the javascript associated with the page. It can slightly improve cold start performance for ISR/SSG route on cached routes.
As of now, cache interception does not work with PPR and is not enabled by default.
The adapter provides several implementations for each of those components configured in open-next.config.ts.
This guide provides guidelines for common use cases before detailing all the configuration options.
Everything in this page only concerns SSG/ISR and the data cache, SSR route will work out of the box without any caching config.You should use the following implementation for a small site:
- Incremental Cache: use R2 to store the data
- Queue: use a Queue backed by Durable Objects
- Tag Cache:
D1NextModeTagCache
<Tabs items={["wrangler.jsonc", "open-next.config.ts"]}> <Tabs.Tab>
</Tabs.Tab> <Tabs.Tab>
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import d1NextTagCache from "@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache";
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
//import { withFilter, softTagFilter } from "@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
queue: doQueue,
// This is only required if you use On-demand revalidation
tagCache: d1NextTagCache,
//If you don't use `revalidatePath`, you can also filter internal soft tags using the `softTagFilter`
// tagCache: withFilter({
// tagCache: d1NextTagCache,
// filterFn: softTagFilter,
// }),
// Disable this if you want to use PPR
enableCacheInterception: true,
});</Tabs.Tab>
For a larger site, you should use the ShardedDOTagCache that can handle a higher load than the D1NextModeTagCache:
<Tabs items={["wrangler.jsonc", "open-next.config.ts"]}> <Tabs.Tab>
{
"name": "<WORKER_NAME>",
// ...
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "<WORKER_NAME>",
},
],
// R2 incremental cache
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "<BUCKET_NAME>",
},
],
// DO Queue and DO Sharded Tag Cache
"durable_objects": {
"bindings": [
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler",
},
// This is only required if you use On-demand revalidation
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache",
},
],
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"DOQueueHandler",
// This is only required if you use On-demand revalidation
"DOShardedTagCache",
],
},
],
}</Tabs.Tab> <Tabs.Tab>
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import { withRegionalCache } from "@opennextjs/cloudflare/overrides/incremental-cache/regional-cache";
import doShardedTagCache from "@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache";
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
export default defineCloudflareConfig({
incrementalCache: withRegionalCache(r2IncrementalCache, { mode: "short-lived" }),
queue: doQueue,
// This is only required if you use On-demand revalidation
tagCache: doShardedTagCache({
baseShardSize: 12,
regionalCache: true, // Enable regional cache to reduce the load on the DOs
regionalCacheTtlSec: 5, // The TTL for the regional cache
shardReplication: {
numberOfSoftReplicas: 4,
numberOfHardReplicas: 2,
regionalReplicationOptions: {
defaultRegion: "enam",
},
},
}),
// Disable this if you want to use PPR
enableCacheInterception: true,
});</Tabs.Tab>
If your site is static, you do not need a Queue nor a Tag Cache. You can use a read-only Workers Static Assets-based incremental cache for the prerendered routes.
<Tabs items={["open-next.config.ts"]}> <Tabs.Tab>
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: staticAssetsIncrementalCache,
enableCacheInterception: true,
});</Tabs.Tab>
For staging, when your site receives low traffic from a single IP, you can replace the DO queue with a memory queue.
The worker doesn't run **in front** of static assets, so the `headers` option of `next.config.ts` doesn't apply to public files (`public`) and immutable build files (like `_next/static`).By default, Cloudflare Static Assets headers use max-age=0 with must-revalidate, allowing the browser to cache assets but with a revalidation request. This is the same default behavior as the public folder on Next.js.
Next.js also generates immutable files that don't change between builds. Those files will also be served from Static Assets. To match the default cache behavior of immutable assets in Next.js, avoiding unnecessary revalidation requests, add the following header to the public/_headers file:
/_next/static/*
Cache-Control: public,max-age=31536000,immutableThere are 3 storage options for the incremental cache:
- R2 Object Storage: A cost-effective S3-compatible object storage option for large amounts of unstructured data. Data is stored in a single region, meaning cache interactions may be slower - this can be mitigated with a regional cache.
- Workers KV: A fast key value store, it uses Cloudflare's Tiered Cache to increase cache hit rates. When you write cached data to Workers KV, you write to storage that can be read by any Cloudflare location. This means your app can fetch data, cache it in KV, and then subsequent requests anywhere around the world can read from this cache.
- Workers Static Assets: A read-only store for the incremental cache, serving build-time values from Workers Static Assets. Revalidation is not supported with this cache.
<Tabs items={["R2 Object Storage", "Workers KV", "Workers Static Assets"]}> <Tabs.Tab>
npx wrangler@latest r2 bucket create <YOUR_BUCKET_NAME>The binding name used in your app's worker is NEXT_INC_CACHE_R2_BUCKET. The service binding should be a self reference to your worker where <WORKER_NAME> is the name in your wrangler configuration file.
The prefix used by the R2 bucket can be configured with the NEXT_INC_CACHE_R2_PREFIX environment variable, and defaults to incremental-cache.
// wrangler.jsonc
{
// ...
"name": "<WORKER_NAME>",
"r2_buckets": [
{
"binding": "NEXT_INC_CACHE_R2_BUCKET",
"bucket_name": "<BUCKET_NAME>",
},
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "<WORKER_NAME>",
},
],
}In your project's OpenNext config, enable the R2 cache.
You can optionally setup a regional cache to use with the R2 incremental cache. This will enable faster retrieval of cache entries and reduce the amount of requests being sent to object storage.
The regional cache has two modes:
short-lived: Responses are re-used for up to a minute.long-lived: Fetch responses are re-used until revalidated, and ISR/SSG responses are re-used for up to 30 minutes.
Additionally, lazy updating of the regional cache can be enabled with the shouldLazilyUpdateOnCacheHit option. When requesting data from the cache, it sends a background request to the R2 bucket to get the latest entry. This is enabled by default for the long-lived mode.
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import { withRegionalCache } from "@opennextjs/cloudflare/overrides/incremental-cache/regional-cache";
// ...
// With regional cache enabled:
export default defineCloudflareConfig({
incrementalCache: withRegionalCache(r2IncrementalCache, {
mode: "long-lived",
shouldLazilyUpdateOnCacheHit: true,
}),
// ...
});
// Without regional cache:
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
// ...
});</Tabs.Tab> <Tabs.Tab>
Workers KV is eventually consistent, which means that it can take up to 60 seconds for updates to be reflected globally, when using the default TTL of 60 seconds.Create a KV namespace
npx wrangler@latest kv namespace create <YOUR_NAMESPACE_NAME>Add the KV namespace and Service Binding to your Worker
The binding name used in your app's worker is NEXT_INC_CACHE_KV.
The WORKER_SELF_REFERENCE service binding should be a self reference to your worker where <WORKER_NAME> is the name in your wrangler configuration file.
The prefix used by the cache keys can be configured with the NEXT_INC_CACHE_KV_PREFIX environment variable, and defaults to incremental-cache.
// wrangler.jsonc
{
// ...
"name": "<WORKER_NAME>",
"kv_namespaces": [
{
"binding": "NEXT_INC_CACHE_KV",
"id": "<BINDING_ID>",
},
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "<WORKER_NAME>",
},
],
}Configure the cache
In your project's OpenNext config, enable the KV cache.
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";
// ...
export default defineCloudflareConfig({
incrementalCache: kvIncrementalCache,
// ...
});</Tabs.Tab>
<Tabs.Tab>
The Workers Static Assets cache is read-only. Requests that attempt to modify it will be ignored.Configure the cache
In your project's OpenNext config, enable the static assets cache.
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: staticAssetsIncrementalCache,
});</Tabs.Tab>
A queue must be setup for projects using revalidation (either Time based or On-demand).
Configure the queue
In your project's OpenNext config, enable the cache and set up a queue.
The Durable Object Queue will send revalidation requests to a page when needed, and offers support for de-duplicating requests. By default there will be a maximum of 10 instance of the Durables Object Queue and they can each process up to 5 requests in parallel, for up to 50 concurrent ISR revalidations.
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
// ...
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
export default defineCloudflareConfig({
// ...
incrementalCache: r2IncrementalCache,
queue: doQueue,
});You will also need to add some binding to your wrangler.jsonc file.
"durable_objects": {
"bindings": [
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["DOQueueHandler"]
}
],You can customize the behaviors of the queue with environment variables:
- The max number of revalidations that can be processed by an instance of durable object at the same time (
NEXT_CACHE_DO_QUEUE_MAX_RETRIES) - The max time in milliseconds that a revalidation can take before being considered as failed (
NEXT_CACHE_DO_QUEUE_REVALIDATION_TIMEOUT_MS) - The amount of time after which a revalidation will be attempted again if it failed. If it fails again it will exponentially back off until it reaches the max retry interval (
NEXT_CACHE_DO_QUEUE_RETRY_INTERVAL_MS) - The maximum number of attempts that can be made to revalidate a path (
NEXT_CACHE_DO_QUEUE_MAX_RETRIES) - Disable SQLite for this durable object. It should only be used if your incremental cache is not eventually consistent (
NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE)
-
The memory queue will dedupe request but only on a per isolate basis. It is not fully suitable for production deployments, you can use it at your own risk!
-
The
directmode for the queue is intended for debugging purposes and is not recommended for use in production. It only works in preview mode (i.e.wrangler dev)For apps using the Page Router,
res.revalidaterequires to provide a self reference service binding namedWORKER_SELF_REFERENCE.
The tag revalidation mechanism can use either a Cloudflare D1 database or Durable Objects with SqliteStorage as its backing store for information about tags, paths, and revalidation times.
To use on-demand revalidation, you should also follow the ISR setup steps.
If your app **only** uses the pages router, it does not need to have a tag cache and should skip this step. You can also skip this step if your app doesn't use `revalidateTag` nor `revalidatePath`.There are 2 different options to choose from for the tag cache: d1NextTagCache, doShardedTagCache.
Which one to choose should be based on two key factors:
- Expected Load: Consider the volume of traffic or data you anticipate.
- Usage of
revalidateTag/revalidatePath: Evaluate how frequently these features will be utilized.
If either of these factors is significant, opting for a sharded database is recommended. Additionally, incorporating a regional cache can further enhance performance.
<Tabs items={["D1NextTagCache", "doShardedTagCache", "withFilter"]}> <Tabs.Tab>
Create a D1 database and Service Binding
The binding name used in your app's worker is NEXT_TAG_CACHE_D1. The WORKER_SELF_REFERENCE service binding should be a self reference to your worker where <WORKER_NAME> is the name in your wrangler configuration file.
// wrangler.jsonc
{
// ...
"d1_databases": [
{
"binding": "NEXT_TAG_CACHE_D1",
"database_id": "<DATABASE_ID>",
"database_name": "<DATABASE_NAME>",
},
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "<WORKER_NAME>",
},
],
}Create table for tag revalidations
The D1 tag cache requires a revalidations table that tracks On-Demand revalidation times.
Configure the cache
In your project's OpenNext config, enable the R2 cache and set up a queue (see above). The queue will send a revalidation request to a page when needed, but it will not dedupe requests.
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
import d1NextTagCache from "@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
queue: doQueue,
tagCache: d1NextTagCache,
});In order for the cache to be properly initialised with the build-time revalidation data, you need to run a command as part of your deploy step. This should be run as part of each deployment to ensure that the cache is being populated with each build's data.
To populate remote bindings and create a new version of your application at the same time, you can use either the deploy command or the upload command. Similarly, the preview command will populate your local bindings and start a Wrangler dev server.
# Populate remote and deploy the worker immediately.
opennextjs-cloudflare deploy
# Populate remote and upload a new version of the worker.
opennextjs-cloudflare upload
# Populate local and start dev server.
opennextjs-cloudflare previewIt is possible to only populate the cache without any other steps with the populateCache command.
# The target is passed as an option, either `local` or `remote`.
opennextjs-cloudflare populateCache local</Tabs.Tab> <Tabs.Tab>
Create a Durable Object and Service Binding
The service binding should be a self reference to your worker where <WORKER_NAME> is the name in your wrangler configuration file.
// wrangler.jsonc
{
// ...
"durable_objects": {
"bindings": [
{
"name": "NEXT_CACHE_DO_QUEUE",
"class_name": "DOQueueHandler",
},
{
"name": "NEXT_TAG_CACHE_DO_SHARDED",
"class_name": "DOShardedTagCache",
},
],
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"],
},
],
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "<WORKER_NAME>",
},
],
}Configure the cache
In your project's OpenNext config, enable the R2 cache and set up a queue.
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
import doShardedTagCache from "@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
tagCache: doShardedTagCache({ baseShardSize: 12, regionalCache: true }),
queue: doQueue,
});doShardedTagCache takes the following options:
baseShardSize: The number of shards to use for the cache. The more shards you have, the more evenly the cache will be distributed across the shards. The default is 4. Soft (internal next tags used forrevalidatePath) and hard tags (the ones you define in your app) will be split in different shardsregionalCache: Whether to use regional cache for the cache. The default is false. This option is useful when you want to reduce the stress on the durable objectregionalCacheTtlSec: The TTL for the regional cache. The default is 5 seconds. Increasing this value will increase the time it takes for the cache to be invalidated across regionsshardReplication: Enable replicating the Shard. Shard replication will duplicate each shards into replicas to spread the load even morenumberOfSoftReplicas: Number of replicas for the soft tag shardsnumberOfHardReplicas: Number of replicas for the hard tag shardsregionalReplicationOptions: This setting enables you to replicate shards across different regions, thereby reducing read latency and distributing the load more evenly. By enabling this option, each shard will be replicated in all available regions. Please note that this will increase the number of Durable Objects (DOs) created and the volume of write requests sent to the DOs.defaultRegion: The default region to use for the replication.
maxWriteRetries: The number of retries to perform when writing tags
</Tabs.Tab>
<Tabs.Tab>
The withFilter option is a specialized configuration that enhances your tagCache by layering an additional filter. It requires another tag cache to be passed in as the tagCache (e.g., d1NextTagCache, doShardedTagCache or your own).
This enhanced tag cache selectively filters which tags trigger revalidations, allowing you to focus on a specific subset and reduce unnecessary load on the underlying tag cache. For convenience, we provide the ready-to-use softTagFilter that automatically filters out tags used by the revalidatePath function making it a no-op.
Its primary purpose is to filter out soft tags (i.e., those used by revalidatePath) from the tag cache, which are not relevant for your application. It could also be used to filter out hard tags (i.e., those you define in your app) if one of your dependencies uses them.
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
import d1NextTagCache from "@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache";
import { withFilter, softTagFilter } from "@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
queue: doQueue,
tagCache: withFilter({
tagCache: d1NextTagCache,
filterFn: softTagFilter,
}),
});You can also create your own custom filter function. This function must return a boolean value indicating whether a tag should be included. It will be invoked with a single tag (as a string) as its argument. Please note that "soft tags" (i.e., those used by revalidatePath) always start with the prefix _N_T.
</Tabs.Tab>
{ "name": "<WORKER_NAME>", // ... "services": [ { "binding": "WORKER_SELF_REFERENCE", "service": "<WORKER_NAME>", }, ], // R2 incremental cache "r2_buckets": [ { "binding": "NEXT_INC_CACHE_R2_BUCKET", "bucket_name": "<BUCKET_NAME>", }, ], // DO Queue "durable_objects": { "bindings": [ { "name": "NEXT_CACHE_DO_QUEUE", "class_name": "DOQueueHandler", }, ], }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["DOQueueHandler"], }, ], // D1 Tag Cache (Next mode) // This is only required if you use On-demand revalidation "d1_databases": [ { "binding": "NEXT_TAG_CACHE_D1", "database_id": "<DATABASE_ID>", "database_name": "<DATABASE_NAME>", }, ], }