How OpenNext's regional cache reduces Workers CPU on every cache hit
Next.js is built to run on Vercel by default. It expects a filesystem, an image optimization service, and a particular runtime model. To run Next.js anywhere else, like Cloudflare Workers, you need an adapter that bridges those expectations to the target platform's primitives.
OpenNext is the open-source project that does exactly that. It started as a Next.js adapter for AWS Lambda and now ships official adapters for Cloudflare and Netlify too. The package we care about here is @opennextjs/cloudflare, which lets you deploy a Next.js app to Cloudflare Workers and wires Next.js's caching layer up to Cloudflare's storage primitives.
If you've followed OpenNext's recommended Cloudflare setup, your open-next.config.ts wires up two of those primitives. Workers KV holds the cached HTML of your pre-rendered pages. D1 holds the cache-tag metadata that revalidateTag() writes to. (By default, defineCloudflareConfig() uses "dummy" no-op caches; KV and D1 are the recommended replacements you wire in yourself.)
Both KV and D1 are remote services. Both cost real CPU on every request, even when the page hasn't changed in days. withRegionalCache is the wrapper that fixes that.
What it is
withRegionalCache ships with @opennextjs/cloudflare as an override you wrap around your existing incremental cache implementation. It's a one-import, one-wrapping change in open-next.config.ts.
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";
import { withRegionalCache } from "@opennextjs/cloudflare/overrides/incremental-cache/regional-cache";
import d1TagCache from "@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache";
export default defineCloudflareConfig({
incrementalCache: withRegionalCache(kvIncrementalCache, {
mode: "long-lived",
bypassTagCacheOnCacheHit: true,
}),
tagCache: d1TagCache,
});It works the same way around r2IncrementalCache or staticAssetsIncrementalCache if those are the backends you've chosen. The wrapper is independent of which underlying cache you use.
What it does
Two things, both worth understanding.
1. A Workers Cache API layer in front of KV
The first behavior is a regional cache layer using caches.default, the Workers Cache API. The Cache API is local to each Cloudflare data center. It is not globally replicated, but that is the whole point. Reads from caches.default are essentially free in CPU terms and finish in 1 to 5ms.
So when a request comes in for a cached page, the worker checks the local data center's Cache API first. If the entry is there, it returns immediately. If not, it falls back to KV (a global ~30 to 100ms read), populates the local Cache API for next time, and returns the response.
The mode: "long-lived" option keeps regional entries around for up to 30 minutes by default, which works well for ISR/SSG pages.
2. Optional D1 tag-cache bypass on hits
The second behavior is opt-in: bypassTagCacheOnCacheHit: true.
By default, every request to a cached page also queries D1 to check whether any of that page's revalidation tags have been invalidated. D1 reads take ~30 to 200ms wall time and burn CPU on query setup and result parsing.
With the bypass enabled, the worker skips that D1 round trip on cache hits. The trade-off: if you call revalidateTag(), the regional cache may continue serving the previous version of a page until its regional entry expires. For content that doesn't change minute-to-minute, that's invisible to users.
How it helps
Without the wrapper, a typical "cache hit" looks like this on the worker:
- Wake up the isolate.
- Query D1 for tag invalidations on this URL.
- Read the cached HTML from KV.
- Build the response and return.
Two remote round trips and meaningful CPU work, on every single request.
With the wrapper:
- Wake up the isolate.
- Read from the local Cache API.
- Return.
Same outcome from the user's perspective. Far less work for the worker.
On a small content site doing roughly 33,000 requests/day, the median CPU per request dropped from 668ms to ~40ms after enabling this, about a 94% reduction. The total CPU time billed against Cloudflare's $0.02 per million CPU-ms went from ~22M ms/day to ~1.3M ms/day. The shape of the cost curve in the billing dashboard changed the same day the deploy went out.
The savings scale linearly with traffic. The busier the site, the bigger the gap between "two remote round trips per request" and "one local Cache API read per request."
When to use it
If your OpenNext config is using kvIncrementalCache with d1TagCache (the default recommended setup for content sites on Cloudflare), turn on withRegionalCache with bypassTagCacheOnCacheHit: true. Blogs, documentation sites, marketing pages, and similar mostly-static workloads benefit immediately and the staleness trade-off is invisible.
If you're running an app where freshness matters in real time (pricing, inventory, chat) keep bypassTagCacheOnCacheHit: false. You still get the regional Cache API layer, faster KV reads, lower CPU per request, without the staleness window.
It's a five-line diff. The kind of change that pays for itself the first day.