@@ -243,19 +243,64 @@ const markStartGraphEntered = (): void => {
243243// `servedByAppPlane` (./app-paths) decides which paths qualify — two under
244244// `/api` are claimed by Start's middleware first and must keep their old route.
245245
246- // Instantiated on the first request that needs it and memoized per isolate,
247- // mirroring `start.ts`'s `getApp`. The import stays dynamic so an isolate that
248- // only serves pages or proxies never evaluates the app graph at all.
249- let appPlane : ReturnType < typeof import ( "./app" ) . cloudApiHandler > | undefined ;
246+ // Instantiated once per isolate and memoized as a promise, mirroring
247+ // `start.ts`'s `getApp`. The import stays dynamic so the Worker's static
248+ // startup closure does not include the app graph; the promise memo means a
249+ // pre-warm and a real request racing on a fresh isolate share one import.
250+ type AppPlane = ReturnType < typeof import ( "./app" ) . cloudApiHandler > ;
251+ let appPlanePromise : Promise < AppPlane > | undefined ;
250252let appGraphEntered = false ;
251253
252- const getAppPlane = async ( ) : Promise < NonNullable < typeof appPlane > > => {
253- if ( appPlane === undefined ) {
254- const { cloudApiHandler } = await import ( "./app" ) ;
255- appPlane = cloudApiHandler ( ) ;
256- appGraphEntered = true ;
254+ const getAppPlane = ( ) : Promise < AppPlane > => {
255+ if ( appPlanePromise === undefined ) {
256+ appPlanePromise = import ( "./app" ) . then (
257+ ( { cloudApiHandler } ) => {
258+ const plane = cloudApiHandler ( ) ;
259+ appGraphEntered = true ;
260+ return plane ;
261+ } ,
262+ ( cause : unknown ) => {
263+ // Do not memoize a failure: the next request re-imports, as the
264+ // un-memoized version did, instead of failing every request after.
265+ appPlanePromise = undefined ;
266+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: re-raise the import failure to the awaiting request
267+ throw cause ;
268+ } ,
269+ ) ;
257270 }
258- return appPlane ;
271+ return appPlanePromise ;
272+ } ;
273+
274+ // ---------------------------------------------------------------------------
275+ // Pre-warming the app plane.
276+ // ---------------------------------------------------------------------------
277+ //
278+ // Measured on production 2026-09-18: 30% of `/api/*` dispatches landed on an
279+ // isolate that had not yet evaluated the app graph, and paid ~2s (p50) for it
280+ // against ~100ms warm. Only 43% of those isolates were under 5s old. The rest
281+ // had been alive for seconds to minutes serving `/mcp`, discovery documents,
282+ // or proxies, none of which enter the app graph, so the dashboard's first
283+ // call was the one that paid. Isolates live about a minute at the median, so
284+ // there is rarely a second dashboard request to benefit.
285+ //
286+ // So any request that does NOT need the app plane starts its import in the
287+ // background. The request itself returns as before; the import runs under
288+ // `waitUntil`, so the isolate stays up until it finishes. A dashboard call
289+ // arriving afterwards finds the graph evaluated. The truly fresh isolate
290+ // (first request IS a dashboard call) still pays; that cost is the graph's
291+ // evaluation itself, addressed separately.
292+ //
293+ // Failure is swallowed on purpose: a pre-warm that fails must not fail the
294+ // request that triggered it, and the next real app-plane request re-imports
295+ // through the same memo and surfaces the error where it belongs.
296+ const prewarmAppPlane = ( ctx : ExecutionContext ) : void => {
297+ if ( appGraphEntered ) return ;
298+ ctx . waitUntil (
299+ getAppPlane ( ) . then (
300+ ( ) => undefined ,
301+ ( ) => undefined ,
302+ ) ,
303+ ) ;
259304} ;
260305
261306const cloudflareHandler : ExportedHandler < Env > = {
@@ -266,6 +311,12 @@ const cloudflareHandler: ExportedHandler<Env> = {
266311 // import loads the entire React + Effect server graph and can take seconds
267312 // on a cold isolate. Classify and service-bind marketing at the Worker
268313 // entry, before telemetry or fetchHandler touches that graph.
314+ // Everything that returns before the app-plane dispatch below leaves the
315+ // graph unevaluated for the next request; warm it in the background.
316+ if ( ! servedByAppPlane ( new URL ( request . url ) . pathname , request . method ) ) {
317+ prewarmAppPlane ( ctx ) ;
318+ }
319+
269320 const marketingRequest = marketingProxyRequest ( request ) ;
270321 const marketing : Fetcher | undefined = env . MARKETING ;
271322 if ( marketingRequest && marketing ) return marketing . fetch ( marketingRequest ) ;
@@ -445,6 +496,9 @@ const cloudflareHandler: ExportedHandler<Env> = {
445496 // isolate goes idle.
446497 scheduled : async ( _controller , _env , ctx ) => {
447498 installTracerProvider ( ) ;
499+ // The cron fires every minute, often on an isolate that has served no
500+ // dashboard request yet: the cheapest pre-warm there is.
501+ prewarmAppPlane ( ctx ) ;
448502 await runWorkOsEventsSync ( ) ;
449503 ctx . waitUntil ( flushTracerProvider ( ) ) ;
450504 } ,
0 commit comments