Skip to content

Commit 1aa6fd9

Browse files
RhysSullivanclaude
andauthored
Pre-warm the app plane on requests that do not need it (#2072)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent c6d1083 commit 1aa6fd9

1 file changed

Lines changed: 64 additions & 10 deletions

File tree

‎apps/cloud/src/server.ts‎

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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;
250252
let 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

261306
const 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

Comments
 (0)