Skip to content

fix(world-vercel): cancel v4 event frame stream on early exit to release undici connections#2547

Merged
VaguelySerious merged 1 commit into
mainfrom
shohei/conn-leak
Jun 20, 2026
Merged

fix(world-vercel): cancel v4 event frame stream on early exit to release undici connections#2547
VaguelySerious merged 1 commit into
mainfrom
shohei/conn-leak

Conversation

@smaeda-ks

@smaeda-ks smaeda-ks commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

decodeFrames (the v4 event frame-stream reader) never cancelled its underlying response.body when a consumer stopped reading before EOF. With Node's fetch/undici, a response body that is neither fully drained nor cancelled keeps its socket checked out of the connection pool — so every such read leaks a connection until keepalive timeout/GC.

Two live read paths stop early by design and hit this:

Caller Early exit Frequency
getEventV4 returns after the first frame (single-frame response) per event read
consumeListFrameStream (getWorkflowRunEventsV4 / getEventsByCorrelationIdV4) breaks at the {_end:1} sentinel per event-list, i.e. ~every replay

This is the v4 wire format that events.ts uses "throughout" (it replaced the v2/v3 readers), so the leak is on the current, hot replay path — not legacy code.

Why it matters

In production every event read goes to a single origin — vercel-workflow.com (direct) or api.vercel.com (proxy) — and the dispatcher caps that origin at connections: 8 (HTTP/1.1). A leaked body holds its connection "in use," so it isn't returned to the pool. Pin ~8 of them and the pool is saturated: subsequent reads queue until a connection frees (the dropped response is GC-reclaimed, or the 60s request timeout fires). The user-visible result is intermittent stalls and timeouts on the event-read path; because each pinned connection ends up single-use instead of kept-alive, connection churn rises too.

Fix

packages/world-vercel/src/frames.ts:

  • Wrap the decodeFrames read loop in try/finally and call chunks.return?.() on exit, which cancels the source stream (releasing the socket) on early break/return, normal completion, and error paths alike. No-op once drained.
  • readerToIterator now cancels its reader in a finally (the non-async-iterable fallback branch).
  • Both share a small internal closeQuietly helper that swallows only cleanup errors, so it can't mask the original outcome.
  • Collapsed the redundant bodyLen > 0 / else branch into one path (slice(0, 0) already yields an empty body) — behavior identical.

Returned values are unaffected: frame.body is an owned slice copy, and the cancel runs after the result is assembled, so cancelling the remaining body can't corrupt what callers receive.

Test plan

  • packages/world-vercel/src/frames.test.ts: new tests asserting the underlying stream is cancelled when the consumer breaks early — for both the async-iterable branch and the getReader/readerToIterator branch — plus a guard that full consumption still decodes every frame. The spyStream helper models a kept-alive socket (highWaterMark: 0, never signals EOF); a toy stream that auto-closes would make cancel() a no-op and give a false pass.
  • packages/world-vercel/src/events-v4.test.ts: new getEventV4 HTTP round-trip via undici MockAgent (this path previously had no test). Includes a trailing frame the reader must never read, proving the early return + cancel returns correct data and doesn't hang.
  • Verified the new tests fail against pre-fix code and pass with the fix.
  • pnpm test (world-vercel): 167 passed. pnpm typecheck and biome check: clean.

Scope

Targets the decodeFrames early-exit leak only. A few lower-frequency error/edge paths (streamer.ts get()/list() throwing without draining; the content-type / missing-header throws in events-v4.ts) are the same class of issue and can be a follow-up.

🤖 Generated with Claude Code

decodeFrames never cancelled response.body when a consumer stopped reading
before EOF — getEventV4 returns after the first frame and consumeListFrameStream
breaks at the sentinel — so the undici connection stayed checked out of the pool
(8 per origin) instead of being released, causing stalls/timeouts on the
event-read path.

Cancel the source in a try/finally (and cancel the reader in readerToIterator)
via a shared closeQuietly helper. Add regression tests for both decode branches
and a getEventV4 HTTP round-trip through undici MockAgent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8fc2f47

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

This PR includes changesets to release 17 packages
Name Type
@workflow/world-vercel Patch
@workflow/cli Patch
@workflow/core Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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

@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Jun 20, 2026 7:07pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jun 20, 2026 7:07pm
example-workflow Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workbench-astro-workflow Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workbench-express-workflow Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workbench-fastify-workflow Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workbench-hono-workflow Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workbench-nitro-workflow Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workbench-nuxt-workflow Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workbench-vite-workflow Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Jun 20, 2026 7:07pm
workflow-swc-playground Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workflow-tarballs Ready Ready Preview, Comment Jun 20, 2026 7:07pm
workflow-web Ready Ready Preview, Comment Jun 20, 2026 7:07pm

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 1143 1 224 1368
✅ 💻 Local Development 1909 0 219 2128
✅ 📦 Local Production 1909 0 219 2128
✅ 🐘 Local Postgres 1895 0 233 2128
✅ 🪟 Windows 152 0 0 152
✅ 📋 Other 885 0 179 1064
Total 7893 1 1074 8968

❌ Failed Tests

▲ Vercel Production (1 failed)

nuxt (1 failed):

  • AbortController abortFromStepWorkflow: step abort cancels an in-flight sibling step

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 125 0 27
✅ example 125 0 27
✅ express 125 0 27
✅ fastify 125 0 27
✅ hono 125 0 27
✅ nitro 125 0 27
❌ nuxt 124 1 27
✅ sveltekit 144 0 8
✅ vite 125 0 27
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 127 0 25
✅ express-stable 127 0 25
✅ fastify-stable 127 0 25
✅ hono-stable 127 0 25
✅ nextjs-turbopack-canary 133 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 152 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 152 0 0
✅ nextjs-webpack-canary 133 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 152 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 152 0 0
✅ nitro-stable 127 0 25
✅ nuxt-stable 127 0 25
✅ sveltekit-stable 146 0 6
✅ vite-stable 127 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 127 0 25
✅ express-stable 127 0 25
✅ fastify-stable 127 0 25
✅ hono-stable 127 0 25
✅ nextjs-turbopack-canary 133 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 152 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 152 0 0
✅ nextjs-webpack-canary 133 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 152 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 152 0 0
✅ nitro-stable 127 0 25
✅ nuxt-stable 127 0 25
✅ sveltekit-stable 146 0 6
✅ vite-stable 127 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 126 0 26
✅ express-stable 126 0 26
✅ fastify-stable 126 0 26
✅ hono-stable 126 0 26
✅ nextjs-turbopack-canary 132 0 20
✅ nextjs-turbopack-stable-lazy-discovery-disabled 151 0 1
✅ nextjs-turbopack-stable-lazy-discovery-enabled 151 0 1
✅ nextjs-webpack-canary 132 0 20
✅ nextjs-webpack-stable-lazy-discovery-disabled 151 0 1
✅ nextjs-webpack-stable-lazy-discovery-enabled 151 0 1
✅ nitro-stable 126 0 26
✅ nuxt-stable 126 0 26
✅ sveltekit-stable 145 0 7
✅ vite-stable 126 0 26
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 152 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 127 0 25
✅ e2e-local-dev-tanstack-start- 127 0 25
✅ e2e-local-postgres-nest-stable 126 0 26
✅ e2e-local-postgres-tanstack-start- 126 0 26
✅ e2e-local-prod-nest-stable 127 0 25
✅ e2e-local-prod-tanstack-start- 127 0 25
✅ e2e-vercel-prod-tanstack-start 125 0 27

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.043s (+6.1% 🔺) 1.006s (~) 0.963s 10 1.00x
💻 Local Express 0.045s (+9.7% 🔺) 1.006s (~) 0.961s 10 1.04x
💻 Local Next.js (Turbopack) 0.051s (+5.6% 🔺) 1.006s (~) 0.955s 10 1.17x
🐘 Postgres Next.js (Turbopack) 0.054s (-6.2% 🟢) 1.011s (~) 0.957s 10 1.25x
🐘 Postgres Nitro 0.065s (+3.3%) 1.012s (~) 0.947s 10 1.50x
🐘 Postgres Express 0.094s (+53.0% 🔺) 1.033s (+2.1%) 0.939s 10 2.17x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.364s (-8.4% 🟢) 2.332s (-4.3%) 1.968s 10 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.090s (-0.9%) 2.007s (~) 0.916s 10 1.00x
💻 Local Next.js (Turbopack) 1.093s (~) 2.007s (~) 0.914s 10 1.00x
💻 Local Nitro 1.096s (+0.7%) 2.007s (~) 0.911s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.102s (~) 2.011s (~) 0.909s 10 1.01x
🐘 Postgres Nitro 1.112s (+0.7%) 2.009s (~) 0.897s 10 1.02x
🐘 Postgres Express 1.112s (+0.8%) 2.010s (~) 0.898s 10 1.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.893s (+24.5% 🔺) 3.512s (-1.6%) 1.619s 10 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 10.505s (~) 11.012s (~) 0.507s 3 1.00x
💻 Local Nitro 10.519s (~) 11.023s (~) 0.504s 3 1.00x
💻 Local Express 10.523s (~) 11.024s (~) 0.501s 3 1.00x
🐘 Postgres Next.js (Turbopack) 10.527s (~) 11.021s (~) 0.494s 3 1.00x
💻 Local Next.js (Turbopack) 10.544s (~) 11.023s (~) 0.479s 3 1.00x
🐘 Postgres Nitro 10.548s (~) 11.018s (~) 0.470s 3 1.00x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 13.799s (+5.4% 🔺) 15.362s (+1.0%) 1.564s 2 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 13.737s (~) 14.018s (~) 0.281s 5 1.00x
💻 Local Express 13.742s (-1.8%) 14.027s (-1.4%) 0.284s 5 1.00x
💻 Local Nitro 13.782s (+0.7%) 14.028s (~) 0.246s 5 1.00x
🐘 Postgres Nitro 13.805s (-0.7%) 14.018s (~) 0.213s 5 1.00x
🐘 Postgres Next.js (Turbopack) 13.837s (-0.6%) 14.023s (~) 0.186s 5 1.01x
💻 Local Next.js (Turbopack) 13.855s (+1.1%) 14.028s (~) 0.174s 5 1.01x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 22.314s (+10.7% 🔺) 23.407s (+4.4%) 1.092s 3 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 12.376s (-1.2%) 13.016s (~) 0.639s 7 1.00x
💻 Local Express 12.387s (-1.6%) 13.025s (~) 0.638s 7 1.00x
🐘 Postgres Nitro 12.475s (+0.6%) 13.022s (~) 0.547s 7 1.01x
💻 Local Next.js (Turbopack) 12.485s (~) 13.025s (~) 0.541s 7 1.01x
💻 Local Nitro 12.649s (+2.9%) 13.027s (~) 0.378s 7 1.02x
🐘 Postgres Express 13.722s (+10.2% 🔺) 14.042s (+7.9% 🔺) 0.320s 7 1.11x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 31.031s (+34.8% 🔺) 33.465s (+33.0% 🔺) 2.434s 3 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.155s (~) 2.008s (~) 0.854s 15 1.00x
🐘 Postgres Nitro 1.193s (~) 2.009s (~) 0.816s 15 1.03x
💻 Local Express 1.223s (-0.6%) 2.006s (~) 0.783s 15 1.06x
🐘 Postgres Express 1.236s (+3.4%) 2.012s (~) 0.776s 15 1.07x
💻 Local Nitro 1.238s (+2.9%) 2.007s (~) 0.769s 15 1.07x
💻 Local Next.js (Turbopack) 1.294s (-5.9% 🟢) 2.006s (~) 0.712s 15 1.12x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.811s (+4.4%) 4.319s (-3.1%) 1.508s 8 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.264s (+1.0%) 2.009s (~) 0.744s 15 1.00x
🐘 Postgres Express 1.271s (-2.8%) 2.012s (-3.0%) 0.741s 15 1.01x
🐘 Postgres Nitro 1.276s (+0.5%) 2.008s (~) 0.732s 15 1.01x
💻 Local Nitro 2.000s (-0.8%) 2.316s (-7.6% 🟢) 0.316s 13 1.58x
💻 Local Express 2.152s (+5.7% 🔺) 2.392s (-4.6%) 0.241s 13 1.70x
💻 Local Next.js (Turbopack) 2.297s (-2.0%) 2.735s (-6.3% 🟢) 0.438s 11 1.82x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.236s (+14.1% 🔺) 4.742s (+3.3%) 1.506s 7 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.486s (+2.5%) 3.762s (+2.2%) 2.276s 8 1.00x
🐘 Postgres Next.js (Turbopack) 1.516s (+2.3%) 3.885s (~) 2.369s 8 1.02x
🐘 Postgres Express 1.617s (-2.4%) 3.570s (-5.1% 🟢) 1.953s 9 1.09x
💻 Local Nitro 5.496s (+22.6% 🔺) 6.015s (+20.0% 🔺) 0.519s 5 3.70x
💻 Local Express 5.652s (-2.1%) 6.016s (-6.2% 🟢) 0.364s 5 3.80x
💻 Local Next.js (Turbopack) 6.781s (+1.4%) 7.416s (~) 0.635s 5 4.56x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 6.669s (+117.1% 🔺) 8.889s (+80.3% 🔺) 2.220s 4 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.144s (-2.5%) 2.007s (~) 0.864s 15 1.00x
🐘 Postgres Express 1.188s (-0.6%) 2.014s (~) 0.826s 15 1.04x
🐘 Postgres Nitro 1.203s (~) 2.007s (~) 0.804s 15 1.05x
💻 Local Express 1.204s (-2.1%) 2.006s (~) 0.802s 15 1.05x
💻 Local Nitro 1.231s (~) 2.007s (~) 0.776s 15 1.08x
💻 Local Next.js (Turbopack) 1.393s (+3.1%) 2.006s (~) 0.614s 15 1.22x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.013s (+48.9% 🔺) 4.449s (+13.9% 🔺) 1.436s 7 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.257s (-0.5%) 2.150s (+3.6%) 0.893s 14 1.00x
🐘 Postgres Express 1.263s (-2.5%) 2.006s (~) 0.744s 15 1.00x
🐘 Postgres Nitro 1.292s (+1.3%) 2.008s (~) 0.716s 15 1.03x
💻 Local Express 1.988s (-2.4%) 2.470s (-9.8% 🟢) 0.482s 13 1.58x
💻 Local Nitro 1.997s (-0.6%) 2.508s (+8.4% 🔺) 0.511s 12 1.59x
💻 Local Next.js (Turbopack) 2.250s (-7.0% 🟢) 2.734s (-6.3% 🟢) 0.485s 11 1.79x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.587s (+39.0% 🔺) 5.426s (+24.8% 🔺) 1.839s 6 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.446s (-0.7%) 3.760s (~) 2.314s 8 1.00x
🐘 Postgres Express 1.451s (+1.4%) 3.677s (-2.2%) 2.226s 9 1.00x
🐘 Postgres Next.js (Turbopack) 1.477s (+0.5%) 4.009s (+3.2%) 2.532s 8 1.02x
💻 Local Express 5.477s (-8.2% 🟢) 6.017s (-11.7% 🟢) 0.540s 5 3.79x
💻 Local Nitro 5.878s (+18.9% 🔺) 6.417s (+16.4% 🔺) 0.539s 5 4.07x
💻 Local Next.js (Turbopack) 6.700s (-1.3%) 7.219s (-3.9%) 0.519s 5 4.63x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.783s (+35.9% 🔺) 7.504s (+23.1% 🔺) 1.721s 4 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.518s (-8.3% 🟢) 1.006s (-1.7%) 0.488s 60 1.00x
🐘 Postgres Express 0.525s (-8.8% 🟢) 1.025s (~) 0.500s 59 1.01x
🐘 Postgres Nitro 0.551s (-2.0%) 1.006s (-1.6%) 0.456s 60 1.06x
💻 Local Express 0.572s (-6.3% 🟢) 1.005s (~) 0.433s 60 1.10x
💻 Local Next.js (Turbopack) 0.585s (-4.4%) 1.005s (-1.7%) 0.420s 60 1.13x
💻 Local Nitro 0.604s (+5.9% 🔺) 1.005s (-1.6%) 0.401s 60 1.17x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.117s (+20.0% 🔺) 5.558s (+2.4%) 1.441s 11 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.258s (-1.4%) 2.007s (~) 0.749s 45 1.00x
🐘 Postgres Express 1.304s (-1.6%) 1.946s (-3.1%) 0.641s 47 1.04x
🐘 Postgres Nitro 1.310s (+2.8%) 2.008s (~) 0.698s 45 1.04x
💻 Local Express 1.493s (-4.2%) 2.028s (~) 0.535s 45 1.19x
💻 Local Next.js (Turbopack) 1.506s (+2.1%) 2.006s (~) 0.500s 45 1.20x
💻 Local Nitro 1.506s (+8.9% 🔺) 2.007s (~) 0.500s 45 1.20x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 10.059s (+17.3% 🔺) 12.286s (+17.8% 🔺) 2.227s 8 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.356s (-10.6% 🟢) 2.984s (-2.4%) 0.628s 41 1.00x
🐘 Postgres Next.js (Turbopack) 2.527s (-0.9%) 3.009s (~) 0.482s 40 1.07x
🐘 Postgres Nitro 2.608s (-1.7%) 3.059s (-2.5%) 0.451s 40 1.11x
💻 Local Express 3.231s (-0.6%) 3.977s (-0.8%) 0.746s 31 1.37x
💻 Local Next.js (Turbopack) 3.258s (+1.7%) 4.010s (+0.8%) 0.751s 30 1.38x
💻 Local Nitro 3.296s (+10.0% 🔺) 4.010s (+16.7% 🔺) 0.714s 30 1.40x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 19.195s (+27.6% 🔺) 21.256s (+23.4% 🔺) 2.061s 6 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.167s (-6.5% 🟢) 1.007s (~) 0.840s 60 1.00x
🐘 Postgres Express 0.200s (-8.5% 🟢) 1.012s (+0.6%) 0.812s 60 1.20x
🐘 Postgres Nitro 0.229s (+8.3% 🔺) 1.006s (~) 0.777s 60 1.37x
💻 Local Express 0.345s (+1.0%) 1.005s (~) 0.660s 60 2.06x
💻 Local Nitro 0.358s (+6.0% 🔺) 1.005s (~) 0.647s 60 2.14x
💻 Local Next.js (Turbopack) 0.633s (+4.6%) 1.039s (+3.4%) 0.406s 58 3.79x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.608s (+27.1% 🔺) 3.173s (+7.4% 🔺) 1.565s 20 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.271s (+1.1%) 1.029s (+2.3%) 0.758s 88 1.00x
🐘 Postgres Nitro 0.345s (+6.7% 🔺) 1.007s (~) 0.662s 90 1.27x
🐘 Postgres Express 0.514s (+60.2% 🔺) 1.043s (+3.7%) 0.529s 87 1.90x
💻 Local Nitro 2.066s (+2.0%) 2.537s (+2.8%) 0.471s 36 7.62x
💻 Local Express 2.070s (+5.8% 🔺) 2.470s (~) 0.400s 37 7.64x
💻 Local Next.js (Turbopack) 2.643s (-0.9%) 3.296s (+7.2% 🔺) 0.652s 28 9.75x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.327s (+37.2% 🔺) 4.125s (+16.7% 🔺) 1.798s 23 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.486s (+0.8%) 2.936s (-3.2%) 2.451s 41 1.00x
🐘 Postgres Nitro 0.516s (-1.7%) 1.068s (+1.8%) 0.553s 113 1.06x
🐘 Postgres Express 0.579s (+17.4% 🔺) 1.120s (+10.4% 🔺) 0.541s 108 1.19x
💻 Local Nitro 9.822s (+5.0%) 10.445s (+3.3%) 0.622s 12 20.23x
💻 Local Express 9.969s (+1.9%) 10.695s (+0.8%) 0.726s 12 20.53x
💻 Local Next.js (Turbopack) 11.201s (+11.3% 🔺) 12.131s (+9.1% 🔺) 0.929s 10 23.07x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.258s (+75.6% 🔺) 6.218s (+42.1% 🔺) 1.959s 20 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.155s (-1.3%) 2.001s (~) 0.001s (~) 2.011s (~) 0.856s 10 1.00x
💻 Local Next.js (Turbopack) 1.157s (~) 2.003s (~) 0.012s (-3.2%) 2.019s (~) 0.862s 10 1.00x
💻 Local Express 1.169s (~) 2.005s (~) 0.012s (-4.1%) 2.020s (~) 0.851s 10 1.01x
💻 Local Nitro 1.171s (+1.8%) 2.005s (~) 0.013s (+37.0% 🔺) 2.020s (~) 0.850s 10 1.01x
🐘 Postgres Nitro 1.176s (+0.6%) 1.995s (~) 0.001s (-31.6% 🟢) 2.011s (~) 0.835s 10 1.02x
🐘 Postgres Express 1.269s (+7.7% 🔺) 1.971s (-1.2%) 0.001s (-42.9% 🟢) 2.029s (+0.9%) 0.760s 10 1.10x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.523s (+11.7% 🔺) 3.515s (+3.1%) 2.599s (-4.0%) 6.612s (-3.0%) 4.090s 10 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.576s (-0.5%) 2.010s (~) 0.012s (~) 2.025s (~) 0.449s 30 1.00x
🐘 Postgres Next.js (Turbopack) 1.595s (~) 2.010s (~) 0.005s (+10.1% 🔺) 2.026s (~) 0.430s 30 1.01x
🐘 Postgres Nitro 1.599s (-2.6%) 2.005s (-1.6%) 0.005s (+1.3%) 2.027s (-1.5%) 0.428s 30 1.01x
💻 Local Nitro 1.611s (+2.0%) 2.010s (~) 0.012s (~) 2.025s (~) 0.415s 30 1.02x
💻 Local Next.js (Turbopack) 1.645s (+1.7%) 2.008s (~) 0.013s (-1.6%) 2.025s (~) 0.380s 30 1.04x
🐘 Postgres Express 1.722s (+8.6% 🔺) 2.144s (+7.0% 🔺) 0.004s (-15.1% 🟢) 2.169s (+7.0% 🔺) 0.447s 28 1.09x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 6.853s (+11.7% 🔺) 8.340s (+6.3% 🔺) 0.224s (-21.0% 🟢) 9.063s (+4.5%) 2.210s 7 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.793s (+3.1%) 1.070s (-3.1%) 0.000s (-1.8%) 1.097s (-1.9%) 0.304s 55 1.00x
🐘 Postgres Next.js (Turbopack) 0.810s (+6.4% 🔺) 1.111s (+2.1%) 0.000s (+111.1% 🔺) 1.119s (+2.1%) 0.309s 54 1.02x
🐘 Postgres Express 1.124s (+47.2% 🔺) 1.532s (+44.0% 🔺) 0.000s (+Infinity% 🔺) 1.560s (+44.6% 🔺) 0.436s 39 1.42x
💻 Local Express 1.361s (+2.9%) 2.013s (~) 0.001s (+54.5% 🔺) 2.016s (~) 0.655s 30 1.72x
💻 Local Nitro 1.377s (+0.9%) 2.014s (~) 0.001s (+50.0% 🔺) 2.017s (~) 0.640s 30 1.74x
💻 Local Next.js (Turbopack) 1.496s (+3.3%) 2.013s (~) 0.001s (+77.8% 🔺) 2.017s (~) 0.521s 30 1.89x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.729s (+10.4% 🔺) 4.651s (-7.8% 🟢) 0.000s (-54.2% 🟢) 5.149s (-7.5% 🟢) 1.419s 12 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.578s (-4.7%) 2.257s (+1.9%) 0.000s (-100.0% 🟢) 2.289s (+2.6%) 0.711s 27 1.00x
🐘 Postgres Next.js (Turbopack) 1.935s (+1.4%) 2.544s (~) 0.000s (+Infinity% 🔺) 2.552s (~) 0.617s 24 1.23x
🐘 Postgres Express 2.497s (+63.3% 🔺) 3.006s (+43.1% 🔺) 0.000s (+33.3% 🔺) 3.068s (+42.6% 🔺) 0.571s 21 1.58x
💻 Local Express 3.453s (-5.2% 🟢) 3.965s (-6.2% 🟢) 0.000s (-43.7% 🟢) 3.969s (-6.2% 🟢) 0.517s 16 2.19x
💻 Local Nitro 3.647s (+4.2%) 4.092s (+3.3%) 0.000s (-64.4% 🟢) 4.099s (+3.2%) 0.452s 15 2.31x
💻 Local Next.js (Turbopack) 3.962s (-3.8%) 4.529s (-1.5%) 0.000s (-72.2% 🟢) 4.534s (-1.6%) 0.572s 14 2.51x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 6.001s (-27.3% 🟢) 7.053s (-28.6% 🟢) 0.000s (-100.0% 🟢) 7.582s (-27.4% 🟢) 1.581s 8 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Express 14/21
🐘 Postgres Next.js (Turbopack) 14/21
▲ Vercel Express 21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 15/21
Next.js (Turbopack) 🐘 Postgres 19/21
Nitro 🐘 Postgres 16/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)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@vercel vercel Bot requested a deployment to Preview – workbench-express-workflow June 20, 2026 19:05 Abandoned
@smaeda-ks smaeda-ks marked this pull request as ready for review June 20, 2026 19:12
@smaeda-ks smaeda-ks requested a review from a team as a code owner June 20, 2026 19:12
@VaguelySerious VaguelySerious merged commit e3672e8 into main Jun 20, 2026
113 of 119 checks passed
@VaguelySerious VaguelySerious deleted the shohei/conn-leak branch June 20, 2026 19:36
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for e3672e8 (AI decision).

This fix targets the decodeFrames/readerToIterator machinery in packages/world-vercel/src/frames.ts (and tests in frames.test.ts / events-v4.test.ts), none of which exist on stable — verified that stable has no frames.ts, no events-v4.*, and an events.ts with no streaming/frame-decoding logic at all. The v4 frame-stream wire format is main-only, so the bug being fixed does not exist on stable and the change cannot apply there.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

e3672e84f4996fbd35fc2542d11d401979d76924

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants