I moved joshmatz.com from Hugo to TanStack Start on Cloudflare Workers in February 2026. HMR began failing almost immediately, and it later failed again in July in two distinct ways.
The specific problems we reproduced now appear to be resolved:
- An old Cloudflare compatibility flag, added as an early workaround, eventually made the current runtime less stable and caused the development worker to return 500s or stop responding during live HMR.
- Cloudflare Vite plugin 1.25.6 selected the wrong Vite module runner for Vite 7.3.1. During SSR HMR this could load two React instances, producing
Invalid hook calland nulluseContexterrors while rendering TanStack'sHeadContent.
Removing the obsolete flag and upgrading the Cloudflare plugin, Vite, Wrangler, TanStack Start, and React resolved both reproduced failures. I would describe this as a problem in the TanStack Start + Cloudflare SSR development integration. The concrete upstream bug we identified was in Cloudflare's Vite plugin.
The site moved to TanStack Start in this February 28 migration. The initial stack included:
- TanStack Start and Router 1.163
- Vite 7.3.1
- Cloudflare Vite plugin 1.25.6
- Wrangler 4.69
- React 19.2.4
Nine minutes later, Claude committed an HMR crash workaround. Its commit message describes "alternating SSR cancellation errors during HMR." It added:
- Cloudflare's
no_handle_cross_request_promise_resolutioncompatibility flag - A 200 ms
awaitWriteFinishdelay to Vite's watcher
This establishes that the July failure was not the first incident, even though I no longer have the February conversation transcript.
In July, the development worker could remain bound to port 3000 while returning 500s or no longer accepting HTTP requests.
We tested SSR invalidations both with and without a browser maintaining an HMR connection:
- 40 invalidations without a live browser connection completed successfully.
- With live browser HMR connected, the third invalidation failed.
- After removing only
no_handle_cross_request_promise_resolution, 80 consecutive live-browser invalidations completed successfully.
That flag had become counterproductive. Cloudflare's corrected cross-request promise behavior has been the default since compatibility date 2024-10-14; the no_handle_... flag disables it. Cloudflare documents the behavior in its compatibility flags reference.
Removing the compatibility flag fixed the persistent 500s, but another HMR failure remained. A page would update automatically, show an error, and work again only after a manual refresh.
Once captured in the server logs, the failure included:
[vite] program reloadInvalid hook callCannot read properties of null (reading 'useContext')- A stack involving TanStack's
HeadContent
Cloudflare Vite plugin 1.25.6 was selecting an incompatible vite/module-runner for Vite 7.3.1. During SSR HMR, TanStack's HeadContent could consequently render with a different React instance from react-dom/server.
Cloudflare fixed the module-runner selection problem in Workers SDK PR #12936, released in @cloudflare/vite-plugin 1.29.1. The site's lockfile still had plugin 1.25.6 in July, so it was not receiving that fix.
The final fix is in Update site dependencies. It removed the obsolete compatibility flag and upgraded the stack to:
- Cloudflare Vite plugin 1.47.0
- Vite 8.1.5
- Wrangler 4.114.0
- TanStack Start 1.168.32
- TanStack Router 1.170.18
- React and React DOM 19.2.8
After the upgrade, six consecutive real MDX edits caused six SSR reloads without an invalid hook call, null useContext, 500 response, or manual refresh. The production build and typecheck also passed.
Cloudflare now documents TanStack Start as supported on Workers, including SSR and HMR. The current project configuration follows the core setup in Cloudflare's TanStack Start guide and Vite plugin documentation.
The failures we reproduced appear to be fixed both upstream and in this project. Six successful edit cycles are not proof that every HMR edge case is gone, but I have not found another HMR-specific repair in the repository after the July dependency update.
The agent-generated setup mattered. The original workaround was reasonable before the upstream module-runner fix existed, but it remained in the project after Cloudflare's behavior changed, while the lockfile kept the plugin version old enough to miss the upstream repair. That combination turned an old workaround and an old integration bug into two different failures with similar symptoms.