Skip to content

next.revalidate: false is reset to 0 under dynamic = 'force-dynamic', so the fetch is never cached #99767

Description

@markus-wtfoxtrot

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 } } }
  1. Run next dev. Request / several times with curl (no Cache-Control header).
  2. 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

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions