Caching
Next.js offers multiple ways to improve an application's performance by caching (opens in a new tab) 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 (opens in a new tab)" updates the cache data after the revalidation delay specified by the applications expires
- "On-demand revalidation (opens in a new tab)" allows to invalid cache entries with a specific tag (via
revalidateTag
) or at a given path (viarevalidatePath
). You can also useres.revalidate
in 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
revalidateTag
(opens in a new tab) andrevalidatePath
(opens in a new tab).
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.
Guidelines
Small site using revalidation
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
{
"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>",
},
],
}
Large site using revalidation
For a larger site, you should use the ShardedDOTagCache
that can handle a higher load than the D1NextModeTagCache
:
{
"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",
],
},
],
}
SSG site
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.
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: staticAssetsIncrementalCache,
});
Staging
For staging, when your site receives low traffic from a single IP, you can replace the DO queue with a memory queue.
References
Incremental Static Regeneration (ISR)
There are 3 storage options for the incremental cache:
- R2 Object Storage: A cost-effective (opens in a new tab) 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 (opens in a new tab) key value store, it uses Cloudflare's Tiered Cache (opens in a new tab) 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 (opens in a new tab). Revalidation is not supported with this cache.
1. Create an R2 Bucket
npx wrangler@latest r2 bucket create <YOUR_BUCKET_NAME>
2. Add the R2 Bucket and Service Binding to your Worker
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>",
},
],
}
3. Configure the cache
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,
// ...
});
Queue
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
)
There is 2 additional modes that you can use for the queue direct
and the memory queue
-
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
direct
mode 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.revalidate
requires to provide a self reference service binding namedWORKER_SELF_REFERENCE
.
Tag Cache for On-Demand Revalidation
The tag revalidation mechanism can use either a Cloudflare D1 (opens in a new tab) database or Durable Objects (opens in a new tab) 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.
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,
});
4. Initialise the cache during deployments
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 (opens in a new tab) 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 preview
It 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