Build hangs after prerender: SSR timeouts in transformStreamWithRouter are never cleaned up
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Description
This issue was filed with AI assistance. My builds were hanging indefinitely when prerender was enabled. I read the docs multiple times and reviewed related open issues (e.g. #6425), but couldn't resolve it. After ruling out misconfiguration, I spent about an hour debugging with Claude Opus and we identified the root cause below.
When using TanStack Start with prerendering enabled, the vite build process hangs indefinitely after all pages are prerendered. The process never exits, blocking CI pipelines and requiring manual termination.
Root Cause
@tanstack/router-core/dist/esm/ssr/transformStreamWithRouter.js creates 60-second setTimeout handles (lifetime timeout and serialization timeout) for each SSR-rendered page. During prerendering, these timeouts are never cleaned up because:
- Each call to
transformStreamWithRouter()sets alifetimeTimeoutHandle(60s) at line ~231 - After the app stream finishes rendering,
serializationFinishedisfalse, so aserializationTimeoutHandle(60s) is also set at line ~315 - The
onSerializationFinishedevent never fires during prerendering, sotryFinish()→cleanup()is never called - This leaves one timeout per prerendered page as active Node.js handles, preventing the process from exiting
With 49 prerendered pages, there are ~50 active Timeout handles keeping the event loop alive.
Reproduction
Any TanStack Start project with prerendering enabled:
// vite.config.ts
tanstackStart({
pages: [
{ path: '/page1', prerender: { enabled: true } },
{ path: '/page2', prerender: { enabled: true } },
// ...
],
})
Run vite build — the process will hang after printing "Prerendered N pages".
Environment
@tanstack/react-start: 1.158.3@tanstack/router-core: 1.158.3- Vite: 7.3.1
- Node.js: v24.7.0
Suggested Fix
Add .unref() to all setTimeout calls in transformStreamWithRouter.ts so they don't prevent the Node.js process from exiting. These are safety timeouts — they should fire if the process is still running, but shouldn't keep the process running:
// Line ~96 (early return path lifetime timeout)
lifetimeTimeoutHandle2 = setTimeout(() => {
// ...
}, lifetimeMs2);
+ lifetimeTimeoutHandle2.unref();
// Line ~231 (main path lifetime timeout)
lifetimeTimeoutHandle = setTimeout(() => {
// ...
}, lifetimeMs);
+ lifetimeTimeoutHandle.unref();
// Line ~315 (serialization timeout)
serializationTimeoutHandle = setTimeout(() => {
// ...
}, timeoutMs);
+ serializationTimeoutHandle.unref();
Alternatively, the prerender flow should ensure onSerializationFinished fires (or cleanup() is called) after each page response is fully consumed, so the timeouts are properly cleared.
Workaround
As a temporary workaround, add process.exit(0) at the end of postServerBuild in @tanstack/start-plugin-core/dist/esm/post-server-build.js:
if (startConfig.prerender?.enabled) {
process.exit(0);
}
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/router-core/src/ssr/transformStreamWithRouter.ts and inspect the lifetime and serialization timeout paths around the referenced lines. Reproduce the issue with a TanStack Start vite.config.ts containing prerendered pages, then run vite build and verify that the process exits after all pages are prerendered without lingering timeout handles.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript, vite
- Domain
- backend, build-system
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 67/100