Link to the code that reproduces this issue
https://github.com/markus-wtfoxtrot/next-revalidate-false-force-dynamic
To Reproduce
app/layout.tsx:
export const dynamic = 'force-dynamic'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html><body>{children}</body></html>
}
app/page.tsx:
export default async function Page() {
const forever = await fetch('https://httpbin.org/uuid', { next: { revalidate: false } }).then((r) => r.json())
const oneYear = await fetch('https://httpbin.org/uuid?y', { next: { revalidate: 31_536_000 } }).then((r) => r.json())
return <pre>{JSON.stringify({ forever, oneYear }, null, 2)}</pre>
}
next.config.ts:
export default { logging: { fetches: { fullUrl: true } } }
- Run
next dev. Request / several times with curl (no Cache-Control header).
- Read the fetch log.
Current vs. Expected behavior
Current
The revalidate: false fetch logs cache skip, reason revalidate: 0, on every request, and its UUID changes on every request. The revalidate: 31_536_000 fetch logs cache hit from the second request on.
Reproduces with next dev and with next build && next start on 16.5.0-canary.1 and on 16.2.6.
Control: without dynamic = 'force-dynamic' (route kept dynamic via await connection()), the revalidate: false fetch is cached as expected. The first request in dev logs cache-control: no-cache (hard refresh) for every fetch. That is dev warm-up and not related to this issue.
Expected
The docs for options.next.revalidate say false means "cache the resource indefinitely. Semantically equivalent to revalidate: Infinity". It should behave like the one-year fetch.
Cause, in packages/next/src/server/lib/patch-fetch.ts:
const noFetchConfigAndForceDynamic =
!pageFetchCacheMode &&
!currentFetchCacheConfig &&
!currentFetchRevalidate && // false is falsy, so explicit config counts as "no config"
workStore.forceDynamic
The comment above this block says the guard is for the case "if no explicit fetch cache mode is set". revalidate: false is explicit, but the truthiness check treats it as undefined, so the next branch sets currentFetchRevalidate = 0.
Suggested fix
Check with typeof currentFetchRevalidate === 'undefined' instead of !currentFetchRevalidate. For undefined, 0 and positive numbers the result stays the same. Only false changes, to the documented behavior. If the current behavior is intended, the docs for revalidate: false should say that force-dynamic overrides it, while it does not override a numeric revalidate.
Impact
In production we used revalidate: false with tag-based on-demand invalidation, as the docs describe. Every fetch made after the force-dynamic layout segment went to the origin. Our CMS API usage almost doubled before we found the cause. Preview environments did not show it, because they used a numeric revalidate there, ensuring fresh data. The Vercel Data Cache dashboard also did not show it, because fetches that skip the cache never appear in its hit rate.
Related
#95493 fixed Infinity serialising to null, so Infinity was not a safe alternative either before that fix.
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.6.0: Fri Jul 31 19:11:03 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T8132
Available memory (MB): 24576
Available CPU cores: 10
Binaries:
Node: 24.3.0
npm: 11.4.2
Yarn: 1.22.22
pnpm: 11.8.0
Relevant Packages:
next: 16.5.0-canary.1 // Latest available version is detected (16.5.0-canary.1).
eslint-config-next: N/A
react: 19.3.0
react-dom: 19.3.0
typescript: 5.9.3
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
Dynamic Routes
Which stage(s) are affected? (Select all that apply)
next dev (local), next start (local), Vercel (Deployed)
Additional context
No response
Link to the code that reproduces this issue
https://github.com/markus-wtfoxtrot/next-revalidate-false-force-dynamic
To Reproduce
app/layout.tsx:app/page.tsx:next.config.ts:next dev. Request/several times withcurl(noCache-Controlheader).Current vs. Expected behavior
Current
The
revalidate: falsefetch logscache skip, reasonrevalidate: 0, on every request, and its UUID changes on every request. Therevalidate: 31_536_000fetch logscache hitfrom the second request on.Reproduces with
next devand withnext build && next starton16.5.0-canary.1and on16.2.6.Control: without
dynamic = 'force-dynamic'(route kept dynamic viaawait connection()), therevalidate: falsefetch is cached as expected. The first request in dev logscache-control: no-cache(hard refresh) for every fetch. That is dev warm-up and not related to this issue.Expected
The docs for
options.next.revalidatesayfalsemeans "cache the resource indefinitely. Semantically equivalent torevalidate: Infinity". It should behave like the one-year fetch.Cause, in
packages/next/src/server/lib/patch-fetch.ts:The comment above this block says the guard is for the case "if no explicit fetch cache mode is set".
revalidate: falseis explicit, but the truthiness check treats it asundefined, so the next branch setscurrentFetchRevalidate = 0.Suggested fix
Check with
typeof currentFetchRevalidate === 'undefined'instead of!currentFetchRevalidate. Forundefined,0and positive numbers the result stays the same. Onlyfalsechanges, to the documented behavior. If the current behavior is intended, the docs forrevalidate: falseshould say thatforce-dynamicoverrides it, while it does not override a numeric revalidate.Impact
In production we used
revalidate: falsewith tag-based on-demand invalidation, as the docs describe. Everyfetchmade after theforce-dynamiclayout segment went to the origin. Our CMS API usage almost doubled before we found the cause. Preview environments did not show it, because they used a numericrevalidatethere, ensuring fresh data. The Vercel Data Cache dashboard also did not show it, because fetches that skip the cache never appear in its hit rate.Related
#95493 fixed
Infinityserialising tonull, soInfinitywas not a safe alternative either before that fix.Provide environment information
Operating System: Platform: darwin Arch: arm64 Version: Darwin Kernel Version 25.6.0: Fri Jul 31 19:11:03 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T8132 Available memory (MB): 24576 Available CPU cores: 10 Binaries: Node: 24.3.0 npm: 11.4.2 Yarn: 1.22.22 pnpm: 11.8.0 Relevant Packages: next: 16.5.0-canary.1 // Latest available version is detected (16.5.0-canary.1). eslint-config-next: N/A react: 19.3.0 react-dom: 19.3.0 typescript: 5.9.3 Next.js Config: output: N/AWhich area(s) are affected? (Select all that apply)
Dynamic Routes
Which stage(s) are affected? (Select all that apply)
next dev (local), next start (local), Vercel (Deployed)
Additional context
No response