You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We're running into an issue where a page is still serving old data after revalidation. We're observing the following on a page (/download) whose data is tagged releases:
Initial request → served from the regional cache (x-opennext-cache: HIT).
We call revalidateTag("releases", { expire: 0 }).
Next request → no x-opennext-cache header and x-powered-by: Next.js (so cache interception fell through to NextServer), but x-nextjs-cache: HIT and the old release version is still served. It stays stale on subsequent requests too.
Setting bypassTagCacheOnCacheHit: false makes revalidation work correctly, so it's specifically tied to that option. But since the bypassTagCacheOnCacheHit significantly improves our performance, we prefer to keep that on.
We've met what the docs describe as the requirements for using it on Next 16+ (cache purge configured with Enterprise plan, and only ever using revalidateTag(tag, { expire: 0 }) rather than SWR-style revalidation), which is why we're unsure whether this is a bug or whether we're using regional cache / bypassTagCacheOnCacheHit incorrectly.
I've analyzed the code with an agent, which pointed to the following:
On a regional cache miss this unconditionally re-populates the Cache API from the underlying store (R2). After revalidateTag, the store still holds the previous render (revalidate only marks the tag + purges the regional/CDN copy — it doesn't rewrite the store or regenerate), so this refill re-seeds a stale value. With bypassTagCacheOnCacheHit: true, the next regional hit is returned with shouldBypassTagCache: true, so the tag cache is never re-checked and the stale entry is served as fresh — and since the handler reports a fresh hit, NextServer never regenerates, so the store stays stale until the regional TTL expires. With enableCacheInterception it's near-deterministic: the interceptor's miss re-seeds the cache right before NextServer's own read hits it.
To verify this I created the following patch which resolves it in our local testing:
// PATCH(regional-cache stale refill): only the bypass-trusted Cache API can// serve a stale entry as "fresh", so don't re-seed it with a value the tag// cache already considers revalidated. When bypass is off, hits re-check tags// anyway, so a stale refill is harmless and we skip the extra read.letisRevalidated=false;if(this.opts.bypassTagCacheOnCacheHit){try{consttags=getTagsFromCacheEntry({ value, lastModified })??[key];isRevalidated=awaithasBeenRevalidated(key,tags,{ value, lastModified });}catch(e){error("Failed to check revalidation on regional cache miss",e);}}if(isRevalidated){debugCache("RegionalCache",`get ${key} -> stale, skipping regional refill`);}else{debugCache("RegionalCache",`get ${key} -> put to cache`);// Update the locale cache after retrieving from the store.getCloudflareContext().ctx.waitUntil(this.putToCache({ key, cacheType,entry: { value, lastModified }}));}
Sequence — current behavior (stale served)
sequenceDiagram
participant C as Client
participant I as Interceptor
participant RC as RegionalCache
participant CA as "Cache API"
participant R2 as "R2 store"
participant TC as "Tag cache (D1)"
participant NS as NextServer
Note over RC,CA: releases revalidated + regional entry purged
C->>I: GET /download
I->>RC: get("/download")
RC->>CA: match -> MISS
RC->>R2: get -> STALE value
RC-->>CA: re-seed (putToCache) STALE
Note right of CA: POISON - stale re-seeded
RC-->>I: stale (no bypass flag)
I->>TC: hasBeenRevalidated -> true
I-->>NS: fall through (no x-opennext-cache)
NS->>RC: get("/download")
RC->>CA: match -> HIT (poisoned stale)
RC-->>NS: stale + shouldBypassTagCache=true
Note over NS: tag check skipped -> treated as fresh
NS-->>C: OLD data, x-nextjs-cache HIT (no regeneration)
LoadingSequence — with the patch (regenerates fresh)
sequenceDiagram
participant C as Client
participant I as Interceptor
participant RC as RegionalCache
participant CA as "Cache API"
participant R2 as "R2 store"
participant TC as "Tag cache (D1)"
participant NS as NextServer
Note over RC,CA: releases revalidated + regional entry purged
C->>I: GET /download
I->>RC: get("/download")
RC->>CA: match -> MISS
RC->>R2: get -> STALE value
RC->>TC: hasBeenRevalidated -> true
Note right of RC: stale -> skip refill (no poison)
RC-->>I: stale (no bypass flag)
I->>TC: hasBeenRevalidated -> true
I-->>NS: fall through
NS->>RC: get("/download")
RC->>CA: match -> MISS (still empty)
RC->>TC: hasBeenRevalidated -> true (skip refill)
RC-->>NS: stale (no bypass flag)
Note over NS: handler re-checks tags -> regenerate
NS->>NS: regenerate /download (fresh)
NS->>RC: set fresh
NS-->>C: FRESH data
Loading
Is this indeed a bug in the miss-path refill when bypassTagCacheOnCacheHit is enabled, or are we misconfiguring / misunderstanding how the regional cache works? If it is a bug, I'd be happy to open a PR but I wanted check in about the expected behavior first.
Deploy a Next 16 app with the config above (regional cache + bypassTagCacheOnCacheHit: true + enableCacheInterception + cachePurge).
Have an ISR/SSG page whose content comes from data tagged releases. Request it so it's cached (x-opennext-cache: HIT).
Call revalidateTag("releases", { expire: 0 }).
Request the page again → it's served stale (x-nextjs-cache: HIT, old content) and does not regenerate.
Expected behavior
After revalidateTag(tag, { expire: 0 }), the next request to a page using that tag should regenerate and serve fresh content (as it does with bypassTagCacheOnCacheHit: false, and as self-hosted Next / Vercel do).
@opennextjs/cloudflare version
1.19.10
Wrangler version
4.91.0
next info output
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.5.0: Mon Apr 27 20:41:15 PDT 2026; root:xnu-12377.121.6~2/RELEASE_ARM64_T6041
Available memory (MB): 36864
Available CPU cores: 14
Binaries:
Node: 22.22.2
npm: 10.9.7
Yarn: N/A
pnpm: 9.15.9
Relevant Packages:
next: 16.2.6 // There is a newer version (16.2.9) available, upgrade recommended!
eslint-config-next: N/A
react: 19.2.4
react-dom: 19.2.4
typescript: 5.3.3
Next.js Config:
output: N/A
Describe the bug
We're running into an issue where a page is still serving old data after revalidation. We're observing the following on a page (
/download) whose data is taggedreleases:x-opennext-cache: HIT).revalidateTag("releases", { expire: 0 }).x-opennext-cacheheader andx-powered-by: Next.js(so cache interception fell through toNextServer), butx-nextjs-cache: HITand the old release version is still served. It stays stale on subsequent requests too.Setting
bypassTagCacheOnCacheHit: falsemakes revalidation work correctly, so it's specifically tied to that option. But since thebypassTagCacheOnCacheHitsignificantly improves our performance, we prefer to keep that on.We've met what the docs describe as the requirements for using it on Next 16+ (cache purge configured with Enterprise plan, and only ever using
revalidateTag(tag, { expire: 0 })rather than SWR-style revalidation), which is why we're unsure whether this is a bug or whether we're using regional cache /bypassTagCacheOnCacheHitincorrectly.I've analyzed the code with an agent, which pointed to the following:
opennextjs-cloudflare/packages/cloudflare/src/api/overrides/incremental-cache/regional-cache.ts
Lines 159 to 168 in 127942d
To verify this I created the following patch which resolves it in our local testing:
Sequence — current behavior (stale served)
sequenceDiagram participant C as Client participant I as Interceptor participant RC as RegionalCache participant CA as "Cache API" participant R2 as "R2 store" participant TC as "Tag cache (D1)" participant NS as NextServer Note over RC,CA: releases revalidated + regional entry purged C->>I: GET /download I->>RC: get("/download") RC->>CA: match -> MISS RC->>R2: get -> STALE value RC-->>CA: re-seed (putToCache) STALE Note right of CA: POISON - stale re-seeded RC-->>I: stale (no bypass flag) I->>TC: hasBeenRevalidated -> true I-->>NS: fall through (no x-opennext-cache) NS->>RC: get("/download") RC->>CA: match -> HIT (poisoned stale) RC-->>NS: stale + shouldBypassTagCache=true Note over NS: tag check skipped -> treated as fresh NS-->>C: OLD data, x-nextjs-cache HIT (no regeneration)Sequence — with the patch (regenerates fresh)
sequenceDiagram participant C as Client participant I as Interceptor participant RC as RegionalCache participant CA as "Cache API" participant R2 as "R2 store" participant TC as "Tag cache (D1)" participant NS as NextServer Note over RC,CA: releases revalidated + regional entry purged C->>I: GET /download I->>RC: get("/download") RC->>CA: match -> MISS RC->>R2: get -> STALE value RC->>TC: hasBeenRevalidated -> true Note right of RC: stale -> skip refill (no poison) RC-->>I: stale (no bypass flag) I->>TC: hasBeenRevalidated -> true I-->>NS: fall through NS->>RC: get("/download") RC->>CA: match -> MISS (still empty) RC->>TC: hasBeenRevalidated -> true (skip refill) RC-->>NS: stale (no bypass flag) Note over NS: handler re-checks tags -> regenerate NS->>NS: regenerate /download (fresh) NS->>RC: set fresh NS-->>C: FRESH dataIs this indeed a bug in the miss-path refill when
bypassTagCacheOnCacheHitis enabled, or are we misconfiguring / misunderstanding how the regional cache works? If it is a bug, I'd be happy to open a PR but I wanted check in about the expected behavior first.Steps to reproduce
bypassTagCacheOnCacheHit: true+enableCacheInterception+cachePurge).releases. Request it so it's cached (x-opennext-cache: HIT).revalidateTag("releases", { expire: 0 }).x-nextjs-cache: HIT, old content) and does not regenerate.Expected behavior
After
revalidateTag(tag, { expire: 0 }), the next request to a page using that tag should regenerate and serve fresh content (as it does withbypassTagCacheOnCacheHit: false, and as self-hosted Next / Vercel do).@opennextjs/cloudflare version
1.19.10
Wrangler version
4.91.0
next info output