Skip to content

[BUG] Intermittent React hydration mismatch (#418) on Workers: ~9% of loads, 0% with next start on the identical build #1321

Description

@raphaeljordao86

Describe the bug

In production on Workers, roughly 9 out of 100 page loads log React error #418 (hydration mismatch) and React discards the server-rendered tree and re-renders on the client. The same build served locally with next start gives 0 out of 100.

Same Chrome, same Supabase backend, same account, same routes, same build output — the only variable is the runtime serving it.

environment #418 in 100 loads
Cloudflare Worker (@opennextjs/cloudflare) 9
local next start, identical build 0

The rate is stable across builds: an independent run three days earlier, on a different commit, gave 8/100 in production.

What we ruled out, each with the measurement that killed it:

  • Route-specific or client-component size. 25 loads on each of 4 routes. Failures spread across routes and the distribution moves between runs (run A: 2/25, 3/25, 0/25, 3/25 — run B on a later build: 2/25, 0/25, 0/25, 7/25). A route that is a pure Server Component fails; the route with the largest client island gave 0/25 in both runs. No correlation with route.
  • Non-deterministic server HTML. Repeated fetches of the same route return byte-identical documents (a single distinct content-length across 10 loads).
  • Intl / locale divergence between the Workers runtime and the browser. One consistently failing route has zero Intl/currency formatting in its tree.
  • Deploy-window artifact (HTML from one version, JS chunks from another). Reproduced with the Worker untouched for hours.
  • Cloudflare HTML rewriting (Rocket Loader, email obfuscation). No cdn-cgi markers in the document.
  • Our own version-skew banner (a client component that compares a build stamp). It initialises to the same state on both sides and only mutates inside useEffect, so it cannot diverge during hydration — and it did not exist when the first measurement was taken.
  • cacheComponents. Not enabled; our next.config.ts sets only reactStrictMode and one env value.

The one concrete difference we can measure is delivery. The Worker coalesces the stream — same route, same build, measured today:

route production (Worker) local (next start)
A 84,786 B, 16 network chunks, 7 self.__next_f.push, br 84,872 B, 18 chunks, 9 pushes, gzip
B 63,983 B, 6 chunks, 4 pushes, br 64,112 B, 13 chunks, 7 pushes, gzip

Payload sizes match within 0.2%, which rules out "local renders less content" as the explanation for the different failure rates.

Note the inversion that kills the intuitive theory: in an earlier run the longest delivery window of all (local, ~1,050 ms first-to-last byte) never failed, while a production route arriving in ~38 ms did. So "hydration started before the RSC payload finished" does not fit the data. The coalescing is a visible symptom of something the runtime does differently, not a proven cause.

After a failing load, the first DOM difference against the server HTML is a Radix-generated id (radix-_R_… server-side vs radix-_r_0_ client-style) — that is, the tree had already been regenerated on the client. It is the consequence of the mismatch, not its origin. next dev, where React prints the named diff, does not reproduce.

Steps to reproduce

We do not have a minimal public repro yet — the failure is probabilistic (~9%) and environment-specific, so a single load usually looks fine. What reliably reproduces it on our app:

  1. Deploy a Next.js 15 App Router app as a Worker via @opennextjs/cloudflare.
  2. Load an authenticated page 25+ times in headless Chrome, collecting console errors on each load.
  3. Count loads reporting React error [BUG] e2e do not work locally #418. We see ~9%.
  4. Run next start on the same build output and repeat, same browser, same account.

Two things that cost us time and are worth stating for anyone trying to confirm this:

  • 12 loads per route is not enough. At a ~9% rate, a dozen loads routinely produces false "this route is clean" conclusions — it did for us, and it produced a route-correlation "finding" that a larger sample later demolished.
  • Compare against the local pair of the same build. Comparing against a different build makes the result meaningless.

We are happy to share the measurement scripts (headless Chrome loop, an RSC-delivery profiler, and a server-HTML vs post-hydration DOM differ), and can run instrumented builds if that helps narrow it down.

Expected behavior

The same build should hydrate as consistently on Workers as it does under next start — that is, no hydration mismatch on loads where the server HTML is byte-identical and the client bundle is the same.

@opennextjs/cloudflare version

1.19.11

Wrangler version

4.101.0

next info output

Operating System:
  Platform: win32
  Arch: x64
  Version: Windows 11 Pro
  Available memory (MB): 32159
  Available CPU cores: 16
Binaries:
  Node: 24.15.0
  npm: 11.12.1
  Yarn: N/A
  pnpm: N/A
Relevant Packages:
  next: 15.5.19
  eslint-config-next: 15.5.19
  react: 19.0.0
  react-dom: 19.0.0
  typescript: 5.9.3
Next.js Config:
  output: N/A

Additional context

Impact for us is low and we are not blocked: no data loss, no broken interaction — the cost is a full client re-render and, at worst, a brief flash, on a minority of loads. We are reporting it because it is reproducible at a stable rate across builds and is specific to the Workers runtime, which makes it look like something you would want to know about rather than something we can fix on our side.

Happy to provide raw numbers, the scripts, or to test a patched build.

Is this a known issue, and is there instrumentation you would like enabled to catch the mismatch at its source?

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