Skip to content

Incremental cache writes block the response on Workers #1343

Description

@km-tr

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions