Describe the bug
On Cloudflare Workers, writing an ISR entry to the incremental cache blocks the response.
Next.js awaits incrementalCache.set while generating the response, and neither the
OpenNext adapter nor the Cloudflare override defers that write, so the R2 round-trip is
paid before the first byte is sent.
Next.js response-cache/index.js await incrementalCache.set(...)
└─ OpenNext adapters/cache.ts await globalThis.incrementalCache.set(...)
└─ RegionalCache.set await this.store.set(...) → r2.put()
The detachedPromise in adapters/cache.ts is registered with pendingPromiseRunner,
but it only signals completion of set — it does not move the write off the response path.
Why this looks unintentional
regional-cache.ts already uses ctx.waitUntil for writes on the read path:
// get(): refresh the regional cache in the background
getCloudflareContext().ctx.waitUntil(this.store.get(key, cacheType).then(...))
// get(): populate the regional cache after a store hit
getCloudflareContext().ctx.waitUntil(this.putToCache({ key, cacheType, entry }))
But set() awaits both the store write and the very same putToCache:
async set(key, value, cacheType) {
await this.store.set(key, value, cacheType)
await this.putToCache({ key, cacheType, entry: { value, lastModified: Date.now() } })
}
So putToCache is deferred when reached from get, and awaited when reached from set.
Observed impact
Cloudflare Workers + R2 incremental cache, measured with Workers automatic tracing.
r2_put spans: median 976 ms, up to 2,048 ms (14 of 20 sampled traces)
- Measured TTFB on ISR misses: 2.4–4.6 s
- Time until
r2_put starts (the TTFB we would expect if the write were deferred): ~0.9–1.0 s
Our workload is a long tail of per-restaurant pages, so misses are common.
Confirmed by wrapping the cache locally
We wrapped incrementalCache in our own override that hands set to ctx.waitUntil
and deployed it to a staging preview. Measured on ISR misses, four requests each:
|
TTFB (median) |
| with the wrapper |
1.97 s |
| without |
2.45 s |
Cache behaviour is unchanged — repeated requests to the same page still return
x-opennext-cache: HIT at 0.32–0.69 s.
Expected behavior
On Workers, the write could run in ctx.waitUntil so the response is not held back.
Next.js does not use the return value of set (response-cache/index.js returns the same
entry it passed in), so deferring it does not change what is served.
Trade-off we are aware of
Deferring the write widens the window in which a concurrent request for the same page
misses the cache and re-renders. That window already exists today for the duration of the
write; deferring shifts its start rather than creating it.
Versions
@opennextjs/cloudflare: 1.20.1 (behaviour confirmed unchanged on main)
@opennextjs/aws: 4.0.2
- Next.js: 16.2.10
Describe the bug
On Cloudflare Workers, writing an ISR entry to the incremental cache blocks the response.
Next.js awaits
incrementalCache.setwhile generating the response, and neither theOpenNext adapter nor the Cloudflare override defers that write, so the R2 round-trip is
paid before the first byte is sent.
The
detachedPromiseinadapters/cache.tsis registered withpendingPromiseRunner,but it only signals completion of
set— it does not move the write off the response path.Why this looks unintentional
regional-cache.tsalready usesctx.waitUntilfor writes on the read path:But
set()awaits both the store write and the very sameputToCache:So
putToCacheis deferred when reached fromget, and awaited when reached fromset.Observed impact
Cloudflare Workers + R2 incremental cache, measured with Workers automatic tracing.
r2_putspans: median 976 ms, up to 2,048 ms (14 of 20 sampled traces)r2_putstarts (the TTFB we would expect if the write were deferred): ~0.9–1.0 sOur workload is a long tail of per-restaurant pages, so misses are common.
Confirmed by wrapping the cache locally
We wrapped
incrementalCachein our own override that handssettoctx.waitUntiland deployed it to a staging preview. Measured on ISR misses, four requests each:
Cache behaviour is unchanged — repeated requests to the same page still return
x-opennext-cache: HITat 0.32–0.69 s.Expected behavior
On Workers, the write could run in
ctx.waitUntilso the response is not held back.Next.js does not use the return value of
set(response-cache/index.jsreturns the sameentry it passed in), so deferring it does not change what is served.
Trade-off we are aware of
Deferring the write widens the window in which a concurrent request for the same page
misses the cache and re-renders. That window already exists today for the duration of the
write; deferring shifts its start rather than creating it.
Versions
@opennextjs/cloudflare: 1.20.1 (behaviour confirmed unchanged onmain)@opennextjs/aws: 4.0.2