Skip to content

[world-vercel] Propagate cancel from streams.get to upstream fetch#1802

Draft
VaguelySerious wants to merge 2 commits intomainfrom
peter/fix-stream-cancel-reconnect-main
Draft

[world-vercel] Propagate cancel from streams.get to upstream fetch#1802
VaguelySerious wants to merge 2 commits intomainfrom
peter/fix-stream-cancel-reconnect-main

Conversation

@VaguelySerious
Copy link
Copy Markdown
Member

Summary

Forward-port of #1801 onto main. Same class of bug, different function shape (streams.get(runId, name, startIndex) here vs. readFromStream(name, startIndex) on stable).

When a consumer cancels the ReadableStream returned by streams.get() (e.g. an HTTP client disconnects from an API route that pipes run.getReadable()), the pull loop in the streamer kept running and could trigger further reconnects in the background. The in-flight upstream fetch was never aborted, so the endpoint kept fetching long after the client had gone away.

Root cause

  1. No cancelled flag — pull had no way to know the consumer asked to stop.
  2. No AbortSignal on the upstream fetch() — even if we noticed, there was no way to unblock a pending request.

The cancel handler only called reader.cancel() on whichever reader was captured at that moment. If pull was mid-reconnect (reader = await connect()), cancel cancelled the stale reader; the new fetch then connected and kept streaming.

Fix

  • Track a cancelled flag that pull checks before and after each reader.read() and before each connect().
  • Plumb an AbortController.signal into fetch, aborted from cancel, so the in-flight request (including a pending reconnect) unblocks.
  • connect() failures triggered by abort are swallowed silently when cancelled.

Tests

Three new tests under a new streams.get consumer cancel describe block:

  • aborts the in-flight upstream fetch via AbortSignal — fails on main without the fix; asserts fetch was called with an AbortSignal and that the signal is aborted after stream.cancel().
  • does not reconnect after the consumer cancels mid-timeout — documents that no further fetch is initiated after cancel.
  • cancels the active reader when cancel is called during a read — documents that cancel unblocks a pending reader.read().

All 32 world-vercel streamer tests pass locally.

Companion

Test plan

  • pnpm vitest run — all world-vercel tests pass
  • Typecheck clean
  • CI + workflow-server auto e2e

🤖 Generated with Claude Code

When a consumer cancels the ReadableStream returned by streams.get
(e.g. an HTTP client hanging up on an endpoint that pipes
`run.getReadable()`), the pull loop could continue running and even
trigger a fresh reconnect via `connect()` — the new fetch was never
tied to the cancellation, so the request kept running in the
background.

Fix:
- Track a `cancelled` flag that `pull` checks before and after each
  read and before each reconnect.
- Plumb an `AbortController.signal` into `fetch` so the in-flight
  upstream request (including a pending reconnect) is aborted when
  the consumer cancels.

Regression tests cover the abort-signal contract, the mid-timeout
reconnect race, and cancel-during-read.

Port of #1801 (targeting `stable`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Signed-off-by: Peter Wielander <[email protected]>
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 17, 2026

🦋 Changeset detected

Latest commit: 9de9271

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

This PR includes changesets to release 18 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/ai 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
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 17, 2026

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 Apr 17, 2026 7:38pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Apr 17, 2026 7:38pm
example-workflow Ready Ready Preview, Comment Apr 17, 2026 7:38pm
workbench-astro-workflow Ready Ready Preview, Comment Apr 17, 2026 7:38pm
workbench-express-workflow Ready Ready Preview, Comment Apr 17, 2026 7:38pm
workbench-fastify-workflow Ready Ready Preview, Comment Apr 17, 2026 7:38pm
workbench-hono-workflow Ready Ready Preview, Comment Apr 17, 2026 7:38pm
workbench-nitro-workflow Ready Ready Preview, Comment Apr 17, 2026 7:38pm
workbench-nuxt-workflow Ready Ready Preview, Comment Apr 17, 2026 7:38pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Apr 17, 2026 7:38pm
workbench-vite-workflow Ready Ready Preview, Comment Apr 17, 2026 7:38pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Apr 17, 2026 7:38pm
workflow-swc-playground Ready Ready Preview, Comment Apr 17, 2026 7:38pm
workflow-web Ready Ready Preview, Comment Apr 17, 2026 7:38pm

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 17, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 945 0 67 1012
❌ 💻 Local Development 1017 1 86 1104
✅ 📦 Local Production 1018 0 86 1104
✅ 🐘 Local Postgres 1018 0 86 1104
✅ 📋 Other 258 0 18 276
Total 4256 1 343 4600

❌ Failed Tests

💻 Local Development (1 failed)

vite-stable (1 failed):

  • hookWorkflow | wrun_01KPEF76M376DM0WQEFASYBKMY

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 85 0 7
✅ example 85 0 7
✅ express 85 0 7
✅ fastify 85 0 7
✅ hono 85 0 7
✅ nextjs-turbopack 90 0 2
✅ nextjs-webpack 90 0 2
✅ nitro 85 0 7
✅ nuxt 85 0 7
✅ sveltekit 85 0 7
✅ vite 85 0 7
❌ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 86 0 6
✅ express-stable 86 0 6
✅ fastify-stable 86 0 6
✅ hono-stable 86 0 6
✅ nextjs-turbopack-canary 73 0 19
✅ nextjs-turbopack-stable 92 0 0
✅ nextjs-webpack-canary 73 0 19
✅ nextjs-webpack-stable 92 0 0
✅ nitro-stable 86 0 6
✅ nuxt-stable 86 0 6
✅ sveltekit-stable 86 0 6
❌ vite-stable 85 1 6
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 86 0 6
✅ express-stable 86 0 6
✅ fastify-stable 86 0 6
✅ hono-stable 86 0 6
✅ nextjs-turbopack-canary 73 0 19
✅ nextjs-turbopack-stable 92 0 0
✅ nextjs-webpack-canary 73 0 19
✅ nextjs-webpack-stable 92 0 0
✅ nitro-stable 86 0 6
✅ nuxt-stable 86 0 6
✅ sveltekit-stable 86 0 6
✅ vite-stable 86 0 6
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 86 0 6
✅ express-stable 86 0 6
✅ fastify-stable 86 0 6
✅ hono-stable 86 0 6
✅ nextjs-turbopack-canary 73 0 19
✅ nextjs-turbopack-stable 92 0 0
✅ nextjs-webpack-canary 73 0 19
✅ nextjs-webpack-stable 92 0 0
✅ nitro-stable 86 0 6
✅ nuxt-stable 86 0 6
✅ sveltekit-stable 86 0 6
✅ vite-stable 86 0 6
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 86 0 6
✅ e2e-local-postgres-nest-stable 86 0 6
✅ e2e-local-prod-nest-stable 86 0 6

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 17, 2026

📊 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.044s (+1.4%) 1.005s (~) 0.962s 10 1.00x
💻 Local Express 0.044s (-0.9%) 1.005s (~) 0.961s 10 1.00x
💻 Local Next.js (Turbopack) 0.048s 1.005s 0.957s 10 1.09x
🐘 Postgres Express 0.057s (-2.1%) 1.011s (~) 0.955s 10 1.30x
🐘 Postgres Next.js (Turbopack) 0.058s 1.009s 0.951s 10 1.33x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.215s (-8.6% 🟢) 1.900s (-11.0% 🟢) 1.685s 10 1.00x
▲ Vercel Nitro 0.254s (-38.0% 🟢) 1.794s (-28.5% 🟢) 1.540s 10 1.18x
▲ Vercel Next.js (Turbopack) 0.264s (+5.1% 🔺) 2.153s (-7.7% 🟢) 1.889s 10 1.23x

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

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 1.123s 2.005s 0.882s 10 1.00x
💻 Local Nitro 1.128s (~) 2.006s (~) 0.878s 10 1.00x
💻 Local Express 1.129s (~) 2.005s (~) 0.876s 10 1.00x
🐘 Postgres Next.js (Turbopack) 1.139s 2.009s 0.870s 10 1.01x
🐘 Postgres Express 1.153s (+0.5%) 2.010s (~) 0.857s 10 1.03x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.945s (+3.7%) 3.789s (~) 1.844s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.087s (+2.5%) 3.450s (-9.9% 🟢) 1.363s 10 1.07x
▲ Vercel Nitro 2.112s (-45.7% 🟢) 3.337s (-43.5% 🟢) 1.225s 10 1.09x

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

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 10.778s 11.022s 0.245s 3 1.00x
🐘 Postgres Express 10.871s (-0.8%) 11.023s (~) 0.152s 3 1.01x
💻 Local Express 10.927s (~) 11.023s (~) 0.097s 3 1.01x
🐘 Postgres Next.js (Turbopack) 10.939s 11.020s 0.082s 3 1.01x
💻 Local Nitro 10.942s (~) 11.023s (~) 0.081s 3 1.02x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 19.107s (+10.3% 🔺) 20.648s (+6.4% 🔺) 1.542s 2 1.00x
▲ Vercel Express 20.625s (+21.5% 🔺) 22.950s (+14.6% 🔺) 2.326s 2 1.08x
▲ Vercel Nitro 22.424s (-5.5% 🟢) 24.009s (-4.4%) 1.585s 2 1.17x

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

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 14.499s 15.020s 0.520s 4 1.00x
💻 Local Next.js (Turbopack) 14.627s 15.029s 0.402s 4 1.01x
🐘 Postgres Express 14.633s (~) 15.025s (~) 0.392s 4 1.01x
💻 Local Nitro 14.994s (~) 15.280s (-4.7%) 0.286s 4 1.03x
💻 Local Express 15.033s (~) 15.280s (+1.7%) 0.247s 4 1.04x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 31.730s (-36.9% 🟢) 33.288s (-36.7% 🟢) 1.558s 2 1.00x
▲ Vercel Nitro 32.513s (-49.6% 🟢) 33.549s (-49.6% 🟢) 1.037s 2 1.02x
▲ Vercel Next.js (Turbopack) 34.645s (-34.1% 🟢) 36.196s (-33.7% 🟢) 1.551s 2 1.09x

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

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 13.972s (~) 14.310s (-1.9%) 0.338s 7 1.00x
🐘 Postgres Next.js (Turbopack) 13.996s 14.306s 0.310s 7 1.00x
💻 Local Next.js (Turbopack) 16.008s 16.363s 0.354s 6 1.15x
💻 Local Nitro 16.765s (~) 17.031s (~) 0.266s 6 1.20x
💻 Local Express 16.821s (+1.3%) 17.199s (+1.0%) 0.378s 6 1.20x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 55.760s (-54.0% 🟢) 57.850s (-53.2% 🟢) 2.090s 2 1.00x
▲ Vercel Next.js (Turbopack) 58.773s (-85.1% 🟢) 60.892s (-84.6% 🟢) 2.119s 2 1.05x
▲ Vercel Nitro 58.992s (-86.1% 🟢) 59.843s (-85.9% 🟢) 0.852s 2 1.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.237s 2.009s 0.771s 15 1.00x
🐘 Postgres Express 1.271s (+0.8%) 2.010s (~) 0.739s 15 1.03x
💻 Local Next.js (Turbopack) 1.565s 2.073s 0.508s 15 1.26x
💻 Local Nitro 1.569s (-3.8%) 2.006s (-3.3%) 0.438s 15 1.27x
💻 Local Express 1.591s (+6.9% 🔺) 2.072s (+3.3%) 0.481s 15 1.29x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.177s (-23.9% 🟢) 3.957s (-14.4% 🟢) 1.780s 8 1.00x
▲ Vercel Nitro 2.266s (-19.6% 🟢) 3.575s (-17.3% 🟢) 1.309s 9 1.04x
▲ Vercel Next.js (Turbopack) 2.491s (-26.7% 🟢) 3.966s (-19.6% 🟢) 1.475s 8 1.14x

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

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.326s (-1.5%) 3.008s (~) 0.682s 10 1.00x
🐘 Postgres Next.js (Turbopack) 2.414s 3.009s 0.595s 10 1.04x
💻 Local Express 2.853s (-3.4%) 3.209s (-7.1% 🟢) 0.355s 10 1.23x
💻 Local Next.js (Turbopack) 2.870s 3.109s 0.239s 10 1.23x
💻 Local Nitro 3.075s (-2.2%) 3.676s (-5.4% 🟢) 0.602s 9 1.32x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.427s (-40.1% 🟢) 3.659s (-38.2% 🟢) 1.232s 9 1.00x
▲ Vercel Express 2.597s (-28.2% 🟢) 3.934s (-23.0% 🟢) 1.337s 8 1.07x
▲ Vercel Next.js (Turbopack) 2.605s (-63.3% 🟢) 4.047s (-54.6% 🟢) 1.442s 8 1.07x

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

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.487s (~) 4.009s (~) 0.522s 8 1.00x
🐘 Postgres Next.js (Turbopack) 3.660s 4.010s 0.350s 8 1.05x
💻 Local Next.js (Turbopack) 7.234s 7.765s 0.532s 4 2.07x
💻 Local Express 7.298s (-12.5% 🟢) 8.019s (-11.2% 🟢) 0.721s 4 2.09x
💻 Local Nitro 8.550s (+2.4%) 9.025s (~) 0.475s 4 2.45x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.074s (-12.8% 🟢) 4.414s (-20.2% 🟢) 1.340s 8 1.00x
▲ Vercel Express 3.138s (-26.0% 🟢) 5.027s (-18.0% 🟢) 1.889s 7 1.02x
▲ Vercel Next.js (Turbopack) 4.736s (-46.9% 🟢) 6.298s (-42.5% 🟢) 1.562s 5 1.54x

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

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.235s 2.009s 0.774s 15 1.00x
🐘 Postgres Express 1.264s (+0.5%) 2.007s (~) 0.744s 15 1.02x
💻 Local Next.js (Turbopack) 1.508s 2.005s 0.497s 15 1.22x
💻 Local Express 1.517s (-19.9% 🟢) 2.006s (-15.1% 🟢) 0.489s 15 1.23x
💻 Local Nitro 1.557s (-16.5% 🟢) 2.006s (-14.3% 🟢) 0.449s 15 1.26x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.144s (-16.9% 🟢) 4.110s (-5.5% 🟢) 1.966s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.156s (-26.5% 🟢) 3.524s (-24.1% 🟢) 1.368s 9 1.01x
▲ Vercel Nitro 2.427s (-1.3%) 3.595s (-13.8% 🟢) 1.168s 9 1.13x

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

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.352s (~) 3.009s (~) 0.657s 10 1.00x
🐘 Postgres Next.js (Turbopack) 2.387s 3.009s 0.622s 10 1.01x
💻 Local Express 2.940s (-6.1% 🟢) 3.341s (-11.2% 🟢) 0.401s 9 1.25x
💻 Local Next.js (Turbopack) 2.961s 3.760s 0.799s 8 1.26x
💻 Local Nitro 3.055s (~) 3.759s (-3.3%) 0.704s 8 1.30x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.296s (-28.1% 🟢) 3.931s (-18.0% 🟢) 1.635s 8 1.00x
▲ Vercel Nitro 2.679s (-17.1% 🟢) 3.957s (-22.1% 🟢) 1.277s 9 1.17x
▲ Vercel Next.js (Turbopack) 2.758s (-12.2% 🟢) 4.045s (-10.5% 🟢) 1.287s 8 1.20x

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

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.489s (~) 4.011s (~) 0.521s 8 1.00x
🐘 Postgres Next.js (Turbopack) 3.668s 4.010s 0.341s 8 1.05x
💻 Local Express 7.974s (-9.4% 🟢) 8.524s (-8.1% 🟢) 0.549s 4 2.29x
💻 Local Next.js (Turbopack) 8.849s 9.524s 0.675s 4 2.54x
💻 Local Nitro 9.056s (-1.0%) 9.523s (-5.0% 🟢) 0.467s 4 2.60x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.854s (-44.0% 🟢) 4.359s (-36.1% 🟢) 1.505s 7 1.00x
▲ Vercel Express 2.952s (-54.0% 🟢) 4.606s (-43.7% 🟢) 1.655s 7 1.03x
▲ Vercel Next.js (Turbopack) 3.820s (-43.5% 🟢) 5.366s (-37.2% 🟢) 1.546s 6 1.34x

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

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.778s 1.023s 0.244s 59 1.00x
🐘 Postgres Express 0.822s (-2.1%) 1.006s (-1.6%) 0.185s 60 1.06x
💻 Local Next.js (Turbopack) 0.844s 1.004s 0.160s 60 1.08x
💻 Local Express 1.013s (+3.0%) 1.672s (+55.4% 🔺) 0.659s 36 1.30x
💻 Local Nitro 1.026s (+4.7%) 1.800s (+64.6% 🔺) 0.774s 34 1.32x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 9.041s (-37.7% 🟢) 10.605s (-34.1% 🟢) 1.564s 6 1.00x
▲ Vercel Nitro 9.167s (-58.4% 🟢) 10.774s (-55.2% 🟢) 1.608s 6 1.01x
▲ Vercel Express 9.420s (-50.5% 🟢) 10.934s (-48.7% 🟢) 1.514s 6 1.04x

🔍 Observability: Next.js (Turbopack) | Nitro | 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.908s 2.124s 0.216s 43 1.00x
🐘 Postgres Express 1.976s (~) 2.258s (~) 0.282s 40 1.04x
💻 Local Next.js (Turbopack) 2.641s 3.007s 0.366s 30 1.38x
💻 Local Express 3.022s (~) 3.689s (+2.9%) 0.667s 25 1.58x
💻 Local Nitro 3.063s (+0.9%) 3.922s (+4.4%) 0.859s 23 1.61x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 28.897s (-42.0% 🟢) 30.300s (-41.4% 🟢) 1.404s 3 1.00x
▲ Vercel Nitro 29.540s (-25.2% 🟢) 31.066s (-24.8% 🟢) 1.525s 3 1.02x
▲ Vercel Express 29.717s (-13.9% 🟢) 31.535s (-14.3% 🟢) 1.818s 3 1.03x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 3.867s 4.010s 0.143s 30 1.00x
🐘 Postgres Express 4.012s (+0.5%) 4.493s (+2.8%) 0.482s 27 1.04x
💻 Local Next.js (Turbopack) 8.486s 9.017s 0.531s 14 2.19x
💻 Local Express 9.064s (-1.6%) 9.478s (-5.4% 🟢) 0.414s 13 2.34x
💻 Local Nitro 9.278s (~) 9.942s (-0.8%) 0.664s 13 2.40x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 71.447s (-26.3% 🟢) 73.075s (-25.8% 🟢) 1.628s 2 1.00x
▲ Vercel Express 72.056s (-44.6% 🟢) 73.881s (-44.1% 🟢) 1.825s 2 1.01x
▲ Vercel Next.js (Turbopack) 74.567s (-30.4% 🟢) 76.256s (-30.0% 🟢) 1.690s 2 1.04x

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

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.249s 1.007s 0.758s 60 1.00x
🐘 Postgres Express 0.289s (+2.3%) 1.007s (~) 0.718s 60 1.16x
💻 Local Next.js (Turbopack) 0.544s 1.004s 0.461s 60 2.19x
💻 Local Nitro 0.592s (-2.1%) 1.022s (~) 0.430s 59 2.38x
💻 Local Express 0.604s (+7.8% 🔺) 1.021s (+1.7%) 0.417s 59 2.43x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.416s (-27.6% 🟢) 2.982s (-18.0% 🟢) 1.567s 21 1.00x
▲ Vercel Nitro 1.604s (-3.4%) 2.969s (-11.4% 🟢) 1.365s 21 1.13x
▲ Vercel Next.js (Turbopack) 1.608s (-20.5% 🟢) 3.112s (-18.0% 🟢) 1.504s 20 1.14x

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

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.486s 1.006s 0.520s 90 1.00x
🐘 Postgres Express 0.501s (-1.7%) 1.007s (~) 0.506s 90 1.03x
💻 Local Express 2.376s (-5.5% 🟢) 3.008s (~) 0.632s 30 4.89x
💻 Local Next.js (Turbopack) 2.525s 3.009s 0.484s 30 5.20x
💻 Local Nitro 2.585s (+1.9%) 3.009s (~) 0.424s 30 5.32x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.851s (-11.6% 🟢) 4.168s (-13.5% 🟢) 1.317s 22 1.00x
▲ Vercel Express 2.858s (-6.2% 🟢) 4.555s (-5.2% 🟢) 1.697s 20 1.00x
▲ Vercel Next.js (Turbopack) 3.341s (-5.5% 🟢) 4.972s (-4.3%) 1.631s 19 1.17x

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

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.781s 1.007s 0.225s 120 1.00x
🐘 Postgres Express 0.822s (~) 1.010s (-0.7%) 0.188s 119 1.05x
💻 Local Express 10.571s (-5.5% 🟢) 11.027s (-7.7% 🟢) 0.456s 11 13.53x
💻 Local Next.js (Turbopack) 10.643s 11.207s 0.564s 11 13.63x
💻 Local Nitro 11.261s (+0.6%) 11.848s (+1.6%) 0.587s 11 14.42x
🐘 Postgres Nitro ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 7.395s (-28.4% 🟢) 9.144s (-25.6% 🟢) 1.748s 14 1.00x
▲ Vercel Express 7.507s (+1.2%) 9.507s (+2.8%) 2.000s 13 1.02x
▲ Vercel Nitro 7.762s (+0.5%) 9.268s (-1.4%) 1.505s 13 1.05x

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

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 0.169s 1.003s 0.012s 1.018s 0.848s 10 1.00x
🐘 Postgres Express 0.203s (-1.0%) 1.000s (~) 0.002s (-6.3% 🟢) 1.010s (~) 0.807s 10 1.20x
💻 Local Nitro 0.204s (-4.6%) 1.004s (~) 0.011s (-9.6% 🟢) 1.018s (~) 0.814s 10 1.21x
🐘 Postgres Next.js (Turbopack) 0.204s 1.001s 0.001s 1.011s 0.806s 10 1.21x
💻 Local Express 0.227s (+14.2% 🔺) 1.004s (~) 0.010s (-18.2% 🟢) 1.016s (~) 0.788s 10 1.34x
🐘 Postgres Nitro ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 1.495s (-78.2% 🟢) 2.793s (-67.7% 🟢) 0.933s (+47.7% 🔺) 4.135s (-57.8% 🟢) 2.641s 10 1.00x
▲ Vercel Express 1.627s (-35.1% 🟢) 3.231s (-21.0% 🟢) 0.668s (-30.5% 🟢) 4.341s (-22.3% 🟢) 2.715s 10 1.09x
▲ Vercel Nitro 1.672s (-56.4% 🟢) 2.805s (-46.8% 🟢) 1.109s (+49.4% 🔺) 4.338s (-33.1% 🟢) 2.665s 10 1.12x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.609s 1.009s 0.005s 1.022s 0.413s 59 1.00x
🐘 Postgres Express 0.632s (~) 1.021s (+1.4%) 0.004s (+4.4%) 1.042s (+1.8%) 0.410s 58 1.04x
💻 Local Next.js (Turbopack) 0.756s 1.011s 0.010s 1.117s 0.361s 54 1.24x
💻 Local Nitro 0.769s (-8.3% 🟢) 1.012s (~) 0.012s (+25.4% 🔺) 1.026s (-8.1% 🟢) 0.257s 59 1.26x
💻 Local Express 0.857s (+13.1% 🔺) 1.012s (-1.7%) 0.012s (+22.4% 🔺) 1.118s (+7.5% 🔺) 0.262s 54 1.41x
🐘 Postgres Nitro ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.777s (-11.2% 🟢) 7.473s (-6.7% 🟢) 0.185s (-54.8% 🟢) 8.112s (-8.2% 🟢) 2.335s 8 1.00x
▲ Vercel Next.js (Turbopack) 6.719s (-60.3% 🟢) 8.047s (-55.9% 🟢) 0.186s (-11.8% 🟢) 8.612s (-54.5% 🟢) 1.892s 7 1.16x
▲ Vercel Nitro 8.246s (-72.0% 🟢) 9.624s (-68.8% 🟢) 0.167s (+48.7% 🔺) 10.158s (-68.0% 🟢) 1.912s 6 1.43x

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

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.919s 1.072s 0.000s 1.080s 0.161s 56 1.00x
🐘 Postgres Express 0.984s (+2.4%) 1.301s (+1.8%) 0.000s (~) 1.328s (+1.7%) 0.344s 46 1.07x
💻 Local Express 1.238s (+1.0%) 2.020s (~) 0.000s (+30.0% 🔺) 2.022s (~) 0.785s 30 1.35x
💻 Local Next.js (Turbopack) 1.241s 2.019s 0.000s 2.022s 0.781s 30 1.35x
💻 Local Nitro 1.433s (+17.2% 🔺) 2.023s (~) 0.000s (+292.9% 🔺) 2.204s (+9.0% 🔺) 0.771s 28 1.56x
🐘 Postgres Nitro ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.869s (-6.0% 🟢) 4.073s (-7.3% 🟢) 0.000s (-7.1% 🟢) 4.417s (-8.2% 🟢) 1.548s 14 1.00x
▲ Vercel Express 2.934s (-21.6% 🟢) 4.316s (-15.4% 🟢) 0.000s (-100.0% 🟢) 4.733s (-14.4% 🟢) 1.799s 13 1.02x
▲ Vercel Next.js (Turbopack) 3.017s (-70.4% 🟢) 4.293s (-62.7% 🟢) 0.000s (+Infinity% 🔺) 4.672s (-61.2% 🟢) 1.655s 14 1.05x

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

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.793s (+1.2%) 2.143s (-1.6%) 0.000s (+Infinity% 🔺) 2.152s (-2.1%) 0.359s 28 1.00x
🐘 Postgres Next.js (Turbopack) 1.844s 2.145s 0.000s 2.152s 0.308s 28 1.03x
💻 Local Next.js (Turbopack) 3.508s 4.033s 0.000s 4.036s 0.528s 15 1.96x
💻 Local Express 3.603s (+3.9%) 4.101s (+1.7%) 0.001s (-8.3% 🟢) 4.105s (+1.7%) 0.502s 15 2.01x
💻 Local Nitro 3.674s (+8.5% 🔺) 4.164s (+3.3%) 0.001s (~) 4.167s (+3.2%) 0.493s 15 2.05x
🐘 Postgres Nitro ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.063s (-0.8%) 5.253s (-2.2%) 0.000s (-100.0% 🟢) 5.588s (-3.5%) 1.526s 11 1.00x
▲ Vercel Express 4.167s (-9.2% 🟢) 5.682s (-5.6% 🟢) 0.000s (NaN%) 6.087s (-5.7% 🟢) 1.920s 11 1.03x
▲ Vercel Next.js (Turbopack) 4.455s (-20.7% 🟢) 5.836s (-16.4% 🟢) 0.000s (-100.0% 🟢) 6.230s (-17.4% 🟢) 1.774s 10 1.10x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

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

Winner determined by most benchmark wins

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment thread .changeset/stream-cancel-propagation-main.md Outdated
Comment thread packages/world-vercel/src/streamer.ts Outdated
Co-authored-by: Peter Wielander <[email protected]>
Signed-off-by: Peter Wielander <[email protected]>
Copy link
Copy Markdown
Member

@TooTallNate TooTallNate left a comment

Choose a reason for hiding this comment

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

Review

Clean, well-scoped fix. Forward-port of #1801 for main. The approach — cancelled flag as the source of truth plus a single shared AbortController for the whole stream lifetime — is the right pattern for this class of bug.

What looks good

  • Cancellation checks at all the right points: before reader.read(), after reader.read() (covers the case where read completed naturally between abort and flag check), and before reconnect(). Combined with the AbortController plumbed into fetch, this covers all the race windows:

    • Cancel before any pull → initial reader gets cancelled, pull is never invoked
    • Cancel during active read → AbortError swallowed, exits cleanly
    • Cancel during reconnect → in-flight fetch aborted, caught, exits cleanly
    • Cancel between reconnect completion and next read → flag checked at top of loop
  • Shared AbortController across all reconnects is correct. Once aborted, it stays aborted, which means all future connect() calls abort instantly. Right primitive for whole-stream cancellation.

  • Reconnect try/catch properly distinguishes abort-from-cancel (return silently) vs. real errors (propagate via controller.error).

  • reader.cancel().catch() on the closure variable — the comment explicitly acknowledges the TOCTOU window and explains why the cancelled flag is the real guarantee. Good defense-in-depth comment.

  • Changeset correctly scoped to @workflow/world-vercel only.

Test coverage observations

The PR description calls out test 1 (aborts the in-flight upstream fetch via AbortSignal) as failing on main without the fix — I agree, that's the strongest test.

Tests 2 and 3 are more like documentation/regression guards than fix verifications:

  • Test 2 (does not reconnect after the consumer cancels mid-timeout): the test never calls read() after the first one, so pull is never re-invoked — no reconnect path is actually exercised. Pre-fix, fetchCount would also be 1 simply because there's no trigger for a second pull. To really exercise the race, the test would need to let pull enter the reconnect branch before cancel (e.g. drain all data + timeout frame before cancelling). That said, it's still useful as a guard against future regressions where someone might trigger reconnects asynchronously.
  • Test 3 (cancels the active reader when cancel is called during a read): WHATWG streams semantics already make reader.cancel() propagate to the upstream body reader, so this likely passed pre-fix too.

Not blocking — the primary fix is well-verified by test 1.

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