Skip to content

[world-vercel] Enable HTTP/2 for the events API and stream writes - #2573

Merged
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2
Jun 24, 2026
Merged

[world-vercel] Enable HTTP/2 for the events API and stream writes#2573
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

Enable HTTP/2 for two hot @workflow/world-vercel request paths, and fix the framework bundling that previously made H2 impossible on Vite-based adapters.

  • v4 events API (events-v4.ts) — an event write per step transition plus event-log reads on replay. Gets its own H2 undici dispatcher (getEventsDispatcher).
  • Stream writes (streamer.tswrite/writeMulti/close — the PUT path) — routed through a dedicated H2 stream dispatcher (getStreamDispatcher). These send a fully-buffered body (or none), so they get H2 multiplexing without the duplex issues that keep the long-lived live-read (GET) on plain fetch. The stream dispatcher is a bare Agentwithout the RetryAgent wrapper: stream appends aren't idempotent, so undici-level retry is deliberately omitted to avoid duplicating chunks.

Everything else stays on HTTP/1.1 (queue client / webhook respondWith, v3 makeRequest, deployment resolution, run-key fetch, live-read GET, list).

The bundling fix (why H2 was blocked before)

H2 had been disabled because it "broke SvelteKit on Vercel prod." Root cause: Vite/Rollup-based framework builds (SvelteKit, TanStack Start, plain Vite) bundle undici into the ESM server output. undici imports most node: builtins as ESM, but loads node:http2 lazily via a bare require('node:http2') inside a try/catch. The bundler leaves that bare require un-wired, so in the ESM bundle it throws and undici silently falls back to a stub whose http2.connect is undefined. Any H2 request then fails with fetch failed → the workflow flow-route callback can't reach the backend → runs never leave pending → 30-minute E2E timeouts. Frameworks that externalize undici (Express, Astro, Next, Hono, Nitro, …) were unaffected.

Fix: inject a createRequire-backed global require into the server build (server-only — a node:module import would break the client bundle) so the real node:http2 resolves:

  • @workflow/sveltekit — via configResolved (SvelteKit doesn't set env.isSsrBuild for its server pass; build.ssr on the resolved config is the reliable signal).
  • @workflow/nitro — via the existing rollup:before hook, covering Vite, TanStack Start, and Nitro from one place.

Verified end-to-end: with the fix, the sveltekit / vite / tanstack-start E2E Vercel Prod lanes pass, and http2.connect is not a function errors drop to zero (previously deterministic on this branch only).

Why the default dispatcher stays H1

Enabling H2 on the shared dispatcher deadlocks the queue's webhook respondWith mechanism (120s timeouts on the webhook/hook workflows). So H2 is scoped to the events + stream-write paths, which don't use that mechanism.

Tests

  • Regression guards: EVENTS_AGENT_OPTIONS.allowH2 === trueandDEFAULT_AGENT_OPTIONS.allowH2 === false.
  • getEventsDispatcher and getStreamDispatcher each return their own shared singleton — distinct from the default and from each other — and honor a caller-supplied dispatcher.
  • Local node:http2 TLS server test: a request through the global fetch with an allowH2 dispatcher negotiates h2 (the mechanism these paths rely on).
  • Full @workflow/world-vercel unit suite passes (172 tests).

Notes

  • events-v4.ts and the stream writes stay on the Vercel-patched global fetch (preserves observability instrumentation); only the dispatcher they pass changes.
  • The sleepingWorkflow and distributedAbortController failures seen on some lanes are pre-existing flakes (zero-tolerance server-side sleep timing / abort timeout) that also fail on main — unrelated to this change.

🤖 Generated with Claude Code

Flip allowH2 to true in the shared undici dispatcher so every
dispatcher-backed path (v3 makeRequest, v4 events, the queue client,
deployment resolution, run-key fetch) negotiates HTTP/2. H2 was
previously disabled for a SvelteKit-on-Vercel-prod hang; re-enable it
globally and rely on the E2E suite to guard the regression. Streaming
paths in streamer.ts intentionally bypass the dispatcher and are
unaffected.
Adds a regression guard on the agent options plus a local http2 server
test proving global fetch honors an allowH2 dispatcher (the mechanism
the v4 events path relies on).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJune 22, 2026 21:51
@vercel

vercelBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2ec9d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/world-vercelPatch
@workflow/nuxtPatch
workflowPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7282010788360

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ nitro-stable126026
✅ nuxt-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.045s (-5.3% 🟢)1.006s (~)0.961s101.00x
💻 LocalNitro0.047s (+0.6%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.050s (-4.2%)1.006s (~)0.955s101.12x
🐘 PostgresNitro0.065s (-0.6%)1.012s (~)0.947s101.44x
🐘 PostgresExpress0.066s (-4.5%)1.013s (~)0.946s101.47x
🐘 PostgresNext.js (Turbopack)0.067s (+20.8% 🔺)1.028s (+1.6%)0.962s101.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.232s (~)1.976s (-6.1% 🟢)1.744s101.00x
▲ VercelNext.js (Turbopack)0.260s (-3.8%)2.042s (~)1.782s101.12x
▲ VercelNitro0.301s (+19.7% 🔺)2.042s (-5.9% 🟢)1.741s101.30x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.078s (-0.8%)2.006s (~)0.928s101.00x
💻 LocalNext.js (Turbopack)1.079s (~)2.006s (~)0.927s101.00x
🐘 PostgresNext.js (Turbopack)1.079s (~)2.009s (~)0.930s101.00x
💻 LocalNitro1.082s (~)2.007s (~)0.925s101.00x
🐘 PostgresExpress1.097s (~)2.009s (~)0.912s101.02x
🐘 PostgresNitro1.100s (~)2.009s (~)0.909s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.421s (-0.9%)3.085s (-8.0% 🟢)1.664s101.00x
▲ VercelExpress1.445s (+4.5%)3.251s (+14.9% 🔺)1.806s101.02x
▲ VercelNitro1.468s (+4.0%)3.195s (~)1.727s101.03x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)10.420s (~)11.026s (~)0.606s31.00x
💻 LocalExpress10.435s (~)11.023s (~)0.588s31.00x
💻 LocalNitro10.455s (~)11.024s (~)0.569s31.00x
🐘 PostgresExpress10.460s (~)11.021s (~)0.560s31.00x
🐘 PostgresNitro10.476s (~)11.017s (~)0.541s31.01x
💻 LocalNext.js (Turbopack)10.504s (~)11.022s (~)0.518s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.937s (+1.8%)13.411s (~)1.473s31.00x
▲ VercelNext.js (Turbopack)12.028s (+1.9%)13.455s (-3.5%)1.427s31.01x
▲ VercelExpress12.325s (~)14.494s (+4.5%)2.169s31.03x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.554s (~)14.028s (~)0.473s51.00x
🐘 PostgresExpress13.559s (~)14.020s (~)0.461s51.00x
💻 LocalNext.js (Turbopack)13.625s (~)14.028s (~)0.403s51.01x
💻 LocalNitro13.734s (+0.7%)14.030s (~)0.295s51.01x
🐘 PostgresNext.js (Turbopack)13.750s (+0.6%)14.245s (+1.6%)0.495s51.01x
🐘 PostgresNitro13.773s (~)14.023s (~)0.250s51.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express18.265s (+11.1% 🔺)20.019s (+11.2% 🔺)1.754s31.00x
▲ VercelNext.js (Turbopack)18.771s (+8.2% 🔺)20.473s (+6.9% 🔺)1.702s41.03x
▲ VercelNitro19.188s (+17.2% 🔺)20.559s (+16.0% 🔺)1.372s31.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express12.044s (-2.4%)12.774s (-1.9%)0.730s81.00x
🐘 PostgresNext.js (Turbopack)12.160s (~)12.869s (-1.1%)0.709s71.01x
🐘 PostgresExpress12.263s (+1.3%)13.019s (~)0.757s71.02x
💻 LocalNitro12.272s (+0.7%)13.025s (~)0.753s71.02x
🐘 PostgresNitro12.273s (~)13.018s (~)0.746s71.02x
💻 LocalNext.js (Turbopack)12.414s (+1.3%)13.026s (~)0.612s71.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)22.253s (+15.0% 🔺)24.838s (+17.0% 🔺)2.586s41.00x
▲ VercelExpress22.303s (+21.4% 🔺)25.182s (+24.9% 🔺)2.879s41.00x
▲ VercelNitro22.749s (+19.8% 🔺)25.174s (+22.0% 🔺)2.425s41.02x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.172s (~)2.007s (~)0.835s151.00x
🐘 PostgresNitro1.192s (+0.9%)2.008s (~)0.815s151.02x
🐘 PostgresNext.js (Turbopack)1.215s (+3.2%)2.009s (~)0.794s151.04x
💻 LocalExpress1.351s (-2.7%)2.006s (~)0.655s151.15x
💻 LocalNitro1.423s (-1.1%)2.007s (~)0.584s151.21x
💻 LocalNext.js (Turbopack)1.444s (-3.6%)2.007s (~)0.563s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.284s (+12.5% 🔺)3.862s (+10.3% 🔺)1.578s81.00x
▲ VercelNitro2.364s (+11.0% 🔺)3.813s (+10.7% 🔺)1.448s81.04x
▲ VercelNext.js (Turbopack)2.432s (+13.1% 🔺)3.659s (-3.4%)1.228s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.302s (-2.8%)2.317s (-7.6% 🟢)1.015s131.00x
🐘 PostgresExpress1.332s (-4.4%)2.223s (-16.9% 🟢)0.891s141.02x
🐘 PostgresNext.js (Turbopack)1.468s (+11.8% 🔺)2.685s (-8.0% 🟢)1.217s121.13x
💻 LocalExpress2.446s (+3.7%)2.826s (-3.1%)0.381s111.88x
💻 LocalNext.js (Turbopack)2.530s (-7.5% 🟢)3.008s (-3.2%)0.477s101.94x
💻 LocalNitro2.671s (+10.9% 🔺)3.110s (+6.5% 🔺)0.439s102.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.780s (-9.7% 🟢)4.586s (-11.5% 🟢)1.806s71.00x
▲ VercelExpress3.603s (-0.9%)5.649s (+9.4% 🔺)2.047s61.30x
▲ VercelNext.js (Turbopack)4.146s (+24.1% 🔺)5.917s (+17.3% 🔺)1.772s61.49x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (~)4.135s (-3.9%)2.553s81.00x
🐘 PostgresNitro1.627s (-0.8%)4.136s (-3.0%)2.508s81.03x
🐘 PostgresNext.js (Turbopack)2.703s (-4.0%)4.582s (-16.9% 🟢)1.879s71.71x
💻 LocalNitro6.077s (-5.7% 🟢)6.818s (-5.5% 🟢)0.741s53.84x
💻 LocalExpress6.158s (-2.6%)6.819s (-2.8%)0.661s53.89x
💻 LocalNext.js (Turbopack)7.337s (+34.1% 🔺)8.017s (+33.3% 🔺)0.680s44.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.321s (+24.1% 🔺)6.144s (+18.1% 🔺)1.824s51.00x
▲ VercelNitro5.108s (+52.3% 🔺)6.685s (+30.6% 🔺)1.578s51.18x
▲ VercelExpress5.501s (+79.5% 🔺)7.636s (+51.4% 🔺)2.135s51.27x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.189s (+0.9%)2.007s (~)0.819s151.00x
🐘 PostgresNitro1.203s (~)2.009s (~)0.806s151.01x
🐘 PostgresNext.js (Turbopack)1.225s (+4.7%)2.015s (~)0.790s151.03x
💻 LocalExpress1.415s (-1.6%)2.007s (~)0.592s151.19x
💻 LocalNext.js (Turbopack)1.455s (+3.7%)2.006s (~)0.552s151.22x
💻 LocalNitro1.472s (+3.5%)2.007s (~)0.535s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.605s (+26.2% 🔺)4.403s (+25.9% 🔺)1.799s71.00x
▲ VercelNext.js (Turbopack)2.633s (-54.5% 🟢)4.124s (-44.6% 🟢)1.491s81.01x
▲ VercelNitro2.750s (+28.3% 🔺)4.412s (+18.7% 🔺)1.661s71.06x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.318s (-4.0%)2.393s (-3.2%)1.075s131.00x
🐘 PostgresExpress1.401s (+4.5%)2.676s (+8.3% 🔺)1.274s121.06x
🐘 PostgresNext.js (Turbopack)1.487s (+15.3% 🔺)2.515s (-13.8% 🟢)1.028s121.13x
💻 LocalNitro2.433s (-5.8% 🟢)3.009s (-3.2%)0.576s101.85x
💻 LocalNext.js (Turbopack)2.587s (-1.9%)3.109s (+3.3%)0.522s101.96x
💻 LocalExpress2.643s (+9.2% 🔺)3.008s (-3.2%)0.365s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.822s (-41.1% 🟢)5.218s (-21.4% 🟢)2.396s61.00x
▲ VercelNitro3.363s (+23.9% 🔺)5.021s (+16.3% 🔺)1.658s71.19x
▲ VercelNext.js (Turbopack)3.396s (-28.4% 🟢)5.116s (-26.4% 🟢)1.721s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.599s (-3.7%)4.138s (+3.1%)2.539s81.00x
🐘 PostgresExpress1.658s (+4.6%)4.011s (-3.0%)2.353s81.04x
🐘 PostgresNext.js (Turbopack)2.514s (-12.1% 🟢)5.041s (-13.8% 🟢)2.527s61.57x
💻 LocalExpress5.944s (-11.3% 🟢)6.415s (-11.1% 🟢)0.472s53.72x
💻 LocalNitro6.441s (-5.0% 🟢)7.015s (-2.8%)0.574s54.03x
💻 LocalNext.js (Turbopack)7.009s (+22.5% 🔺)8.020s (+33.3% 🔺)1.010s44.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.521s (+1.9%)5.720s (+13.6% 🔺)2.199s61.00x
▲ VercelNext.js (Turbopack)4.714s (+50.9% 🔺)6.326s (+22.5% 🔺)1.611s51.34x
▲ VercelExpress4.774s (+1.8%)7.280s (+15.7% 🔺)2.506s51.36x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.546s (+6.9% 🔺)1.027s (+2.1%)0.481s591.00x
🐘 PostgresExpress0.557s (+4.5%)1.041s (+1.7%)0.483s581.02x
🐘 PostgresNitro0.563s (-5.3% 🟢)1.024s (-1.7%)0.461s591.03x
💻 LocalExpress0.570s (~)1.005s (~)0.435s601.04x
💻 LocalNitro0.585s (~)1.005s (~)0.420s601.07x
💻 LocalNext.js (Turbopack)0.673s (+11.5% 🔺)1.058s (+3.5%)0.385s571.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.301s (+24.8% 🔺)4.990s (+16.1% 🔺)1.689s131.00x
▲ VercelExpress3.342s (+19.4% 🔺)5.325s (+18.7% 🔺)1.983s121.01x
▲ VercelNext.js (Turbopack)3.380s (+11.2% 🔺)4.843s (-3.1%)1.463s131.02x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.237s (-1.1%)2.007s (~)0.770s451.00x
🐘 PostgresNitro1.333s (-5.7% 🟢)2.030s (-1.2%)0.696s451.08x
💻 LocalExpress1.425s (-2.1%)2.006s (~)0.581s451.15x
💻 LocalNext.js (Turbopack)1.474s (~)2.006s (~)0.532s451.19x
🐘 PostgresNext.js (Turbopack)1.500s (+20.5% 🔺)2.105s (+3.7%)0.605s431.21x
💻 LocalNitro1.565s (+5.5% 🔺)2.052s (+2.3%)0.488s441.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.650s (+19.6% 🔺)9.459s (+21.8% 🔺)1.808s101.00x
▲ VercelExpress7.692s (+19.0% 🔺)9.613s (+19.6% 🔺)1.921s101.01x
▲ VercelNext.js (Turbopack)9.201s (+28.8% 🔺)11.021s (+24.0% 🔺)1.820s91.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.481s (-4.4%)3.033s (-1.7%)0.552s401.00x
🐘 PostgresNitro2.669s (-1.7%)3.086s (+0.9%)0.417s391.08x
💻 LocalExpress3.122s (-3.8%)3.821s (-3.9%)0.699s321.26x
💻 LocalNitro3.154s (-2.0%)3.978s (-0.8%)0.824s311.27x
🐘 PostgresNext.js (Turbopack)3.207s (+29.5% 🔺)3.749s (+22.6% 🔺)0.542s331.29x
💻 LocalNext.js (Turbopack)3.281s (+0.6%)3.977s (-1.6%)0.696s311.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express15.239s (+10.5% 🔺)17.918s (+15.9% 🔺)2.679s71.00x
▲ VercelNitro15.788s (+12.3% 🔺)18.302s (+17.1% 🔺)2.515s71.04x
▲ VercelNext.js (Turbopack)18.176s (+10.8% 🔺)20.378s (+10.2% 🔺)2.202s61.19x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.197s (-6.3% 🟢)1.006s (~)0.809s601.00x
🐘 PostgresNext.js (Turbopack)0.213s (+27.2% 🔺)1.010s (~)0.797s601.08x
🐘 PostgresNitro0.222s (-2.1%)1.007s (~)0.784s601.13x
💻 LocalExpress0.463s (+7.4% 🔺)1.005s (~)0.541s602.35x
💻 LocalNitro0.465s (-0.7%)1.004s (~)0.539s602.36x
💻 LocalNext.js (Turbopack)0.613s (+5.0%)1.005s (~)0.391s603.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.257s (-42.1% 🟢)2.609s (-32.7% 🟢)1.352s231.00x
▲ VercelExpress1.375s (-33.7% 🟢)3.160s (-17.7% 🟢)1.785s191.09x
▲ VercelNext.js (Turbopack)1.468s (-19.3% 🟢)2.862s (-19.3% 🟢)1.395s211.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.314s (-2.7%)1.006s (-1.1%)0.692s901.00x
🐘 PostgresNitro0.336s (~)1.006s (~)0.671s901.07x
🐘 PostgresNext.js (Turbopack)0.347s (+29.3% 🔺)1.017s (+1.1%)0.670s891.10x
💻 LocalExpress2.137s (-1.1%)2.735s (~)0.598s336.80x
💻 LocalNitro2.192s (+2.8%)2.796s (+4.1%)0.604s336.97x
💻 LocalNext.js (Turbopack)2.734s (-2.0%)3.295s (+1.1%)0.560s288.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.855s (-2.7%)3.708s (+6.3% 🔺)1.853s251.00x
▲ VercelExpress2.119s (-14.0% 🟢)4.166s (+2.8%)2.047s221.14x
▲ VercelNext.js (Turbopack)2.680s (+23.5% 🔺)4.477s (+14.0% 🔺)1.797s211.44x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.531s (+0.5%)1.050s (+1.7%)0.519s1151.00x
🐘 PostgresExpress0.567s (+9.8% 🔺)1.160s (+8.6% 🔺)0.594s1041.07x
🐘 PostgresNext.js (Turbopack)0.699s (+48.1% 🔺)1.975s (-34.3% 🟢)1.277s611.32x
💻 LocalExpress9.304s (-7.8% 🟢)10.360s (-6.9% 🟢)1.056s1217.52x
💻 LocalNext.js (Turbopack)9.579s (-2.1%)10.530s (-2.3%)0.951s1218.04x
💻 LocalNitro10.363s (+4.5%)11.302s (+2.5%)0.940s1119.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.326s (+39.1% 🔺)5.768s (+42.3% 🔺)2.443s211.00x
▲ VercelNitro3.641s (+43.2% 🔺)5.915s (+43.4% 🔺)2.274s211.09x
▲ VercelNext.js (Turbopack)4.725s (+0.8%)7.116s (+8.7% 🔺)2.391s171.42x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.137s (-2.7%)1.968s (~)0.011s (-14.5% 🟢)2.018s (~)0.881s101.00x
💻 LocalExpress1.151s (~)2.005s (~)0.010s (-17.5% 🟢)2.017s (~)0.866s101.01x
🐘 PostgresNitro1.156s (-0.9%)1.999s (~)0.001s (+18.2% 🔺)2.010s (~)0.854s101.02x
💻 LocalNitro1.159s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.862s101.02x
🐘 PostgresExpress1.179s (+2.6%)2.000s (~)0.001s (+10.0% 🔺)2.011s (~)0.831s101.04x
🐘 PostgresNext.js (Turbopack)1.209s (+5.5% 🔺)1.996s (~)0.001s (-27.3% 🟢)2.009s (~)0.800s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.129s (+4.0%)3.553s (+15.8% 🔺)2.488s (+42.0% 🔺)6.571s (+21.6% 🔺)4.441s101.00x
▲ VercelNext.js (Turbopack)2.206s (+6.8% 🔺)3.312s (-4.0%)2.584s (+83.3% 🔺)6.338s (+17.2% 🔺)4.132s101.04x
▲ VercelNitro2.374s (+7.0% 🔺)3.275s (~)2.368s (+27.1% 🔺)6.269s (+10.1% 🔺)3.894s101.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.509s (-2.5%)2.010s (~)0.013s (-1.3%)2.026s (~)0.517s301.00x
💻 LocalNext.js (Turbopack)1.563s (-0.9%)1.971s (~)0.012s (-5.4% 🟢)2.025s (~)0.462s301.04x
💻 LocalNitro1.564s (+0.7%)2.010s (~)0.013s (+9.4% 🔺)2.026s (~)0.462s301.04x
🐘 PostgresExpress1.568s (+1.4%)2.010s (~)0.005s (-4.8%)2.027s (~)0.459s301.04x
🐘 PostgresNitro1.577s (-1.8%)2.003s (~)0.005s (~)2.027s (~)0.450s301.04x
🐘 PostgresNext.js (Turbopack)2.009s (+30.4% 🔺)2.447s (+21.7% 🔺)0.004s (-20.0% 🟢)2.478s (+22.3% 🔺)0.469s251.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.025s (+1.4%)7.125s (-3.4%)0.324s (+1.9%)7.978s (-3.4%)1.953s81.00x
▲ VercelExpress6.435s (+9.3% 🔺)7.891s (+9.5% 🔺)0.226s (+4.4%)8.797s (+11.6% 🔺)2.363s71.07x
▲ VercelNitro6.500s (+7.0% 🔺)7.678s (+8.5% 🔺)0.459s (+102.8% 🔺)8.656s (+10.6% 🔺)2.156s71.08x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.782s (+1.0%)1.071s (+2.2%)0.000s (+Infinity% 🔺)1.088s (+2.3%)0.307s561.00x
🐘 PostgresNitro0.821s (~)1.062s (-3.3%)0.000s (-50.9% 🟢)1.092s (-2.4%)0.271s551.05x
🐘 PostgresNext.js (Turbopack)1.037s (+13.1% 🔺)1.437s (+12.6% 🔺)0.000s (+14.6% 🔺)1.473s (+14.7% 🔺)0.436s411.33x
💻 LocalExpress1.209s (-4.7%)1.949s (-3.2%)0.000s (+214.5% 🔺)1.951s (-3.2%)0.743s311.55x
💻 LocalNitro1.280s (-2.9%)1.981s (~)0.001s (+41.7% 🔺)1.984s (~)0.704s311.64x
💻 LocalNext.js (Turbopack)1.411s (+5.7% 🔺)1.980s (~)0.001s (+183.3% 🔺)2.016s (~)0.605s301.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.353s (+7.5% 🔺)4.326s (-1.9%)0.000s (+Infinity% 🔺)4.782s (-1.9%)1.430s131.00x
▲ VercelExpress3.737s (-4.4%)5.137s (+2.2%)0.000s (-100.0% 🟢)5.715s (+3.2%)1.978s111.11x
▲ VercelNext.js (Turbopack)4.240s (+28.8% 🔺)5.616s (+19.4% 🔺)0.000s (+20.0% 🔺)6.124s (+16.5% 🔺)1.884s101.26x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.605s (-5.3% 🟢)2.171s (-3.7%)0.000s (-100.0% 🟢)2.211s (-2.6%)0.607s281.00x
🐘 PostgresNitro1.774s (-7.5% 🟢)2.339s (-6.3% 🟢)0.000s (-100.0% 🟢)2.355s (-6.6% 🟢)0.581s261.11x
🐘 PostgresNext.js (Turbopack)2.353s (+15.8% 🔺)2.908s (+11.3% 🔺)0.000s (-100.0% 🟢)2.925s (+10.7% 🔺)0.572s211.47x
💻 LocalExpress3.387s (-6.8% 🟢)4.027s (-4.8%)0.001s (+50.0% 🔺)4.031s (-4.7%)0.644s152.11x
💻 LocalNext.js (Turbopack)3.389s (-7.5% 🟢)3.869s (-7.7% 🟢)0.001s (-12.1% 🟢)3.912s (-7.6% 🟢)0.523s162.11x
💻 LocalNitro3.475s (-2.5%)4.160s (+3.3%)0.000s (-58.8% 🟢)4.163s (+3.3%)0.689s152.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.911s (+27.6% 🔺)7.295s (+21.2% 🔺)0.001s (+150.0% 🔺)7.812s (+18.7% 🔺)1.901s81.00x
▲ VercelExpress6.192s (+30.8% 🔺)7.470s (+30.1% 🔺)0.000s (+25.0% 🔺)8.018s (+28.2% 🔺)1.826s81.05x
▲ VercelNitro6.213s (+27.0% 🔺)7.485s (+30.9% 🔺)0.000s (NaN%)7.991s (+29.2% 🔺)1.778s81.05x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress18/21
🐘 PostgresExpress11/21
▲ VercelExpress8/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres15/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

Enabling H2 on the single shared dispatcher hung the queue's webhook
respondWith mechanism (120s E2E timeouts on webhook/hook workflows
across frameworks). Instead, give the v4 events API its own
H2-enabled dispatcher (getEventsDispatcher) and keep the default
dispatcher — queue/webhook, v3 makeRequest, streaming, deployment
resolution, run-key fetch — on HTTP/1.1.
The events endpoints are the hottest path (an event write per step
transition plus log reads) and are plain request/response (or a
streamed LIST response), none of which trip the webhook/duplex H2
issues, so they get multiplexing safely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SvelteKit bundles the server (including undici via the world adapter) into
ESM output. undici loads node: builtins as ESM imports except node:http2,
which it pulls in lazily via a bare require('node:http2') inside a try/catch.
The bundler leaves that bare require un-wired, so in the ESM bundle it throws
and undici silently falls back to a stub whose http2.connect is undefined —
breaking every HTTP/2 request (the workflow flow-route callback fails with
"fetch failed", so runs never start).
Inject a createRequire-backed global require into the SSR build only (a
node:module import would break the client bundle) so the real node:http2
resolves. Detected via configResolved.build.ssr because SvelteKit does not
set env.isSsrBuild for its server pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VaguelySeriousand others added 2 commits June 23, 2026 17:32
Nitro bundles undici (via the world adapter) into the ESM server output.
undici loads node: builtins as ESM imports except node:http2, which it pulls
in lazily via a bare require('node:http2') inside a try/catch. The bundler
leaves that bare require un-wired, so in the ESM bundle it throws and undici
silently falls back to a stub whose http2.connect is undefined — breaking
every HTTP/2 request (the workflow flow-route callback fails with "fetch
failed", so runs never start). This affects all nitro-based builds including
the Vite and TanStack Start framework integrations.
Prepend a createRequire-backed global require to the server chunks (via the
rollup:before hook, production builds only) so the real node:http2 resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream writes (write/writeMulti/close) previously used plain fetch() on
HTTP/1.1. Route them through a dedicated H2 stream dispatcher: the PUT
requests send a fully-buffered body (or none), so they gain H2 multiplexing
without the duplex-streaming issues that keep the long-lived live-read (GET)
on plain fetch. The stream dispatcher is a bare undici Agent without the
RetryAgent wrapper — stream appends are not idempotent, so undici-level retry
is deliberately omitted to avoid duplicating chunks (preserving the streamer's
original no-retry behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
headers: httpConfig.headers,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- undici dispatcher type doesn't match @types/node's RequestInit
dispatcher: getStreamDispatcher(config),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] I would block this until the Vercel Nitro v3 benchmark is rerun green or root-caused. The current Benchmark Vercel (nitro-v3) job fails with Stream 1 correctness failure: expected 1048576 bytes, got 983040, exactly one 64 KiB chunk missing from the stream benchmark. Since this PR changes stream PUTs to use the H2 dispatcher here, this looks like a plausible stream-write correctness regression rather than a pure perf failure. Job: https://github.com/vercel/workflow/actions/runs/28066956084/job/83093680965

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passed on re-run

* getDefaultStreamDispatcher for why this is a bare Agent (no retry).
*/
export function getStreamDispatcher(config?: APIConfig): unknown {
return config?.dispatcher ?? getDefaultStreamDispatcher();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This keeps the default stream dispatcher retry-free, but config.dispatcher now also controls stream writes. A caller that passes an undici RetryAgent can re-enable retries for stream PUTs; undici retries PUT by default, and stream appends are not idempotent, so that can duplicate chunks. Can we document this in the custom dispatcher section, or split out a stream-specific dispatcher option / guard so retrying dispatchers are not used for stream writes?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Good catch. Rather than leave stream writes retry-free, I scoped the retry policy so the win (resilience to transient blips) survives without the duplicate-chunk risk:

  • The default stream dispatcher is now a RetryAgent with STREAM_RETRY_OPTIONS = { methods: ['PUT'], statusCodes: [429], retryAfter: true }. It retries only on transient connection errors (undici's default errorCodes) and HTTP 429 — both guarantee the chunk was not persisted. 5xx is excluded (a 5xx can mean the chunk was written but the response failed → retry would duplicate), as are other 4xx.
  • For the caller-override case you flagged: documented in the dispatcher JSDoc (APIConfig in utils.ts) that a custom dispatcher replaces every default including stream writes, and should not retry PUT on 5xx.

Added a regression test asserting statusCodes is exactly [429] and never contains 500/502/503/504.

*/
function addNodeRequireBanner(config: RollupConfig): void {
const banner =
"import { createRequire as __wkfCreateRequire } from 'node:module'; if (typeof require === 'undefined') { globalThis.require = __wkfCreateRequire(import.meta.url); }";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This fixes the bundled undici node:http2 issue, but it also globally changes server-bundle feature detection: dependencies that branch on typeof require will now take their CJS/require path in these ESM bundles. The tested Vercel Node lanes passing is reassuring, but can we call this out as a Node-runtime-only server-bundle shim and keep an eye on optional-require behavior in bundled libraries?

@VaguelySeriousVaguelySeriousJun 24, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Agreed — that global feature-detection change is the right thing to flag. I kept the global globalThis.require (it's required: undici's bundled bare require(...) resolves against the runtime global scope, so a module-local const require wouldn't reliably be in scope for the inlined undici code), but tightened the JSDoc in both nitro/index.ts and sveltekit/plugin.ts to call it out explicitly as a Node-server-runtime-only shim and document the side effect + why it's safe:

  • Server-only — gated to the production server build (!nitro.options.dev / config.build.ssr); never touches the client/browser bundle.
  • Guarded — the typeof require === 'undefined' check makes it a no-op in CJS chunks where a real require already exists; it only fills a genuine ESM gap.
  • Functional — the require we install is a working createRequire, so a lib that switches to the require path gets a real require, not a broken stub (the common try { require(optional) } catch {} pattern still degrades gracefully).

The comment also records the one residual behavior to watch, per your ask: a bundled lib that, on detecting require, does require() of an ESM-only dependency on a Node version without require(ESM) support. The Vercel Node E2E lanes exercise the real undici path that motivated this. Comment-only change in b2ec9d7.

… 429
Stream writes (PUT) append chunks and are not idempotent. The stream
dispatcher was a bare Agent with no retry; switch it to a RetryAgent whose
policy is narrowed so PUTs retry only on transient connection errors and
HTTP 429 — both of which guarantee the chunk was not persisted — and never
on 5xx (where the chunk may already have been written) or other 4xx.
Also document that a caller-supplied `config.dispatcher` overrides the
stream dispatcher and should not retry PUT on 5xx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/world-vercel/src/streamer.ts Outdated
vercelBotand others added 2 commits June 24, 2026 17:34
…nt (no retry)" / "intentionally retry-free", contradicting the actual `RetryAgent` + `STREAM_RETRY_OPTIONS` implementation in a non-idempotent write path.
This commit fixes the issue reported at packages/world-vercel/src/streamer.ts:26
## The issue
`getDefaultStreamDispatcher` in `packages/world-vercel/src/http-client.ts` constructs a `RetryAgent` wrapping `STREAM_RETRY_OPTIONS` (`retryAfter: true`, `methods: ['PUT']`, `statusCodes: [429]`). So the stream-write dispatcher **does** retry — on undici's default transient connection errors and on HTTP 429.
Two comments still claimed the opposite:
1. `packages/world-vercel/src/streamer.ts` (~line 26):
> "That stream dispatcher is intentionally retry-free — stream appends aren't idempotent, so a retried write would duplicate chunks."
2. `packages/world-vercel/src/http-client.ts` (~line 102, JSDoc on `getStreamDispatcher`):
> "See getDefaultStreamDispatcher for why this is a bare Agent (no retry)."
Both are factually wrong: the dispatcher is a `RetryAgent`, not a bare `Agent`, and it is not retry-free. The accurate, nuanced description already lives in the `STREAM_RETRY_OPTIONS` and `getDefaultStreamDispatcher` JSDoc — these two comments were simply not updated when the behavior changed.
## Why it matters
This is the **non-idempotent write path**. A maintainer reasoning about correctness here relies on these comments to understand exactly what is and isn't retried. "Retry-free" vs. "retries on transient errors + 429 but never 5xx" is a meaningful safety distinction, so the stale wording actively misleads.
## The fix
Comment-only changes (no behavior change):
- **streamer.ts**: replaced "intentionally retry-free" with wording reflecting the narrowed policy — retries only on transient connection errors + HTTP 429 (both guaranteeing the chunk was never persisted), never 5xx, so a retry can't duplicate an already-applied write. Points to `STREAM_RETRY_OPTIONS`.
- **http-client.ts**: replaced "why this is a bare Agent (no retry)" with a reference to the deliberately narrowed retry policy in `getDefaultStreamDispatcher` / `STREAM_RETRY_OPTIONS`.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
…ide effect
Document that the createRequire banner deliberately makes `typeof require`
truthy for every bundled dependency in the ESM server output, why that is
safe (server-only, guarded, functional require), and the one residual
behavior to watch (require() of an ESM-only dep on old Node).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2615. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all \x3Cpre>\x3Ccode> blocks (function() { function addCopyButtons() { document.querySelectorAll('pre code').forEach(function(codeBlock) { if (codeBlock.parentElement.hasAttribute('data-copy-added')) return; codeBlock.parentElement.setAttribute('data-copy-added', 'true'); var btn = document.createElement('button'); btn.textContent = 'Copy'; btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;'; btn.onmouseover = function() { this.style.opacity = '1'; }; btn.onmouseout = function() { this.style.opacity = '0.7'; }; btn.onclick = function() { navigator.clipboard.writeText(codeBlock.textContent).then(function() { btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy'; }, 1500); }); }; codeBlock.parentElement.style.position = 'relative'; codeBlock.parentElement.appendChild(btn); }); } addCopyButtons(); // Re-run on dynamic content var observer = new MutationObserver(addCopyButtons); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + ' [world-vercel] Enable HTTP/2 for the events API and stream writes by VaguelySerious · Pull Request #2573 · vercel/workflow · GitHub
Skip to content

[world-vercel] Enable HTTP/2 for the events API and stream writes - #2573

Merged
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2
Jun 24, 2026
Merged

[world-vercel] Enable HTTP/2 for the events API and stream writes#2573
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

Enable HTTP/2 for two hot @workflow/world-vercel request paths, and fix the framework bundling that previously made H2 impossible on Vite-based adapters.

  • v4 events API (events-v4.ts) — an event write per step transition plus event-log reads on replay. Gets its own H2 undici dispatcher (getEventsDispatcher).
  • Stream writes (streamer.tswrite/writeMulti/close — the PUT path) — routed through a dedicated H2 stream dispatcher (getStreamDispatcher). These send a fully-buffered body (or none), so they get H2 multiplexing without the duplex issues that keep the long-lived live-read (GET) on plain fetch. The stream dispatcher is a bare Agentwithout the RetryAgent wrapper: stream appends aren't idempotent, so undici-level retry is deliberately omitted to avoid duplicating chunks.

Everything else stays on HTTP/1.1 (queue client / webhook respondWith, v3 makeRequest, deployment resolution, run-key fetch, live-read GET, list).

The bundling fix (why H2 was blocked before)

H2 had been disabled because it "broke SvelteKit on Vercel prod." Root cause: Vite/Rollup-based framework builds (SvelteKit, TanStack Start, plain Vite) bundle undici into the ESM server output. undici imports most node: builtins as ESM, but loads node:http2 lazily via a bare require('node:http2') inside a try/catch. The bundler leaves that bare require un-wired, so in the ESM bundle it throws and undici silently falls back to a stub whose http2.connect is undefined. Any H2 request then fails with fetch failed → the workflow flow-route callback can't reach the backend → runs never leave pending → 30-minute E2E timeouts. Frameworks that externalize undici (Express, Astro, Next, Hono, Nitro, …) were unaffected.

Fix: inject a createRequire-backed global require into the server build (server-only — a node:module import would break the client bundle) so the real node:http2 resolves:

  • @workflow/sveltekit — via configResolved (SvelteKit doesn't set env.isSsrBuild for its server pass; build.ssr on the resolved config is the reliable signal).
  • @workflow/nitro — via the existing rollup:before hook, covering Vite, TanStack Start, and Nitro from one place.

Verified end-to-end: with the fix, the sveltekit / vite / tanstack-start E2E Vercel Prod lanes pass, and http2.connect is not a function errors drop to zero (previously deterministic on this branch only).

Why the default dispatcher stays H1

Enabling H2 on the shared dispatcher deadlocks the queue's webhook respondWith mechanism (120s timeouts on the webhook/hook workflows). So H2 is scoped to the events + stream-write paths, which don't use that mechanism.

Tests

  • Regression guards: EVENTS_AGENT_OPTIONS.allowH2 === trueandDEFAULT_AGENT_OPTIONS.allowH2 === false.
  • getEventsDispatcher and getStreamDispatcher each return their own shared singleton — distinct from the default and from each other — and honor a caller-supplied dispatcher.
  • Local node:http2 TLS server test: a request through the global fetch with an allowH2 dispatcher negotiates h2 (the mechanism these paths rely on).
  • Full @workflow/world-vercel unit suite passes (172 tests).

Notes

  • events-v4.ts and the stream writes stay on the Vercel-patched global fetch (preserves observability instrumentation); only the dispatcher they pass changes.
  • The sleepingWorkflow and distributedAbortController failures seen on some lanes are pre-existing flakes (zero-tolerance server-side sleep timing / abort timeout) that also fail on main — unrelated to this change.

🤖 Generated with Claude Code

Flip allowH2 to true in the shared undici dispatcher so every
dispatcher-backed path (v3 makeRequest, v4 events, the queue client,
deployment resolution, run-key fetch) negotiates HTTP/2. H2 was
previously disabled for a SvelteKit-on-Vercel-prod hang; re-enable it
globally and rely on the E2E suite to guard the regression. Streaming
paths in streamer.ts intentionally bypass the dispatcher and are
unaffected.
Adds a regression guard on the agent options plus a local http2 server
test proving global fetch honors an allowH2 dispatcher (the mechanism
the v4 events path relies on).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJune 22, 2026 21:51
@vercel

vercelBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2ec9d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/world-vercelPatch
@workflow/nuxtPatch
workflowPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7282010788360

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ nitro-stable126026
✅ nuxt-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.045s (-5.3% 🟢)1.006s (~)0.961s101.00x
💻 LocalNitro0.047s (+0.6%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.050s (-4.2%)1.006s (~)0.955s101.12x
🐘 PostgresNitro0.065s (-0.6%)1.012s (~)0.947s101.44x
🐘 PostgresExpress0.066s (-4.5%)1.013s (~)0.946s101.47x
🐘 PostgresNext.js (Turbopack)0.067s (+20.8% 🔺)1.028s (+1.6%)0.962s101.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.232s (~)1.976s (-6.1% 🟢)1.744s101.00x
▲ VercelNext.js (Turbopack)0.260s (-3.8%)2.042s (~)1.782s101.12x
▲ VercelNitro0.301s (+19.7% 🔺)2.042s (-5.9% 🟢)1.741s101.30x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.078s (-0.8%)2.006s (~)0.928s101.00x
💻 LocalNext.js (Turbopack)1.079s (~)2.006s (~)0.927s101.00x
🐘 PostgresNext.js (Turbopack)1.079s (~)2.009s (~)0.930s101.00x
💻 LocalNitro1.082s (~)2.007s (~)0.925s101.00x
🐘 PostgresExpress1.097s (~)2.009s (~)0.912s101.02x
🐘 PostgresNitro1.100s (~)2.009s (~)0.909s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.421s (-0.9%)3.085s (-8.0% 🟢)1.664s101.00x
▲ VercelExpress1.445s (+4.5%)3.251s (+14.9% 🔺)1.806s101.02x
▲ VercelNitro1.468s (+4.0%)3.195s (~)1.727s101.03x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)10.420s (~)11.026s (~)0.606s31.00x
💻 LocalExpress10.435s (~)11.023s (~)0.588s31.00x
💻 LocalNitro10.455s (~)11.024s (~)0.569s31.00x
🐘 PostgresExpress10.460s (~)11.021s (~)0.560s31.00x
🐘 PostgresNitro10.476s (~)11.017s (~)0.541s31.01x
💻 LocalNext.js (Turbopack)10.504s (~)11.022s (~)0.518s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.937s (+1.8%)13.411s (~)1.473s31.00x
▲ VercelNext.js (Turbopack)12.028s (+1.9%)13.455s (-3.5%)1.427s31.01x
▲ VercelExpress12.325s (~)14.494s (+4.5%)2.169s31.03x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.554s (~)14.028s (~)0.473s51.00x
🐘 PostgresExpress13.559s (~)14.020s (~)0.461s51.00x
💻 LocalNext.js (Turbopack)13.625s (~)14.028s (~)0.403s51.01x
💻 LocalNitro13.734s (+0.7%)14.030s (~)0.295s51.01x
🐘 PostgresNext.js (Turbopack)13.750s (+0.6%)14.245s (+1.6%)0.495s51.01x
🐘 PostgresNitro13.773s (~)14.023s (~)0.250s51.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express18.265s (+11.1% 🔺)20.019s (+11.2% 🔺)1.754s31.00x
▲ VercelNext.js (Turbopack)18.771s (+8.2% 🔺)20.473s (+6.9% 🔺)1.702s41.03x
▲ VercelNitro19.188s (+17.2% 🔺)20.559s (+16.0% 🔺)1.372s31.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express12.044s (-2.4%)12.774s (-1.9%)0.730s81.00x
🐘 PostgresNext.js (Turbopack)12.160s (~)12.869s (-1.1%)0.709s71.01x
🐘 PostgresExpress12.263s (+1.3%)13.019s (~)0.757s71.02x
💻 LocalNitro12.272s (+0.7%)13.025s (~)0.753s71.02x
🐘 PostgresNitro12.273s (~)13.018s (~)0.746s71.02x
💻 LocalNext.js (Turbopack)12.414s (+1.3%)13.026s (~)0.612s71.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)22.253s (+15.0% 🔺)24.838s (+17.0% 🔺)2.586s41.00x
▲ VercelExpress22.303s (+21.4% 🔺)25.182s (+24.9% 🔺)2.879s41.00x
▲ VercelNitro22.749s (+19.8% 🔺)25.174s (+22.0% 🔺)2.425s41.02x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.172s (~)2.007s (~)0.835s151.00x
🐘 PostgresNitro1.192s (+0.9%)2.008s (~)0.815s151.02x
🐘 PostgresNext.js (Turbopack)1.215s (+3.2%)2.009s (~)0.794s151.04x
💻 LocalExpress1.351s (-2.7%)2.006s (~)0.655s151.15x
💻 LocalNitro1.423s (-1.1%)2.007s (~)0.584s151.21x
💻 LocalNext.js (Turbopack)1.444s (-3.6%)2.007s (~)0.563s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.284s (+12.5% 🔺)3.862s (+10.3% 🔺)1.578s81.00x
▲ VercelNitro2.364s (+11.0% 🔺)3.813s (+10.7% 🔺)1.448s81.04x
▲ VercelNext.js (Turbopack)2.432s (+13.1% 🔺)3.659s (-3.4%)1.228s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.302s (-2.8%)2.317s (-7.6% 🟢)1.015s131.00x
🐘 PostgresExpress1.332s (-4.4%)2.223s (-16.9% 🟢)0.891s141.02x
🐘 PostgresNext.js (Turbopack)1.468s (+11.8% 🔺)2.685s (-8.0% 🟢)1.217s121.13x
💻 LocalExpress2.446s (+3.7%)2.826s (-3.1%)0.381s111.88x
💻 LocalNext.js (Turbopack)2.530s (-7.5% 🟢)3.008s (-3.2%)0.477s101.94x
💻 LocalNitro2.671s (+10.9% 🔺)3.110s (+6.5% 🔺)0.439s102.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.780s (-9.7% 🟢)4.586s (-11.5% 🟢)1.806s71.00x
▲ VercelExpress3.603s (-0.9%)5.649s (+9.4% 🔺)2.047s61.30x
▲ VercelNext.js (Turbopack)4.146s (+24.1% 🔺)5.917s (+17.3% 🔺)1.772s61.49x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (~)4.135s (-3.9%)2.553s81.00x
🐘 PostgresNitro1.627s (-0.8%)4.136s (-3.0%)2.508s81.03x
🐘 PostgresNext.js (Turbopack)2.703s (-4.0%)4.582s (-16.9% 🟢)1.879s71.71x
💻 LocalNitro6.077s (-5.7% 🟢)6.818s (-5.5% 🟢)0.741s53.84x
💻 LocalExpress6.158s (-2.6%)6.819s (-2.8%)0.661s53.89x
💻 LocalNext.js (Turbopack)7.337s (+34.1% 🔺)8.017s (+33.3% 🔺)0.680s44.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.321s (+24.1% 🔺)6.144s (+18.1% 🔺)1.824s51.00x
▲ VercelNitro5.108s (+52.3% 🔺)6.685s (+30.6% 🔺)1.578s51.18x
▲ VercelExpress5.501s (+79.5% 🔺)7.636s (+51.4% 🔺)2.135s51.27x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.189s (+0.9%)2.007s (~)0.819s151.00x
🐘 PostgresNitro1.203s (~)2.009s (~)0.806s151.01x
🐘 PostgresNext.js (Turbopack)1.225s (+4.7%)2.015s (~)0.790s151.03x
💻 LocalExpress1.415s (-1.6%)2.007s (~)0.592s151.19x
💻 LocalNext.js (Turbopack)1.455s (+3.7%)2.006s (~)0.552s151.22x
💻 LocalNitro1.472s (+3.5%)2.007s (~)0.535s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.605s (+26.2% 🔺)4.403s (+25.9% 🔺)1.799s71.00x
▲ VercelNext.js (Turbopack)2.633s (-54.5% 🟢)4.124s (-44.6% 🟢)1.491s81.01x
▲ VercelNitro2.750s (+28.3% 🔺)4.412s (+18.7% 🔺)1.661s71.06x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.318s (-4.0%)2.393s (-3.2%)1.075s131.00x
🐘 PostgresExpress1.401s (+4.5%)2.676s (+8.3% 🔺)1.274s121.06x
🐘 PostgresNext.js (Turbopack)1.487s (+15.3% 🔺)2.515s (-13.8% 🟢)1.028s121.13x
💻 LocalNitro2.433s (-5.8% 🟢)3.009s (-3.2%)0.576s101.85x
💻 LocalNext.js (Turbopack)2.587s (-1.9%)3.109s (+3.3%)0.522s101.96x
💻 LocalExpress2.643s (+9.2% 🔺)3.008s (-3.2%)0.365s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.822s (-41.1% 🟢)5.218s (-21.4% 🟢)2.396s61.00x
▲ VercelNitro3.363s (+23.9% 🔺)5.021s (+16.3% 🔺)1.658s71.19x
▲ VercelNext.js (Turbopack)3.396s (-28.4% 🟢)5.116s (-26.4% 🟢)1.721s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.599s (-3.7%)4.138s (+3.1%)2.539s81.00x
🐘 PostgresExpress1.658s (+4.6%)4.011s (-3.0%)2.353s81.04x
🐘 PostgresNext.js (Turbopack)2.514s (-12.1% 🟢)5.041s (-13.8% 🟢)2.527s61.57x
💻 LocalExpress5.944s (-11.3% 🟢)6.415s (-11.1% 🟢)0.472s53.72x
💻 LocalNitro6.441s (-5.0% 🟢)7.015s (-2.8%)0.574s54.03x
💻 LocalNext.js (Turbopack)7.009s (+22.5% 🔺)8.020s (+33.3% 🔺)1.010s44.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.521s (+1.9%)5.720s (+13.6% 🔺)2.199s61.00x
▲ VercelNext.js (Turbopack)4.714s (+50.9% 🔺)6.326s (+22.5% 🔺)1.611s51.34x
▲ VercelExpress4.774s (+1.8%)7.280s (+15.7% 🔺)2.506s51.36x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.546s (+6.9% 🔺)1.027s (+2.1%)0.481s591.00x
🐘 PostgresExpress0.557s (+4.5%)1.041s (+1.7%)0.483s581.02x
🐘 PostgresNitro0.563s (-5.3% 🟢)1.024s (-1.7%)0.461s591.03x
💻 LocalExpress0.570s (~)1.005s (~)0.435s601.04x
💻 LocalNitro0.585s (~)1.005s (~)0.420s601.07x
💻 LocalNext.js (Turbopack)0.673s (+11.5% 🔺)1.058s (+3.5%)0.385s571.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.301s (+24.8% 🔺)4.990s (+16.1% 🔺)1.689s131.00x
▲ VercelExpress3.342s (+19.4% 🔺)5.325s (+18.7% 🔺)1.983s121.01x
▲ VercelNext.js (Turbopack)3.380s (+11.2% 🔺)4.843s (-3.1%)1.463s131.02x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.237s (-1.1%)2.007s (~)0.770s451.00x
🐘 PostgresNitro1.333s (-5.7% 🟢)2.030s (-1.2%)0.696s451.08x
💻 LocalExpress1.425s (-2.1%)2.006s (~)0.581s451.15x
💻 LocalNext.js (Turbopack)1.474s (~)2.006s (~)0.532s451.19x
🐘 PostgresNext.js (Turbopack)1.500s (+20.5% 🔺)2.105s (+3.7%)0.605s431.21x
💻 LocalNitro1.565s (+5.5% 🔺)2.052s (+2.3%)0.488s441.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.650s (+19.6% 🔺)9.459s (+21.8% 🔺)1.808s101.00x
▲ VercelExpress7.692s (+19.0% 🔺)9.613s (+19.6% 🔺)1.921s101.01x
▲ VercelNext.js (Turbopack)9.201s (+28.8% 🔺)11.021s (+24.0% 🔺)1.820s91.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.481s (-4.4%)3.033s (-1.7%)0.552s401.00x
🐘 PostgresNitro2.669s (-1.7%)3.086s (+0.9%)0.417s391.08x
💻 LocalExpress3.122s (-3.8%)3.821s (-3.9%)0.699s321.26x
💻 LocalNitro3.154s (-2.0%)3.978s (-0.8%)0.824s311.27x
🐘 PostgresNext.js (Turbopack)3.207s (+29.5% 🔺)3.749s (+22.6% 🔺)0.542s331.29x
💻 LocalNext.js (Turbopack)3.281s (+0.6%)3.977s (-1.6%)0.696s311.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express15.239s (+10.5% 🔺)17.918s (+15.9% 🔺)2.679s71.00x
▲ VercelNitro15.788s (+12.3% 🔺)18.302s (+17.1% 🔺)2.515s71.04x
▲ VercelNext.js (Turbopack)18.176s (+10.8% 🔺)20.378s (+10.2% 🔺)2.202s61.19x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.197s (-6.3% 🟢)1.006s (~)0.809s601.00x
🐘 PostgresNext.js (Turbopack)0.213s (+27.2% 🔺)1.010s (~)0.797s601.08x
🐘 PostgresNitro0.222s (-2.1%)1.007s (~)0.784s601.13x
💻 LocalExpress0.463s (+7.4% 🔺)1.005s (~)0.541s602.35x
💻 LocalNitro0.465s (-0.7%)1.004s (~)0.539s602.36x
💻 LocalNext.js (Turbopack)0.613s (+5.0%)1.005s (~)0.391s603.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.257s (-42.1% 🟢)2.609s (-32.7% 🟢)1.352s231.00x
▲ VercelExpress1.375s (-33.7% 🟢)3.160s (-17.7% 🟢)1.785s191.09x
▲ VercelNext.js (Turbopack)1.468s (-19.3% 🟢)2.862s (-19.3% 🟢)1.395s211.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.314s (-2.7%)1.006s (-1.1%)0.692s901.00x
🐘 PostgresNitro0.336s (~)1.006s (~)0.671s901.07x
🐘 PostgresNext.js (Turbopack)0.347s (+29.3% 🔺)1.017s (+1.1%)0.670s891.10x
💻 LocalExpress2.137s (-1.1%)2.735s (~)0.598s336.80x
💻 LocalNitro2.192s (+2.8%)2.796s (+4.1%)0.604s336.97x
💻 LocalNext.js (Turbopack)2.734s (-2.0%)3.295s (+1.1%)0.560s288.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.855s (-2.7%)3.708s (+6.3% 🔺)1.853s251.00x
▲ VercelExpress2.119s (-14.0% 🟢)4.166s (+2.8%)2.047s221.14x
▲ VercelNext.js (Turbopack)2.680s (+23.5% 🔺)4.477s (+14.0% 🔺)1.797s211.44x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.531s (+0.5%)1.050s (+1.7%)0.519s1151.00x
🐘 PostgresExpress0.567s (+9.8% 🔺)1.160s (+8.6% 🔺)0.594s1041.07x
🐘 PostgresNext.js (Turbopack)0.699s (+48.1% 🔺)1.975s (-34.3% 🟢)1.277s611.32x
💻 LocalExpress9.304s (-7.8% 🟢)10.360s (-6.9% 🟢)1.056s1217.52x
💻 LocalNext.js (Turbopack)9.579s (-2.1%)10.530s (-2.3%)0.951s1218.04x
💻 LocalNitro10.363s (+4.5%)11.302s (+2.5%)0.940s1119.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.326s (+39.1% 🔺)5.768s (+42.3% 🔺)2.443s211.00x
▲ VercelNitro3.641s (+43.2% 🔺)5.915s (+43.4% 🔺)2.274s211.09x
▲ VercelNext.js (Turbopack)4.725s (+0.8%)7.116s (+8.7% 🔺)2.391s171.42x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.137s (-2.7%)1.968s (~)0.011s (-14.5% 🟢)2.018s (~)0.881s101.00x
💻 LocalExpress1.151s (~)2.005s (~)0.010s (-17.5% 🟢)2.017s (~)0.866s101.01x
🐘 PostgresNitro1.156s (-0.9%)1.999s (~)0.001s (+18.2% 🔺)2.010s (~)0.854s101.02x
💻 LocalNitro1.159s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.862s101.02x
🐘 PostgresExpress1.179s (+2.6%)2.000s (~)0.001s (+10.0% 🔺)2.011s (~)0.831s101.04x
🐘 PostgresNext.js (Turbopack)1.209s (+5.5% 🔺)1.996s (~)0.001s (-27.3% 🟢)2.009s (~)0.800s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.129s (+4.0%)3.553s (+15.8% 🔺)2.488s (+42.0% 🔺)6.571s (+21.6% 🔺)4.441s101.00x
▲ VercelNext.js (Turbopack)2.206s (+6.8% 🔺)3.312s (-4.0%)2.584s (+83.3% 🔺)6.338s (+17.2% 🔺)4.132s101.04x
▲ VercelNitro2.374s (+7.0% 🔺)3.275s (~)2.368s (+27.1% 🔺)6.269s (+10.1% 🔺)3.894s101.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.509s (-2.5%)2.010s (~)0.013s (-1.3%)2.026s (~)0.517s301.00x
💻 LocalNext.js (Turbopack)1.563s (-0.9%)1.971s (~)0.012s (-5.4% 🟢)2.025s (~)0.462s301.04x
💻 LocalNitro1.564s (+0.7%)2.010s (~)0.013s (+9.4% 🔺)2.026s (~)0.462s301.04x
🐘 PostgresExpress1.568s (+1.4%)2.010s (~)0.005s (-4.8%)2.027s (~)0.459s301.04x
🐘 PostgresNitro1.577s (-1.8%)2.003s (~)0.005s (~)2.027s (~)0.450s301.04x
🐘 PostgresNext.js (Turbopack)2.009s (+30.4% 🔺)2.447s (+21.7% 🔺)0.004s (-20.0% 🟢)2.478s (+22.3% 🔺)0.469s251.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.025s (+1.4%)7.125s (-3.4%)0.324s (+1.9%)7.978s (-3.4%)1.953s81.00x
▲ VercelExpress6.435s (+9.3% 🔺)7.891s (+9.5% 🔺)0.226s (+4.4%)8.797s (+11.6% 🔺)2.363s71.07x
▲ VercelNitro6.500s (+7.0% 🔺)7.678s (+8.5% 🔺)0.459s (+102.8% 🔺)8.656s (+10.6% 🔺)2.156s71.08x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.782s (+1.0%)1.071s (+2.2%)0.000s (+Infinity% 🔺)1.088s (+2.3%)0.307s561.00x
🐘 PostgresNitro0.821s (~)1.062s (-3.3%)0.000s (-50.9% 🟢)1.092s (-2.4%)0.271s551.05x
🐘 PostgresNext.js (Turbopack)1.037s (+13.1% 🔺)1.437s (+12.6% 🔺)0.000s (+14.6% 🔺)1.473s (+14.7% 🔺)0.436s411.33x
💻 LocalExpress1.209s (-4.7%)1.949s (-3.2%)0.000s (+214.5% 🔺)1.951s (-3.2%)0.743s311.55x
💻 LocalNitro1.280s (-2.9%)1.981s (~)0.001s (+41.7% 🔺)1.984s (~)0.704s311.64x
💻 LocalNext.js (Turbopack)1.411s (+5.7% 🔺)1.980s (~)0.001s (+183.3% 🔺)2.016s (~)0.605s301.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.353s (+7.5% 🔺)4.326s (-1.9%)0.000s (+Infinity% 🔺)4.782s (-1.9%)1.430s131.00x
▲ VercelExpress3.737s (-4.4%)5.137s (+2.2%)0.000s (-100.0% 🟢)5.715s (+3.2%)1.978s111.11x
▲ VercelNext.js (Turbopack)4.240s (+28.8% 🔺)5.616s (+19.4% 🔺)0.000s (+20.0% 🔺)6.124s (+16.5% 🔺)1.884s101.26x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.605s (-5.3% 🟢)2.171s (-3.7%)0.000s (-100.0% 🟢)2.211s (-2.6%)0.607s281.00x
🐘 PostgresNitro1.774s (-7.5% 🟢)2.339s (-6.3% 🟢)0.000s (-100.0% 🟢)2.355s (-6.6% 🟢)0.581s261.11x
🐘 PostgresNext.js (Turbopack)2.353s (+15.8% 🔺)2.908s (+11.3% 🔺)0.000s (-100.0% 🟢)2.925s (+10.7% 🔺)0.572s211.47x
💻 LocalExpress3.387s (-6.8% 🟢)4.027s (-4.8%)0.001s (+50.0% 🔺)4.031s (-4.7%)0.644s152.11x
💻 LocalNext.js (Turbopack)3.389s (-7.5% 🟢)3.869s (-7.7% 🟢)0.001s (-12.1% 🟢)3.912s (-7.6% 🟢)0.523s162.11x
💻 LocalNitro3.475s (-2.5%)4.160s (+3.3%)0.000s (-58.8% 🟢)4.163s (+3.3%)0.689s152.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.911s (+27.6% 🔺)7.295s (+21.2% 🔺)0.001s (+150.0% 🔺)7.812s (+18.7% 🔺)1.901s81.00x
▲ VercelExpress6.192s (+30.8% 🔺)7.470s (+30.1% 🔺)0.000s (+25.0% 🔺)8.018s (+28.2% 🔺)1.826s81.05x
▲ VercelNitro6.213s (+27.0% 🔺)7.485s (+30.9% 🔺)0.000s (NaN%)7.991s (+29.2% 🔺)1.778s81.05x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress18/21
🐘 PostgresExpress11/21
▲ VercelExpress8/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres15/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

Enabling H2 on the single shared dispatcher hung the queue's webhook
respondWith mechanism (120s E2E timeouts on webhook/hook workflows
across frameworks). Instead, give the v4 events API its own
H2-enabled dispatcher (getEventsDispatcher) and keep the default
dispatcher — queue/webhook, v3 makeRequest, streaming, deployment
resolution, run-key fetch — on HTTP/1.1.
The events endpoints are the hottest path (an event write per step
transition plus log reads) and are plain request/response (or a
streamed LIST response), none of which trip the webhook/duplex H2
issues, so they get multiplexing safely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SvelteKit bundles the server (including undici via the world adapter) into
ESM output. undici loads node: builtins as ESM imports except node:http2,
which it pulls in lazily via a bare require('node:http2') inside a try/catch.
The bundler leaves that bare require un-wired, so in the ESM bundle it throws
and undici silently falls back to a stub whose http2.connect is undefined —
breaking every HTTP/2 request (the workflow flow-route callback fails with
"fetch failed", so runs never start).
Inject a createRequire-backed global require into the SSR build only (a
node:module import would break the client bundle) so the real node:http2
resolves. Detected via configResolved.build.ssr because SvelteKit does not
set env.isSsrBuild for its server pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VaguelySeriousand others added 2 commits June 23, 2026 17:32
Nitro bundles undici (via the world adapter) into the ESM server output.
undici loads node: builtins as ESM imports except node:http2, which it pulls
in lazily via a bare require('node:http2') inside a try/catch. The bundler
leaves that bare require un-wired, so in the ESM bundle it throws and undici
silently falls back to a stub whose http2.connect is undefined — breaking
every HTTP/2 request (the workflow flow-route callback fails with "fetch
failed", so runs never start). This affects all nitro-based builds including
the Vite and TanStack Start framework integrations.
Prepend a createRequire-backed global require to the server chunks (via the
rollup:before hook, production builds only) so the real node:http2 resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream writes (write/writeMulti/close) previously used plain fetch() on
HTTP/1.1. Route them through a dedicated H2 stream dispatcher: the PUT
requests send a fully-buffered body (or none), so they gain H2 multiplexing
without the duplex-streaming issues that keep the long-lived live-read (GET)
on plain fetch. The stream dispatcher is a bare undici Agent without the
RetryAgent wrapper — stream appends are not idempotent, so undici-level retry
is deliberately omitted to avoid duplicating chunks (preserving the streamer's
original no-retry behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
headers: httpConfig.headers,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- undici dispatcher type doesn't match @types/node's RequestInit
dispatcher: getStreamDispatcher(config),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] I would block this until the Vercel Nitro v3 benchmark is rerun green or root-caused. The current Benchmark Vercel (nitro-v3) job fails with Stream 1 correctness failure: expected 1048576 bytes, got 983040, exactly one 64 KiB chunk missing from the stream benchmark. Since this PR changes stream PUTs to use the H2 dispatcher here, this looks like a plausible stream-write correctness regression rather than a pure perf failure. Job: https://github.com/vercel/workflow/actions/runs/28066956084/job/83093680965

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passed on re-run

* getDefaultStreamDispatcher for why this is a bare Agent (no retry).
*/
export function getStreamDispatcher(config?: APIConfig): unknown {
return config?.dispatcher ?? getDefaultStreamDispatcher();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This keeps the default stream dispatcher retry-free, but config.dispatcher now also controls stream writes. A caller that passes an undici RetryAgent can re-enable retries for stream PUTs; undici retries PUT by default, and stream appends are not idempotent, so that can duplicate chunks. Can we document this in the custom dispatcher section, or split out a stream-specific dispatcher option / guard so retrying dispatchers are not used for stream writes?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Good catch. Rather than leave stream writes retry-free, I scoped the retry policy so the win (resilience to transient blips) survives without the duplicate-chunk risk:

  • The default stream dispatcher is now a RetryAgent with STREAM_RETRY_OPTIONS = { methods: ['PUT'], statusCodes: [429], retryAfter: true }. It retries only on transient connection errors (undici's default errorCodes) and HTTP 429 — both guarantee the chunk was not persisted. 5xx is excluded (a 5xx can mean the chunk was written but the response failed → retry would duplicate), as are other 4xx.
  • For the caller-override case you flagged: documented in the dispatcher JSDoc (APIConfig in utils.ts) that a custom dispatcher replaces every default including stream writes, and should not retry PUT on 5xx.

Added a regression test asserting statusCodes is exactly [429] and never contains 500/502/503/504.

*/
function addNodeRequireBanner(config: RollupConfig): void {
const banner =
"import { createRequire as __wkfCreateRequire } from 'node:module'; if (typeof require === 'undefined') { globalThis.require = __wkfCreateRequire(import.meta.url); }";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This fixes the bundled undici node:http2 issue, but it also globally changes server-bundle feature detection: dependencies that branch on typeof require will now take their CJS/require path in these ESM bundles. The tested Vercel Node lanes passing is reassuring, but can we call this out as a Node-runtime-only server-bundle shim and keep an eye on optional-require behavior in bundled libraries?

@VaguelySeriousVaguelySeriousJun 24, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Agreed — that global feature-detection change is the right thing to flag. I kept the global globalThis.require (it's required: undici's bundled bare require(...) resolves against the runtime global scope, so a module-local const require wouldn't reliably be in scope for the inlined undici code), but tightened the JSDoc in both nitro/index.ts and sveltekit/plugin.ts to call it out explicitly as a Node-server-runtime-only shim and document the side effect + why it's safe:

  • Server-only — gated to the production server build (!nitro.options.dev / config.build.ssr); never touches the client/browser bundle.
  • Guarded — the typeof require === 'undefined' check makes it a no-op in CJS chunks where a real require already exists; it only fills a genuine ESM gap.
  • Functional — the require we install is a working createRequire, so a lib that switches to the require path gets a real require, not a broken stub (the common try { require(optional) } catch {} pattern still degrades gracefully).

The comment also records the one residual behavior to watch, per your ask: a bundled lib that, on detecting require, does require() of an ESM-only dependency on a Node version without require(ESM) support. The Vercel Node E2E lanes exercise the real undici path that motivated this. Comment-only change in b2ec9d7.

… 429
Stream writes (PUT) append chunks and are not idempotent. The stream
dispatcher was a bare Agent with no retry; switch it to a RetryAgent whose
policy is narrowed so PUTs retry only on transient connection errors and
HTTP 429 — both of which guarantee the chunk was not persisted — and never
on 5xx (where the chunk may already have been written) or other 4xx.
Also document that a caller-supplied `config.dispatcher` overrides the
stream dispatcher and should not retry PUT on 5xx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/world-vercel/src/streamer.ts Outdated
vercelBotand others added 2 commits June 24, 2026 17:34
…nt (no retry)" / "intentionally retry-free", contradicting the actual `RetryAgent` + `STREAM_RETRY_OPTIONS` implementation in a non-idempotent write path.
This commit fixes the issue reported at packages/world-vercel/src/streamer.ts:26
## The issue
`getDefaultStreamDispatcher` in `packages/world-vercel/src/http-client.ts` constructs a `RetryAgent` wrapping `STREAM_RETRY_OPTIONS` (`retryAfter: true`, `methods: ['PUT']`, `statusCodes: [429]`). So the stream-write dispatcher **does** retry — on undici's default transient connection errors and on HTTP 429.
Two comments still claimed the opposite:
1. `packages/world-vercel/src/streamer.ts` (~line 26):
> "That stream dispatcher is intentionally retry-free — stream appends aren't idempotent, so a retried write would duplicate chunks."
2. `packages/world-vercel/src/http-client.ts` (~line 102, JSDoc on `getStreamDispatcher`):
> "See getDefaultStreamDispatcher for why this is a bare Agent (no retry)."
Both are factually wrong: the dispatcher is a `RetryAgent`, not a bare `Agent`, and it is not retry-free. The accurate, nuanced description already lives in the `STREAM_RETRY_OPTIONS` and `getDefaultStreamDispatcher` JSDoc — these two comments were simply not updated when the behavior changed.
## Why it matters
This is the **non-idempotent write path**. A maintainer reasoning about correctness here relies on these comments to understand exactly what is and isn't retried. "Retry-free" vs. "retries on transient errors + 429 but never 5xx" is a meaningful safety distinction, so the stale wording actively misleads.
## The fix
Comment-only changes (no behavior change):
- **streamer.ts**: replaced "intentionally retry-free" with wording reflecting the narrowed policy — retries only on transient connection errors + HTTP 429 (both guaranteeing the chunk was never persisted), never 5xx, so a retry can't duplicate an already-applied write. Points to `STREAM_RETRY_OPTIONS`.
- **http-client.ts**: replaced "why this is a bare Agent (no retry)" with a reference to the deliberately narrowed retry policy in `getDefaultStreamDispatcher` / `STREAM_RETRY_OPTIONS`.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
…ide effect
Document that the createRequire banner deliberately makes `typeof require`
truthy for every bundled dependency in the ESM server output, why that is
safe (server-only, guarded, functional require), and the one residual
behavior to watch (require() of an ESM-only dep on old Node).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2615. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [world-vercel] Enable HTTP/2 for the events API and stream writes by VaguelySerious · Pull Request #2573 · vercel/workflow · GitHub
Skip to content

[world-vercel] Enable HTTP/2 for the events API and stream writes - #2573

Merged
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2
Jun 24, 2026
Merged

[world-vercel] Enable HTTP/2 for the events API and stream writes#2573
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

Enable HTTP/2 for two hot @workflow/world-vercel request paths, and fix the framework bundling that previously made H2 impossible on Vite-based adapters.

  • v4 events API (events-v4.ts) — an event write per step transition plus event-log reads on replay. Gets its own H2 undici dispatcher (getEventsDispatcher).
  • Stream writes (streamer.tswrite/writeMulti/close — the PUT path) — routed through a dedicated H2 stream dispatcher (getStreamDispatcher). These send a fully-buffered body (or none), so they get H2 multiplexing without the duplex issues that keep the long-lived live-read (GET) on plain fetch. The stream dispatcher is a bare Agentwithout the RetryAgent wrapper: stream appends aren't idempotent, so undici-level retry is deliberately omitted to avoid duplicating chunks.

Everything else stays on HTTP/1.1 (queue client / webhook respondWith, v3 makeRequest, deployment resolution, run-key fetch, live-read GET, list).

The bundling fix (why H2 was blocked before)

H2 had been disabled because it "broke SvelteKit on Vercel prod." Root cause: Vite/Rollup-based framework builds (SvelteKit, TanStack Start, plain Vite) bundle undici into the ESM server output. undici imports most node: builtins as ESM, but loads node:http2 lazily via a bare require('node:http2') inside a try/catch. The bundler leaves that bare require un-wired, so in the ESM bundle it throws and undici silently falls back to a stub whose http2.connect is undefined. Any H2 request then fails with fetch failed → the workflow flow-route callback can't reach the backend → runs never leave pending → 30-minute E2E timeouts. Frameworks that externalize undici (Express, Astro, Next, Hono, Nitro, …) were unaffected.

Fix: inject a createRequire-backed global require into the server build (server-only — a node:module import would break the client bundle) so the real node:http2 resolves:

  • @workflow/sveltekit — via configResolved (SvelteKit doesn't set env.isSsrBuild for its server pass; build.ssr on the resolved config is the reliable signal).
  • @workflow/nitro — via the existing rollup:before hook, covering Vite, TanStack Start, and Nitro from one place.

Verified end-to-end: with the fix, the sveltekit / vite / tanstack-start E2E Vercel Prod lanes pass, and http2.connect is not a function errors drop to zero (previously deterministic on this branch only).

Why the default dispatcher stays H1

Enabling H2 on the shared dispatcher deadlocks the queue's webhook respondWith mechanism (120s timeouts on the webhook/hook workflows). So H2 is scoped to the events + stream-write paths, which don't use that mechanism.

Tests

  • Regression guards: EVENTS_AGENT_OPTIONS.allowH2 === trueandDEFAULT_AGENT_OPTIONS.allowH2 === false.
  • getEventsDispatcher and getStreamDispatcher each return their own shared singleton — distinct from the default and from each other — and honor a caller-supplied dispatcher.
  • Local node:http2 TLS server test: a request through the global fetch with an allowH2 dispatcher negotiates h2 (the mechanism these paths rely on).
  • Full @workflow/world-vercel unit suite passes (172 tests).

Notes

  • events-v4.ts and the stream writes stay on the Vercel-patched global fetch (preserves observability instrumentation); only the dispatcher they pass changes.
  • The sleepingWorkflow and distributedAbortController failures seen on some lanes are pre-existing flakes (zero-tolerance server-side sleep timing / abort timeout) that also fail on main — unrelated to this change.

🤖 Generated with Claude Code

Flip allowH2 to true in the shared undici dispatcher so every
dispatcher-backed path (v3 makeRequest, v4 events, the queue client,
deployment resolution, run-key fetch) negotiates HTTP/2. H2 was
previously disabled for a SvelteKit-on-Vercel-prod hang; re-enable it
globally and rely on the E2E suite to guard the regression. Streaming
paths in streamer.ts intentionally bypass the dispatcher and are
unaffected.
Adds a regression guard on the agent options plus a local http2 server
test proving global fetch honors an allowH2 dispatcher (the mechanism
the v4 events path relies on).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJune 22, 2026 21:51
@vercel

vercelBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2ec9d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/world-vercelPatch
@workflow/nuxtPatch
workflowPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7282010788360

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ nitro-stable126026
✅ nuxt-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.045s (-5.3% 🟢)1.006s (~)0.961s101.00x
💻 LocalNitro0.047s (+0.6%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.050s (-4.2%)1.006s (~)0.955s101.12x
🐘 PostgresNitro0.065s (-0.6%)1.012s (~)0.947s101.44x
🐘 PostgresExpress0.066s (-4.5%)1.013s (~)0.946s101.47x
🐘 PostgresNext.js (Turbopack)0.067s (+20.8% 🔺)1.028s (+1.6%)0.962s101.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.232s (~)1.976s (-6.1% 🟢)1.744s101.00x
▲ VercelNext.js (Turbopack)0.260s (-3.8%)2.042s (~)1.782s101.12x
▲ VercelNitro0.301s (+19.7% 🔺)2.042s (-5.9% 🟢)1.741s101.30x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.078s (-0.8%)2.006s (~)0.928s101.00x
💻 LocalNext.js (Turbopack)1.079s (~)2.006s (~)0.927s101.00x
🐘 PostgresNext.js (Turbopack)1.079s (~)2.009s (~)0.930s101.00x
💻 LocalNitro1.082s (~)2.007s (~)0.925s101.00x
🐘 PostgresExpress1.097s (~)2.009s (~)0.912s101.02x
🐘 PostgresNitro1.100s (~)2.009s (~)0.909s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.421s (-0.9%)3.085s (-8.0% 🟢)1.664s101.00x
▲ VercelExpress1.445s (+4.5%)3.251s (+14.9% 🔺)1.806s101.02x
▲ VercelNitro1.468s (+4.0%)3.195s (~)1.727s101.03x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)10.420s (~)11.026s (~)0.606s31.00x
💻 LocalExpress10.435s (~)11.023s (~)0.588s31.00x
💻 LocalNitro10.455s (~)11.024s (~)0.569s31.00x
🐘 PostgresExpress10.460s (~)11.021s (~)0.560s31.00x
🐘 PostgresNitro10.476s (~)11.017s (~)0.541s31.01x
💻 LocalNext.js (Turbopack)10.504s (~)11.022s (~)0.518s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.937s (+1.8%)13.411s (~)1.473s31.00x
▲ VercelNext.js (Turbopack)12.028s (+1.9%)13.455s (-3.5%)1.427s31.01x
▲ VercelExpress12.325s (~)14.494s (+4.5%)2.169s31.03x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.554s (~)14.028s (~)0.473s51.00x
🐘 PostgresExpress13.559s (~)14.020s (~)0.461s51.00x
💻 LocalNext.js (Turbopack)13.625s (~)14.028s (~)0.403s51.01x
💻 LocalNitro13.734s (+0.7%)14.030s (~)0.295s51.01x
🐘 PostgresNext.js (Turbopack)13.750s (+0.6%)14.245s (+1.6%)0.495s51.01x
🐘 PostgresNitro13.773s (~)14.023s (~)0.250s51.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express18.265s (+11.1% 🔺)20.019s (+11.2% 🔺)1.754s31.00x
▲ VercelNext.js (Turbopack)18.771s (+8.2% 🔺)20.473s (+6.9% 🔺)1.702s41.03x
▲ VercelNitro19.188s (+17.2% 🔺)20.559s (+16.0% 🔺)1.372s31.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express12.044s (-2.4%)12.774s (-1.9%)0.730s81.00x
🐘 PostgresNext.js (Turbopack)12.160s (~)12.869s (-1.1%)0.709s71.01x
🐘 PostgresExpress12.263s (+1.3%)13.019s (~)0.757s71.02x
💻 LocalNitro12.272s (+0.7%)13.025s (~)0.753s71.02x
🐘 PostgresNitro12.273s (~)13.018s (~)0.746s71.02x
💻 LocalNext.js (Turbopack)12.414s (+1.3%)13.026s (~)0.612s71.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)22.253s (+15.0% 🔺)24.838s (+17.0% 🔺)2.586s41.00x
▲ VercelExpress22.303s (+21.4% 🔺)25.182s (+24.9% 🔺)2.879s41.00x
▲ VercelNitro22.749s (+19.8% 🔺)25.174s (+22.0% 🔺)2.425s41.02x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.172s (~)2.007s (~)0.835s151.00x
🐘 PostgresNitro1.192s (+0.9%)2.008s (~)0.815s151.02x
🐘 PostgresNext.js (Turbopack)1.215s (+3.2%)2.009s (~)0.794s151.04x
💻 LocalExpress1.351s (-2.7%)2.006s (~)0.655s151.15x
💻 LocalNitro1.423s (-1.1%)2.007s (~)0.584s151.21x
💻 LocalNext.js (Turbopack)1.444s (-3.6%)2.007s (~)0.563s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.284s (+12.5% 🔺)3.862s (+10.3% 🔺)1.578s81.00x
▲ VercelNitro2.364s (+11.0% 🔺)3.813s (+10.7% 🔺)1.448s81.04x
▲ VercelNext.js (Turbopack)2.432s (+13.1% 🔺)3.659s (-3.4%)1.228s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.302s (-2.8%)2.317s (-7.6% 🟢)1.015s131.00x
🐘 PostgresExpress1.332s (-4.4%)2.223s (-16.9% 🟢)0.891s141.02x
🐘 PostgresNext.js (Turbopack)1.468s (+11.8% 🔺)2.685s (-8.0% 🟢)1.217s121.13x
💻 LocalExpress2.446s (+3.7%)2.826s (-3.1%)0.381s111.88x
💻 LocalNext.js (Turbopack)2.530s (-7.5% 🟢)3.008s (-3.2%)0.477s101.94x
💻 LocalNitro2.671s (+10.9% 🔺)3.110s (+6.5% 🔺)0.439s102.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.780s (-9.7% 🟢)4.586s (-11.5% 🟢)1.806s71.00x
▲ VercelExpress3.603s (-0.9%)5.649s (+9.4% 🔺)2.047s61.30x
▲ VercelNext.js (Turbopack)4.146s (+24.1% 🔺)5.917s (+17.3% 🔺)1.772s61.49x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (~)4.135s (-3.9%)2.553s81.00x
🐘 PostgresNitro1.627s (-0.8%)4.136s (-3.0%)2.508s81.03x
🐘 PostgresNext.js (Turbopack)2.703s (-4.0%)4.582s (-16.9% 🟢)1.879s71.71x
💻 LocalNitro6.077s (-5.7% 🟢)6.818s (-5.5% 🟢)0.741s53.84x
💻 LocalExpress6.158s (-2.6%)6.819s (-2.8%)0.661s53.89x
💻 LocalNext.js (Turbopack)7.337s (+34.1% 🔺)8.017s (+33.3% 🔺)0.680s44.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.321s (+24.1% 🔺)6.144s (+18.1% 🔺)1.824s51.00x
▲ VercelNitro5.108s (+52.3% 🔺)6.685s (+30.6% 🔺)1.578s51.18x
▲ VercelExpress5.501s (+79.5% 🔺)7.636s (+51.4% 🔺)2.135s51.27x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.189s (+0.9%)2.007s (~)0.819s151.00x
🐘 PostgresNitro1.203s (~)2.009s (~)0.806s151.01x
🐘 PostgresNext.js (Turbopack)1.225s (+4.7%)2.015s (~)0.790s151.03x
💻 LocalExpress1.415s (-1.6%)2.007s (~)0.592s151.19x
💻 LocalNext.js (Turbopack)1.455s (+3.7%)2.006s (~)0.552s151.22x
💻 LocalNitro1.472s (+3.5%)2.007s (~)0.535s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.605s (+26.2% 🔺)4.403s (+25.9% 🔺)1.799s71.00x
▲ VercelNext.js (Turbopack)2.633s (-54.5% 🟢)4.124s (-44.6% 🟢)1.491s81.01x
▲ VercelNitro2.750s (+28.3% 🔺)4.412s (+18.7% 🔺)1.661s71.06x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.318s (-4.0%)2.393s (-3.2%)1.075s131.00x
🐘 PostgresExpress1.401s (+4.5%)2.676s (+8.3% 🔺)1.274s121.06x
🐘 PostgresNext.js (Turbopack)1.487s (+15.3% 🔺)2.515s (-13.8% 🟢)1.028s121.13x
💻 LocalNitro2.433s (-5.8% 🟢)3.009s (-3.2%)0.576s101.85x
💻 LocalNext.js (Turbopack)2.587s (-1.9%)3.109s (+3.3%)0.522s101.96x
💻 LocalExpress2.643s (+9.2% 🔺)3.008s (-3.2%)0.365s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.822s (-41.1% 🟢)5.218s (-21.4% 🟢)2.396s61.00x
▲ VercelNitro3.363s (+23.9% 🔺)5.021s (+16.3% 🔺)1.658s71.19x
▲ VercelNext.js (Turbopack)3.396s (-28.4% 🟢)5.116s (-26.4% 🟢)1.721s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.599s (-3.7%)4.138s (+3.1%)2.539s81.00x
🐘 PostgresExpress1.658s (+4.6%)4.011s (-3.0%)2.353s81.04x
🐘 PostgresNext.js (Turbopack)2.514s (-12.1% 🟢)5.041s (-13.8% 🟢)2.527s61.57x
💻 LocalExpress5.944s (-11.3% 🟢)6.415s (-11.1% 🟢)0.472s53.72x
💻 LocalNitro6.441s (-5.0% 🟢)7.015s (-2.8%)0.574s54.03x
💻 LocalNext.js (Turbopack)7.009s (+22.5% 🔺)8.020s (+33.3% 🔺)1.010s44.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.521s (+1.9%)5.720s (+13.6% 🔺)2.199s61.00x
▲ VercelNext.js (Turbopack)4.714s (+50.9% 🔺)6.326s (+22.5% 🔺)1.611s51.34x
▲ VercelExpress4.774s (+1.8%)7.280s (+15.7% 🔺)2.506s51.36x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.546s (+6.9% 🔺)1.027s (+2.1%)0.481s591.00x
🐘 PostgresExpress0.557s (+4.5%)1.041s (+1.7%)0.483s581.02x
🐘 PostgresNitro0.563s (-5.3% 🟢)1.024s (-1.7%)0.461s591.03x
💻 LocalExpress0.570s (~)1.005s (~)0.435s601.04x
💻 LocalNitro0.585s (~)1.005s (~)0.420s601.07x
💻 LocalNext.js (Turbopack)0.673s (+11.5% 🔺)1.058s (+3.5%)0.385s571.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.301s (+24.8% 🔺)4.990s (+16.1% 🔺)1.689s131.00x
▲ VercelExpress3.342s (+19.4% 🔺)5.325s (+18.7% 🔺)1.983s121.01x
▲ VercelNext.js (Turbopack)3.380s (+11.2% 🔺)4.843s (-3.1%)1.463s131.02x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.237s (-1.1%)2.007s (~)0.770s451.00x
🐘 PostgresNitro1.333s (-5.7% 🟢)2.030s (-1.2%)0.696s451.08x
💻 LocalExpress1.425s (-2.1%)2.006s (~)0.581s451.15x
💻 LocalNext.js (Turbopack)1.474s (~)2.006s (~)0.532s451.19x
🐘 PostgresNext.js (Turbopack)1.500s (+20.5% 🔺)2.105s (+3.7%)0.605s431.21x
💻 LocalNitro1.565s (+5.5% 🔺)2.052s (+2.3%)0.488s441.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.650s (+19.6% 🔺)9.459s (+21.8% 🔺)1.808s101.00x
▲ VercelExpress7.692s (+19.0% 🔺)9.613s (+19.6% 🔺)1.921s101.01x
▲ VercelNext.js (Turbopack)9.201s (+28.8% 🔺)11.021s (+24.0% 🔺)1.820s91.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.481s (-4.4%)3.033s (-1.7%)0.552s401.00x
🐘 PostgresNitro2.669s (-1.7%)3.086s (+0.9%)0.417s391.08x
💻 LocalExpress3.122s (-3.8%)3.821s (-3.9%)0.699s321.26x
💻 LocalNitro3.154s (-2.0%)3.978s (-0.8%)0.824s311.27x
🐘 PostgresNext.js (Turbopack)3.207s (+29.5% 🔺)3.749s (+22.6% 🔺)0.542s331.29x
💻 LocalNext.js (Turbopack)3.281s (+0.6%)3.977s (-1.6%)0.696s311.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express15.239s (+10.5% 🔺)17.918s (+15.9% 🔺)2.679s71.00x
▲ VercelNitro15.788s (+12.3% 🔺)18.302s (+17.1% 🔺)2.515s71.04x
▲ VercelNext.js (Turbopack)18.176s (+10.8% 🔺)20.378s (+10.2% 🔺)2.202s61.19x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.197s (-6.3% 🟢)1.006s (~)0.809s601.00x
🐘 PostgresNext.js (Turbopack)0.213s (+27.2% 🔺)1.010s (~)0.797s601.08x
🐘 PostgresNitro0.222s (-2.1%)1.007s (~)0.784s601.13x
💻 LocalExpress0.463s (+7.4% 🔺)1.005s (~)0.541s602.35x
💻 LocalNitro0.465s (-0.7%)1.004s (~)0.539s602.36x
💻 LocalNext.js (Turbopack)0.613s (+5.0%)1.005s (~)0.391s603.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.257s (-42.1% 🟢)2.609s (-32.7% 🟢)1.352s231.00x
▲ VercelExpress1.375s (-33.7% 🟢)3.160s (-17.7% 🟢)1.785s191.09x
▲ VercelNext.js (Turbopack)1.468s (-19.3% 🟢)2.862s (-19.3% 🟢)1.395s211.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.314s (-2.7%)1.006s (-1.1%)0.692s901.00x
🐘 PostgresNitro0.336s (~)1.006s (~)0.671s901.07x
🐘 PostgresNext.js (Turbopack)0.347s (+29.3% 🔺)1.017s (+1.1%)0.670s891.10x
💻 LocalExpress2.137s (-1.1%)2.735s (~)0.598s336.80x
💻 LocalNitro2.192s (+2.8%)2.796s (+4.1%)0.604s336.97x
💻 LocalNext.js (Turbopack)2.734s (-2.0%)3.295s (+1.1%)0.560s288.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.855s (-2.7%)3.708s (+6.3% 🔺)1.853s251.00x
▲ VercelExpress2.119s (-14.0% 🟢)4.166s (+2.8%)2.047s221.14x
▲ VercelNext.js (Turbopack)2.680s (+23.5% 🔺)4.477s (+14.0% 🔺)1.797s211.44x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.531s (+0.5%)1.050s (+1.7%)0.519s1151.00x
🐘 PostgresExpress0.567s (+9.8% 🔺)1.160s (+8.6% 🔺)0.594s1041.07x
🐘 PostgresNext.js (Turbopack)0.699s (+48.1% 🔺)1.975s (-34.3% 🟢)1.277s611.32x
💻 LocalExpress9.304s (-7.8% 🟢)10.360s (-6.9% 🟢)1.056s1217.52x
💻 LocalNext.js (Turbopack)9.579s (-2.1%)10.530s (-2.3%)0.951s1218.04x
💻 LocalNitro10.363s (+4.5%)11.302s (+2.5%)0.940s1119.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.326s (+39.1% 🔺)5.768s (+42.3% 🔺)2.443s211.00x
▲ VercelNitro3.641s (+43.2% 🔺)5.915s (+43.4% 🔺)2.274s211.09x
▲ VercelNext.js (Turbopack)4.725s (+0.8%)7.116s (+8.7% 🔺)2.391s171.42x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.137s (-2.7%)1.968s (~)0.011s (-14.5% 🟢)2.018s (~)0.881s101.00x
💻 LocalExpress1.151s (~)2.005s (~)0.010s (-17.5% 🟢)2.017s (~)0.866s101.01x
🐘 PostgresNitro1.156s (-0.9%)1.999s (~)0.001s (+18.2% 🔺)2.010s (~)0.854s101.02x
💻 LocalNitro1.159s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.862s101.02x
🐘 PostgresExpress1.179s (+2.6%)2.000s (~)0.001s (+10.0% 🔺)2.011s (~)0.831s101.04x
🐘 PostgresNext.js (Turbopack)1.209s (+5.5% 🔺)1.996s (~)0.001s (-27.3% 🟢)2.009s (~)0.800s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.129s (+4.0%)3.553s (+15.8% 🔺)2.488s (+42.0% 🔺)6.571s (+21.6% 🔺)4.441s101.00x
▲ VercelNext.js (Turbopack)2.206s (+6.8% 🔺)3.312s (-4.0%)2.584s (+83.3% 🔺)6.338s (+17.2% 🔺)4.132s101.04x
▲ VercelNitro2.374s (+7.0% 🔺)3.275s (~)2.368s (+27.1% 🔺)6.269s (+10.1% 🔺)3.894s101.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.509s (-2.5%)2.010s (~)0.013s (-1.3%)2.026s (~)0.517s301.00x
💻 LocalNext.js (Turbopack)1.563s (-0.9%)1.971s (~)0.012s (-5.4% 🟢)2.025s (~)0.462s301.04x
💻 LocalNitro1.564s (+0.7%)2.010s (~)0.013s (+9.4% 🔺)2.026s (~)0.462s301.04x
🐘 PostgresExpress1.568s (+1.4%)2.010s (~)0.005s (-4.8%)2.027s (~)0.459s301.04x
🐘 PostgresNitro1.577s (-1.8%)2.003s (~)0.005s (~)2.027s (~)0.450s301.04x
🐘 PostgresNext.js (Turbopack)2.009s (+30.4% 🔺)2.447s (+21.7% 🔺)0.004s (-20.0% 🟢)2.478s (+22.3% 🔺)0.469s251.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.025s (+1.4%)7.125s (-3.4%)0.324s (+1.9%)7.978s (-3.4%)1.953s81.00x
▲ VercelExpress6.435s (+9.3% 🔺)7.891s (+9.5% 🔺)0.226s (+4.4%)8.797s (+11.6% 🔺)2.363s71.07x
▲ VercelNitro6.500s (+7.0% 🔺)7.678s (+8.5% 🔺)0.459s (+102.8% 🔺)8.656s (+10.6% 🔺)2.156s71.08x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.782s (+1.0%)1.071s (+2.2%)0.000s (+Infinity% 🔺)1.088s (+2.3%)0.307s561.00x
🐘 PostgresNitro0.821s (~)1.062s (-3.3%)0.000s (-50.9% 🟢)1.092s (-2.4%)0.271s551.05x
🐘 PostgresNext.js (Turbopack)1.037s (+13.1% 🔺)1.437s (+12.6% 🔺)0.000s (+14.6% 🔺)1.473s (+14.7% 🔺)0.436s411.33x
💻 LocalExpress1.209s (-4.7%)1.949s (-3.2%)0.000s (+214.5% 🔺)1.951s (-3.2%)0.743s311.55x
💻 LocalNitro1.280s (-2.9%)1.981s (~)0.001s (+41.7% 🔺)1.984s (~)0.704s311.64x
💻 LocalNext.js (Turbopack)1.411s (+5.7% 🔺)1.980s (~)0.001s (+183.3% 🔺)2.016s (~)0.605s301.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.353s (+7.5% 🔺)4.326s (-1.9%)0.000s (+Infinity% 🔺)4.782s (-1.9%)1.430s131.00x
▲ VercelExpress3.737s (-4.4%)5.137s (+2.2%)0.000s (-100.0% 🟢)5.715s (+3.2%)1.978s111.11x
▲ VercelNext.js (Turbopack)4.240s (+28.8% 🔺)5.616s (+19.4% 🔺)0.000s (+20.0% 🔺)6.124s (+16.5% 🔺)1.884s101.26x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.605s (-5.3% 🟢)2.171s (-3.7%)0.000s (-100.0% 🟢)2.211s (-2.6%)0.607s281.00x
🐘 PostgresNitro1.774s (-7.5% 🟢)2.339s (-6.3% 🟢)0.000s (-100.0% 🟢)2.355s (-6.6% 🟢)0.581s261.11x
🐘 PostgresNext.js (Turbopack)2.353s (+15.8% 🔺)2.908s (+11.3% 🔺)0.000s (-100.0% 🟢)2.925s (+10.7% 🔺)0.572s211.47x
💻 LocalExpress3.387s (-6.8% 🟢)4.027s (-4.8%)0.001s (+50.0% 🔺)4.031s (-4.7%)0.644s152.11x
💻 LocalNext.js (Turbopack)3.389s (-7.5% 🟢)3.869s (-7.7% 🟢)0.001s (-12.1% 🟢)3.912s (-7.6% 🟢)0.523s162.11x
💻 LocalNitro3.475s (-2.5%)4.160s (+3.3%)0.000s (-58.8% 🟢)4.163s (+3.3%)0.689s152.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.911s (+27.6% 🔺)7.295s (+21.2% 🔺)0.001s (+150.0% 🔺)7.812s (+18.7% 🔺)1.901s81.00x
▲ VercelExpress6.192s (+30.8% 🔺)7.470s (+30.1% 🔺)0.000s (+25.0% 🔺)8.018s (+28.2% 🔺)1.826s81.05x
▲ VercelNitro6.213s (+27.0% 🔺)7.485s (+30.9% 🔺)0.000s (NaN%)7.991s (+29.2% 🔺)1.778s81.05x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress18/21
🐘 PostgresExpress11/21
▲ VercelExpress8/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres15/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

Enabling H2 on the single shared dispatcher hung the queue's webhook
respondWith mechanism (120s E2E timeouts on webhook/hook workflows
across frameworks). Instead, give the v4 events API its own
H2-enabled dispatcher (getEventsDispatcher) and keep the default
dispatcher — queue/webhook, v3 makeRequest, streaming, deployment
resolution, run-key fetch — on HTTP/1.1.
The events endpoints are the hottest path (an event write per step
transition plus log reads) and are plain request/response (or a
streamed LIST response), none of which trip the webhook/duplex H2
issues, so they get multiplexing safely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SvelteKit bundles the server (including undici via the world adapter) into
ESM output. undici loads node: builtins as ESM imports except node:http2,
which it pulls in lazily via a bare require('node:http2') inside a try/catch.
The bundler leaves that bare require un-wired, so in the ESM bundle it throws
and undici silently falls back to a stub whose http2.connect is undefined —
breaking every HTTP/2 request (the workflow flow-route callback fails with
"fetch failed", so runs never start).
Inject a createRequire-backed global require into the SSR build only (a
node:module import would break the client bundle) so the real node:http2
resolves. Detected via configResolved.build.ssr because SvelteKit does not
set env.isSsrBuild for its server pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VaguelySeriousand others added 2 commits June 23, 2026 17:32
Nitro bundles undici (via the world adapter) into the ESM server output.
undici loads node: builtins as ESM imports except node:http2, which it pulls
in lazily via a bare require('node:http2') inside a try/catch. The bundler
leaves that bare require un-wired, so in the ESM bundle it throws and undici
silently falls back to a stub whose http2.connect is undefined — breaking
every HTTP/2 request (the workflow flow-route callback fails with "fetch
failed", so runs never start). This affects all nitro-based builds including
the Vite and TanStack Start framework integrations.
Prepend a createRequire-backed global require to the server chunks (via the
rollup:before hook, production builds only) so the real node:http2 resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream writes (write/writeMulti/close) previously used plain fetch() on
HTTP/1.1. Route them through a dedicated H2 stream dispatcher: the PUT
requests send a fully-buffered body (or none), so they gain H2 multiplexing
without the duplex-streaming issues that keep the long-lived live-read (GET)
on plain fetch. The stream dispatcher is a bare undici Agent without the
RetryAgent wrapper — stream appends are not idempotent, so undici-level retry
is deliberately omitted to avoid duplicating chunks (preserving the streamer's
original no-retry behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
headers: httpConfig.headers,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- undici dispatcher type doesn't match @types/node's RequestInit
dispatcher: getStreamDispatcher(config),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] I would block this until the Vercel Nitro v3 benchmark is rerun green or root-caused. The current Benchmark Vercel (nitro-v3) job fails with Stream 1 correctness failure: expected 1048576 bytes, got 983040, exactly one 64 KiB chunk missing from the stream benchmark. Since this PR changes stream PUTs to use the H2 dispatcher here, this looks like a plausible stream-write correctness regression rather than a pure perf failure. Job: https://github.com/vercel/workflow/actions/runs/28066956084/job/83093680965

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passed on re-run

* getDefaultStreamDispatcher for why this is a bare Agent (no retry).
*/
export function getStreamDispatcher(config?: APIConfig): unknown {
return config?.dispatcher ?? getDefaultStreamDispatcher();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This keeps the default stream dispatcher retry-free, but config.dispatcher now also controls stream writes. A caller that passes an undici RetryAgent can re-enable retries for stream PUTs; undici retries PUT by default, and stream appends are not idempotent, so that can duplicate chunks. Can we document this in the custom dispatcher section, or split out a stream-specific dispatcher option / guard so retrying dispatchers are not used for stream writes?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Good catch. Rather than leave stream writes retry-free, I scoped the retry policy so the win (resilience to transient blips) survives without the duplicate-chunk risk:

  • The default stream dispatcher is now a RetryAgent with STREAM_RETRY_OPTIONS = { methods: ['PUT'], statusCodes: [429], retryAfter: true }. It retries only on transient connection errors (undici's default errorCodes) and HTTP 429 — both guarantee the chunk was not persisted. 5xx is excluded (a 5xx can mean the chunk was written but the response failed → retry would duplicate), as are other 4xx.
  • For the caller-override case you flagged: documented in the dispatcher JSDoc (APIConfig in utils.ts) that a custom dispatcher replaces every default including stream writes, and should not retry PUT on 5xx.

Added a regression test asserting statusCodes is exactly [429] and never contains 500/502/503/504.

*/
function addNodeRequireBanner(config: RollupConfig): void {
const banner =
"import { createRequire as __wkfCreateRequire } from 'node:module'; if (typeof require === 'undefined') { globalThis.require = __wkfCreateRequire(import.meta.url); }";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This fixes the bundled undici node:http2 issue, but it also globally changes server-bundle feature detection: dependencies that branch on typeof require will now take their CJS/require path in these ESM bundles. The tested Vercel Node lanes passing is reassuring, but can we call this out as a Node-runtime-only server-bundle shim and keep an eye on optional-require behavior in bundled libraries?

@VaguelySeriousVaguelySeriousJun 24, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Agreed — that global feature-detection change is the right thing to flag. I kept the global globalThis.require (it's required: undici's bundled bare require(...) resolves against the runtime global scope, so a module-local const require wouldn't reliably be in scope for the inlined undici code), but tightened the JSDoc in both nitro/index.ts and sveltekit/plugin.ts to call it out explicitly as a Node-server-runtime-only shim and document the side effect + why it's safe:

  • Server-only — gated to the production server build (!nitro.options.dev / config.build.ssr); never touches the client/browser bundle.
  • Guarded — the typeof require === 'undefined' check makes it a no-op in CJS chunks where a real require already exists; it only fills a genuine ESM gap.
  • Functional — the require we install is a working createRequire, so a lib that switches to the require path gets a real require, not a broken stub (the common try { require(optional) } catch {} pattern still degrades gracefully).

The comment also records the one residual behavior to watch, per your ask: a bundled lib that, on detecting require, does require() of an ESM-only dependency on a Node version without require(ESM) support. The Vercel Node E2E lanes exercise the real undici path that motivated this. Comment-only change in b2ec9d7.

… 429
Stream writes (PUT) append chunks and are not idempotent. The stream
dispatcher was a bare Agent with no retry; switch it to a RetryAgent whose
policy is narrowed so PUTs retry only on transient connection errors and
HTTP 429 — both of which guarantee the chunk was not persisted — and never
on 5xx (where the chunk may already have been written) or other 4xx.
Also document that a caller-supplied `config.dispatcher` overrides the
stream dispatcher and should not retry PUT on 5xx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/world-vercel/src/streamer.ts Outdated
vercelBotand others added 2 commits June 24, 2026 17:34
…nt (no retry)" / "intentionally retry-free", contradicting the actual `RetryAgent` + `STREAM_RETRY_OPTIONS` implementation in a non-idempotent write path.
This commit fixes the issue reported at packages/world-vercel/src/streamer.ts:26
## The issue
`getDefaultStreamDispatcher` in `packages/world-vercel/src/http-client.ts` constructs a `RetryAgent` wrapping `STREAM_RETRY_OPTIONS` (`retryAfter: true`, `methods: ['PUT']`, `statusCodes: [429]`). So the stream-write dispatcher **does** retry — on undici's default transient connection errors and on HTTP 429.
Two comments still claimed the opposite:
1. `packages/world-vercel/src/streamer.ts` (~line 26):
> "That stream dispatcher is intentionally retry-free — stream appends aren't idempotent, so a retried write would duplicate chunks."
2. `packages/world-vercel/src/http-client.ts` (~line 102, JSDoc on `getStreamDispatcher`):
> "See getDefaultStreamDispatcher for why this is a bare Agent (no retry)."
Both are factually wrong: the dispatcher is a `RetryAgent`, not a bare `Agent`, and it is not retry-free. The accurate, nuanced description already lives in the `STREAM_RETRY_OPTIONS` and `getDefaultStreamDispatcher` JSDoc — these two comments were simply not updated when the behavior changed.
## Why it matters
This is the **non-idempotent write path**. A maintainer reasoning about correctness here relies on these comments to understand exactly what is and isn't retried. "Retry-free" vs. "retries on transient errors + 429 but never 5xx" is a meaningful safety distinction, so the stale wording actively misleads.
## The fix
Comment-only changes (no behavior change):
- **streamer.ts**: replaced "intentionally retry-free" with wording reflecting the narrowed policy — retries only on transient connection errors + HTTP 429 (both guaranteeing the chunk was never persisted), never 5xx, so a retry can't duplicate an already-applied write. Points to `STREAM_RETRY_OPTIONS`.
- **http-client.ts**: replaced "why this is a bare Agent (no retry)" with a reference to the deliberately narrowed retry policy in `getDefaultStreamDispatcher` / `STREAM_RETRY_OPTIONS`.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
…ide effect
Document that the createRequire banner deliberately makes `typeof require`
truthy for every bundled dependency in the ESM server output, why that is
safe (server-only, guarded, functional require), and the one residual
behavior to watch (require() of an ESM-only dep on old Node).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2615. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [world-vercel] Enable HTTP/2 for the events API and stream writes by VaguelySerious · Pull Request #2573 · vercel/workflow · GitHub
Skip to content

[world-vercel] Enable HTTP/2 for the events API and stream writes - #2573

Merged
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2
Jun 24, 2026
Merged

[world-vercel] Enable HTTP/2 for the events API and stream writes#2573
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

Enable HTTP/2 for two hot @workflow/world-vercel request paths, and fix the framework bundling that previously made H2 impossible on Vite-based adapters.

  • v4 events API (events-v4.ts) — an event write per step transition plus event-log reads on replay. Gets its own H2 undici dispatcher (getEventsDispatcher).
  • Stream writes (streamer.tswrite/writeMulti/close — the PUT path) — routed through a dedicated H2 stream dispatcher (getStreamDispatcher). These send a fully-buffered body (or none), so they get H2 multiplexing without the duplex issues that keep the long-lived live-read (GET) on plain fetch. The stream dispatcher is a bare Agentwithout the RetryAgent wrapper: stream appends aren't idempotent, so undici-level retry is deliberately omitted to avoid duplicating chunks.

Everything else stays on HTTP/1.1 (queue client / webhook respondWith, v3 makeRequest, deployment resolution, run-key fetch, live-read GET, list).

The bundling fix (why H2 was blocked before)

H2 had been disabled because it "broke SvelteKit on Vercel prod." Root cause: Vite/Rollup-based framework builds (SvelteKit, TanStack Start, plain Vite) bundle undici into the ESM server output. undici imports most node: builtins as ESM, but loads node:http2 lazily via a bare require('node:http2') inside a try/catch. The bundler leaves that bare require un-wired, so in the ESM bundle it throws and undici silently falls back to a stub whose http2.connect is undefined. Any H2 request then fails with fetch failed → the workflow flow-route callback can't reach the backend → runs never leave pending → 30-minute E2E timeouts. Frameworks that externalize undici (Express, Astro, Next, Hono, Nitro, …) were unaffected.

Fix: inject a createRequire-backed global require into the server build (server-only — a node:module import would break the client bundle) so the real node:http2 resolves:

  • @workflow/sveltekit — via configResolved (SvelteKit doesn't set env.isSsrBuild for its server pass; build.ssr on the resolved config is the reliable signal).
  • @workflow/nitro — via the existing rollup:before hook, covering Vite, TanStack Start, and Nitro from one place.

Verified end-to-end: with the fix, the sveltekit / vite / tanstack-start E2E Vercel Prod lanes pass, and http2.connect is not a function errors drop to zero (previously deterministic on this branch only).

Why the default dispatcher stays H1

Enabling H2 on the shared dispatcher deadlocks the queue's webhook respondWith mechanism (120s timeouts on the webhook/hook workflows). So H2 is scoped to the events + stream-write paths, which don't use that mechanism.

Tests

  • Regression guards: EVENTS_AGENT_OPTIONS.allowH2 === trueandDEFAULT_AGENT_OPTIONS.allowH2 === false.
  • getEventsDispatcher and getStreamDispatcher each return their own shared singleton — distinct from the default and from each other — and honor a caller-supplied dispatcher.
  • Local node:http2 TLS server test: a request through the global fetch with an allowH2 dispatcher negotiates h2 (the mechanism these paths rely on).
  • Full @workflow/world-vercel unit suite passes (172 tests).

Notes

  • events-v4.ts and the stream writes stay on the Vercel-patched global fetch (preserves observability instrumentation); only the dispatcher they pass changes.
  • The sleepingWorkflow and distributedAbortController failures seen on some lanes are pre-existing flakes (zero-tolerance server-side sleep timing / abort timeout) that also fail on main — unrelated to this change.

🤖 Generated with Claude Code

Flip allowH2 to true in the shared undici dispatcher so every
dispatcher-backed path (v3 makeRequest, v4 events, the queue client,
deployment resolution, run-key fetch) negotiates HTTP/2. H2 was
previously disabled for a SvelteKit-on-Vercel-prod hang; re-enable it
globally and rely on the E2E suite to guard the regression. Streaming
paths in streamer.ts intentionally bypass the dispatcher and are
unaffected.
Adds a regression guard on the agent options plus a local http2 server
test proving global fetch honors an allowH2 dispatcher (the mechanism
the v4 events path relies on).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJune 22, 2026 21:51
@vercel

vercelBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2ec9d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/world-vercelPatch
@workflow/nuxtPatch
workflowPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7282010788360

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ nitro-stable126026
✅ nuxt-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.045s (-5.3% 🟢)1.006s (~)0.961s101.00x
💻 LocalNitro0.047s (+0.6%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.050s (-4.2%)1.006s (~)0.955s101.12x
🐘 PostgresNitro0.065s (-0.6%)1.012s (~)0.947s101.44x
🐘 PostgresExpress0.066s (-4.5%)1.013s (~)0.946s101.47x
🐘 PostgresNext.js (Turbopack)0.067s (+20.8% 🔺)1.028s (+1.6%)0.962s101.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.232s (~)1.976s (-6.1% 🟢)1.744s101.00x
▲ VercelNext.js (Turbopack)0.260s (-3.8%)2.042s (~)1.782s101.12x
▲ VercelNitro0.301s (+19.7% 🔺)2.042s (-5.9% 🟢)1.741s101.30x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.078s (-0.8%)2.006s (~)0.928s101.00x
💻 LocalNext.js (Turbopack)1.079s (~)2.006s (~)0.927s101.00x
🐘 PostgresNext.js (Turbopack)1.079s (~)2.009s (~)0.930s101.00x
💻 LocalNitro1.082s (~)2.007s (~)0.925s101.00x
🐘 PostgresExpress1.097s (~)2.009s (~)0.912s101.02x
🐘 PostgresNitro1.100s (~)2.009s (~)0.909s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.421s (-0.9%)3.085s (-8.0% 🟢)1.664s101.00x
▲ VercelExpress1.445s (+4.5%)3.251s (+14.9% 🔺)1.806s101.02x
▲ VercelNitro1.468s (+4.0%)3.195s (~)1.727s101.03x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)10.420s (~)11.026s (~)0.606s31.00x
💻 LocalExpress10.435s (~)11.023s (~)0.588s31.00x
💻 LocalNitro10.455s (~)11.024s (~)0.569s31.00x
🐘 PostgresExpress10.460s (~)11.021s (~)0.560s31.00x
🐘 PostgresNitro10.476s (~)11.017s (~)0.541s31.01x
💻 LocalNext.js (Turbopack)10.504s (~)11.022s (~)0.518s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.937s (+1.8%)13.411s (~)1.473s31.00x
▲ VercelNext.js (Turbopack)12.028s (+1.9%)13.455s (-3.5%)1.427s31.01x
▲ VercelExpress12.325s (~)14.494s (+4.5%)2.169s31.03x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.554s (~)14.028s (~)0.473s51.00x
🐘 PostgresExpress13.559s (~)14.020s (~)0.461s51.00x
💻 LocalNext.js (Turbopack)13.625s (~)14.028s (~)0.403s51.01x
💻 LocalNitro13.734s (+0.7%)14.030s (~)0.295s51.01x
🐘 PostgresNext.js (Turbopack)13.750s (+0.6%)14.245s (+1.6%)0.495s51.01x
🐘 PostgresNitro13.773s (~)14.023s (~)0.250s51.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express18.265s (+11.1% 🔺)20.019s (+11.2% 🔺)1.754s31.00x
▲ VercelNext.js (Turbopack)18.771s (+8.2% 🔺)20.473s (+6.9% 🔺)1.702s41.03x
▲ VercelNitro19.188s (+17.2% 🔺)20.559s (+16.0% 🔺)1.372s31.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express12.044s (-2.4%)12.774s (-1.9%)0.730s81.00x
🐘 PostgresNext.js (Turbopack)12.160s (~)12.869s (-1.1%)0.709s71.01x
🐘 PostgresExpress12.263s (+1.3%)13.019s (~)0.757s71.02x
💻 LocalNitro12.272s (+0.7%)13.025s (~)0.753s71.02x
🐘 PostgresNitro12.273s (~)13.018s (~)0.746s71.02x
💻 LocalNext.js (Turbopack)12.414s (+1.3%)13.026s (~)0.612s71.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)22.253s (+15.0% 🔺)24.838s (+17.0% 🔺)2.586s41.00x
▲ VercelExpress22.303s (+21.4% 🔺)25.182s (+24.9% 🔺)2.879s41.00x
▲ VercelNitro22.749s (+19.8% 🔺)25.174s (+22.0% 🔺)2.425s41.02x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.172s (~)2.007s (~)0.835s151.00x
🐘 PostgresNitro1.192s (+0.9%)2.008s (~)0.815s151.02x
🐘 PostgresNext.js (Turbopack)1.215s (+3.2%)2.009s (~)0.794s151.04x
💻 LocalExpress1.351s (-2.7%)2.006s (~)0.655s151.15x
💻 LocalNitro1.423s (-1.1%)2.007s (~)0.584s151.21x
💻 LocalNext.js (Turbopack)1.444s (-3.6%)2.007s (~)0.563s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.284s (+12.5% 🔺)3.862s (+10.3% 🔺)1.578s81.00x
▲ VercelNitro2.364s (+11.0% 🔺)3.813s (+10.7% 🔺)1.448s81.04x
▲ VercelNext.js (Turbopack)2.432s (+13.1% 🔺)3.659s (-3.4%)1.228s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.302s (-2.8%)2.317s (-7.6% 🟢)1.015s131.00x
🐘 PostgresExpress1.332s (-4.4%)2.223s (-16.9% 🟢)0.891s141.02x
🐘 PostgresNext.js (Turbopack)1.468s (+11.8% 🔺)2.685s (-8.0% 🟢)1.217s121.13x
💻 LocalExpress2.446s (+3.7%)2.826s (-3.1%)0.381s111.88x
💻 LocalNext.js (Turbopack)2.530s (-7.5% 🟢)3.008s (-3.2%)0.477s101.94x
💻 LocalNitro2.671s (+10.9% 🔺)3.110s (+6.5% 🔺)0.439s102.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.780s (-9.7% 🟢)4.586s (-11.5% 🟢)1.806s71.00x
▲ VercelExpress3.603s (-0.9%)5.649s (+9.4% 🔺)2.047s61.30x
▲ VercelNext.js (Turbopack)4.146s (+24.1% 🔺)5.917s (+17.3% 🔺)1.772s61.49x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (~)4.135s (-3.9%)2.553s81.00x
🐘 PostgresNitro1.627s (-0.8%)4.136s (-3.0%)2.508s81.03x
🐘 PostgresNext.js (Turbopack)2.703s (-4.0%)4.582s (-16.9% 🟢)1.879s71.71x
💻 LocalNitro6.077s (-5.7% 🟢)6.818s (-5.5% 🟢)0.741s53.84x
💻 LocalExpress6.158s (-2.6%)6.819s (-2.8%)0.661s53.89x
💻 LocalNext.js (Turbopack)7.337s (+34.1% 🔺)8.017s (+33.3% 🔺)0.680s44.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.321s (+24.1% 🔺)6.144s (+18.1% 🔺)1.824s51.00x
▲ VercelNitro5.108s (+52.3% 🔺)6.685s (+30.6% 🔺)1.578s51.18x
▲ VercelExpress5.501s (+79.5% 🔺)7.636s (+51.4% 🔺)2.135s51.27x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.189s (+0.9%)2.007s (~)0.819s151.00x
🐘 PostgresNitro1.203s (~)2.009s (~)0.806s151.01x
🐘 PostgresNext.js (Turbopack)1.225s (+4.7%)2.015s (~)0.790s151.03x
💻 LocalExpress1.415s (-1.6%)2.007s (~)0.592s151.19x
💻 LocalNext.js (Turbopack)1.455s (+3.7%)2.006s (~)0.552s151.22x
💻 LocalNitro1.472s (+3.5%)2.007s (~)0.535s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.605s (+26.2% 🔺)4.403s (+25.9% 🔺)1.799s71.00x
▲ VercelNext.js (Turbopack)2.633s (-54.5% 🟢)4.124s (-44.6% 🟢)1.491s81.01x
▲ VercelNitro2.750s (+28.3% 🔺)4.412s (+18.7% 🔺)1.661s71.06x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.318s (-4.0%)2.393s (-3.2%)1.075s131.00x
🐘 PostgresExpress1.401s (+4.5%)2.676s (+8.3% 🔺)1.274s121.06x
🐘 PostgresNext.js (Turbopack)1.487s (+15.3% 🔺)2.515s (-13.8% 🟢)1.028s121.13x
💻 LocalNitro2.433s (-5.8% 🟢)3.009s (-3.2%)0.576s101.85x
💻 LocalNext.js (Turbopack)2.587s (-1.9%)3.109s (+3.3%)0.522s101.96x
💻 LocalExpress2.643s (+9.2% 🔺)3.008s (-3.2%)0.365s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.822s (-41.1% 🟢)5.218s (-21.4% 🟢)2.396s61.00x
▲ VercelNitro3.363s (+23.9% 🔺)5.021s (+16.3% 🔺)1.658s71.19x
▲ VercelNext.js (Turbopack)3.396s (-28.4% 🟢)5.116s (-26.4% 🟢)1.721s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.599s (-3.7%)4.138s (+3.1%)2.539s81.00x
🐘 PostgresExpress1.658s (+4.6%)4.011s (-3.0%)2.353s81.04x
🐘 PostgresNext.js (Turbopack)2.514s (-12.1% 🟢)5.041s (-13.8% 🟢)2.527s61.57x
💻 LocalExpress5.944s (-11.3% 🟢)6.415s (-11.1% 🟢)0.472s53.72x
💻 LocalNitro6.441s (-5.0% 🟢)7.015s (-2.8%)0.574s54.03x
💻 LocalNext.js (Turbopack)7.009s (+22.5% 🔺)8.020s (+33.3% 🔺)1.010s44.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.521s (+1.9%)5.720s (+13.6% 🔺)2.199s61.00x
▲ VercelNext.js (Turbopack)4.714s (+50.9% 🔺)6.326s (+22.5% 🔺)1.611s51.34x
▲ VercelExpress4.774s (+1.8%)7.280s (+15.7% 🔺)2.506s51.36x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.546s (+6.9% 🔺)1.027s (+2.1%)0.481s591.00x
🐘 PostgresExpress0.557s (+4.5%)1.041s (+1.7%)0.483s581.02x
🐘 PostgresNitro0.563s (-5.3% 🟢)1.024s (-1.7%)0.461s591.03x
💻 LocalExpress0.570s (~)1.005s (~)0.435s601.04x
💻 LocalNitro0.585s (~)1.005s (~)0.420s601.07x
💻 LocalNext.js (Turbopack)0.673s (+11.5% 🔺)1.058s (+3.5%)0.385s571.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.301s (+24.8% 🔺)4.990s (+16.1% 🔺)1.689s131.00x
▲ VercelExpress3.342s (+19.4% 🔺)5.325s (+18.7% 🔺)1.983s121.01x
▲ VercelNext.js (Turbopack)3.380s (+11.2% 🔺)4.843s (-3.1%)1.463s131.02x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.237s (-1.1%)2.007s (~)0.770s451.00x
🐘 PostgresNitro1.333s (-5.7% 🟢)2.030s (-1.2%)0.696s451.08x
💻 LocalExpress1.425s (-2.1%)2.006s (~)0.581s451.15x
💻 LocalNext.js (Turbopack)1.474s (~)2.006s (~)0.532s451.19x
🐘 PostgresNext.js (Turbopack)1.500s (+20.5% 🔺)2.105s (+3.7%)0.605s431.21x
💻 LocalNitro1.565s (+5.5% 🔺)2.052s (+2.3%)0.488s441.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.650s (+19.6% 🔺)9.459s (+21.8% 🔺)1.808s101.00x
▲ VercelExpress7.692s (+19.0% 🔺)9.613s (+19.6% 🔺)1.921s101.01x
▲ VercelNext.js (Turbopack)9.201s (+28.8% 🔺)11.021s (+24.0% 🔺)1.820s91.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.481s (-4.4%)3.033s (-1.7%)0.552s401.00x
🐘 PostgresNitro2.669s (-1.7%)3.086s (+0.9%)0.417s391.08x
💻 LocalExpress3.122s (-3.8%)3.821s (-3.9%)0.699s321.26x
💻 LocalNitro3.154s (-2.0%)3.978s (-0.8%)0.824s311.27x
🐘 PostgresNext.js (Turbopack)3.207s (+29.5% 🔺)3.749s (+22.6% 🔺)0.542s331.29x
💻 LocalNext.js (Turbopack)3.281s (+0.6%)3.977s (-1.6%)0.696s311.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express15.239s (+10.5% 🔺)17.918s (+15.9% 🔺)2.679s71.00x
▲ VercelNitro15.788s (+12.3% 🔺)18.302s (+17.1% 🔺)2.515s71.04x
▲ VercelNext.js (Turbopack)18.176s (+10.8% 🔺)20.378s (+10.2% 🔺)2.202s61.19x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.197s (-6.3% 🟢)1.006s (~)0.809s601.00x
🐘 PostgresNext.js (Turbopack)0.213s (+27.2% 🔺)1.010s (~)0.797s601.08x
🐘 PostgresNitro0.222s (-2.1%)1.007s (~)0.784s601.13x
💻 LocalExpress0.463s (+7.4% 🔺)1.005s (~)0.541s602.35x
💻 LocalNitro0.465s (-0.7%)1.004s (~)0.539s602.36x
💻 LocalNext.js (Turbopack)0.613s (+5.0%)1.005s (~)0.391s603.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.257s (-42.1% 🟢)2.609s (-32.7% 🟢)1.352s231.00x
▲ VercelExpress1.375s (-33.7% 🟢)3.160s (-17.7% 🟢)1.785s191.09x
▲ VercelNext.js (Turbopack)1.468s (-19.3% 🟢)2.862s (-19.3% 🟢)1.395s211.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.314s (-2.7%)1.006s (-1.1%)0.692s901.00x
🐘 PostgresNitro0.336s (~)1.006s (~)0.671s901.07x
🐘 PostgresNext.js (Turbopack)0.347s (+29.3% 🔺)1.017s (+1.1%)0.670s891.10x
💻 LocalExpress2.137s (-1.1%)2.735s (~)0.598s336.80x
💻 LocalNitro2.192s (+2.8%)2.796s (+4.1%)0.604s336.97x
💻 LocalNext.js (Turbopack)2.734s (-2.0%)3.295s (+1.1%)0.560s288.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.855s (-2.7%)3.708s (+6.3% 🔺)1.853s251.00x
▲ VercelExpress2.119s (-14.0% 🟢)4.166s (+2.8%)2.047s221.14x
▲ VercelNext.js (Turbopack)2.680s (+23.5% 🔺)4.477s (+14.0% 🔺)1.797s211.44x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.531s (+0.5%)1.050s (+1.7%)0.519s1151.00x
🐘 PostgresExpress0.567s (+9.8% 🔺)1.160s (+8.6% 🔺)0.594s1041.07x
🐘 PostgresNext.js (Turbopack)0.699s (+48.1% 🔺)1.975s (-34.3% 🟢)1.277s611.32x
💻 LocalExpress9.304s (-7.8% 🟢)10.360s (-6.9% 🟢)1.056s1217.52x
💻 LocalNext.js (Turbopack)9.579s (-2.1%)10.530s (-2.3%)0.951s1218.04x
💻 LocalNitro10.363s (+4.5%)11.302s (+2.5%)0.940s1119.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.326s (+39.1% 🔺)5.768s (+42.3% 🔺)2.443s211.00x
▲ VercelNitro3.641s (+43.2% 🔺)5.915s (+43.4% 🔺)2.274s211.09x
▲ VercelNext.js (Turbopack)4.725s (+0.8%)7.116s (+8.7% 🔺)2.391s171.42x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.137s (-2.7%)1.968s (~)0.011s (-14.5% 🟢)2.018s (~)0.881s101.00x
💻 LocalExpress1.151s (~)2.005s (~)0.010s (-17.5% 🟢)2.017s (~)0.866s101.01x
🐘 PostgresNitro1.156s (-0.9%)1.999s (~)0.001s (+18.2% 🔺)2.010s (~)0.854s101.02x
💻 LocalNitro1.159s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.862s101.02x
🐘 PostgresExpress1.179s (+2.6%)2.000s (~)0.001s (+10.0% 🔺)2.011s (~)0.831s101.04x
🐘 PostgresNext.js (Turbopack)1.209s (+5.5% 🔺)1.996s (~)0.001s (-27.3% 🟢)2.009s (~)0.800s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.129s (+4.0%)3.553s (+15.8% 🔺)2.488s (+42.0% 🔺)6.571s (+21.6% 🔺)4.441s101.00x
▲ VercelNext.js (Turbopack)2.206s (+6.8% 🔺)3.312s (-4.0%)2.584s (+83.3% 🔺)6.338s (+17.2% 🔺)4.132s101.04x
▲ VercelNitro2.374s (+7.0% 🔺)3.275s (~)2.368s (+27.1% 🔺)6.269s (+10.1% 🔺)3.894s101.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.509s (-2.5%)2.010s (~)0.013s (-1.3%)2.026s (~)0.517s301.00x
💻 LocalNext.js (Turbopack)1.563s (-0.9%)1.971s (~)0.012s (-5.4% 🟢)2.025s (~)0.462s301.04x
💻 LocalNitro1.564s (+0.7%)2.010s (~)0.013s (+9.4% 🔺)2.026s (~)0.462s301.04x
🐘 PostgresExpress1.568s (+1.4%)2.010s (~)0.005s (-4.8%)2.027s (~)0.459s301.04x
🐘 PostgresNitro1.577s (-1.8%)2.003s (~)0.005s (~)2.027s (~)0.450s301.04x
🐘 PostgresNext.js (Turbopack)2.009s (+30.4% 🔺)2.447s (+21.7% 🔺)0.004s (-20.0% 🟢)2.478s (+22.3% 🔺)0.469s251.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.025s (+1.4%)7.125s (-3.4%)0.324s (+1.9%)7.978s (-3.4%)1.953s81.00x
▲ VercelExpress6.435s (+9.3% 🔺)7.891s (+9.5% 🔺)0.226s (+4.4%)8.797s (+11.6% 🔺)2.363s71.07x
▲ VercelNitro6.500s (+7.0% 🔺)7.678s (+8.5% 🔺)0.459s (+102.8% 🔺)8.656s (+10.6% 🔺)2.156s71.08x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.782s (+1.0%)1.071s (+2.2%)0.000s (+Infinity% 🔺)1.088s (+2.3%)0.307s561.00x
🐘 PostgresNitro0.821s (~)1.062s (-3.3%)0.000s (-50.9% 🟢)1.092s (-2.4%)0.271s551.05x
🐘 PostgresNext.js (Turbopack)1.037s (+13.1% 🔺)1.437s (+12.6% 🔺)0.000s (+14.6% 🔺)1.473s (+14.7% 🔺)0.436s411.33x
💻 LocalExpress1.209s (-4.7%)1.949s (-3.2%)0.000s (+214.5% 🔺)1.951s (-3.2%)0.743s311.55x
💻 LocalNitro1.280s (-2.9%)1.981s (~)0.001s (+41.7% 🔺)1.984s (~)0.704s311.64x
💻 LocalNext.js (Turbopack)1.411s (+5.7% 🔺)1.980s (~)0.001s (+183.3% 🔺)2.016s (~)0.605s301.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.353s (+7.5% 🔺)4.326s (-1.9%)0.000s (+Infinity% 🔺)4.782s (-1.9%)1.430s131.00x
▲ VercelExpress3.737s (-4.4%)5.137s (+2.2%)0.000s (-100.0% 🟢)5.715s (+3.2%)1.978s111.11x
▲ VercelNext.js (Turbopack)4.240s (+28.8% 🔺)5.616s (+19.4% 🔺)0.000s (+20.0% 🔺)6.124s (+16.5% 🔺)1.884s101.26x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.605s (-5.3% 🟢)2.171s (-3.7%)0.000s (-100.0% 🟢)2.211s (-2.6%)0.607s281.00x
🐘 PostgresNitro1.774s (-7.5% 🟢)2.339s (-6.3% 🟢)0.000s (-100.0% 🟢)2.355s (-6.6% 🟢)0.581s261.11x
🐘 PostgresNext.js (Turbopack)2.353s (+15.8% 🔺)2.908s (+11.3% 🔺)0.000s (-100.0% 🟢)2.925s (+10.7% 🔺)0.572s211.47x
💻 LocalExpress3.387s (-6.8% 🟢)4.027s (-4.8%)0.001s (+50.0% 🔺)4.031s (-4.7%)0.644s152.11x
💻 LocalNext.js (Turbopack)3.389s (-7.5% 🟢)3.869s (-7.7% 🟢)0.001s (-12.1% 🟢)3.912s (-7.6% 🟢)0.523s162.11x
💻 LocalNitro3.475s (-2.5%)4.160s (+3.3%)0.000s (-58.8% 🟢)4.163s (+3.3%)0.689s152.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.911s (+27.6% 🔺)7.295s (+21.2% 🔺)0.001s (+150.0% 🔺)7.812s (+18.7% 🔺)1.901s81.00x
▲ VercelExpress6.192s (+30.8% 🔺)7.470s (+30.1% 🔺)0.000s (+25.0% 🔺)8.018s (+28.2% 🔺)1.826s81.05x
▲ VercelNitro6.213s (+27.0% 🔺)7.485s (+30.9% 🔺)0.000s (NaN%)7.991s (+29.2% 🔺)1.778s81.05x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress18/21
🐘 PostgresExpress11/21
▲ VercelExpress8/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres15/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

Enabling H2 on the single shared dispatcher hung the queue's webhook
respondWith mechanism (120s E2E timeouts on webhook/hook workflows
across frameworks). Instead, give the v4 events API its own
H2-enabled dispatcher (getEventsDispatcher) and keep the default
dispatcher — queue/webhook, v3 makeRequest, streaming, deployment
resolution, run-key fetch — on HTTP/1.1.
The events endpoints are the hottest path (an event write per step
transition plus log reads) and are plain request/response (or a
streamed LIST response), none of which trip the webhook/duplex H2
issues, so they get multiplexing safely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SvelteKit bundles the server (including undici via the world adapter) into
ESM output. undici loads node: builtins as ESM imports except node:http2,
which it pulls in lazily via a bare require('node:http2') inside a try/catch.
The bundler leaves that bare require un-wired, so in the ESM bundle it throws
and undici silently falls back to a stub whose http2.connect is undefined —
breaking every HTTP/2 request (the workflow flow-route callback fails with
"fetch failed", so runs never start).
Inject a createRequire-backed global require into the SSR build only (a
node:module import would break the client bundle) so the real node:http2
resolves. Detected via configResolved.build.ssr because SvelteKit does not
set env.isSsrBuild for its server pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VaguelySeriousand others added 2 commits June 23, 2026 17:32
Nitro bundles undici (via the world adapter) into the ESM server output.
undici loads node: builtins as ESM imports except node:http2, which it pulls
in lazily via a bare require('node:http2') inside a try/catch. The bundler
leaves that bare require un-wired, so in the ESM bundle it throws and undici
silently falls back to a stub whose http2.connect is undefined — breaking
every HTTP/2 request (the workflow flow-route callback fails with "fetch
failed", so runs never start). This affects all nitro-based builds including
the Vite and TanStack Start framework integrations.
Prepend a createRequire-backed global require to the server chunks (via the
rollup:before hook, production builds only) so the real node:http2 resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream writes (write/writeMulti/close) previously used plain fetch() on
HTTP/1.1. Route them through a dedicated H2 stream dispatcher: the PUT
requests send a fully-buffered body (or none), so they gain H2 multiplexing
without the duplex-streaming issues that keep the long-lived live-read (GET)
on plain fetch. The stream dispatcher is a bare undici Agent without the
RetryAgent wrapper — stream appends are not idempotent, so undici-level retry
is deliberately omitted to avoid duplicating chunks (preserving the streamer's
original no-retry behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
headers: httpConfig.headers,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- undici dispatcher type doesn't match @types/node's RequestInit
dispatcher: getStreamDispatcher(config),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] I would block this until the Vercel Nitro v3 benchmark is rerun green or root-caused. The current Benchmark Vercel (nitro-v3) job fails with Stream 1 correctness failure: expected 1048576 bytes, got 983040, exactly one 64 KiB chunk missing from the stream benchmark. Since this PR changes stream PUTs to use the H2 dispatcher here, this looks like a plausible stream-write correctness regression rather than a pure perf failure. Job: https://github.com/vercel/workflow/actions/runs/28066956084/job/83093680965

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passed on re-run

* getDefaultStreamDispatcher for why this is a bare Agent (no retry).
*/
export function getStreamDispatcher(config?: APIConfig): unknown {
return config?.dispatcher ?? getDefaultStreamDispatcher();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This keeps the default stream dispatcher retry-free, but config.dispatcher now also controls stream writes. A caller that passes an undici RetryAgent can re-enable retries for stream PUTs; undici retries PUT by default, and stream appends are not idempotent, so that can duplicate chunks. Can we document this in the custom dispatcher section, or split out a stream-specific dispatcher option / guard so retrying dispatchers are not used for stream writes?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Good catch. Rather than leave stream writes retry-free, I scoped the retry policy so the win (resilience to transient blips) survives without the duplicate-chunk risk:

  • The default stream dispatcher is now a RetryAgent with STREAM_RETRY_OPTIONS = { methods: ['PUT'], statusCodes: [429], retryAfter: true }. It retries only on transient connection errors (undici's default errorCodes) and HTTP 429 — both guarantee the chunk was not persisted. 5xx is excluded (a 5xx can mean the chunk was written but the response failed → retry would duplicate), as are other 4xx.
  • For the caller-override case you flagged: documented in the dispatcher JSDoc (APIConfig in utils.ts) that a custom dispatcher replaces every default including stream writes, and should not retry PUT on 5xx.

Added a regression test asserting statusCodes is exactly [429] and never contains 500/502/503/504.

*/
function addNodeRequireBanner(config: RollupConfig): void {
const banner =
"import { createRequire as __wkfCreateRequire } from 'node:module'; if (typeof require === 'undefined') { globalThis.require = __wkfCreateRequire(import.meta.url); }";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This fixes the bundled undici node:http2 issue, but it also globally changes server-bundle feature detection: dependencies that branch on typeof require will now take their CJS/require path in these ESM bundles. The tested Vercel Node lanes passing is reassuring, but can we call this out as a Node-runtime-only server-bundle shim and keep an eye on optional-require behavior in bundled libraries?

@VaguelySeriousVaguelySeriousJun 24, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Agreed — that global feature-detection change is the right thing to flag. I kept the global globalThis.require (it's required: undici's bundled bare require(...) resolves against the runtime global scope, so a module-local const require wouldn't reliably be in scope for the inlined undici code), but tightened the JSDoc in both nitro/index.ts and sveltekit/plugin.ts to call it out explicitly as a Node-server-runtime-only shim and document the side effect + why it's safe:

  • Server-only — gated to the production server build (!nitro.options.dev / config.build.ssr); never touches the client/browser bundle.
  • Guarded — the typeof require === 'undefined' check makes it a no-op in CJS chunks where a real require already exists; it only fills a genuine ESM gap.
  • Functional — the require we install is a working createRequire, so a lib that switches to the require path gets a real require, not a broken stub (the common try { require(optional) } catch {} pattern still degrades gracefully).

The comment also records the one residual behavior to watch, per your ask: a bundled lib that, on detecting require, does require() of an ESM-only dependency on a Node version without require(ESM) support. The Vercel Node E2E lanes exercise the real undici path that motivated this. Comment-only change in b2ec9d7.

… 429
Stream writes (PUT) append chunks and are not idempotent. The stream
dispatcher was a bare Agent with no retry; switch it to a RetryAgent whose
policy is narrowed so PUTs retry only on transient connection errors and
HTTP 429 — both of which guarantee the chunk was not persisted — and never
on 5xx (where the chunk may already have been written) or other 4xx.
Also document that a caller-supplied `config.dispatcher` overrides the
stream dispatcher and should not retry PUT on 5xx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/world-vercel/src/streamer.ts Outdated
vercelBotand others added 2 commits June 24, 2026 17:34
…nt (no retry)" / "intentionally retry-free", contradicting the actual `RetryAgent` + `STREAM_RETRY_OPTIONS` implementation in a non-idempotent write path.
This commit fixes the issue reported at packages/world-vercel/src/streamer.ts:26
## The issue
`getDefaultStreamDispatcher` in `packages/world-vercel/src/http-client.ts` constructs a `RetryAgent` wrapping `STREAM_RETRY_OPTIONS` (`retryAfter: true`, `methods: ['PUT']`, `statusCodes: [429]`). So the stream-write dispatcher **does** retry — on undici's default transient connection errors and on HTTP 429.
Two comments still claimed the opposite:
1. `packages/world-vercel/src/streamer.ts` (~line 26):
> "That stream dispatcher is intentionally retry-free — stream appends aren't idempotent, so a retried write would duplicate chunks."
2. `packages/world-vercel/src/http-client.ts` (~line 102, JSDoc on `getStreamDispatcher`):
> "See getDefaultStreamDispatcher for why this is a bare Agent (no retry)."
Both are factually wrong: the dispatcher is a `RetryAgent`, not a bare `Agent`, and it is not retry-free. The accurate, nuanced description already lives in the `STREAM_RETRY_OPTIONS` and `getDefaultStreamDispatcher` JSDoc — these two comments were simply not updated when the behavior changed.
## Why it matters
This is the **non-idempotent write path**. A maintainer reasoning about correctness here relies on these comments to understand exactly what is and isn't retried. "Retry-free" vs. "retries on transient errors + 429 but never 5xx" is a meaningful safety distinction, so the stale wording actively misleads.
## The fix
Comment-only changes (no behavior change):
- **streamer.ts**: replaced "intentionally retry-free" with wording reflecting the narrowed policy — retries only on transient connection errors + HTTP 429 (both guaranteeing the chunk was never persisted), never 5xx, so a retry can't duplicate an already-applied write. Points to `STREAM_RETRY_OPTIONS`.
- **http-client.ts**: replaced "why this is a bare Agent (no retry)" with a reference to the deliberately narrowed retry policy in `getDefaultStreamDispatcher` / `STREAM_RETRY_OPTIONS`.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
…ide effect
Document that the createRequire banner deliberately makes `typeof require`
truthy for every bundled dependency in the ESM server output, why that is
safe (server-only, guarded, functional require), and the one residual
behavior to watch (require() of an ESM-only dep on old Node).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2615. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [world-vercel] Enable HTTP/2 for the events API and stream writes by VaguelySerious · Pull Request #2573 · vercel/workflow · GitHub
Skip to content

[world-vercel] Enable HTTP/2 for the events API and stream writes - #2573

Merged
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2
Jun 24, 2026
Merged

[world-vercel] Enable HTTP/2 for the events API and stream writes#2573
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

Enable HTTP/2 for two hot @workflow/world-vercel request paths, and fix the framework bundling that previously made H2 impossible on Vite-based adapters.

  • v4 events API (events-v4.ts) — an event write per step transition plus event-log reads on replay. Gets its own H2 undici dispatcher (getEventsDispatcher).
  • Stream writes (streamer.tswrite/writeMulti/close — the PUT path) — routed through a dedicated H2 stream dispatcher (getStreamDispatcher). These send a fully-buffered body (or none), so they get H2 multiplexing without the duplex issues that keep the long-lived live-read (GET) on plain fetch. The stream dispatcher is a bare Agentwithout the RetryAgent wrapper: stream appends aren't idempotent, so undici-level retry is deliberately omitted to avoid duplicating chunks.

Everything else stays on HTTP/1.1 (queue client / webhook respondWith, v3 makeRequest, deployment resolution, run-key fetch, live-read GET, list).

The bundling fix (why H2 was blocked before)

H2 had been disabled because it "broke SvelteKit on Vercel prod." Root cause: Vite/Rollup-based framework builds (SvelteKit, TanStack Start, plain Vite) bundle undici into the ESM server output. undici imports most node: builtins as ESM, but loads node:http2 lazily via a bare require('node:http2') inside a try/catch. The bundler leaves that bare require un-wired, so in the ESM bundle it throws and undici silently falls back to a stub whose http2.connect is undefined. Any H2 request then fails with fetch failed → the workflow flow-route callback can't reach the backend → runs never leave pending → 30-minute E2E timeouts. Frameworks that externalize undici (Express, Astro, Next, Hono, Nitro, …) were unaffected.

Fix: inject a createRequire-backed global require into the server build (server-only — a node:module import would break the client bundle) so the real node:http2 resolves:

  • @workflow/sveltekit — via configResolved (SvelteKit doesn't set env.isSsrBuild for its server pass; build.ssr on the resolved config is the reliable signal).
  • @workflow/nitro — via the existing rollup:before hook, covering Vite, TanStack Start, and Nitro from one place.

Verified end-to-end: with the fix, the sveltekit / vite / tanstack-start E2E Vercel Prod lanes pass, and http2.connect is not a function errors drop to zero (previously deterministic on this branch only).

Why the default dispatcher stays H1

Enabling H2 on the shared dispatcher deadlocks the queue's webhook respondWith mechanism (120s timeouts on the webhook/hook workflows). So H2 is scoped to the events + stream-write paths, which don't use that mechanism.

Tests

  • Regression guards: EVENTS_AGENT_OPTIONS.allowH2 === trueandDEFAULT_AGENT_OPTIONS.allowH2 === false.
  • getEventsDispatcher and getStreamDispatcher each return their own shared singleton — distinct from the default and from each other — and honor a caller-supplied dispatcher.
  • Local node:http2 TLS server test: a request through the global fetch with an allowH2 dispatcher negotiates h2 (the mechanism these paths rely on).
  • Full @workflow/world-vercel unit suite passes (172 tests).

Notes

  • events-v4.ts and the stream writes stay on the Vercel-patched global fetch (preserves observability instrumentation); only the dispatcher they pass changes.
  • The sleepingWorkflow and distributedAbortController failures seen on some lanes are pre-existing flakes (zero-tolerance server-side sleep timing / abort timeout) that also fail on main — unrelated to this change.

🤖 Generated with Claude Code

Flip allowH2 to true in the shared undici dispatcher so every
dispatcher-backed path (v3 makeRequest, v4 events, the queue client,
deployment resolution, run-key fetch) negotiates HTTP/2. H2 was
previously disabled for a SvelteKit-on-Vercel-prod hang; re-enable it
globally and rely on the E2E suite to guard the regression. Streaming
paths in streamer.ts intentionally bypass the dispatcher and are
unaffected.
Adds a regression guard on the agent options plus a local http2 server
test proving global fetch honors an allowH2 dispatcher (the mechanism
the v4 events path relies on).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJune 22, 2026 21:51
@vercel

vercelBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2ec9d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/world-vercelPatch
@workflow/nuxtPatch
workflowPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7282010788360

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ nitro-stable126026
✅ nuxt-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.045s (-5.3% 🟢)1.006s (~)0.961s101.00x
💻 LocalNitro0.047s (+0.6%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.050s (-4.2%)1.006s (~)0.955s101.12x
🐘 PostgresNitro0.065s (-0.6%)1.012s (~)0.947s101.44x
🐘 PostgresExpress0.066s (-4.5%)1.013s (~)0.946s101.47x
🐘 PostgresNext.js (Turbopack)0.067s (+20.8% 🔺)1.028s (+1.6%)0.962s101.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.232s (~)1.976s (-6.1% 🟢)1.744s101.00x
▲ VercelNext.js (Turbopack)0.260s (-3.8%)2.042s (~)1.782s101.12x
▲ VercelNitro0.301s (+19.7% 🔺)2.042s (-5.9% 🟢)1.741s101.30x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.078s (-0.8%)2.006s (~)0.928s101.00x
💻 LocalNext.js (Turbopack)1.079s (~)2.006s (~)0.927s101.00x
🐘 PostgresNext.js (Turbopack)1.079s (~)2.009s (~)0.930s101.00x
💻 LocalNitro1.082s (~)2.007s (~)0.925s101.00x
🐘 PostgresExpress1.097s (~)2.009s (~)0.912s101.02x
🐘 PostgresNitro1.100s (~)2.009s (~)0.909s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.421s (-0.9%)3.085s (-8.0% 🟢)1.664s101.00x
▲ VercelExpress1.445s (+4.5%)3.251s (+14.9% 🔺)1.806s101.02x
▲ VercelNitro1.468s (+4.0%)3.195s (~)1.727s101.03x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)10.420s (~)11.026s (~)0.606s31.00x
💻 LocalExpress10.435s (~)11.023s (~)0.588s31.00x
💻 LocalNitro10.455s (~)11.024s (~)0.569s31.00x
🐘 PostgresExpress10.460s (~)11.021s (~)0.560s31.00x
🐘 PostgresNitro10.476s (~)11.017s (~)0.541s31.01x
💻 LocalNext.js (Turbopack)10.504s (~)11.022s (~)0.518s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.937s (+1.8%)13.411s (~)1.473s31.00x
▲ VercelNext.js (Turbopack)12.028s (+1.9%)13.455s (-3.5%)1.427s31.01x
▲ VercelExpress12.325s (~)14.494s (+4.5%)2.169s31.03x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.554s (~)14.028s (~)0.473s51.00x
🐘 PostgresExpress13.559s (~)14.020s (~)0.461s51.00x
💻 LocalNext.js (Turbopack)13.625s (~)14.028s (~)0.403s51.01x
💻 LocalNitro13.734s (+0.7%)14.030s (~)0.295s51.01x
🐘 PostgresNext.js (Turbopack)13.750s (+0.6%)14.245s (+1.6%)0.495s51.01x
🐘 PostgresNitro13.773s (~)14.023s (~)0.250s51.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express18.265s (+11.1% 🔺)20.019s (+11.2% 🔺)1.754s31.00x
▲ VercelNext.js (Turbopack)18.771s (+8.2% 🔺)20.473s (+6.9% 🔺)1.702s41.03x
▲ VercelNitro19.188s (+17.2% 🔺)20.559s (+16.0% 🔺)1.372s31.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express12.044s (-2.4%)12.774s (-1.9%)0.730s81.00x
🐘 PostgresNext.js (Turbopack)12.160s (~)12.869s (-1.1%)0.709s71.01x
🐘 PostgresExpress12.263s (+1.3%)13.019s (~)0.757s71.02x
💻 LocalNitro12.272s (+0.7%)13.025s (~)0.753s71.02x
🐘 PostgresNitro12.273s (~)13.018s (~)0.746s71.02x
💻 LocalNext.js (Turbopack)12.414s (+1.3%)13.026s (~)0.612s71.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)22.253s (+15.0% 🔺)24.838s (+17.0% 🔺)2.586s41.00x
▲ VercelExpress22.303s (+21.4% 🔺)25.182s (+24.9% 🔺)2.879s41.00x
▲ VercelNitro22.749s (+19.8% 🔺)25.174s (+22.0% 🔺)2.425s41.02x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.172s (~)2.007s (~)0.835s151.00x
🐘 PostgresNitro1.192s (+0.9%)2.008s (~)0.815s151.02x
🐘 PostgresNext.js (Turbopack)1.215s (+3.2%)2.009s (~)0.794s151.04x
💻 LocalExpress1.351s (-2.7%)2.006s (~)0.655s151.15x
💻 LocalNitro1.423s (-1.1%)2.007s (~)0.584s151.21x
💻 LocalNext.js (Turbopack)1.444s (-3.6%)2.007s (~)0.563s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.284s (+12.5% 🔺)3.862s (+10.3% 🔺)1.578s81.00x
▲ VercelNitro2.364s (+11.0% 🔺)3.813s (+10.7% 🔺)1.448s81.04x
▲ VercelNext.js (Turbopack)2.432s (+13.1% 🔺)3.659s (-3.4%)1.228s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.302s (-2.8%)2.317s (-7.6% 🟢)1.015s131.00x
🐘 PostgresExpress1.332s (-4.4%)2.223s (-16.9% 🟢)0.891s141.02x
🐘 PostgresNext.js (Turbopack)1.468s (+11.8% 🔺)2.685s (-8.0% 🟢)1.217s121.13x
💻 LocalExpress2.446s (+3.7%)2.826s (-3.1%)0.381s111.88x
💻 LocalNext.js (Turbopack)2.530s (-7.5% 🟢)3.008s (-3.2%)0.477s101.94x
💻 LocalNitro2.671s (+10.9% 🔺)3.110s (+6.5% 🔺)0.439s102.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.780s (-9.7% 🟢)4.586s (-11.5% 🟢)1.806s71.00x
▲ VercelExpress3.603s (-0.9%)5.649s (+9.4% 🔺)2.047s61.30x
▲ VercelNext.js (Turbopack)4.146s (+24.1% 🔺)5.917s (+17.3% 🔺)1.772s61.49x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (~)4.135s (-3.9%)2.553s81.00x
🐘 PostgresNitro1.627s (-0.8%)4.136s (-3.0%)2.508s81.03x
🐘 PostgresNext.js (Turbopack)2.703s (-4.0%)4.582s (-16.9% 🟢)1.879s71.71x
💻 LocalNitro6.077s (-5.7% 🟢)6.818s (-5.5% 🟢)0.741s53.84x
💻 LocalExpress6.158s (-2.6%)6.819s (-2.8%)0.661s53.89x
💻 LocalNext.js (Turbopack)7.337s (+34.1% 🔺)8.017s (+33.3% 🔺)0.680s44.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.321s (+24.1% 🔺)6.144s (+18.1% 🔺)1.824s51.00x
▲ VercelNitro5.108s (+52.3% 🔺)6.685s (+30.6% 🔺)1.578s51.18x
▲ VercelExpress5.501s (+79.5% 🔺)7.636s (+51.4% 🔺)2.135s51.27x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.189s (+0.9%)2.007s (~)0.819s151.00x
🐘 PostgresNitro1.203s (~)2.009s (~)0.806s151.01x
🐘 PostgresNext.js (Turbopack)1.225s (+4.7%)2.015s (~)0.790s151.03x
💻 LocalExpress1.415s (-1.6%)2.007s (~)0.592s151.19x
💻 LocalNext.js (Turbopack)1.455s (+3.7%)2.006s (~)0.552s151.22x
💻 LocalNitro1.472s (+3.5%)2.007s (~)0.535s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.605s (+26.2% 🔺)4.403s (+25.9% 🔺)1.799s71.00x
▲ VercelNext.js (Turbopack)2.633s (-54.5% 🟢)4.124s (-44.6% 🟢)1.491s81.01x
▲ VercelNitro2.750s (+28.3% 🔺)4.412s (+18.7% 🔺)1.661s71.06x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.318s (-4.0%)2.393s (-3.2%)1.075s131.00x
🐘 PostgresExpress1.401s (+4.5%)2.676s (+8.3% 🔺)1.274s121.06x
🐘 PostgresNext.js (Turbopack)1.487s (+15.3% 🔺)2.515s (-13.8% 🟢)1.028s121.13x
💻 LocalNitro2.433s (-5.8% 🟢)3.009s (-3.2%)0.576s101.85x
💻 LocalNext.js (Turbopack)2.587s (-1.9%)3.109s (+3.3%)0.522s101.96x
💻 LocalExpress2.643s (+9.2% 🔺)3.008s (-3.2%)0.365s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.822s (-41.1% 🟢)5.218s (-21.4% 🟢)2.396s61.00x
▲ VercelNitro3.363s (+23.9% 🔺)5.021s (+16.3% 🔺)1.658s71.19x
▲ VercelNext.js (Turbopack)3.396s (-28.4% 🟢)5.116s (-26.4% 🟢)1.721s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.599s (-3.7%)4.138s (+3.1%)2.539s81.00x
🐘 PostgresExpress1.658s (+4.6%)4.011s (-3.0%)2.353s81.04x
🐘 PostgresNext.js (Turbopack)2.514s (-12.1% 🟢)5.041s (-13.8% 🟢)2.527s61.57x
💻 LocalExpress5.944s (-11.3% 🟢)6.415s (-11.1% 🟢)0.472s53.72x
💻 LocalNitro6.441s (-5.0% 🟢)7.015s (-2.8%)0.574s54.03x
💻 LocalNext.js (Turbopack)7.009s (+22.5% 🔺)8.020s (+33.3% 🔺)1.010s44.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.521s (+1.9%)5.720s (+13.6% 🔺)2.199s61.00x
▲ VercelNext.js (Turbopack)4.714s (+50.9% 🔺)6.326s (+22.5% 🔺)1.611s51.34x
▲ VercelExpress4.774s (+1.8%)7.280s (+15.7% 🔺)2.506s51.36x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.546s (+6.9% 🔺)1.027s (+2.1%)0.481s591.00x
🐘 PostgresExpress0.557s (+4.5%)1.041s (+1.7%)0.483s581.02x
🐘 PostgresNitro0.563s (-5.3% 🟢)1.024s (-1.7%)0.461s591.03x
💻 LocalExpress0.570s (~)1.005s (~)0.435s601.04x
💻 LocalNitro0.585s (~)1.005s (~)0.420s601.07x
💻 LocalNext.js (Turbopack)0.673s (+11.5% 🔺)1.058s (+3.5%)0.385s571.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.301s (+24.8% 🔺)4.990s (+16.1% 🔺)1.689s131.00x
▲ VercelExpress3.342s (+19.4% 🔺)5.325s (+18.7% 🔺)1.983s121.01x
▲ VercelNext.js (Turbopack)3.380s (+11.2% 🔺)4.843s (-3.1%)1.463s131.02x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.237s (-1.1%)2.007s (~)0.770s451.00x
🐘 PostgresNitro1.333s (-5.7% 🟢)2.030s (-1.2%)0.696s451.08x
💻 LocalExpress1.425s (-2.1%)2.006s (~)0.581s451.15x
💻 LocalNext.js (Turbopack)1.474s (~)2.006s (~)0.532s451.19x
🐘 PostgresNext.js (Turbopack)1.500s (+20.5% 🔺)2.105s (+3.7%)0.605s431.21x
💻 LocalNitro1.565s (+5.5% 🔺)2.052s (+2.3%)0.488s441.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.650s (+19.6% 🔺)9.459s (+21.8% 🔺)1.808s101.00x
▲ VercelExpress7.692s (+19.0% 🔺)9.613s (+19.6% 🔺)1.921s101.01x
▲ VercelNext.js (Turbopack)9.201s (+28.8% 🔺)11.021s (+24.0% 🔺)1.820s91.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.481s (-4.4%)3.033s (-1.7%)0.552s401.00x
🐘 PostgresNitro2.669s (-1.7%)3.086s (+0.9%)0.417s391.08x
💻 LocalExpress3.122s (-3.8%)3.821s (-3.9%)0.699s321.26x
💻 LocalNitro3.154s (-2.0%)3.978s (-0.8%)0.824s311.27x
🐘 PostgresNext.js (Turbopack)3.207s (+29.5% 🔺)3.749s (+22.6% 🔺)0.542s331.29x
💻 LocalNext.js (Turbopack)3.281s (+0.6%)3.977s (-1.6%)0.696s311.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express15.239s (+10.5% 🔺)17.918s (+15.9% 🔺)2.679s71.00x
▲ VercelNitro15.788s (+12.3% 🔺)18.302s (+17.1% 🔺)2.515s71.04x
▲ VercelNext.js (Turbopack)18.176s (+10.8% 🔺)20.378s (+10.2% 🔺)2.202s61.19x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.197s (-6.3% 🟢)1.006s (~)0.809s601.00x
🐘 PostgresNext.js (Turbopack)0.213s (+27.2% 🔺)1.010s (~)0.797s601.08x
🐘 PostgresNitro0.222s (-2.1%)1.007s (~)0.784s601.13x
💻 LocalExpress0.463s (+7.4% 🔺)1.005s (~)0.541s602.35x
💻 LocalNitro0.465s (-0.7%)1.004s (~)0.539s602.36x
💻 LocalNext.js (Turbopack)0.613s (+5.0%)1.005s (~)0.391s603.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.257s (-42.1% 🟢)2.609s (-32.7% 🟢)1.352s231.00x
▲ VercelExpress1.375s (-33.7% 🟢)3.160s (-17.7% 🟢)1.785s191.09x
▲ VercelNext.js (Turbopack)1.468s (-19.3% 🟢)2.862s (-19.3% 🟢)1.395s211.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.314s (-2.7%)1.006s (-1.1%)0.692s901.00x
🐘 PostgresNitro0.336s (~)1.006s (~)0.671s901.07x
🐘 PostgresNext.js (Turbopack)0.347s (+29.3% 🔺)1.017s (+1.1%)0.670s891.10x
💻 LocalExpress2.137s (-1.1%)2.735s (~)0.598s336.80x
💻 LocalNitro2.192s (+2.8%)2.796s (+4.1%)0.604s336.97x
💻 LocalNext.js (Turbopack)2.734s (-2.0%)3.295s (+1.1%)0.560s288.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.855s (-2.7%)3.708s (+6.3% 🔺)1.853s251.00x
▲ VercelExpress2.119s (-14.0% 🟢)4.166s (+2.8%)2.047s221.14x
▲ VercelNext.js (Turbopack)2.680s (+23.5% 🔺)4.477s (+14.0% 🔺)1.797s211.44x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.531s (+0.5%)1.050s (+1.7%)0.519s1151.00x
🐘 PostgresExpress0.567s (+9.8% 🔺)1.160s (+8.6% 🔺)0.594s1041.07x
🐘 PostgresNext.js (Turbopack)0.699s (+48.1% 🔺)1.975s (-34.3% 🟢)1.277s611.32x
💻 LocalExpress9.304s (-7.8% 🟢)10.360s (-6.9% 🟢)1.056s1217.52x
💻 LocalNext.js (Turbopack)9.579s (-2.1%)10.530s (-2.3%)0.951s1218.04x
💻 LocalNitro10.363s (+4.5%)11.302s (+2.5%)0.940s1119.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.326s (+39.1% 🔺)5.768s (+42.3% 🔺)2.443s211.00x
▲ VercelNitro3.641s (+43.2% 🔺)5.915s (+43.4% 🔺)2.274s211.09x
▲ VercelNext.js (Turbopack)4.725s (+0.8%)7.116s (+8.7% 🔺)2.391s171.42x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.137s (-2.7%)1.968s (~)0.011s (-14.5% 🟢)2.018s (~)0.881s101.00x
💻 LocalExpress1.151s (~)2.005s (~)0.010s (-17.5% 🟢)2.017s (~)0.866s101.01x
🐘 PostgresNitro1.156s (-0.9%)1.999s (~)0.001s (+18.2% 🔺)2.010s (~)0.854s101.02x
💻 LocalNitro1.159s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.862s101.02x
🐘 PostgresExpress1.179s (+2.6%)2.000s (~)0.001s (+10.0% 🔺)2.011s (~)0.831s101.04x
🐘 PostgresNext.js (Turbopack)1.209s (+5.5% 🔺)1.996s (~)0.001s (-27.3% 🟢)2.009s (~)0.800s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.129s (+4.0%)3.553s (+15.8% 🔺)2.488s (+42.0% 🔺)6.571s (+21.6% 🔺)4.441s101.00x
▲ VercelNext.js (Turbopack)2.206s (+6.8% 🔺)3.312s (-4.0%)2.584s (+83.3% 🔺)6.338s (+17.2% 🔺)4.132s101.04x
▲ VercelNitro2.374s (+7.0% 🔺)3.275s (~)2.368s (+27.1% 🔺)6.269s (+10.1% 🔺)3.894s101.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.509s (-2.5%)2.010s (~)0.013s (-1.3%)2.026s (~)0.517s301.00x
💻 LocalNext.js (Turbopack)1.563s (-0.9%)1.971s (~)0.012s (-5.4% 🟢)2.025s (~)0.462s301.04x
💻 LocalNitro1.564s (+0.7%)2.010s (~)0.013s (+9.4% 🔺)2.026s (~)0.462s301.04x
🐘 PostgresExpress1.568s (+1.4%)2.010s (~)0.005s (-4.8%)2.027s (~)0.459s301.04x
🐘 PostgresNitro1.577s (-1.8%)2.003s (~)0.005s (~)2.027s (~)0.450s301.04x
🐘 PostgresNext.js (Turbopack)2.009s (+30.4% 🔺)2.447s (+21.7% 🔺)0.004s (-20.0% 🟢)2.478s (+22.3% 🔺)0.469s251.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.025s (+1.4%)7.125s (-3.4%)0.324s (+1.9%)7.978s (-3.4%)1.953s81.00x
▲ VercelExpress6.435s (+9.3% 🔺)7.891s (+9.5% 🔺)0.226s (+4.4%)8.797s (+11.6% 🔺)2.363s71.07x
▲ VercelNitro6.500s (+7.0% 🔺)7.678s (+8.5% 🔺)0.459s (+102.8% 🔺)8.656s (+10.6% 🔺)2.156s71.08x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.782s (+1.0%)1.071s (+2.2%)0.000s (+Infinity% 🔺)1.088s (+2.3%)0.307s561.00x
🐘 PostgresNitro0.821s (~)1.062s (-3.3%)0.000s (-50.9% 🟢)1.092s (-2.4%)0.271s551.05x
🐘 PostgresNext.js (Turbopack)1.037s (+13.1% 🔺)1.437s (+12.6% 🔺)0.000s (+14.6% 🔺)1.473s (+14.7% 🔺)0.436s411.33x
💻 LocalExpress1.209s (-4.7%)1.949s (-3.2%)0.000s (+214.5% 🔺)1.951s (-3.2%)0.743s311.55x
💻 LocalNitro1.280s (-2.9%)1.981s (~)0.001s (+41.7% 🔺)1.984s (~)0.704s311.64x
💻 LocalNext.js (Turbopack)1.411s (+5.7% 🔺)1.980s (~)0.001s (+183.3% 🔺)2.016s (~)0.605s301.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.353s (+7.5% 🔺)4.326s (-1.9%)0.000s (+Infinity% 🔺)4.782s (-1.9%)1.430s131.00x
▲ VercelExpress3.737s (-4.4%)5.137s (+2.2%)0.000s (-100.0% 🟢)5.715s (+3.2%)1.978s111.11x
▲ VercelNext.js (Turbopack)4.240s (+28.8% 🔺)5.616s (+19.4% 🔺)0.000s (+20.0% 🔺)6.124s (+16.5% 🔺)1.884s101.26x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.605s (-5.3% 🟢)2.171s (-3.7%)0.000s (-100.0% 🟢)2.211s (-2.6%)0.607s281.00x
🐘 PostgresNitro1.774s (-7.5% 🟢)2.339s (-6.3% 🟢)0.000s (-100.0% 🟢)2.355s (-6.6% 🟢)0.581s261.11x
🐘 PostgresNext.js (Turbopack)2.353s (+15.8% 🔺)2.908s (+11.3% 🔺)0.000s (-100.0% 🟢)2.925s (+10.7% 🔺)0.572s211.47x
💻 LocalExpress3.387s (-6.8% 🟢)4.027s (-4.8%)0.001s (+50.0% 🔺)4.031s (-4.7%)0.644s152.11x
💻 LocalNext.js (Turbopack)3.389s (-7.5% 🟢)3.869s (-7.7% 🟢)0.001s (-12.1% 🟢)3.912s (-7.6% 🟢)0.523s162.11x
💻 LocalNitro3.475s (-2.5%)4.160s (+3.3%)0.000s (-58.8% 🟢)4.163s (+3.3%)0.689s152.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.911s (+27.6% 🔺)7.295s (+21.2% 🔺)0.001s (+150.0% 🔺)7.812s (+18.7% 🔺)1.901s81.00x
▲ VercelExpress6.192s (+30.8% 🔺)7.470s (+30.1% 🔺)0.000s (+25.0% 🔺)8.018s (+28.2% 🔺)1.826s81.05x
▲ VercelNitro6.213s (+27.0% 🔺)7.485s (+30.9% 🔺)0.000s (NaN%)7.991s (+29.2% 🔺)1.778s81.05x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress18/21
🐘 PostgresExpress11/21
▲ VercelExpress8/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres15/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

Enabling H2 on the single shared dispatcher hung the queue's webhook
respondWith mechanism (120s E2E timeouts on webhook/hook workflows
across frameworks). Instead, give the v4 events API its own
H2-enabled dispatcher (getEventsDispatcher) and keep the default
dispatcher — queue/webhook, v3 makeRequest, streaming, deployment
resolution, run-key fetch — on HTTP/1.1.
The events endpoints are the hottest path (an event write per step
transition plus log reads) and are plain request/response (or a
streamed LIST response), none of which trip the webhook/duplex H2
issues, so they get multiplexing safely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SvelteKit bundles the server (including undici via the world adapter) into
ESM output. undici loads node: builtins as ESM imports except node:http2,
which it pulls in lazily via a bare require('node:http2') inside a try/catch.
The bundler leaves that bare require un-wired, so in the ESM bundle it throws
and undici silently falls back to a stub whose http2.connect is undefined —
breaking every HTTP/2 request (the workflow flow-route callback fails with
"fetch failed", so runs never start).
Inject a createRequire-backed global require into the SSR build only (a
node:module import would break the client bundle) so the real node:http2
resolves. Detected via configResolved.build.ssr because SvelteKit does not
set env.isSsrBuild for its server pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VaguelySeriousand others added 2 commits June 23, 2026 17:32
Nitro bundles undici (via the world adapter) into the ESM server output.
undici loads node: builtins as ESM imports except node:http2, which it pulls
in lazily via a bare require('node:http2') inside a try/catch. The bundler
leaves that bare require un-wired, so in the ESM bundle it throws and undici
silently falls back to a stub whose http2.connect is undefined — breaking
every HTTP/2 request (the workflow flow-route callback fails with "fetch
failed", so runs never start). This affects all nitro-based builds including
the Vite and TanStack Start framework integrations.
Prepend a createRequire-backed global require to the server chunks (via the
rollup:before hook, production builds only) so the real node:http2 resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream writes (write/writeMulti/close) previously used plain fetch() on
HTTP/1.1. Route them through a dedicated H2 stream dispatcher: the PUT
requests send a fully-buffered body (or none), so they gain H2 multiplexing
without the duplex-streaming issues that keep the long-lived live-read (GET)
on plain fetch. The stream dispatcher is a bare undici Agent without the
RetryAgent wrapper — stream appends are not idempotent, so undici-level retry
is deliberately omitted to avoid duplicating chunks (preserving the streamer's
original no-retry behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
headers: httpConfig.headers,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- undici dispatcher type doesn't match @types/node's RequestInit
dispatcher: getStreamDispatcher(config),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] I would block this until the Vercel Nitro v3 benchmark is rerun green or root-caused. The current Benchmark Vercel (nitro-v3) job fails with Stream 1 correctness failure: expected 1048576 bytes, got 983040, exactly one 64 KiB chunk missing from the stream benchmark. Since this PR changes stream PUTs to use the H2 dispatcher here, this looks like a plausible stream-write correctness regression rather than a pure perf failure. Job: https://github.com/vercel/workflow/actions/runs/28066956084/job/83093680965

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passed on re-run

* getDefaultStreamDispatcher for why this is a bare Agent (no retry).
*/
export function getStreamDispatcher(config?: APIConfig): unknown {
return config?.dispatcher ?? getDefaultStreamDispatcher();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This keeps the default stream dispatcher retry-free, but config.dispatcher now also controls stream writes. A caller that passes an undici RetryAgent can re-enable retries for stream PUTs; undici retries PUT by default, and stream appends are not idempotent, so that can duplicate chunks. Can we document this in the custom dispatcher section, or split out a stream-specific dispatcher option / guard so retrying dispatchers are not used for stream writes?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Good catch. Rather than leave stream writes retry-free, I scoped the retry policy so the win (resilience to transient blips) survives without the duplicate-chunk risk:

  • The default stream dispatcher is now a RetryAgent with STREAM_RETRY_OPTIONS = { methods: ['PUT'], statusCodes: [429], retryAfter: true }. It retries only on transient connection errors (undici's default errorCodes) and HTTP 429 — both guarantee the chunk was not persisted. 5xx is excluded (a 5xx can mean the chunk was written but the response failed → retry would duplicate), as are other 4xx.
  • For the caller-override case you flagged: documented in the dispatcher JSDoc (APIConfig in utils.ts) that a custom dispatcher replaces every default including stream writes, and should not retry PUT on 5xx.

Added a regression test asserting statusCodes is exactly [429] and never contains 500/502/503/504.

*/
function addNodeRequireBanner(config: RollupConfig): void {
const banner =
"import { createRequire as __wkfCreateRequire } from 'node:module'; if (typeof require === 'undefined') { globalThis.require = __wkfCreateRequire(import.meta.url); }";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This fixes the bundled undici node:http2 issue, but it also globally changes server-bundle feature detection: dependencies that branch on typeof require will now take their CJS/require path in these ESM bundles. The tested Vercel Node lanes passing is reassuring, but can we call this out as a Node-runtime-only server-bundle shim and keep an eye on optional-require behavior in bundled libraries?

@VaguelySeriousVaguelySeriousJun 24, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Agreed — that global feature-detection change is the right thing to flag. I kept the global globalThis.require (it's required: undici's bundled bare require(...) resolves against the runtime global scope, so a module-local const require wouldn't reliably be in scope for the inlined undici code), but tightened the JSDoc in both nitro/index.ts and sveltekit/plugin.ts to call it out explicitly as a Node-server-runtime-only shim and document the side effect + why it's safe:

  • Server-only — gated to the production server build (!nitro.options.dev / config.build.ssr); never touches the client/browser bundle.
  • Guarded — the typeof require === 'undefined' check makes it a no-op in CJS chunks where a real require already exists; it only fills a genuine ESM gap.
  • Functional — the require we install is a working createRequire, so a lib that switches to the require path gets a real require, not a broken stub (the common try { require(optional) } catch {} pattern still degrades gracefully).

The comment also records the one residual behavior to watch, per your ask: a bundled lib that, on detecting require, does require() of an ESM-only dependency on a Node version without require(ESM) support. The Vercel Node E2E lanes exercise the real undici path that motivated this. Comment-only change in b2ec9d7.

… 429
Stream writes (PUT) append chunks and are not idempotent. The stream
dispatcher was a bare Agent with no retry; switch it to a RetryAgent whose
policy is narrowed so PUTs retry only on transient connection errors and
HTTP 429 — both of which guarantee the chunk was not persisted — and never
on 5xx (where the chunk may already have been written) or other 4xx.
Also document that a caller-supplied `config.dispatcher` overrides the
stream dispatcher and should not retry PUT on 5xx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/world-vercel/src/streamer.ts Outdated
vercelBotand others added 2 commits June 24, 2026 17:34
…nt (no retry)" / "intentionally retry-free", contradicting the actual `RetryAgent` + `STREAM_RETRY_OPTIONS` implementation in a non-idempotent write path.
This commit fixes the issue reported at packages/world-vercel/src/streamer.ts:26
## The issue
`getDefaultStreamDispatcher` in `packages/world-vercel/src/http-client.ts` constructs a `RetryAgent` wrapping `STREAM_RETRY_OPTIONS` (`retryAfter: true`, `methods: ['PUT']`, `statusCodes: [429]`). So the stream-write dispatcher **does** retry — on undici's default transient connection errors and on HTTP 429.
Two comments still claimed the opposite:
1. `packages/world-vercel/src/streamer.ts` (~line 26):
> "That stream dispatcher is intentionally retry-free — stream appends aren't idempotent, so a retried write would duplicate chunks."
2. `packages/world-vercel/src/http-client.ts` (~line 102, JSDoc on `getStreamDispatcher`):
> "See getDefaultStreamDispatcher for why this is a bare Agent (no retry)."
Both are factually wrong: the dispatcher is a `RetryAgent`, not a bare `Agent`, and it is not retry-free. The accurate, nuanced description already lives in the `STREAM_RETRY_OPTIONS` and `getDefaultStreamDispatcher` JSDoc — these two comments were simply not updated when the behavior changed.
## Why it matters
This is the **non-idempotent write path**. A maintainer reasoning about correctness here relies on these comments to understand exactly what is and isn't retried. "Retry-free" vs. "retries on transient errors + 429 but never 5xx" is a meaningful safety distinction, so the stale wording actively misleads.
## The fix
Comment-only changes (no behavior change):
- **streamer.ts**: replaced "intentionally retry-free" with wording reflecting the narrowed policy — retries only on transient connection errors + HTTP 429 (both guaranteeing the chunk was never persisted), never 5xx, so a retry can't duplicate an already-applied write. Points to `STREAM_RETRY_OPTIONS`.
- **http-client.ts**: replaced "why this is a bare Agent (no retry)" with a reference to the deliberately narrowed retry policy in `getDefaultStreamDispatcher` / `STREAM_RETRY_OPTIONS`.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
…ide effect
Document that the createRequire banner deliberately makes `typeof require`
truthy for every bundled dependency in the ESM server output, why that is
safe (server-only, guarded, functional require), and the one residual
behavior to watch (require() of an ESM-only dep on old Node).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2615. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [world-vercel] Enable HTTP/2 for the events API and stream writes by VaguelySerious · Pull Request #2573 · vercel/workflow · GitHub
Skip to content

[world-vercel] Enable HTTP/2 for the events API and stream writes - #2573

Merged
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2
Jun 24, 2026
Merged

[world-vercel] Enable HTTP/2 for the events API and stream writes#2573
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

Enable HTTP/2 for two hot @workflow/world-vercel request paths, and fix the framework bundling that previously made H2 impossible on Vite-based adapters.

  • v4 events API (events-v4.ts) — an event write per step transition plus event-log reads on replay. Gets its own H2 undici dispatcher (getEventsDispatcher).
  • Stream writes (streamer.tswrite/writeMulti/close — the PUT path) — routed through a dedicated H2 stream dispatcher (getStreamDispatcher). These send a fully-buffered body (or none), so they get H2 multiplexing without the duplex issues that keep the long-lived live-read (GET) on plain fetch. The stream dispatcher is a bare Agentwithout the RetryAgent wrapper: stream appends aren't idempotent, so undici-level retry is deliberately omitted to avoid duplicating chunks.

Everything else stays on HTTP/1.1 (queue client / webhook respondWith, v3 makeRequest, deployment resolution, run-key fetch, live-read GET, list).

The bundling fix (why H2 was blocked before)

H2 had been disabled because it "broke SvelteKit on Vercel prod." Root cause: Vite/Rollup-based framework builds (SvelteKit, TanStack Start, plain Vite) bundle undici into the ESM server output. undici imports most node: builtins as ESM, but loads node:http2 lazily via a bare require('node:http2') inside a try/catch. The bundler leaves that bare require un-wired, so in the ESM bundle it throws and undici silently falls back to a stub whose http2.connect is undefined. Any H2 request then fails with fetch failed → the workflow flow-route callback can't reach the backend → runs never leave pending → 30-minute E2E timeouts. Frameworks that externalize undici (Express, Astro, Next, Hono, Nitro, …) were unaffected.

Fix: inject a createRequire-backed global require into the server build (server-only — a node:module import would break the client bundle) so the real node:http2 resolves:

  • @workflow/sveltekit — via configResolved (SvelteKit doesn't set env.isSsrBuild for its server pass; build.ssr on the resolved config is the reliable signal).
  • @workflow/nitro — via the existing rollup:before hook, covering Vite, TanStack Start, and Nitro from one place.

Verified end-to-end: with the fix, the sveltekit / vite / tanstack-start E2E Vercel Prod lanes pass, and http2.connect is not a function errors drop to zero (previously deterministic on this branch only).

Why the default dispatcher stays H1

Enabling H2 on the shared dispatcher deadlocks the queue's webhook respondWith mechanism (120s timeouts on the webhook/hook workflows). So H2 is scoped to the events + stream-write paths, which don't use that mechanism.

Tests

  • Regression guards: EVENTS_AGENT_OPTIONS.allowH2 === trueandDEFAULT_AGENT_OPTIONS.allowH2 === false.
  • getEventsDispatcher and getStreamDispatcher each return their own shared singleton — distinct from the default and from each other — and honor a caller-supplied dispatcher.
  • Local node:http2 TLS server test: a request through the global fetch with an allowH2 dispatcher negotiates h2 (the mechanism these paths rely on).
  • Full @workflow/world-vercel unit suite passes (172 tests).

Notes

  • events-v4.ts and the stream writes stay on the Vercel-patched global fetch (preserves observability instrumentation); only the dispatcher they pass changes.
  • The sleepingWorkflow and distributedAbortController failures seen on some lanes are pre-existing flakes (zero-tolerance server-side sleep timing / abort timeout) that also fail on main — unrelated to this change.

🤖 Generated with Claude Code

Flip allowH2 to true in the shared undici dispatcher so every
dispatcher-backed path (v3 makeRequest, v4 events, the queue client,
deployment resolution, run-key fetch) negotiates HTTP/2. H2 was
previously disabled for a SvelteKit-on-Vercel-prod hang; re-enable it
globally and rely on the E2E suite to guard the regression. Streaming
paths in streamer.ts intentionally bypass the dispatcher and are
unaffected.
Adds a regression guard on the agent options plus a local http2 server
test proving global fetch honors an allowH2 dispatcher (the mechanism
the v4 events path relies on).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJune 22, 2026 21:51
@vercel

vercelBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2ec9d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/world-vercelPatch
@workflow/nuxtPatch
workflowPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7282010788360

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ nitro-stable126026
✅ nuxt-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.045s (-5.3% 🟢)1.006s (~)0.961s101.00x
💻 LocalNitro0.047s (+0.6%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.050s (-4.2%)1.006s (~)0.955s101.12x
🐘 PostgresNitro0.065s (-0.6%)1.012s (~)0.947s101.44x
🐘 PostgresExpress0.066s (-4.5%)1.013s (~)0.946s101.47x
🐘 PostgresNext.js (Turbopack)0.067s (+20.8% 🔺)1.028s (+1.6%)0.962s101.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.232s (~)1.976s (-6.1% 🟢)1.744s101.00x
▲ VercelNext.js (Turbopack)0.260s (-3.8%)2.042s (~)1.782s101.12x
▲ VercelNitro0.301s (+19.7% 🔺)2.042s (-5.9% 🟢)1.741s101.30x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.078s (-0.8%)2.006s (~)0.928s101.00x
💻 LocalNext.js (Turbopack)1.079s (~)2.006s (~)0.927s101.00x
🐘 PostgresNext.js (Turbopack)1.079s (~)2.009s (~)0.930s101.00x
💻 LocalNitro1.082s (~)2.007s (~)0.925s101.00x
🐘 PostgresExpress1.097s (~)2.009s (~)0.912s101.02x
🐘 PostgresNitro1.100s (~)2.009s (~)0.909s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.421s (-0.9%)3.085s (-8.0% 🟢)1.664s101.00x
▲ VercelExpress1.445s (+4.5%)3.251s (+14.9% 🔺)1.806s101.02x
▲ VercelNitro1.468s (+4.0%)3.195s (~)1.727s101.03x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)10.420s (~)11.026s (~)0.606s31.00x
💻 LocalExpress10.435s (~)11.023s (~)0.588s31.00x
💻 LocalNitro10.455s (~)11.024s (~)0.569s31.00x
🐘 PostgresExpress10.460s (~)11.021s (~)0.560s31.00x
🐘 PostgresNitro10.476s (~)11.017s (~)0.541s31.01x
💻 LocalNext.js (Turbopack)10.504s (~)11.022s (~)0.518s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.937s (+1.8%)13.411s (~)1.473s31.00x
▲ VercelNext.js (Turbopack)12.028s (+1.9%)13.455s (-3.5%)1.427s31.01x
▲ VercelExpress12.325s (~)14.494s (+4.5%)2.169s31.03x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.554s (~)14.028s (~)0.473s51.00x
🐘 PostgresExpress13.559s (~)14.020s (~)0.461s51.00x
💻 LocalNext.js (Turbopack)13.625s (~)14.028s (~)0.403s51.01x
💻 LocalNitro13.734s (+0.7%)14.030s (~)0.295s51.01x
🐘 PostgresNext.js (Turbopack)13.750s (+0.6%)14.245s (+1.6%)0.495s51.01x
🐘 PostgresNitro13.773s (~)14.023s (~)0.250s51.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express18.265s (+11.1% 🔺)20.019s (+11.2% 🔺)1.754s31.00x
▲ VercelNext.js (Turbopack)18.771s (+8.2% 🔺)20.473s (+6.9% 🔺)1.702s41.03x
▲ VercelNitro19.188s (+17.2% 🔺)20.559s (+16.0% 🔺)1.372s31.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express12.044s (-2.4%)12.774s (-1.9%)0.730s81.00x
🐘 PostgresNext.js (Turbopack)12.160s (~)12.869s (-1.1%)0.709s71.01x
🐘 PostgresExpress12.263s (+1.3%)13.019s (~)0.757s71.02x
💻 LocalNitro12.272s (+0.7%)13.025s (~)0.753s71.02x
🐘 PostgresNitro12.273s (~)13.018s (~)0.746s71.02x
💻 LocalNext.js (Turbopack)12.414s (+1.3%)13.026s (~)0.612s71.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)22.253s (+15.0% 🔺)24.838s (+17.0% 🔺)2.586s41.00x
▲ VercelExpress22.303s (+21.4% 🔺)25.182s (+24.9% 🔺)2.879s41.00x
▲ VercelNitro22.749s (+19.8% 🔺)25.174s (+22.0% 🔺)2.425s41.02x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.172s (~)2.007s (~)0.835s151.00x
🐘 PostgresNitro1.192s (+0.9%)2.008s (~)0.815s151.02x
🐘 PostgresNext.js (Turbopack)1.215s (+3.2%)2.009s (~)0.794s151.04x
💻 LocalExpress1.351s (-2.7%)2.006s (~)0.655s151.15x
💻 LocalNitro1.423s (-1.1%)2.007s (~)0.584s151.21x
💻 LocalNext.js (Turbopack)1.444s (-3.6%)2.007s (~)0.563s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.284s (+12.5% 🔺)3.862s (+10.3% 🔺)1.578s81.00x
▲ VercelNitro2.364s (+11.0% 🔺)3.813s (+10.7% 🔺)1.448s81.04x
▲ VercelNext.js (Turbopack)2.432s (+13.1% 🔺)3.659s (-3.4%)1.228s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.302s (-2.8%)2.317s (-7.6% 🟢)1.015s131.00x
🐘 PostgresExpress1.332s (-4.4%)2.223s (-16.9% 🟢)0.891s141.02x
🐘 PostgresNext.js (Turbopack)1.468s (+11.8% 🔺)2.685s (-8.0% 🟢)1.217s121.13x
💻 LocalExpress2.446s (+3.7%)2.826s (-3.1%)0.381s111.88x
💻 LocalNext.js (Turbopack)2.530s (-7.5% 🟢)3.008s (-3.2%)0.477s101.94x
💻 LocalNitro2.671s (+10.9% 🔺)3.110s (+6.5% 🔺)0.439s102.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.780s (-9.7% 🟢)4.586s (-11.5% 🟢)1.806s71.00x
▲ VercelExpress3.603s (-0.9%)5.649s (+9.4% 🔺)2.047s61.30x
▲ VercelNext.js (Turbopack)4.146s (+24.1% 🔺)5.917s (+17.3% 🔺)1.772s61.49x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (~)4.135s (-3.9%)2.553s81.00x
🐘 PostgresNitro1.627s (-0.8%)4.136s (-3.0%)2.508s81.03x
🐘 PostgresNext.js (Turbopack)2.703s (-4.0%)4.582s (-16.9% 🟢)1.879s71.71x
💻 LocalNitro6.077s (-5.7% 🟢)6.818s (-5.5% 🟢)0.741s53.84x
💻 LocalExpress6.158s (-2.6%)6.819s (-2.8%)0.661s53.89x
💻 LocalNext.js (Turbopack)7.337s (+34.1% 🔺)8.017s (+33.3% 🔺)0.680s44.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.321s (+24.1% 🔺)6.144s (+18.1% 🔺)1.824s51.00x
▲ VercelNitro5.108s (+52.3% 🔺)6.685s (+30.6% 🔺)1.578s51.18x
▲ VercelExpress5.501s (+79.5% 🔺)7.636s (+51.4% 🔺)2.135s51.27x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.189s (+0.9%)2.007s (~)0.819s151.00x
🐘 PostgresNitro1.203s (~)2.009s (~)0.806s151.01x
🐘 PostgresNext.js (Turbopack)1.225s (+4.7%)2.015s (~)0.790s151.03x
💻 LocalExpress1.415s (-1.6%)2.007s (~)0.592s151.19x
💻 LocalNext.js (Turbopack)1.455s (+3.7%)2.006s (~)0.552s151.22x
💻 LocalNitro1.472s (+3.5%)2.007s (~)0.535s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.605s (+26.2% 🔺)4.403s (+25.9% 🔺)1.799s71.00x
▲ VercelNext.js (Turbopack)2.633s (-54.5% 🟢)4.124s (-44.6% 🟢)1.491s81.01x
▲ VercelNitro2.750s (+28.3% 🔺)4.412s (+18.7% 🔺)1.661s71.06x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.318s (-4.0%)2.393s (-3.2%)1.075s131.00x
🐘 PostgresExpress1.401s (+4.5%)2.676s (+8.3% 🔺)1.274s121.06x
🐘 PostgresNext.js (Turbopack)1.487s (+15.3% 🔺)2.515s (-13.8% 🟢)1.028s121.13x
💻 LocalNitro2.433s (-5.8% 🟢)3.009s (-3.2%)0.576s101.85x
💻 LocalNext.js (Turbopack)2.587s (-1.9%)3.109s (+3.3%)0.522s101.96x
💻 LocalExpress2.643s (+9.2% 🔺)3.008s (-3.2%)0.365s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.822s (-41.1% 🟢)5.218s (-21.4% 🟢)2.396s61.00x
▲ VercelNitro3.363s (+23.9% 🔺)5.021s (+16.3% 🔺)1.658s71.19x
▲ VercelNext.js (Turbopack)3.396s (-28.4% 🟢)5.116s (-26.4% 🟢)1.721s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.599s (-3.7%)4.138s (+3.1%)2.539s81.00x
🐘 PostgresExpress1.658s (+4.6%)4.011s (-3.0%)2.353s81.04x
🐘 PostgresNext.js (Turbopack)2.514s (-12.1% 🟢)5.041s (-13.8% 🟢)2.527s61.57x
💻 LocalExpress5.944s (-11.3% 🟢)6.415s (-11.1% 🟢)0.472s53.72x
💻 LocalNitro6.441s (-5.0% 🟢)7.015s (-2.8%)0.574s54.03x
💻 LocalNext.js (Turbopack)7.009s (+22.5% 🔺)8.020s (+33.3% 🔺)1.010s44.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.521s (+1.9%)5.720s (+13.6% 🔺)2.199s61.00x
▲ VercelNext.js (Turbopack)4.714s (+50.9% 🔺)6.326s (+22.5% 🔺)1.611s51.34x
▲ VercelExpress4.774s (+1.8%)7.280s (+15.7% 🔺)2.506s51.36x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.546s (+6.9% 🔺)1.027s (+2.1%)0.481s591.00x
🐘 PostgresExpress0.557s (+4.5%)1.041s (+1.7%)0.483s581.02x
🐘 PostgresNitro0.563s (-5.3% 🟢)1.024s (-1.7%)0.461s591.03x
💻 LocalExpress0.570s (~)1.005s (~)0.435s601.04x
💻 LocalNitro0.585s (~)1.005s (~)0.420s601.07x
💻 LocalNext.js (Turbopack)0.673s (+11.5% 🔺)1.058s (+3.5%)0.385s571.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.301s (+24.8% 🔺)4.990s (+16.1% 🔺)1.689s131.00x
▲ VercelExpress3.342s (+19.4% 🔺)5.325s (+18.7% 🔺)1.983s121.01x
▲ VercelNext.js (Turbopack)3.380s (+11.2% 🔺)4.843s (-3.1%)1.463s131.02x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.237s (-1.1%)2.007s (~)0.770s451.00x
🐘 PostgresNitro1.333s (-5.7% 🟢)2.030s (-1.2%)0.696s451.08x
💻 LocalExpress1.425s (-2.1%)2.006s (~)0.581s451.15x
💻 LocalNext.js (Turbopack)1.474s (~)2.006s (~)0.532s451.19x
🐘 PostgresNext.js (Turbopack)1.500s (+20.5% 🔺)2.105s (+3.7%)0.605s431.21x
💻 LocalNitro1.565s (+5.5% 🔺)2.052s (+2.3%)0.488s441.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.650s (+19.6% 🔺)9.459s (+21.8% 🔺)1.808s101.00x
▲ VercelExpress7.692s (+19.0% 🔺)9.613s (+19.6% 🔺)1.921s101.01x
▲ VercelNext.js (Turbopack)9.201s (+28.8% 🔺)11.021s (+24.0% 🔺)1.820s91.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.481s (-4.4%)3.033s (-1.7%)0.552s401.00x
🐘 PostgresNitro2.669s (-1.7%)3.086s (+0.9%)0.417s391.08x
💻 LocalExpress3.122s (-3.8%)3.821s (-3.9%)0.699s321.26x
💻 LocalNitro3.154s (-2.0%)3.978s (-0.8%)0.824s311.27x
🐘 PostgresNext.js (Turbopack)3.207s (+29.5% 🔺)3.749s (+22.6% 🔺)0.542s331.29x
💻 LocalNext.js (Turbopack)3.281s (+0.6%)3.977s (-1.6%)0.696s311.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express15.239s (+10.5% 🔺)17.918s (+15.9% 🔺)2.679s71.00x
▲ VercelNitro15.788s (+12.3% 🔺)18.302s (+17.1% 🔺)2.515s71.04x
▲ VercelNext.js (Turbopack)18.176s (+10.8% 🔺)20.378s (+10.2% 🔺)2.202s61.19x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.197s (-6.3% 🟢)1.006s (~)0.809s601.00x
🐘 PostgresNext.js (Turbopack)0.213s (+27.2% 🔺)1.010s (~)0.797s601.08x
🐘 PostgresNitro0.222s (-2.1%)1.007s (~)0.784s601.13x
💻 LocalExpress0.463s (+7.4% 🔺)1.005s (~)0.541s602.35x
💻 LocalNitro0.465s (-0.7%)1.004s (~)0.539s602.36x
💻 LocalNext.js (Turbopack)0.613s (+5.0%)1.005s (~)0.391s603.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.257s (-42.1% 🟢)2.609s (-32.7% 🟢)1.352s231.00x
▲ VercelExpress1.375s (-33.7% 🟢)3.160s (-17.7% 🟢)1.785s191.09x
▲ VercelNext.js (Turbopack)1.468s (-19.3% 🟢)2.862s (-19.3% 🟢)1.395s211.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.314s (-2.7%)1.006s (-1.1%)0.692s901.00x
🐘 PostgresNitro0.336s (~)1.006s (~)0.671s901.07x
🐘 PostgresNext.js (Turbopack)0.347s (+29.3% 🔺)1.017s (+1.1%)0.670s891.10x
💻 LocalExpress2.137s (-1.1%)2.735s (~)0.598s336.80x
💻 LocalNitro2.192s (+2.8%)2.796s (+4.1%)0.604s336.97x
💻 LocalNext.js (Turbopack)2.734s (-2.0%)3.295s (+1.1%)0.560s288.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.855s (-2.7%)3.708s (+6.3% 🔺)1.853s251.00x
▲ VercelExpress2.119s (-14.0% 🟢)4.166s (+2.8%)2.047s221.14x
▲ VercelNext.js (Turbopack)2.680s (+23.5% 🔺)4.477s (+14.0% 🔺)1.797s211.44x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.531s (+0.5%)1.050s (+1.7%)0.519s1151.00x
🐘 PostgresExpress0.567s (+9.8% 🔺)1.160s (+8.6% 🔺)0.594s1041.07x
🐘 PostgresNext.js (Turbopack)0.699s (+48.1% 🔺)1.975s (-34.3% 🟢)1.277s611.32x
💻 LocalExpress9.304s (-7.8% 🟢)10.360s (-6.9% 🟢)1.056s1217.52x
💻 LocalNext.js (Turbopack)9.579s (-2.1%)10.530s (-2.3%)0.951s1218.04x
💻 LocalNitro10.363s (+4.5%)11.302s (+2.5%)0.940s1119.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.326s (+39.1% 🔺)5.768s (+42.3% 🔺)2.443s211.00x
▲ VercelNitro3.641s (+43.2% 🔺)5.915s (+43.4% 🔺)2.274s211.09x
▲ VercelNext.js (Turbopack)4.725s (+0.8%)7.116s (+8.7% 🔺)2.391s171.42x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.137s (-2.7%)1.968s (~)0.011s (-14.5% 🟢)2.018s (~)0.881s101.00x
💻 LocalExpress1.151s (~)2.005s (~)0.010s (-17.5% 🟢)2.017s (~)0.866s101.01x
🐘 PostgresNitro1.156s (-0.9%)1.999s (~)0.001s (+18.2% 🔺)2.010s (~)0.854s101.02x
💻 LocalNitro1.159s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.862s101.02x
🐘 PostgresExpress1.179s (+2.6%)2.000s (~)0.001s (+10.0% 🔺)2.011s (~)0.831s101.04x
🐘 PostgresNext.js (Turbopack)1.209s (+5.5% 🔺)1.996s (~)0.001s (-27.3% 🟢)2.009s (~)0.800s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.129s (+4.0%)3.553s (+15.8% 🔺)2.488s (+42.0% 🔺)6.571s (+21.6% 🔺)4.441s101.00x
▲ VercelNext.js (Turbopack)2.206s (+6.8% 🔺)3.312s (-4.0%)2.584s (+83.3% 🔺)6.338s (+17.2% 🔺)4.132s101.04x
▲ VercelNitro2.374s (+7.0% 🔺)3.275s (~)2.368s (+27.1% 🔺)6.269s (+10.1% 🔺)3.894s101.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.509s (-2.5%)2.010s (~)0.013s (-1.3%)2.026s (~)0.517s301.00x
💻 LocalNext.js (Turbopack)1.563s (-0.9%)1.971s (~)0.012s (-5.4% 🟢)2.025s (~)0.462s301.04x
💻 LocalNitro1.564s (+0.7%)2.010s (~)0.013s (+9.4% 🔺)2.026s (~)0.462s301.04x
🐘 PostgresExpress1.568s (+1.4%)2.010s (~)0.005s (-4.8%)2.027s (~)0.459s301.04x
🐘 PostgresNitro1.577s (-1.8%)2.003s (~)0.005s (~)2.027s (~)0.450s301.04x
🐘 PostgresNext.js (Turbopack)2.009s (+30.4% 🔺)2.447s (+21.7% 🔺)0.004s (-20.0% 🟢)2.478s (+22.3% 🔺)0.469s251.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.025s (+1.4%)7.125s (-3.4%)0.324s (+1.9%)7.978s (-3.4%)1.953s81.00x
▲ VercelExpress6.435s (+9.3% 🔺)7.891s (+9.5% 🔺)0.226s (+4.4%)8.797s (+11.6% 🔺)2.363s71.07x
▲ VercelNitro6.500s (+7.0% 🔺)7.678s (+8.5% 🔺)0.459s (+102.8% 🔺)8.656s (+10.6% 🔺)2.156s71.08x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.782s (+1.0%)1.071s (+2.2%)0.000s (+Infinity% 🔺)1.088s (+2.3%)0.307s561.00x
🐘 PostgresNitro0.821s (~)1.062s (-3.3%)0.000s (-50.9% 🟢)1.092s (-2.4%)0.271s551.05x
🐘 PostgresNext.js (Turbopack)1.037s (+13.1% 🔺)1.437s (+12.6% 🔺)0.000s (+14.6% 🔺)1.473s (+14.7% 🔺)0.436s411.33x
💻 LocalExpress1.209s (-4.7%)1.949s (-3.2%)0.000s (+214.5% 🔺)1.951s (-3.2%)0.743s311.55x
💻 LocalNitro1.280s (-2.9%)1.981s (~)0.001s (+41.7% 🔺)1.984s (~)0.704s311.64x
💻 LocalNext.js (Turbopack)1.411s (+5.7% 🔺)1.980s (~)0.001s (+183.3% 🔺)2.016s (~)0.605s301.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.353s (+7.5% 🔺)4.326s (-1.9%)0.000s (+Infinity% 🔺)4.782s (-1.9%)1.430s131.00x
▲ VercelExpress3.737s (-4.4%)5.137s (+2.2%)0.000s (-100.0% 🟢)5.715s (+3.2%)1.978s111.11x
▲ VercelNext.js (Turbopack)4.240s (+28.8% 🔺)5.616s (+19.4% 🔺)0.000s (+20.0% 🔺)6.124s (+16.5% 🔺)1.884s101.26x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.605s (-5.3% 🟢)2.171s (-3.7%)0.000s (-100.0% 🟢)2.211s (-2.6%)0.607s281.00x
🐘 PostgresNitro1.774s (-7.5% 🟢)2.339s (-6.3% 🟢)0.000s (-100.0% 🟢)2.355s (-6.6% 🟢)0.581s261.11x
🐘 PostgresNext.js (Turbopack)2.353s (+15.8% 🔺)2.908s (+11.3% 🔺)0.000s (-100.0% 🟢)2.925s (+10.7% 🔺)0.572s211.47x
💻 LocalExpress3.387s (-6.8% 🟢)4.027s (-4.8%)0.001s (+50.0% 🔺)4.031s (-4.7%)0.644s152.11x
💻 LocalNext.js (Turbopack)3.389s (-7.5% 🟢)3.869s (-7.7% 🟢)0.001s (-12.1% 🟢)3.912s (-7.6% 🟢)0.523s162.11x
💻 LocalNitro3.475s (-2.5%)4.160s (+3.3%)0.000s (-58.8% 🟢)4.163s (+3.3%)0.689s152.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.911s (+27.6% 🔺)7.295s (+21.2% 🔺)0.001s (+150.0% 🔺)7.812s (+18.7% 🔺)1.901s81.00x
▲ VercelExpress6.192s (+30.8% 🔺)7.470s (+30.1% 🔺)0.000s (+25.0% 🔺)8.018s (+28.2% 🔺)1.826s81.05x
▲ VercelNitro6.213s (+27.0% 🔺)7.485s (+30.9% 🔺)0.000s (NaN%)7.991s (+29.2% 🔺)1.778s81.05x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress18/21
🐘 PostgresExpress11/21
▲ VercelExpress8/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres15/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

Enabling H2 on the single shared dispatcher hung the queue's webhook
respondWith mechanism (120s E2E timeouts on webhook/hook workflows
across frameworks). Instead, give the v4 events API its own
H2-enabled dispatcher (getEventsDispatcher) and keep the default
dispatcher — queue/webhook, v3 makeRequest, streaming, deployment
resolution, run-key fetch — on HTTP/1.1.
The events endpoints are the hottest path (an event write per step
transition plus log reads) and are plain request/response (or a
streamed LIST response), none of which trip the webhook/duplex H2
issues, so they get multiplexing safely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SvelteKit bundles the server (including undici via the world adapter) into
ESM output. undici loads node: builtins as ESM imports except node:http2,
which it pulls in lazily via a bare require('node:http2') inside a try/catch.
The bundler leaves that bare require un-wired, so in the ESM bundle it throws
and undici silently falls back to a stub whose http2.connect is undefined —
breaking every HTTP/2 request (the workflow flow-route callback fails with
"fetch failed", so runs never start).
Inject a createRequire-backed global require into the SSR build only (a
node:module import would break the client bundle) so the real node:http2
resolves. Detected via configResolved.build.ssr because SvelteKit does not
set env.isSsrBuild for its server pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VaguelySeriousand others added 2 commits June 23, 2026 17:32
Nitro bundles undici (via the world adapter) into the ESM server output.
undici loads node: builtins as ESM imports except node:http2, which it pulls
in lazily via a bare require('node:http2') inside a try/catch. The bundler
leaves that bare require un-wired, so in the ESM bundle it throws and undici
silently falls back to a stub whose http2.connect is undefined — breaking
every HTTP/2 request (the workflow flow-route callback fails with "fetch
failed", so runs never start). This affects all nitro-based builds including
the Vite and TanStack Start framework integrations.
Prepend a createRequire-backed global require to the server chunks (via the
rollup:before hook, production builds only) so the real node:http2 resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream writes (write/writeMulti/close) previously used plain fetch() on
HTTP/1.1. Route them through a dedicated H2 stream dispatcher: the PUT
requests send a fully-buffered body (or none), so they gain H2 multiplexing
without the duplex-streaming issues that keep the long-lived live-read (GET)
on plain fetch. The stream dispatcher is a bare undici Agent without the
RetryAgent wrapper — stream appends are not idempotent, so undici-level retry
is deliberately omitted to avoid duplicating chunks (preserving the streamer's
original no-retry behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
headers: httpConfig.headers,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- undici dispatcher type doesn't match @types/node's RequestInit
dispatcher: getStreamDispatcher(config),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] I would block this until the Vercel Nitro v3 benchmark is rerun green or root-caused. The current Benchmark Vercel (nitro-v3) job fails with Stream 1 correctness failure: expected 1048576 bytes, got 983040, exactly one 64 KiB chunk missing from the stream benchmark. Since this PR changes stream PUTs to use the H2 dispatcher here, this looks like a plausible stream-write correctness regression rather than a pure perf failure. Job: https://github.com/vercel/workflow/actions/runs/28066956084/job/83093680965

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passed on re-run

* getDefaultStreamDispatcher for why this is a bare Agent (no retry).
*/
export function getStreamDispatcher(config?: APIConfig): unknown {
return config?.dispatcher ?? getDefaultStreamDispatcher();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This keeps the default stream dispatcher retry-free, but config.dispatcher now also controls stream writes. A caller that passes an undici RetryAgent can re-enable retries for stream PUTs; undici retries PUT by default, and stream appends are not idempotent, so that can duplicate chunks. Can we document this in the custom dispatcher section, or split out a stream-specific dispatcher option / guard so retrying dispatchers are not used for stream writes?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Good catch. Rather than leave stream writes retry-free, I scoped the retry policy so the win (resilience to transient blips) survives without the duplicate-chunk risk:

  • The default stream dispatcher is now a RetryAgent with STREAM_RETRY_OPTIONS = { methods: ['PUT'], statusCodes: [429], retryAfter: true }. It retries only on transient connection errors (undici's default errorCodes) and HTTP 429 — both guarantee the chunk was not persisted. 5xx is excluded (a 5xx can mean the chunk was written but the response failed → retry would duplicate), as are other 4xx.
  • For the caller-override case you flagged: documented in the dispatcher JSDoc (APIConfig in utils.ts) that a custom dispatcher replaces every default including stream writes, and should not retry PUT on 5xx.

Added a regression test asserting statusCodes is exactly [429] and never contains 500/502/503/504.

*/
function addNodeRequireBanner(config: RollupConfig): void {
const banner =
"import { createRequire as __wkfCreateRequire } from 'node:module'; if (typeof require === 'undefined') { globalThis.require = __wkfCreateRequire(import.meta.url); }";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This fixes the bundled undici node:http2 issue, but it also globally changes server-bundle feature detection: dependencies that branch on typeof require will now take their CJS/require path in these ESM bundles. The tested Vercel Node lanes passing is reassuring, but can we call this out as a Node-runtime-only server-bundle shim and keep an eye on optional-require behavior in bundled libraries?

@VaguelySeriousVaguelySeriousJun 24, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Agreed — that global feature-detection change is the right thing to flag. I kept the global globalThis.require (it's required: undici's bundled bare require(...) resolves against the runtime global scope, so a module-local const require wouldn't reliably be in scope for the inlined undici code), but tightened the JSDoc in both nitro/index.ts and sveltekit/plugin.ts to call it out explicitly as a Node-server-runtime-only shim and document the side effect + why it's safe:

  • Server-only — gated to the production server build (!nitro.options.dev / config.build.ssr); never touches the client/browser bundle.
  • Guarded — the typeof require === 'undefined' check makes it a no-op in CJS chunks where a real require already exists; it only fills a genuine ESM gap.
  • Functional — the require we install is a working createRequire, so a lib that switches to the require path gets a real require, not a broken stub (the common try { require(optional) } catch {} pattern still degrades gracefully).

The comment also records the one residual behavior to watch, per your ask: a bundled lib that, on detecting require, does require() of an ESM-only dependency on a Node version without require(ESM) support. The Vercel Node E2E lanes exercise the real undici path that motivated this. Comment-only change in b2ec9d7.

… 429
Stream writes (PUT) append chunks and are not idempotent. The stream
dispatcher was a bare Agent with no retry; switch it to a RetryAgent whose
policy is narrowed so PUTs retry only on transient connection errors and
HTTP 429 — both of which guarantee the chunk was not persisted — and never
on 5xx (where the chunk may already have been written) or other 4xx.
Also document that a caller-supplied `config.dispatcher` overrides the
stream dispatcher and should not retry PUT on 5xx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/world-vercel/src/streamer.ts Outdated
vercelBotand others added 2 commits June 24, 2026 17:34
…nt (no retry)" / "intentionally retry-free", contradicting the actual `RetryAgent` + `STREAM_RETRY_OPTIONS` implementation in a non-idempotent write path.
This commit fixes the issue reported at packages/world-vercel/src/streamer.ts:26
## The issue
`getDefaultStreamDispatcher` in `packages/world-vercel/src/http-client.ts` constructs a `RetryAgent` wrapping `STREAM_RETRY_OPTIONS` (`retryAfter: true`, `methods: ['PUT']`, `statusCodes: [429]`). So the stream-write dispatcher **does** retry — on undici's default transient connection errors and on HTTP 429.
Two comments still claimed the opposite:
1. `packages/world-vercel/src/streamer.ts` (~line 26):
> "That stream dispatcher is intentionally retry-free — stream appends aren't idempotent, so a retried write would duplicate chunks."
2. `packages/world-vercel/src/http-client.ts` (~line 102, JSDoc on `getStreamDispatcher`):
> "See getDefaultStreamDispatcher for why this is a bare Agent (no retry)."
Both are factually wrong: the dispatcher is a `RetryAgent`, not a bare `Agent`, and it is not retry-free. The accurate, nuanced description already lives in the `STREAM_RETRY_OPTIONS` and `getDefaultStreamDispatcher` JSDoc — these two comments were simply not updated when the behavior changed.
## Why it matters
This is the **non-idempotent write path**. A maintainer reasoning about correctness here relies on these comments to understand exactly what is and isn't retried. "Retry-free" vs. "retries on transient errors + 429 but never 5xx" is a meaningful safety distinction, so the stale wording actively misleads.
## The fix
Comment-only changes (no behavior change):
- **streamer.ts**: replaced "intentionally retry-free" with wording reflecting the narrowed policy — retries only on transient connection errors + HTTP 429 (both guaranteeing the chunk was never persisted), never 5xx, so a retry can't duplicate an already-applied write. Points to `STREAM_RETRY_OPTIONS`.
- **http-client.ts**: replaced "why this is a bare Agent (no retry)" with a reference to the deliberately narrowed retry policy in `getDefaultStreamDispatcher` / `STREAM_RETRY_OPTIONS`.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
…ide effect
Document that the createRequire banner deliberately makes `typeof require`
truthy for every bundled dependency in the ESM server output, why that is
safe (server-only, guarded, functional require), and the one residual
behavior to watch (require() of an ESM-only dep on old Node).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2615. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [world-vercel] Enable HTTP/2 for the events API and stream writes by VaguelySerious · Pull Request #2573 · vercel/workflow · GitHub
Skip to content

[world-vercel] Enable HTTP/2 for the events API and stream writes - #2573

Merged
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2
Jun 24, 2026
Merged

[world-vercel] Enable HTTP/2 for the events API and stream writes#2573
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

Enable HTTP/2 for two hot @workflow/world-vercel request paths, and fix the framework bundling that previously made H2 impossible on Vite-based adapters.

  • v4 events API (events-v4.ts) — an event write per step transition plus event-log reads on replay. Gets its own H2 undici dispatcher (getEventsDispatcher).
  • Stream writes (streamer.tswrite/writeMulti/close — the PUT path) — routed through a dedicated H2 stream dispatcher (getStreamDispatcher). These send a fully-buffered body (or none), so they get H2 multiplexing without the duplex issues that keep the long-lived live-read (GET) on plain fetch. The stream dispatcher is a bare Agentwithout the RetryAgent wrapper: stream appends aren't idempotent, so undici-level retry is deliberately omitted to avoid duplicating chunks.

Everything else stays on HTTP/1.1 (queue client / webhook respondWith, v3 makeRequest, deployment resolution, run-key fetch, live-read GET, list).

The bundling fix (why H2 was blocked before)

H2 had been disabled because it "broke SvelteKit on Vercel prod." Root cause: Vite/Rollup-based framework builds (SvelteKit, TanStack Start, plain Vite) bundle undici into the ESM server output. undici imports most node: builtins as ESM, but loads node:http2 lazily via a bare require('node:http2') inside a try/catch. The bundler leaves that bare require un-wired, so in the ESM bundle it throws and undici silently falls back to a stub whose http2.connect is undefined. Any H2 request then fails with fetch failed → the workflow flow-route callback can't reach the backend → runs never leave pending → 30-minute E2E timeouts. Frameworks that externalize undici (Express, Astro, Next, Hono, Nitro, …) were unaffected.

Fix: inject a createRequire-backed global require into the server build (server-only — a node:module import would break the client bundle) so the real node:http2 resolves:

  • @workflow/sveltekit — via configResolved (SvelteKit doesn't set env.isSsrBuild for its server pass; build.ssr on the resolved config is the reliable signal).
  • @workflow/nitro — via the existing rollup:before hook, covering Vite, TanStack Start, and Nitro from one place.

Verified end-to-end: with the fix, the sveltekit / vite / tanstack-start E2E Vercel Prod lanes pass, and http2.connect is not a function errors drop to zero (previously deterministic on this branch only).

Why the default dispatcher stays H1

Enabling H2 on the shared dispatcher deadlocks the queue's webhook respondWith mechanism (120s timeouts on the webhook/hook workflows). So H2 is scoped to the events + stream-write paths, which don't use that mechanism.

Tests

  • Regression guards: EVENTS_AGENT_OPTIONS.allowH2 === trueandDEFAULT_AGENT_OPTIONS.allowH2 === false.
  • getEventsDispatcher and getStreamDispatcher each return their own shared singleton — distinct from the default and from each other — and honor a caller-supplied dispatcher.
  • Local node:http2 TLS server test: a request through the global fetch with an allowH2 dispatcher negotiates h2 (the mechanism these paths rely on).
  • Full @workflow/world-vercel unit suite passes (172 tests).

Notes

  • events-v4.ts and the stream writes stay on the Vercel-patched global fetch (preserves observability instrumentation); only the dispatcher they pass changes.
  • The sleepingWorkflow and distributedAbortController failures seen on some lanes are pre-existing flakes (zero-tolerance server-side sleep timing / abort timeout) that also fail on main — unrelated to this change.

🤖 Generated with Claude Code

Flip allowH2 to true in the shared undici dispatcher so every
dispatcher-backed path (v3 makeRequest, v4 events, the queue client,
deployment resolution, run-key fetch) negotiates HTTP/2. H2 was
previously disabled for a SvelteKit-on-Vercel-prod hang; re-enable it
globally and rely on the E2E suite to guard the regression. Streaming
paths in streamer.ts intentionally bypass the dispatcher and are
unaffected.
Adds a regression guard on the agent options plus a local http2 server
test proving global fetch honors an allowH2 dispatcher (the mechanism
the v4 events path relies on).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJune 22, 2026 21:51
@vercel

vercelBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2ec9d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/world-vercelPatch
@workflow/nuxtPatch
workflowPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7282010788360

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ nitro-stable126026
✅ nuxt-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.045s (-5.3% 🟢)1.006s (~)0.961s101.00x
💻 LocalNitro0.047s (+0.6%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.050s (-4.2%)1.006s (~)0.955s101.12x
🐘 PostgresNitro0.065s (-0.6%)1.012s (~)0.947s101.44x
🐘 PostgresExpress0.066s (-4.5%)1.013s (~)0.946s101.47x
🐘 PostgresNext.js (Turbopack)0.067s (+20.8% 🔺)1.028s (+1.6%)0.962s101.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.232s (~)1.976s (-6.1% 🟢)1.744s101.00x
▲ VercelNext.js (Turbopack)0.260s (-3.8%)2.042s (~)1.782s101.12x
▲ VercelNitro0.301s (+19.7% 🔺)2.042s (-5.9% 🟢)1.741s101.30x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.078s (-0.8%)2.006s (~)0.928s101.00x
💻 LocalNext.js (Turbopack)1.079s (~)2.006s (~)0.927s101.00x
🐘 PostgresNext.js (Turbopack)1.079s (~)2.009s (~)0.930s101.00x
💻 LocalNitro1.082s (~)2.007s (~)0.925s101.00x
🐘 PostgresExpress1.097s (~)2.009s (~)0.912s101.02x
🐘 PostgresNitro1.100s (~)2.009s (~)0.909s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.421s (-0.9%)3.085s (-8.0% 🟢)1.664s101.00x
▲ VercelExpress1.445s (+4.5%)3.251s (+14.9% 🔺)1.806s101.02x
▲ VercelNitro1.468s (+4.0%)3.195s (~)1.727s101.03x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)10.420s (~)11.026s (~)0.606s31.00x
💻 LocalExpress10.435s (~)11.023s (~)0.588s31.00x
💻 LocalNitro10.455s (~)11.024s (~)0.569s31.00x
🐘 PostgresExpress10.460s (~)11.021s (~)0.560s31.00x
🐘 PostgresNitro10.476s (~)11.017s (~)0.541s31.01x
💻 LocalNext.js (Turbopack)10.504s (~)11.022s (~)0.518s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.937s (+1.8%)13.411s (~)1.473s31.00x
▲ VercelNext.js (Turbopack)12.028s (+1.9%)13.455s (-3.5%)1.427s31.01x
▲ VercelExpress12.325s (~)14.494s (+4.5%)2.169s31.03x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.554s (~)14.028s (~)0.473s51.00x
🐘 PostgresExpress13.559s (~)14.020s (~)0.461s51.00x
💻 LocalNext.js (Turbopack)13.625s (~)14.028s (~)0.403s51.01x
💻 LocalNitro13.734s (+0.7%)14.030s (~)0.295s51.01x
🐘 PostgresNext.js (Turbopack)13.750s (+0.6%)14.245s (+1.6%)0.495s51.01x
🐘 PostgresNitro13.773s (~)14.023s (~)0.250s51.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express18.265s (+11.1% 🔺)20.019s (+11.2% 🔺)1.754s31.00x
▲ VercelNext.js (Turbopack)18.771s (+8.2% 🔺)20.473s (+6.9% 🔺)1.702s41.03x
▲ VercelNitro19.188s (+17.2% 🔺)20.559s (+16.0% 🔺)1.372s31.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express12.044s (-2.4%)12.774s (-1.9%)0.730s81.00x
🐘 PostgresNext.js (Turbopack)12.160s (~)12.869s (-1.1%)0.709s71.01x
🐘 PostgresExpress12.263s (+1.3%)13.019s (~)0.757s71.02x
💻 LocalNitro12.272s (+0.7%)13.025s (~)0.753s71.02x
🐘 PostgresNitro12.273s (~)13.018s (~)0.746s71.02x
💻 LocalNext.js (Turbopack)12.414s (+1.3%)13.026s (~)0.612s71.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)22.253s (+15.0% 🔺)24.838s (+17.0% 🔺)2.586s41.00x
▲ VercelExpress22.303s (+21.4% 🔺)25.182s (+24.9% 🔺)2.879s41.00x
▲ VercelNitro22.749s (+19.8% 🔺)25.174s (+22.0% 🔺)2.425s41.02x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.172s (~)2.007s (~)0.835s151.00x
🐘 PostgresNitro1.192s (+0.9%)2.008s (~)0.815s151.02x
🐘 PostgresNext.js (Turbopack)1.215s (+3.2%)2.009s (~)0.794s151.04x
💻 LocalExpress1.351s (-2.7%)2.006s (~)0.655s151.15x
💻 LocalNitro1.423s (-1.1%)2.007s (~)0.584s151.21x
💻 LocalNext.js (Turbopack)1.444s (-3.6%)2.007s (~)0.563s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.284s (+12.5% 🔺)3.862s (+10.3% 🔺)1.578s81.00x
▲ VercelNitro2.364s (+11.0% 🔺)3.813s (+10.7% 🔺)1.448s81.04x
▲ VercelNext.js (Turbopack)2.432s (+13.1% 🔺)3.659s (-3.4%)1.228s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.302s (-2.8%)2.317s (-7.6% 🟢)1.015s131.00x
🐘 PostgresExpress1.332s (-4.4%)2.223s (-16.9% 🟢)0.891s141.02x
🐘 PostgresNext.js (Turbopack)1.468s (+11.8% 🔺)2.685s (-8.0% 🟢)1.217s121.13x
💻 LocalExpress2.446s (+3.7%)2.826s (-3.1%)0.381s111.88x
💻 LocalNext.js (Turbopack)2.530s (-7.5% 🟢)3.008s (-3.2%)0.477s101.94x
💻 LocalNitro2.671s (+10.9% 🔺)3.110s (+6.5% 🔺)0.439s102.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.780s (-9.7% 🟢)4.586s (-11.5% 🟢)1.806s71.00x
▲ VercelExpress3.603s (-0.9%)5.649s (+9.4% 🔺)2.047s61.30x
▲ VercelNext.js (Turbopack)4.146s (+24.1% 🔺)5.917s (+17.3% 🔺)1.772s61.49x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (~)4.135s (-3.9%)2.553s81.00x
🐘 PostgresNitro1.627s (-0.8%)4.136s (-3.0%)2.508s81.03x
🐘 PostgresNext.js (Turbopack)2.703s (-4.0%)4.582s (-16.9% 🟢)1.879s71.71x
💻 LocalNitro6.077s (-5.7% 🟢)6.818s (-5.5% 🟢)0.741s53.84x
💻 LocalExpress6.158s (-2.6%)6.819s (-2.8%)0.661s53.89x
💻 LocalNext.js (Turbopack)7.337s (+34.1% 🔺)8.017s (+33.3% 🔺)0.680s44.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.321s (+24.1% 🔺)6.144s (+18.1% 🔺)1.824s51.00x
▲ VercelNitro5.108s (+52.3% 🔺)6.685s (+30.6% 🔺)1.578s51.18x
▲ VercelExpress5.501s (+79.5% 🔺)7.636s (+51.4% 🔺)2.135s51.27x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.189s (+0.9%)2.007s (~)0.819s151.00x
🐘 PostgresNitro1.203s (~)2.009s (~)0.806s151.01x
🐘 PostgresNext.js (Turbopack)1.225s (+4.7%)2.015s (~)0.790s151.03x
💻 LocalExpress1.415s (-1.6%)2.007s (~)0.592s151.19x
💻 LocalNext.js (Turbopack)1.455s (+3.7%)2.006s (~)0.552s151.22x
💻 LocalNitro1.472s (+3.5%)2.007s (~)0.535s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.605s (+26.2% 🔺)4.403s (+25.9% 🔺)1.799s71.00x
▲ VercelNext.js (Turbopack)2.633s (-54.5% 🟢)4.124s (-44.6% 🟢)1.491s81.01x
▲ VercelNitro2.750s (+28.3% 🔺)4.412s (+18.7% 🔺)1.661s71.06x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.318s (-4.0%)2.393s (-3.2%)1.075s131.00x
🐘 PostgresExpress1.401s (+4.5%)2.676s (+8.3% 🔺)1.274s121.06x
🐘 PostgresNext.js (Turbopack)1.487s (+15.3% 🔺)2.515s (-13.8% 🟢)1.028s121.13x
💻 LocalNitro2.433s (-5.8% 🟢)3.009s (-3.2%)0.576s101.85x
💻 LocalNext.js (Turbopack)2.587s (-1.9%)3.109s (+3.3%)0.522s101.96x
💻 LocalExpress2.643s (+9.2% 🔺)3.008s (-3.2%)0.365s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.822s (-41.1% 🟢)5.218s (-21.4% 🟢)2.396s61.00x
▲ VercelNitro3.363s (+23.9% 🔺)5.021s (+16.3% 🔺)1.658s71.19x
▲ VercelNext.js (Turbopack)3.396s (-28.4% 🟢)5.116s (-26.4% 🟢)1.721s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.599s (-3.7%)4.138s (+3.1%)2.539s81.00x
🐘 PostgresExpress1.658s (+4.6%)4.011s (-3.0%)2.353s81.04x
🐘 PostgresNext.js (Turbopack)2.514s (-12.1% 🟢)5.041s (-13.8% 🟢)2.527s61.57x
💻 LocalExpress5.944s (-11.3% 🟢)6.415s (-11.1% 🟢)0.472s53.72x
💻 LocalNitro6.441s (-5.0% 🟢)7.015s (-2.8%)0.574s54.03x
💻 LocalNext.js (Turbopack)7.009s (+22.5% 🔺)8.020s (+33.3% 🔺)1.010s44.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.521s (+1.9%)5.720s (+13.6% 🔺)2.199s61.00x
▲ VercelNext.js (Turbopack)4.714s (+50.9% 🔺)6.326s (+22.5% 🔺)1.611s51.34x
▲ VercelExpress4.774s (+1.8%)7.280s (+15.7% 🔺)2.506s51.36x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.546s (+6.9% 🔺)1.027s (+2.1%)0.481s591.00x
🐘 PostgresExpress0.557s (+4.5%)1.041s (+1.7%)0.483s581.02x
🐘 PostgresNitro0.563s (-5.3% 🟢)1.024s (-1.7%)0.461s591.03x
💻 LocalExpress0.570s (~)1.005s (~)0.435s601.04x
💻 LocalNitro0.585s (~)1.005s (~)0.420s601.07x
💻 LocalNext.js (Turbopack)0.673s (+11.5% 🔺)1.058s (+3.5%)0.385s571.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.301s (+24.8% 🔺)4.990s (+16.1% 🔺)1.689s131.00x
▲ VercelExpress3.342s (+19.4% 🔺)5.325s (+18.7% 🔺)1.983s121.01x
▲ VercelNext.js (Turbopack)3.380s (+11.2% 🔺)4.843s (-3.1%)1.463s131.02x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.237s (-1.1%)2.007s (~)0.770s451.00x
🐘 PostgresNitro1.333s (-5.7% 🟢)2.030s (-1.2%)0.696s451.08x
💻 LocalExpress1.425s (-2.1%)2.006s (~)0.581s451.15x
💻 LocalNext.js (Turbopack)1.474s (~)2.006s (~)0.532s451.19x
🐘 PostgresNext.js (Turbopack)1.500s (+20.5% 🔺)2.105s (+3.7%)0.605s431.21x
💻 LocalNitro1.565s (+5.5% 🔺)2.052s (+2.3%)0.488s441.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.650s (+19.6% 🔺)9.459s (+21.8% 🔺)1.808s101.00x
▲ VercelExpress7.692s (+19.0% 🔺)9.613s (+19.6% 🔺)1.921s101.01x
▲ VercelNext.js (Turbopack)9.201s (+28.8% 🔺)11.021s (+24.0% 🔺)1.820s91.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.481s (-4.4%)3.033s (-1.7%)0.552s401.00x
🐘 PostgresNitro2.669s (-1.7%)3.086s (+0.9%)0.417s391.08x
💻 LocalExpress3.122s (-3.8%)3.821s (-3.9%)0.699s321.26x
💻 LocalNitro3.154s (-2.0%)3.978s (-0.8%)0.824s311.27x
🐘 PostgresNext.js (Turbopack)3.207s (+29.5% 🔺)3.749s (+22.6% 🔺)0.542s331.29x
💻 LocalNext.js (Turbopack)3.281s (+0.6%)3.977s (-1.6%)0.696s311.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express15.239s (+10.5% 🔺)17.918s (+15.9% 🔺)2.679s71.00x
▲ VercelNitro15.788s (+12.3% 🔺)18.302s (+17.1% 🔺)2.515s71.04x
▲ VercelNext.js (Turbopack)18.176s (+10.8% 🔺)20.378s (+10.2% 🔺)2.202s61.19x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.197s (-6.3% 🟢)1.006s (~)0.809s601.00x
🐘 PostgresNext.js (Turbopack)0.213s (+27.2% 🔺)1.010s (~)0.797s601.08x
🐘 PostgresNitro0.222s (-2.1%)1.007s (~)0.784s601.13x
💻 LocalExpress0.463s (+7.4% 🔺)1.005s (~)0.541s602.35x
💻 LocalNitro0.465s (-0.7%)1.004s (~)0.539s602.36x
💻 LocalNext.js (Turbopack)0.613s (+5.0%)1.005s (~)0.391s603.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.257s (-42.1% 🟢)2.609s (-32.7% 🟢)1.352s231.00x
▲ VercelExpress1.375s (-33.7% 🟢)3.160s (-17.7% 🟢)1.785s191.09x
▲ VercelNext.js (Turbopack)1.468s (-19.3% 🟢)2.862s (-19.3% 🟢)1.395s211.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.314s (-2.7%)1.006s (-1.1%)0.692s901.00x
🐘 PostgresNitro0.336s (~)1.006s (~)0.671s901.07x
🐘 PostgresNext.js (Turbopack)0.347s (+29.3% 🔺)1.017s (+1.1%)0.670s891.10x
💻 LocalExpress2.137s (-1.1%)2.735s (~)0.598s336.80x
💻 LocalNitro2.192s (+2.8%)2.796s (+4.1%)0.604s336.97x
💻 LocalNext.js (Turbopack)2.734s (-2.0%)3.295s (+1.1%)0.560s288.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.855s (-2.7%)3.708s (+6.3% 🔺)1.853s251.00x
▲ VercelExpress2.119s (-14.0% 🟢)4.166s (+2.8%)2.047s221.14x
▲ VercelNext.js (Turbopack)2.680s (+23.5% 🔺)4.477s (+14.0% 🔺)1.797s211.44x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.531s (+0.5%)1.050s (+1.7%)0.519s1151.00x
🐘 PostgresExpress0.567s (+9.8% 🔺)1.160s (+8.6% 🔺)0.594s1041.07x
🐘 PostgresNext.js (Turbopack)0.699s (+48.1% 🔺)1.975s (-34.3% 🟢)1.277s611.32x
💻 LocalExpress9.304s (-7.8% 🟢)10.360s (-6.9% 🟢)1.056s1217.52x
💻 LocalNext.js (Turbopack)9.579s (-2.1%)10.530s (-2.3%)0.951s1218.04x
💻 LocalNitro10.363s (+4.5%)11.302s (+2.5%)0.940s1119.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.326s (+39.1% 🔺)5.768s (+42.3% 🔺)2.443s211.00x
▲ VercelNitro3.641s (+43.2% 🔺)5.915s (+43.4% 🔺)2.274s211.09x
▲ VercelNext.js (Turbopack)4.725s (+0.8%)7.116s (+8.7% 🔺)2.391s171.42x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.137s (-2.7%)1.968s (~)0.011s (-14.5% 🟢)2.018s (~)0.881s101.00x
💻 LocalExpress1.151s (~)2.005s (~)0.010s (-17.5% 🟢)2.017s (~)0.866s101.01x
🐘 PostgresNitro1.156s (-0.9%)1.999s (~)0.001s (+18.2% 🔺)2.010s (~)0.854s101.02x
💻 LocalNitro1.159s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.862s101.02x
🐘 PostgresExpress1.179s (+2.6%)2.000s (~)0.001s (+10.0% 🔺)2.011s (~)0.831s101.04x
🐘 PostgresNext.js (Turbopack)1.209s (+5.5% 🔺)1.996s (~)0.001s (-27.3% 🟢)2.009s (~)0.800s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.129s (+4.0%)3.553s (+15.8% 🔺)2.488s (+42.0% 🔺)6.571s (+21.6% 🔺)4.441s101.00x
▲ VercelNext.js (Turbopack)2.206s (+6.8% 🔺)3.312s (-4.0%)2.584s (+83.3% 🔺)6.338s (+17.2% 🔺)4.132s101.04x
▲ VercelNitro2.374s (+7.0% 🔺)3.275s (~)2.368s (+27.1% 🔺)6.269s (+10.1% 🔺)3.894s101.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.509s (-2.5%)2.010s (~)0.013s (-1.3%)2.026s (~)0.517s301.00x
💻 LocalNext.js (Turbopack)1.563s (-0.9%)1.971s (~)0.012s (-5.4% 🟢)2.025s (~)0.462s301.04x
💻 LocalNitro1.564s (+0.7%)2.010s (~)0.013s (+9.4% 🔺)2.026s (~)0.462s301.04x
🐘 PostgresExpress1.568s (+1.4%)2.010s (~)0.005s (-4.8%)2.027s (~)0.459s301.04x
🐘 PostgresNitro1.577s (-1.8%)2.003s (~)0.005s (~)2.027s (~)0.450s301.04x
🐘 PostgresNext.js (Turbopack)2.009s (+30.4% 🔺)2.447s (+21.7% 🔺)0.004s (-20.0% 🟢)2.478s (+22.3% 🔺)0.469s251.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.025s (+1.4%)7.125s (-3.4%)0.324s (+1.9%)7.978s (-3.4%)1.953s81.00x
▲ VercelExpress6.435s (+9.3% 🔺)7.891s (+9.5% 🔺)0.226s (+4.4%)8.797s (+11.6% 🔺)2.363s71.07x
▲ VercelNitro6.500s (+7.0% 🔺)7.678s (+8.5% 🔺)0.459s (+102.8% 🔺)8.656s (+10.6% 🔺)2.156s71.08x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.782s (+1.0%)1.071s (+2.2%)0.000s (+Infinity% 🔺)1.088s (+2.3%)0.307s561.00x
🐘 PostgresNitro0.821s (~)1.062s (-3.3%)0.000s (-50.9% 🟢)1.092s (-2.4%)0.271s551.05x
🐘 PostgresNext.js (Turbopack)1.037s (+13.1% 🔺)1.437s (+12.6% 🔺)0.000s (+14.6% 🔺)1.473s (+14.7% 🔺)0.436s411.33x
💻 LocalExpress1.209s (-4.7%)1.949s (-3.2%)0.000s (+214.5% 🔺)1.951s (-3.2%)0.743s311.55x
💻 LocalNitro1.280s (-2.9%)1.981s (~)0.001s (+41.7% 🔺)1.984s (~)0.704s311.64x
💻 LocalNext.js (Turbopack)1.411s (+5.7% 🔺)1.980s (~)0.001s (+183.3% 🔺)2.016s (~)0.605s301.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.353s (+7.5% 🔺)4.326s (-1.9%)0.000s (+Infinity% 🔺)4.782s (-1.9%)1.430s131.00x
▲ VercelExpress3.737s (-4.4%)5.137s (+2.2%)0.000s (-100.0% 🟢)5.715s (+3.2%)1.978s111.11x
▲ VercelNext.js (Turbopack)4.240s (+28.8% 🔺)5.616s (+19.4% 🔺)0.000s (+20.0% 🔺)6.124s (+16.5% 🔺)1.884s101.26x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.605s (-5.3% 🟢)2.171s (-3.7%)0.000s (-100.0% 🟢)2.211s (-2.6%)0.607s281.00x
🐘 PostgresNitro1.774s (-7.5% 🟢)2.339s (-6.3% 🟢)0.000s (-100.0% 🟢)2.355s (-6.6% 🟢)0.581s261.11x
🐘 PostgresNext.js (Turbopack)2.353s (+15.8% 🔺)2.908s (+11.3% 🔺)0.000s (-100.0% 🟢)2.925s (+10.7% 🔺)0.572s211.47x
💻 LocalExpress3.387s (-6.8% 🟢)4.027s (-4.8%)0.001s (+50.0% 🔺)4.031s (-4.7%)0.644s152.11x
💻 LocalNext.js (Turbopack)3.389s (-7.5% 🟢)3.869s (-7.7% 🟢)0.001s (-12.1% 🟢)3.912s (-7.6% 🟢)0.523s162.11x
💻 LocalNitro3.475s (-2.5%)4.160s (+3.3%)0.000s (-58.8% 🟢)4.163s (+3.3%)0.689s152.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.911s (+27.6% 🔺)7.295s (+21.2% 🔺)0.001s (+150.0% 🔺)7.812s (+18.7% 🔺)1.901s81.00x
▲ VercelExpress6.192s (+30.8% 🔺)7.470s (+30.1% 🔺)0.000s (+25.0% 🔺)8.018s (+28.2% 🔺)1.826s81.05x
▲ VercelNitro6.213s (+27.0% 🔺)7.485s (+30.9% 🔺)0.000s (NaN%)7.991s (+29.2% 🔺)1.778s81.05x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress18/21
🐘 PostgresExpress11/21
▲ VercelExpress8/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres15/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

Enabling H2 on the single shared dispatcher hung the queue's webhook
respondWith mechanism (120s E2E timeouts on webhook/hook workflows
across frameworks). Instead, give the v4 events API its own
H2-enabled dispatcher (getEventsDispatcher) and keep the default
dispatcher — queue/webhook, v3 makeRequest, streaming, deployment
resolution, run-key fetch — on HTTP/1.1.
The events endpoints are the hottest path (an event write per step
transition plus log reads) and are plain request/response (or a
streamed LIST response), none of which trip the webhook/duplex H2
issues, so they get multiplexing safely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SvelteKit bundles the server (including undici via the world adapter) into
ESM output. undici loads node: builtins as ESM imports except node:http2,
which it pulls in lazily via a bare require('node:http2') inside a try/catch.
The bundler leaves that bare require un-wired, so in the ESM bundle it throws
and undici silently falls back to a stub whose http2.connect is undefined —
breaking every HTTP/2 request (the workflow flow-route callback fails with
"fetch failed", so runs never start).
Inject a createRequire-backed global require into the SSR build only (a
node:module import would break the client bundle) so the real node:http2
resolves. Detected via configResolved.build.ssr because SvelteKit does not
set env.isSsrBuild for its server pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VaguelySeriousand others added 2 commits June 23, 2026 17:32
Nitro bundles undici (via the world adapter) into the ESM server output.
undici loads node: builtins as ESM imports except node:http2, which it pulls
in lazily via a bare require('node:http2') inside a try/catch. The bundler
leaves that bare require un-wired, so in the ESM bundle it throws and undici
silently falls back to a stub whose http2.connect is undefined — breaking
every HTTP/2 request (the workflow flow-route callback fails with "fetch
failed", so runs never start). This affects all nitro-based builds including
the Vite and TanStack Start framework integrations.
Prepend a createRequire-backed global require to the server chunks (via the
rollup:before hook, production builds only) so the real node:http2 resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream writes (write/writeMulti/close) previously used plain fetch() on
HTTP/1.1. Route them through a dedicated H2 stream dispatcher: the PUT
requests send a fully-buffered body (or none), so they gain H2 multiplexing
without the duplex-streaming issues that keep the long-lived live-read (GET)
on plain fetch. The stream dispatcher is a bare undici Agent without the
RetryAgent wrapper — stream appends are not idempotent, so undici-level retry
is deliberately omitted to avoid duplicating chunks (preserving the streamer's
original no-retry behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
headers: httpConfig.headers,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- undici dispatcher type doesn't match @types/node's RequestInit
dispatcher: getStreamDispatcher(config),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] I would block this until the Vercel Nitro v3 benchmark is rerun green or root-caused. The current Benchmark Vercel (nitro-v3) job fails with Stream 1 correctness failure: expected 1048576 bytes, got 983040, exactly one 64 KiB chunk missing from the stream benchmark. Since this PR changes stream PUTs to use the H2 dispatcher here, this looks like a plausible stream-write correctness regression rather than a pure perf failure. Job: https://github.com/vercel/workflow/actions/runs/28066956084/job/83093680965

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passed on re-run

* getDefaultStreamDispatcher for why this is a bare Agent (no retry).
*/
export function getStreamDispatcher(config?: APIConfig): unknown {
return config?.dispatcher ?? getDefaultStreamDispatcher();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This keeps the default stream dispatcher retry-free, but config.dispatcher now also controls stream writes. A caller that passes an undici RetryAgent can re-enable retries for stream PUTs; undici retries PUT by default, and stream appends are not idempotent, so that can duplicate chunks. Can we document this in the custom dispatcher section, or split out a stream-specific dispatcher option / guard so retrying dispatchers are not used for stream writes?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Good catch. Rather than leave stream writes retry-free, I scoped the retry policy so the win (resilience to transient blips) survives without the duplicate-chunk risk:

  • The default stream dispatcher is now a RetryAgent with STREAM_RETRY_OPTIONS = { methods: ['PUT'], statusCodes: [429], retryAfter: true }. It retries only on transient connection errors (undici's default errorCodes) and HTTP 429 — both guarantee the chunk was not persisted. 5xx is excluded (a 5xx can mean the chunk was written but the response failed → retry would duplicate), as are other 4xx.
  • For the caller-override case you flagged: documented in the dispatcher JSDoc (APIConfig in utils.ts) that a custom dispatcher replaces every default including stream writes, and should not retry PUT on 5xx.

Added a regression test asserting statusCodes is exactly [429] and never contains 500/502/503/504.

*/
function addNodeRequireBanner(config: RollupConfig): void {
const banner =
"import { createRequire as __wkfCreateRequire } from 'node:module'; if (typeof require === 'undefined') { globalThis.require = __wkfCreateRequire(import.meta.url); }";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This fixes the bundled undici node:http2 issue, but it also globally changes server-bundle feature detection: dependencies that branch on typeof require will now take their CJS/require path in these ESM bundles. The tested Vercel Node lanes passing is reassuring, but can we call this out as a Node-runtime-only server-bundle shim and keep an eye on optional-require behavior in bundled libraries?

@VaguelySeriousVaguelySeriousJun 24, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Agreed — that global feature-detection change is the right thing to flag. I kept the global globalThis.require (it's required: undici's bundled bare require(...) resolves against the runtime global scope, so a module-local const require wouldn't reliably be in scope for the inlined undici code), but tightened the JSDoc in both nitro/index.ts and sveltekit/plugin.ts to call it out explicitly as a Node-server-runtime-only shim and document the side effect + why it's safe:

  • Server-only — gated to the production server build (!nitro.options.dev / config.build.ssr); never touches the client/browser bundle.
  • Guarded — the typeof require === 'undefined' check makes it a no-op in CJS chunks where a real require already exists; it only fills a genuine ESM gap.
  • Functional — the require we install is a working createRequire, so a lib that switches to the require path gets a real require, not a broken stub (the common try { require(optional) } catch {} pattern still degrades gracefully).

The comment also records the one residual behavior to watch, per your ask: a bundled lib that, on detecting require, does require() of an ESM-only dependency on a Node version without require(ESM) support. The Vercel Node E2E lanes exercise the real undici path that motivated this. Comment-only change in b2ec9d7.

… 429
Stream writes (PUT) append chunks and are not idempotent. The stream
dispatcher was a bare Agent with no retry; switch it to a RetryAgent whose
policy is narrowed so PUTs retry only on transient connection errors and
HTTP 429 — both of which guarantee the chunk was not persisted — and never
on 5xx (where the chunk may already have been written) or other 4xx.
Also document that a caller-supplied `config.dispatcher` overrides the
stream dispatcher and should not retry PUT on 5xx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/world-vercel/src/streamer.ts Outdated
vercelBotand others added 2 commits June 24, 2026 17:34
…nt (no retry)" / "intentionally retry-free", contradicting the actual `RetryAgent` + `STREAM_RETRY_OPTIONS` implementation in a non-idempotent write path.
This commit fixes the issue reported at packages/world-vercel/src/streamer.ts:26
## The issue
`getDefaultStreamDispatcher` in `packages/world-vercel/src/http-client.ts` constructs a `RetryAgent` wrapping `STREAM_RETRY_OPTIONS` (`retryAfter: true`, `methods: ['PUT']`, `statusCodes: [429]`). So the stream-write dispatcher **does** retry — on undici's default transient connection errors and on HTTP 429.
Two comments still claimed the opposite:
1. `packages/world-vercel/src/streamer.ts` (~line 26):
> "That stream dispatcher is intentionally retry-free — stream appends aren't idempotent, so a retried write would duplicate chunks."
2. `packages/world-vercel/src/http-client.ts` (~line 102, JSDoc on `getStreamDispatcher`):
> "See getDefaultStreamDispatcher for why this is a bare Agent (no retry)."
Both are factually wrong: the dispatcher is a `RetryAgent`, not a bare `Agent`, and it is not retry-free. The accurate, nuanced description already lives in the `STREAM_RETRY_OPTIONS` and `getDefaultStreamDispatcher` JSDoc — these two comments were simply not updated when the behavior changed.
## Why it matters
This is the **non-idempotent write path**. A maintainer reasoning about correctness here relies on these comments to understand exactly what is and isn't retried. "Retry-free" vs. "retries on transient errors + 429 but never 5xx" is a meaningful safety distinction, so the stale wording actively misleads.
## The fix
Comment-only changes (no behavior change):
- **streamer.ts**: replaced "intentionally retry-free" with wording reflecting the narrowed policy — retries only on transient connection errors + HTTP 429 (both guaranteeing the chunk was never persisted), never 5xx, so a retry can't duplicate an already-applied write. Points to `STREAM_RETRY_OPTIONS`.
- **http-client.ts**: replaced "why this is a bare Agent (no retry)" with a reference to the deliberately narrowed retry policy in `getDefaultStreamDispatcher` / `STREAM_RETRY_OPTIONS`.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
…ide effect
Document that the createRequire banner deliberately makes `typeof require`
truthy for every bundled dependency in the ESM server output, why that is
safe (server-only, guarded, functional require), and the one residual
behavior to watch (require() of an ESM-only dep on old Node).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2615. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [world-vercel] Enable HTTP/2 for the events API and stream writes by VaguelySerious · Pull Request #2573 · vercel/workflow · GitHub
Skip to content

[world-vercel] Enable HTTP/2 for the events API and stream writes - #2573

Merged
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2
Jun 24, 2026
Merged

[world-vercel] Enable HTTP/2 for the events API and stream writes#2573
VaguelySerious merged 10 commits into
mainfrom
peter/world-vercel-enable-h2

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

Enable HTTP/2 for two hot @workflow/world-vercel request paths, and fix the framework bundling that previously made H2 impossible on Vite-based adapters.

  • v4 events API (events-v4.ts) — an event write per step transition plus event-log reads on replay. Gets its own H2 undici dispatcher (getEventsDispatcher).
  • Stream writes (streamer.tswrite/writeMulti/close — the PUT path) — routed through a dedicated H2 stream dispatcher (getStreamDispatcher). These send a fully-buffered body (or none), so they get H2 multiplexing without the duplex issues that keep the long-lived live-read (GET) on plain fetch. The stream dispatcher is a bare Agentwithout the RetryAgent wrapper: stream appends aren't idempotent, so undici-level retry is deliberately omitted to avoid duplicating chunks.

Everything else stays on HTTP/1.1 (queue client / webhook respondWith, v3 makeRequest, deployment resolution, run-key fetch, live-read GET, list).

The bundling fix (why H2 was blocked before)

H2 had been disabled because it "broke SvelteKit on Vercel prod." Root cause: Vite/Rollup-based framework builds (SvelteKit, TanStack Start, plain Vite) bundle undici into the ESM server output. undici imports most node: builtins as ESM, but loads node:http2 lazily via a bare require('node:http2') inside a try/catch. The bundler leaves that bare require un-wired, so in the ESM bundle it throws and undici silently falls back to a stub whose http2.connect is undefined. Any H2 request then fails with fetch failed → the workflow flow-route callback can't reach the backend → runs never leave pending → 30-minute E2E timeouts. Frameworks that externalize undici (Express, Astro, Next, Hono, Nitro, …) were unaffected.

Fix: inject a createRequire-backed global require into the server build (server-only — a node:module import would break the client bundle) so the real node:http2 resolves:

  • @workflow/sveltekit — via configResolved (SvelteKit doesn't set env.isSsrBuild for its server pass; build.ssr on the resolved config is the reliable signal).
  • @workflow/nitro — via the existing rollup:before hook, covering Vite, TanStack Start, and Nitro from one place.

Verified end-to-end: with the fix, the sveltekit / vite / tanstack-start E2E Vercel Prod lanes pass, and http2.connect is not a function errors drop to zero (previously deterministic on this branch only).

Why the default dispatcher stays H1

Enabling H2 on the shared dispatcher deadlocks the queue's webhook respondWith mechanism (120s timeouts on the webhook/hook workflows). So H2 is scoped to the events + stream-write paths, which don't use that mechanism.

Tests

  • Regression guards: EVENTS_AGENT_OPTIONS.allowH2 === trueandDEFAULT_AGENT_OPTIONS.allowH2 === false.
  • getEventsDispatcher and getStreamDispatcher each return their own shared singleton — distinct from the default and from each other — and honor a caller-supplied dispatcher.
  • Local node:http2 TLS server test: a request through the global fetch with an allowH2 dispatcher negotiates h2 (the mechanism these paths rely on).
  • Full @workflow/world-vercel unit suite passes (172 tests).

Notes

  • events-v4.ts and the stream writes stay on the Vercel-patched global fetch (preserves observability instrumentation); only the dispatcher they pass changes.
  • The sleepingWorkflow and distributedAbortController failures seen on some lanes are pre-existing flakes (zero-tolerance server-side sleep timing / abort timeout) that also fail on main — unrelated to this change.

🤖 Generated with Claude Code

Flip allowH2 to true in the shared undici dispatcher so every
dispatcher-backed path (v3 makeRequest, v4 events, the queue client,
deployment resolution, run-key fetch) negotiates HTTP/2. H2 was
previously disabled for a SvelteKit-on-Vercel-prod hang; re-enable it
globally and rely on the E2E suite to guard the regression. Streaming
paths in streamer.ts intentionally bypass the dispatcher and are
unaffected.
Adds a regression guard on the agent options plus a local http2 server
test proving global fetch honors an allowH2 dispatcher (the mechanism
the v4 events path relies on).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJune 22, 2026 21:51
@vercel

vercelBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b2ec9d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
NameType
@workflow/nitroPatch
@workflow/sveltekitPatch
@workflow/world-vercelPatch
@workflow/nuxtPatch
workflowPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/vitePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7282010788360

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ nitro-stable126026
✅ nuxt-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run

@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.045s (-5.3% 🟢)1.006s (~)0.961s101.00x
💻 LocalNitro0.047s (+0.6%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.050s (-4.2%)1.006s (~)0.955s101.12x
🐘 PostgresNitro0.065s (-0.6%)1.012s (~)0.947s101.44x
🐘 PostgresExpress0.066s (-4.5%)1.013s (~)0.946s101.47x
🐘 PostgresNext.js (Turbopack)0.067s (+20.8% 🔺)1.028s (+1.6%)0.962s101.48x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.232s (~)1.976s (-6.1% 🟢)1.744s101.00x
▲ VercelNext.js (Turbopack)0.260s (-3.8%)2.042s (~)1.782s101.12x
▲ VercelNitro0.301s (+19.7% 🔺)2.042s (-5.9% 🟢)1.741s101.30x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.078s (-0.8%)2.006s (~)0.928s101.00x
💻 LocalNext.js (Turbopack)1.079s (~)2.006s (~)0.927s101.00x
🐘 PostgresNext.js (Turbopack)1.079s (~)2.009s (~)0.930s101.00x
💻 LocalNitro1.082s (~)2.007s (~)0.925s101.00x
🐘 PostgresExpress1.097s (~)2.009s (~)0.912s101.02x
🐘 PostgresNitro1.100s (~)2.009s (~)0.909s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.421s (-0.9%)3.085s (-8.0% 🟢)1.664s101.00x
▲ VercelExpress1.445s (+4.5%)3.251s (+14.9% 🔺)1.806s101.02x
▲ VercelNitro1.468s (+4.0%)3.195s (~)1.727s101.03x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)10.420s (~)11.026s (~)0.606s31.00x
💻 LocalExpress10.435s (~)11.023s (~)0.588s31.00x
💻 LocalNitro10.455s (~)11.024s (~)0.569s31.00x
🐘 PostgresExpress10.460s (~)11.021s (~)0.560s31.00x
🐘 PostgresNitro10.476s (~)11.017s (~)0.541s31.01x
💻 LocalNext.js (Turbopack)10.504s (~)11.022s (~)0.518s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.937s (+1.8%)13.411s (~)1.473s31.00x
▲ VercelNext.js (Turbopack)12.028s (+1.9%)13.455s (-3.5%)1.427s31.01x
▲ VercelExpress12.325s (~)14.494s (+4.5%)2.169s31.03x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.554s (~)14.028s (~)0.473s51.00x
🐘 PostgresExpress13.559s (~)14.020s (~)0.461s51.00x
💻 LocalNext.js (Turbopack)13.625s (~)14.028s (~)0.403s51.01x
💻 LocalNitro13.734s (+0.7%)14.030s (~)0.295s51.01x
🐘 PostgresNext.js (Turbopack)13.750s (+0.6%)14.245s (+1.6%)0.495s51.01x
🐘 PostgresNitro13.773s (~)14.023s (~)0.250s51.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express18.265s (+11.1% 🔺)20.019s (+11.2% 🔺)1.754s31.00x
▲ VercelNext.js (Turbopack)18.771s (+8.2% 🔺)20.473s (+6.9% 🔺)1.702s41.03x
▲ VercelNitro19.188s (+17.2% 🔺)20.559s (+16.0% 🔺)1.372s31.05x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express12.044s (-2.4%)12.774s (-1.9%)0.730s81.00x
🐘 PostgresNext.js (Turbopack)12.160s (~)12.869s (-1.1%)0.709s71.01x
🐘 PostgresExpress12.263s (+1.3%)13.019s (~)0.757s71.02x
💻 LocalNitro12.272s (+0.7%)13.025s (~)0.753s71.02x
🐘 PostgresNitro12.273s (~)13.018s (~)0.746s71.02x
💻 LocalNext.js (Turbopack)12.414s (+1.3%)13.026s (~)0.612s71.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)22.253s (+15.0% 🔺)24.838s (+17.0% 🔺)2.586s41.00x
▲ VercelExpress22.303s (+21.4% 🔺)25.182s (+24.9% 🔺)2.879s41.00x
▲ VercelNitro22.749s (+19.8% 🔺)25.174s (+22.0% 🔺)2.425s41.02x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.172s (~)2.007s (~)0.835s151.00x
🐘 PostgresNitro1.192s (+0.9%)2.008s (~)0.815s151.02x
🐘 PostgresNext.js (Turbopack)1.215s (+3.2%)2.009s (~)0.794s151.04x
💻 LocalExpress1.351s (-2.7%)2.006s (~)0.655s151.15x
💻 LocalNitro1.423s (-1.1%)2.007s (~)0.584s151.21x
💻 LocalNext.js (Turbopack)1.444s (-3.6%)2.007s (~)0.563s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.284s (+12.5% 🔺)3.862s (+10.3% 🔺)1.578s81.00x
▲ VercelNitro2.364s (+11.0% 🔺)3.813s (+10.7% 🔺)1.448s81.04x
▲ VercelNext.js (Turbopack)2.432s (+13.1% 🔺)3.659s (-3.4%)1.228s91.06x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.302s (-2.8%)2.317s (-7.6% 🟢)1.015s131.00x
🐘 PostgresExpress1.332s (-4.4%)2.223s (-16.9% 🟢)0.891s141.02x
🐘 PostgresNext.js (Turbopack)1.468s (+11.8% 🔺)2.685s (-8.0% 🟢)1.217s121.13x
💻 LocalExpress2.446s (+3.7%)2.826s (-3.1%)0.381s111.88x
💻 LocalNext.js (Turbopack)2.530s (-7.5% 🟢)3.008s (-3.2%)0.477s101.94x
💻 LocalNitro2.671s (+10.9% 🔺)3.110s (+6.5% 🔺)0.439s102.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.780s (-9.7% 🟢)4.586s (-11.5% 🟢)1.806s71.00x
▲ VercelExpress3.603s (-0.9%)5.649s (+9.4% 🔺)2.047s61.30x
▲ VercelNext.js (Turbopack)4.146s (+24.1% 🔺)5.917s (+17.3% 🔺)1.772s61.49x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (~)4.135s (-3.9%)2.553s81.00x
🐘 PostgresNitro1.627s (-0.8%)4.136s (-3.0%)2.508s81.03x
🐘 PostgresNext.js (Turbopack)2.703s (-4.0%)4.582s (-16.9% 🟢)1.879s71.71x
💻 LocalNitro6.077s (-5.7% 🟢)6.818s (-5.5% 🟢)0.741s53.84x
💻 LocalExpress6.158s (-2.6%)6.819s (-2.8%)0.661s53.89x
💻 LocalNext.js (Turbopack)7.337s (+34.1% 🔺)8.017s (+33.3% 🔺)0.680s44.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.321s (+24.1% 🔺)6.144s (+18.1% 🔺)1.824s51.00x
▲ VercelNitro5.108s (+52.3% 🔺)6.685s (+30.6% 🔺)1.578s51.18x
▲ VercelExpress5.501s (+79.5% 🔺)7.636s (+51.4% 🔺)2.135s51.27x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.189s (+0.9%)2.007s (~)0.819s151.00x
🐘 PostgresNitro1.203s (~)2.009s (~)0.806s151.01x
🐘 PostgresNext.js (Turbopack)1.225s (+4.7%)2.015s (~)0.790s151.03x
💻 LocalExpress1.415s (-1.6%)2.007s (~)0.592s151.19x
💻 LocalNext.js (Turbopack)1.455s (+3.7%)2.006s (~)0.552s151.22x
💻 LocalNitro1.472s (+3.5%)2.007s (~)0.535s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.605s (+26.2% 🔺)4.403s (+25.9% 🔺)1.799s71.00x
▲ VercelNext.js (Turbopack)2.633s (-54.5% 🟢)4.124s (-44.6% 🟢)1.491s81.01x
▲ VercelNitro2.750s (+28.3% 🔺)4.412s (+18.7% 🔺)1.661s71.06x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.318s (-4.0%)2.393s (-3.2%)1.075s131.00x
🐘 PostgresExpress1.401s (+4.5%)2.676s (+8.3% 🔺)1.274s121.06x
🐘 PostgresNext.js (Turbopack)1.487s (+15.3% 🔺)2.515s (-13.8% 🟢)1.028s121.13x
💻 LocalNitro2.433s (-5.8% 🟢)3.009s (-3.2%)0.576s101.85x
💻 LocalNext.js (Turbopack)2.587s (-1.9%)3.109s (+3.3%)0.522s101.96x
💻 LocalExpress2.643s (+9.2% 🔺)3.008s (-3.2%)0.365s102.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.822s (-41.1% 🟢)5.218s (-21.4% 🟢)2.396s61.00x
▲ VercelNitro3.363s (+23.9% 🔺)5.021s (+16.3% 🔺)1.658s71.19x
▲ VercelNext.js (Turbopack)3.396s (-28.4% 🟢)5.116s (-26.4% 🟢)1.721s71.20x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.599s (-3.7%)4.138s (+3.1%)2.539s81.00x
🐘 PostgresExpress1.658s (+4.6%)4.011s (-3.0%)2.353s81.04x
🐘 PostgresNext.js (Turbopack)2.514s (-12.1% 🟢)5.041s (-13.8% 🟢)2.527s61.57x
💻 LocalExpress5.944s (-11.3% 🟢)6.415s (-11.1% 🟢)0.472s53.72x
💻 LocalNitro6.441s (-5.0% 🟢)7.015s (-2.8%)0.574s54.03x
💻 LocalNext.js (Turbopack)7.009s (+22.5% 🔺)8.020s (+33.3% 🔺)1.010s44.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.521s (+1.9%)5.720s (+13.6% 🔺)2.199s61.00x
▲ VercelNext.js (Turbopack)4.714s (+50.9% 🔺)6.326s (+22.5% 🔺)1.611s51.34x
▲ VercelExpress4.774s (+1.8%)7.280s (+15.7% 🔺)2.506s51.36x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.546s (+6.9% 🔺)1.027s (+2.1%)0.481s591.00x
🐘 PostgresExpress0.557s (+4.5%)1.041s (+1.7%)0.483s581.02x
🐘 PostgresNitro0.563s (-5.3% 🟢)1.024s (-1.7%)0.461s591.03x
💻 LocalExpress0.570s (~)1.005s (~)0.435s601.04x
💻 LocalNitro0.585s (~)1.005s (~)0.420s601.07x
💻 LocalNext.js (Turbopack)0.673s (+11.5% 🔺)1.058s (+3.5%)0.385s571.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.301s (+24.8% 🔺)4.990s (+16.1% 🔺)1.689s131.00x
▲ VercelExpress3.342s (+19.4% 🔺)5.325s (+18.7% 🔺)1.983s121.01x
▲ VercelNext.js (Turbopack)3.380s (+11.2% 🔺)4.843s (-3.1%)1.463s131.02x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.237s (-1.1%)2.007s (~)0.770s451.00x
🐘 PostgresNitro1.333s (-5.7% 🟢)2.030s (-1.2%)0.696s451.08x
💻 LocalExpress1.425s (-2.1%)2.006s (~)0.581s451.15x
💻 LocalNext.js (Turbopack)1.474s (~)2.006s (~)0.532s451.19x
🐘 PostgresNext.js (Turbopack)1.500s (+20.5% 🔺)2.105s (+3.7%)0.605s431.21x
💻 LocalNitro1.565s (+5.5% 🔺)2.052s (+2.3%)0.488s441.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro7.650s (+19.6% 🔺)9.459s (+21.8% 🔺)1.808s101.00x
▲ VercelExpress7.692s (+19.0% 🔺)9.613s (+19.6% 🔺)1.921s101.01x
▲ VercelNext.js (Turbopack)9.201s (+28.8% 🔺)11.021s (+24.0% 🔺)1.820s91.20x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.481s (-4.4%)3.033s (-1.7%)0.552s401.00x
🐘 PostgresNitro2.669s (-1.7%)3.086s (+0.9%)0.417s391.08x
💻 LocalExpress3.122s (-3.8%)3.821s (-3.9%)0.699s321.26x
💻 LocalNitro3.154s (-2.0%)3.978s (-0.8%)0.824s311.27x
🐘 PostgresNext.js (Turbopack)3.207s (+29.5% 🔺)3.749s (+22.6% 🔺)0.542s331.29x
💻 LocalNext.js (Turbopack)3.281s (+0.6%)3.977s (-1.6%)0.696s311.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express15.239s (+10.5% 🔺)17.918s (+15.9% 🔺)2.679s71.00x
▲ VercelNitro15.788s (+12.3% 🔺)18.302s (+17.1% 🔺)2.515s71.04x
▲ VercelNext.js (Turbopack)18.176s (+10.8% 🔺)20.378s (+10.2% 🔺)2.202s61.19x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.197s (-6.3% 🟢)1.006s (~)0.809s601.00x
🐘 PostgresNext.js (Turbopack)0.213s (+27.2% 🔺)1.010s (~)0.797s601.08x
🐘 PostgresNitro0.222s (-2.1%)1.007s (~)0.784s601.13x
💻 LocalExpress0.463s (+7.4% 🔺)1.005s (~)0.541s602.35x
💻 LocalNitro0.465s (-0.7%)1.004s (~)0.539s602.36x
💻 LocalNext.js (Turbopack)0.613s (+5.0%)1.005s (~)0.391s603.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.257s (-42.1% 🟢)2.609s (-32.7% 🟢)1.352s231.00x
▲ VercelExpress1.375s (-33.7% 🟢)3.160s (-17.7% 🟢)1.785s191.09x
▲ VercelNext.js (Turbopack)1.468s (-19.3% 🟢)2.862s (-19.3% 🟢)1.395s211.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.314s (-2.7%)1.006s (-1.1%)0.692s901.00x
🐘 PostgresNitro0.336s (~)1.006s (~)0.671s901.07x
🐘 PostgresNext.js (Turbopack)0.347s (+29.3% 🔺)1.017s (+1.1%)0.670s891.10x
💻 LocalExpress2.137s (-1.1%)2.735s (~)0.598s336.80x
💻 LocalNitro2.192s (+2.8%)2.796s (+4.1%)0.604s336.97x
💻 LocalNext.js (Turbopack)2.734s (-2.0%)3.295s (+1.1%)0.560s288.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.855s (-2.7%)3.708s (+6.3% 🔺)1.853s251.00x
▲ VercelExpress2.119s (-14.0% 🟢)4.166s (+2.8%)2.047s221.14x
▲ VercelNext.js (Turbopack)2.680s (+23.5% 🔺)4.477s (+14.0% 🔺)1.797s211.44x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.531s (+0.5%)1.050s (+1.7%)0.519s1151.00x
🐘 PostgresExpress0.567s (+9.8% 🔺)1.160s (+8.6% 🔺)0.594s1041.07x
🐘 PostgresNext.js (Turbopack)0.699s (+48.1% 🔺)1.975s (-34.3% 🟢)1.277s611.32x
💻 LocalExpress9.304s (-7.8% 🟢)10.360s (-6.9% 🟢)1.056s1217.52x
💻 LocalNext.js (Turbopack)9.579s (-2.1%)10.530s (-2.3%)0.951s1218.04x
💻 LocalNitro10.363s (+4.5%)11.302s (+2.5%)0.940s1119.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.326s (+39.1% 🔺)5.768s (+42.3% 🔺)2.443s211.00x
▲ VercelNitro3.641s (+43.2% 🔺)5.915s (+43.4% 🔺)2.274s211.09x
▲ VercelNext.js (Turbopack)4.725s (+0.8%)7.116s (+8.7% 🔺)2.391s171.42x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.137s (-2.7%)1.968s (~)0.011s (-14.5% 🟢)2.018s (~)0.881s101.00x
💻 LocalExpress1.151s (~)2.005s (~)0.010s (-17.5% 🟢)2.017s (~)0.866s101.01x
🐘 PostgresNitro1.156s (-0.9%)1.999s (~)0.001s (+18.2% 🔺)2.010s (~)0.854s101.02x
💻 LocalNitro1.159s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.862s101.02x
🐘 PostgresExpress1.179s (+2.6%)2.000s (~)0.001s (+10.0% 🔺)2.011s (~)0.831s101.04x
🐘 PostgresNext.js (Turbopack)1.209s (+5.5% 🔺)1.996s (~)0.001s (-27.3% 🟢)2.009s (~)0.800s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.129s (+4.0%)3.553s (+15.8% 🔺)2.488s (+42.0% 🔺)6.571s (+21.6% 🔺)4.441s101.00x
▲ VercelNext.js (Turbopack)2.206s (+6.8% 🔺)3.312s (-4.0%)2.584s (+83.3% 🔺)6.338s (+17.2% 🔺)4.132s101.04x
▲ VercelNitro2.374s (+7.0% 🔺)3.275s (~)2.368s (+27.1% 🔺)6.269s (+10.1% 🔺)3.894s101.12x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.509s (-2.5%)2.010s (~)0.013s (-1.3%)2.026s (~)0.517s301.00x
💻 LocalNext.js (Turbopack)1.563s (-0.9%)1.971s (~)0.012s (-5.4% 🟢)2.025s (~)0.462s301.04x
💻 LocalNitro1.564s (+0.7%)2.010s (~)0.013s (+9.4% 🔺)2.026s (~)0.462s301.04x
🐘 PostgresExpress1.568s (+1.4%)2.010s (~)0.005s (-4.8%)2.027s (~)0.459s301.04x
🐘 PostgresNitro1.577s (-1.8%)2.003s (~)0.005s (~)2.027s (~)0.450s301.04x
🐘 PostgresNext.js (Turbopack)2.009s (+30.4% 🔺)2.447s (+21.7% 🔺)0.004s (-20.0% 🟢)2.478s (+22.3% 🔺)0.469s251.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.025s (+1.4%)7.125s (-3.4%)0.324s (+1.9%)7.978s (-3.4%)1.953s81.00x
▲ VercelExpress6.435s (+9.3% 🔺)7.891s (+9.5% 🔺)0.226s (+4.4%)8.797s (+11.6% 🔺)2.363s71.07x
▲ VercelNitro6.500s (+7.0% 🔺)7.678s (+8.5% 🔺)0.459s (+102.8% 🔺)8.656s (+10.6% 🔺)2.156s71.08x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.782s (+1.0%)1.071s (+2.2%)0.000s (+Infinity% 🔺)1.088s (+2.3%)0.307s561.00x
🐘 PostgresNitro0.821s (~)1.062s (-3.3%)0.000s (-50.9% 🟢)1.092s (-2.4%)0.271s551.05x
🐘 PostgresNext.js (Turbopack)1.037s (+13.1% 🔺)1.437s (+12.6% 🔺)0.000s (+14.6% 🔺)1.473s (+14.7% 🔺)0.436s411.33x
💻 LocalExpress1.209s (-4.7%)1.949s (-3.2%)0.000s (+214.5% 🔺)1.951s (-3.2%)0.743s311.55x
💻 LocalNitro1.280s (-2.9%)1.981s (~)0.001s (+41.7% 🔺)1.984s (~)0.704s311.64x
💻 LocalNext.js (Turbopack)1.411s (+5.7% 🔺)1.980s (~)0.001s (+183.3% 🔺)2.016s (~)0.605s301.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.353s (+7.5% 🔺)4.326s (-1.9%)0.000s (+Infinity% 🔺)4.782s (-1.9%)1.430s131.00x
▲ VercelExpress3.737s (-4.4%)5.137s (+2.2%)0.000s (-100.0% 🟢)5.715s (+3.2%)1.978s111.11x
▲ VercelNext.js (Turbopack)4.240s (+28.8% 🔺)5.616s (+19.4% 🔺)0.000s (+20.0% 🔺)6.124s (+16.5% 🔺)1.884s101.26x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.605s (-5.3% 🟢)2.171s (-3.7%)0.000s (-100.0% 🟢)2.211s (-2.6%)0.607s281.00x
🐘 PostgresNitro1.774s (-7.5% 🟢)2.339s (-6.3% 🟢)0.000s (-100.0% 🟢)2.355s (-6.6% 🟢)0.581s261.11x
🐘 PostgresNext.js (Turbopack)2.353s (+15.8% 🔺)2.908s (+11.3% 🔺)0.000s (-100.0% 🟢)2.925s (+10.7% 🔺)0.572s211.47x
💻 LocalExpress3.387s (-6.8% 🟢)4.027s (-4.8%)0.001s (+50.0% 🔺)4.031s (-4.7%)0.644s152.11x
💻 LocalNext.js (Turbopack)3.389s (-7.5% 🟢)3.869s (-7.7% 🟢)0.001s (-12.1% 🟢)3.912s (-7.6% 🟢)0.523s162.11x
💻 LocalNitro3.475s (-2.5%)4.160s (+3.3%)0.000s (-58.8% 🟢)4.163s (+3.3%)0.689s152.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.911s (+27.6% 🔺)7.295s (+21.2% 🔺)0.001s (+150.0% 🔺)7.812s (+18.7% 🔺)1.901s81.00x
▲ VercelExpress6.192s (+30.8% 🔺)7.470s (+30.1% 🔺)0.000s (+25.0% 🔺)8.018s (+28.2% 🔺)1.826s81.05x
▲ VercelNitro6.213s (+27.0% 🔺)7.485s (+30.9% 🔺)0.000s (NaN%)7.991s (+29.2% 🔺)1.778s81.05x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress18/21
🐘 PostgresExpress11/21
▲ VercelExpress8/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres15/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

Enabling H2 on the single shared dispatcher hung the queue's webhook
respondWith mechanism (120s E2E timeouts on webhook/hook workflows
across frameworks). Instead, give the v4 events API its own
H2-enabled dispatcher (getEventsDispatcher) and keep the default
dispatcher — queue/webhook, v3 makeRequest, streaming, deployment
resolution, run-key fetch — on HTTP/1.1.
The events endpoints are the hottest path (an event write per step
transition plus log reads) and are plain request/response (or a
streamed LIST response), none of which trip the webhook/duplex H2
issues, so they get multiplexing safely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SvelteKit bundles the server (including undici via the world adapter) into
ESM output. undici loads node: builtins as ESM imports except node:http2,
which it pulls in lazily via a bare require('node:http2') inside a try/catch.
The bundler leaves that bare require un-wired, so in the ESM bundle it throws
and undici silently falls back to a stub whose http2.connect is undefined —
breaking every HTTP/2 request (the workflow flow-route callback fails with
"fetch failed", so runs never start).
Inject a createRequire-backed global require into the SSR build only (a
node:module import would break the client bundle) so the real node:http2
resolves. Detected via configResolved.build.ssr because SvelteKit does not
set env.isSsrBuild for its server pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VaguelySeriousand others added 2 commits June 23, 2026 17:32
Nitro bundles undici (via the world adapter) into the ESM server output.
undici loads node: builtins as ESM imports except node:http2, which it pulls
in lazily via a bare require('node:http2') inside a try/catch. The bundler
leaves that bare require un-wired, so in the ESM bundle it throws and undici
silently falls back to a stub whose http2.connect is undefined — breaking
every HTTP/2 request (the workflow flow-route callback fails with "fetch
failed", so runs never start). This affects all nitro-based builds including
the Vite and TanStack Start framework integrations.
Prepend a createRequire-backed global require to the server chunks (via the
rollup:before hook, production builds only) so the real node:http2 resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream writes (write/writeMulti/close) previously used plain fetch() on
HTTP/1.1. Route them through a dedicated H2 stream dispatcher: the PUT
requests send a fully-buffered body (or none), so they gain H2 multiplexing
without the duplex-streaming issues that keep the long-lived live-read (GET)
on plain fetch. The stream dispatcher is a bare undici Agent without the
RetryAgent wrapper — stream appends are not idempotent, so undici-level retry
is deliberately omitted to avoid duplicating chunks (preserving the streamer's
original no-retry behavior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
headers: httpConfig.headers,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- undici dispatcher type doesn't match @types/node's RequestInit
dispatcher: getStreamDispatcher(config),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] I would block this until the Vercel Nitro v3 benchmark is rerun green or root-caused. The current Benchmark Vercel (nitro-v3) job fails with Stream 1 correctness failure: expected 1048576 bytes, got 983040, exactly one 64 KiB chunk missing from the stream benchmark. Since this PR changes stream PUTs to use the H2 dispatcher here, this looks like a plausible stream-write correctness regression rather than a pure perf failure. Job: https://github.com/vercel/workflow/actions/runs/28066956084/job/83093680965

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passed on re-run

* getDefaultStreamDispatcher for why this is a bare Agent (no retry).
*/
export function getStreamDispatcher(config?: APIConfig): unknown {
return config?.dispatcher ?? getDefaultStreamDispatcher();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This keeps the default stream dispatcher retry-free, but config.dispatcher now also controls stream writes. A caller that passes an undici RetryAgent can re-enable retries for stream PUTs; undici retries PUT by default, and stream appends are not idempotent, so that can duplicate chunks. Can we document this in the custom dispatcher section, or split out a stream-specific dispatcher option / guard so retrying dispatchers are not used for stream writes?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Good catch. Rather than leave stream writes retry-free, I scoped the retry policy so the win (resilience to transient blips) survives without the duplicate-chunk risk:

  • The default stream dispatcher is now a RetryAgent with STREAM_RETRY_OPTIONS = { methods: ['PUT'], statusCodes: [429], retryAfter: true }. It retries only on transient connection errors (undici's default errorCodes) and HTTP 429 — both guarantee the chunk was not persisted. 5xx is excluded (a 5xx can mean the chunk was written but the response failed → retry would duplicate), as are other 4xx.
  • For the caller-override case you flagged: documented in the dispatcher JSDoc (APIConfig in utils.ts) that a custom dispatcher replaces every default including stream writes, and should not retry PUT on 5xx.

Added a regression test asserting statusCodes is exactly [429] and never contains 500/502/503/504.

*/
function addNodeRequireBanner(config: RollupConfig): void {
const banner =
"import { createRequire as __wkfCreateRequire } from 'node:module'; if (typeof require === 'undefined') { globalThis.require = __wkfCreateRequire(import.meta.url); }";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This fixes the bundled undici node:http2 issue, but it also globally changes server-bundle feature detection: dependencies that branch on typeof require will now take their CJS/require path in these ESM bundles. The tested Vercel Node lanes passing is reassuring, but can we call this out as a Node-runtime-only server-bundle shim and keep an eye on optional-require behavior in bundled libraries?

@VaguelySeriousVaguelySeriousJun 24, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(AI) Agreed — that global feature-detection change is the right thing to flag. I kept the global globalThis.require (it's required: undici's bundled bare require(...) resolves against the runtime global scope, so a module-local const require wouldn't reliably be in scope for the inlined undici code), but tightened the JSDoc in both nitro/index.ts and sveltekit/plugin.ts to call it out explicitly as a Node-server-runtime-only shim and document the side effect + why it's safe:

  • Server-only — gated to the production server build (!nitro.options.dev / config.build.ssr); never touches the client/browser bundle.
  • Guarded — the typeof require === 'undefined' check makes it a no-op in CJS chunks where a real require already exists; it only fills a genuine ESM gap.
  • Functional — the require we install is a working createRequire, so a lib that switches to the require path gets a real require, not a broken stub (the common try { require(optional) } catch {} pattern still degrades gracefully).

The comment also records the one residual behavior to watch, per your ask: a bundled lib that, on detecting require, does require() of an ESM-only dependency on a Node version without require(ESM) support. The Vercel Node E2E lanes exercise the real undici path that motivated this. Comment-only change in b2ec9d7.

… 429
Stream writes (PUT) append chunks and are not idempotent. The stream
dispatcher was a bare Agent with no retry; switch it to a RetryAgent whose
policy is narrowed so PUTs retry only on transient connection errors and
HTTP 429 — both of which guarantee the chunk was not persisted — and never
on 5xx (where the chunk may already have been written) or other 4xx.
Also document that a caller-supplied `config.dispatcher` overrides the
stream dispatcher and should not retry PUT on 5xx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment threadpackages/world-vercel/src/streamer.ts Outdated
vercelBotand others added 2 commits June 24, 2026 17:34
…nt (no retry)" / "intentionally retry-free", contradicting the actual `RetryAgent` + `STREAM_RETRY_OPTIONS` implementation in a non-idempotent write path.
This commit fixes the issue reported at packages/world-vercel/src/streamer.ts:26
## The issue
`getDefaultStreamDispatcher` in `packages/world-vercel/src/http-client.ts` constructs a `RetryAgent` wrapping `STREAM_RETRY_OPTIONS` (`retryAfter: true`, `methods: ['PUT']`, `statusCodes: [429]`). So the stream-write dispatcher **does** retry — on undici's default transient connection errors and on HTTP 429.
Two comments still claimed the opposite:
1. `packages/world-vercel/src/streamer.ts` (~line 26):
> "That stream dispatcher is intentionally retry-free — stream appends aren't idempotent, so a retried write would duplicate chunks."
2. `packages/world-vercel/src/http-client.ts` (~line 102, JSDoc on `getStreamDispatcher`):
> "See getDefaultStreamDispatcher for why this is a bare Agent (no retry)."
Both are factually wrong: the dispatcher is a `RetryAgent`, not a bare `Agent`, and it is not retry-free. The accurate, nuanced description already lives in the `STREAM_RETRY_OPTIONS` and `getDefaultStreamDispatcher` JSDoc — these two comments were simply not updated when the behavior changed.
## Why it matters
This is the **non-idempotent write path**. A maintainer reasoning about correctness here relies on these comments to understand exactly what is and isn't retried. "Retry-free" vs. "retries on transient errors + 429 but never 5xx" is a meaningful safety distinction, so the stale wording actively misleads.
## The fix
Comment-only changes (no behavior change):
- **streamer.ts**: replaced "intentionally retry-free" with wording reflecting the narrowed policy — retries only on transient connection errors + HTTP 429 (both guaranteeing the chunk was never persisted), never 5xx, so a retry can't duplicate an already-applied write. Points to `STREAM_RETRY_OPTIONS`.
- **http-client.ts**: replaced "why this is a bare Agent (no retry)" with a reference to the deliberately narrowed retry policy in `getDefaultStreamDispatcher` / `STREAM_RETRY_OPTIONS`.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
…ide effect
Document that the createRequire banner deliberately makes `typeof require`
truthy for every bundled dependency in the ESM server output, why that is
safe (server-only, guarded, functional require), and the one residual
behavior to watch (require() of an ESM-only dep on old Node).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2615. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto 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.

2 participants

@VaguelySerious@karthikscale3