Skip to content

Commit 29a0aea

Browse files
authored
Warm the Start server graph on isolate first fetch (#1681)
Page latency was p50 19-49ms daily up to 2026-08-16 and 2728ms the day after the 21:20 deploy. Not load: 2026-08-11 served more total traffic (5.43M vs 4.38M) with pages at p50 41ms, so the volume explanation does not hold. What changed is where MCP work runs. The old hibernatable Agent bridge handed /mcp to the DO almost immediately; the v2 stack authenticates and dispatches in the worker, spinning far more worker isolates. /mcp returns before fetchHandler, so those isolates never load the Start graph, and page requests land on them cold paying loadEntries: p50 3.1s, against 33ms for the request's own work and 9-104ms warm. Re-lands #1628. Its revert came during the Aug 17 storm (11.67M requests against a 2.6-4.3M baseline); volume is back to baseline and exceededMemory is currently zero. START_GRAPH_WARM=false disables it.
1 parent 332955f commit 29a0aea

3 files changed

Lines changed: 62 additions & 0 deletions

File tree

apps/cloud/src/env-augment.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ declare global {
9595
MCP_REQUEST_STATE_KEY?: string;
9696
/** Emergency rollback for inbound MCP 2026-07-28 traffic only. */
9797
MCP_2026_07_28_ENABLED?: string;
98+
// Kill switch for the Start server-graph warmup (server.ts). Set to
99+
// "false" to disable without a deploy if memory pressure returns.
100+
START_GRAPH_WARM?: string;
98101
NODE_ENV?: string;
99102

100103
// Shared with frontend

apps/cloud/src/server.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,59 @@ const mcpAgentHandler = makeCloudMcpAgentHandler({
178178
traceRequest: traceCloudMcpRequest,
179179
});
180180

181+
// ---------------------------------------------------------------------------
182+
// Start server-graph warmup
183+
// ---------------------------------------------------------------------------
184+
//
185+
// Page latency was p50 19-49ms every day up to 2026-08-16 and 2728ms the day
186+
// after the 21:20 UTC deploy. It is not load: 2026-08-11 served MORE total
187+
// traffic (5.43M requests vs 4.38M) with pages at p50 41ms, so the
188+
// reconnect-storm/volume explanation does not hold.
189+
//
190+
// What changed is where MCP work runs. The old hibernatable Agent bridge
191+
// handed `/mcp` to the Durable Object almost immediately; the v2 stack
192+
// authenticates and dispatches in the WORKER (`mcp.auth.jwt_verify` alone runs
193+
// ~35k times per 6h there). That spins up far more worker isolates, and `/mcp`
194+
// returns above without ever touching `fetchHandler` — so those isolates never
195+
// load the Start graph. Page requests then land on them cold and pay
196+
// `loadEntries`: measured p50 **3.1s**, against p50 33ms for the request's own
197+
// work and 9-104ms for a warm isolate. The same bundle in local workerd serves
198+
// the same path in ~3ms, so this is a cold-isolate cost, not slow code.
199+
//
200+
// So warm on the isolate's FIRST fetch, in the background: MCP traffic then
201+
// pre-warms an isolate before a page request reaches it. This is #1628, which
202+
// worked; it was reverted (#1634) when memory-limit kills jumped, but that
203+
// landed during the 2026-08-17 storm — 11.67M requests in a day against a
204+
// 2.6-4.3M baseline. Volume is back to baseline and `exceededMemory` is
205+
// currently zero, so the condition that made it expensive is gone.
206+
// `START_GRAPH_WARM=false` disables it without a deploy.
207+
//
208+
// Deliberately NOT at module scope: a full warmup there trips workerd's
209+
// global-scope I/O restriction, and DO-only isolates should not carry the SSR
210+
// graph.
211+
// ---------------------------------------------------------------------------
212+
213+
let startGraphWarmupStarted = false;
214+
215+
const warmStartGraph = (env: Env): void => {
216+
if (startGraphWarmupStarted) return;
217+
if (env.START_GRAPH_WARM === "false") return;
218+
startGraphWarmupStarted = true;
219+
// oxlint-disable-next-line executor/no-promise-catch -- adapter boundary; fire-and-forget warmup outside any Effect runtime
220+
void Promise.all([import("#tanstack-router-entry"), import("#tanstack-start-entry")]).catch(
221+
() => {
222+
// Advisory only — the request path still loads the graph lazily.
223+
startGraphWarmupStarted = false;
224+
},
225+
);
226+
};
227+
181228
const cloudflareHandler: ExportedHandler<Env> = {
182229
fetch: async (request, env, ctx) => {
230+
// First fetch in this isolate kicks the graph load off in the background,
231+
// including for /mcp — MCP traffic is what reaches these isolates first.
232+
warmStartGraph(env);
233+
183234
// Public pages must not enter TanStack Start: its first-request dynamic
184235
// import loads the entire React + Effect server graph and can take seconds
185236
// on a cold isolate. Classify and service-bind marketing at the Worker
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// TanStack Start's internal virtual server-entry modules (registered by the
2+
// Start vite plugin; the same ids `start-server-core`'s `loadEntries`
3+
// imports). server.ts imports them for the isolate warmup — only the
4+
// module-evaluation side effect matters there, so the value shape is left
5+
// untyped. Kept in a standalone declaration file: shorthand ambient modules
6+
// only register from a non-module file (env-augment.d.ts is a module).
7+
declare module "#tanstack-router-entry";
8+
declare module "#tanstack-start-entry";

0 commit comments

Comments
 (0)