Skip to content

Forward docs and PostHog proxies before loading the Start graph - #1678

Merged
RhysSullivan merged 1 commit into
mainfrom
perf/front-split-passthrough-proxies
Aug 18, 2026
Merged

Forward docs and PostHog proxies before loading the Start graph#1678
RhysSullivan merged 1 commit into
mainfrom
perf/front-split-passthrough-proxies

Conversation

@RhysSullivan

Copy link
Copy Markdown
Collaborator

What this fixes

/docs and the PostHog proxy are pure passthroughs — they forward to an external origin and never touch the router, React, or the Effect app. But they lived in Start's request middleware, so each one paid Start's lazy loadEntries import of the entire server graph before it could forward a request.

The measurement

Timing the two costs apart on a cold isolate, in production:

p50
Start graph import (loadEntries) 3104ms
the request's own work, once loaded 33ms

And essentially every request is cold: worker.dispatch ran 1,666 requests across 1,608 isolates (1.04 req/isolate). /mcp dispatches before fetchHandler, so MCP traffic — the large majority — creates and occupies isolates without ever warming the graph. Page and proxy requests then land on isolates that have only served MCP.

Concretely: a /docs page took 3-6s through the Worker against 0.098s fetched straight from the upstream.

The change

edge/passthrough.ts holds the matching, upstream construction, and the docs client span. It imports neither @tanstack/react-start nor any app module — that import is the cost it exists to avoid — and server.ts dispatches it at the Worker entry, next to marketing (#1637), which is the same seam for the same reason.

The docsProxyMiddleware / posthogProxyMiddleware wrappers stay registered and now source their matching from that module. In the deployed Worker they are unreachable; on any host that reaches Start by another entry (local dev) behavior is unchanged, and the two paths cannot drift.

A test asserts the import boundary directly, since violating it would keep the behavior correct and silently restore the 3.1s.

Also reverts the temporary latency probes (#1670-#1677) from server.ts.

What this does NOT fix

The signed-in app itself. Document and /api/* requests genuinely need Start, so they still pay ~3.1s on a cold isolate. Two things remain, and I'd treat them as separate:

  1. Why a 2.56MB import takes 3.1s at all. cpuTime during those requests is only 67-357ms, so it is not compute-bound — the isolate is waiting through it. That is the real bug and worth its own investigation.
  2. Why every request is cold. Warming (Warm the Start server graph on isolate first-fetch #1628) fixed this but was reverted for memory (kills went 500-900 → 3.7k-9.4k per 5min) because it loaded the SSR graph into every isolate including MCP-only ones. Splitting the app and MCP into separate Workers would get the same result without that cost.

Verification

  • format, lint (0 errors), typecheck (44/44)
  • apps/cloud vitest: 280 passed, including 6 new cases covering matching, /api/docs not being shadowed, cookie stripping, ingest-vs-assets splitting, and the import boundary

Both are pure passthroughs to an external origin — they never touch the
router, React, or the Effect app — but they lived in Start's request
middleware, so each paid Start's lazy loadEntries import of the whole
server graph before it could forward.

Measured in production by timing the two costs apart on a cold isolate:
the graph import is p50 3.1s while the request's own work is p50 33ms.
Essentially every request is cold — worker.dispatch ran 1,666 requests
across 1,608 isolates — because /mcp dispatches before fetchHandler and
so never warms the graph. A /docs page took 3-6s through the Worker
against 0.098s straight from the upstream.

Move matching and forwarding into edge/passthrough.ts, which imports
neither Start nor any app module, and dispatch it from the Worker entry
next to marketing. The middleware wrappers stay registered and now source
their matching from the same module, so local dev is unchanged and the
two cannot drift.

Also reverts the temporary latency probes from server.ts.
@RhysSullivan
RhysSullivan merged commit 54ccca9 into main Aug 18, 2026
36 of 39 checks passed
@RhysSullivan
RhysSullivan deleted the perf/front-split-passthrough-proxies branch August 18, 2026 23:32
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

RhysSullivan added a commit that referenced this pull request Aug 19, 2026
#1690)

Reverts 0b1739b. Restores the SDK v1 stack and the Cloudflare Agents
McpAgent bridge for /mcp.

Dependent work is reverted with it, since it patched code that only
exists under the v2 stack: the dead-session negative cache (#1627) and
its tests, and the standalone-listener/keepalive changes (#1622, #1629,
#1632) that live in the deleted v2 modules.

Kept on top of the revert: the passthrough proxies at the Worker entry
(#1678), version stamping and dispatch spans (#1664), and MCP request
tracing, rewired through traceCloudMcpRequest since the v1 handler takes
no traceRequest option.

Dependencies: agents is pinned to 0.17.3 so patchedDependencies applies
(a floating ^0.17.3 resolved to 0.17.4 and silently dropped the patch).
bun.lock is reconciled from main rather than reverted wholesale, which
otherwise split ajv across 8.18/8.20 and broke codemode-core typecheck.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant