Cloudflare
Caching

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:

The @opennextjs/cloudflare caching supports rely on 3 components:

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 named WORKER_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:

  1. Expected Load: Consider the volume of traffic or data you anticipate.
  2. 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