Skip to content

[codex] Handle stream write failures without unhandled rejections#2142

Merged
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/stream-write-failure-handling
May 28, 2026
Merged

[codex] Handle stream write failures without unhandled rejections#2142
pranaygp merged 1 commit into
mainfrom
pranaygp/codex/stream-write-failure-handling

Conversation

@pranaygp
Copy link
Copy Markdown
Contributor

Summary

  • observe flushable stream-state rejections immediately so failed writes are still propagated through runtime operations without first surfacing as unhandledRejection
  • include the stream PUT endpoint and safe Vercel response correlation headers (x-vercel-id, x-vercel-error) in write/close failure errors while preserving the response body
  • add patch changesets for @workflow/core and @workflow/world-vercel

Root cause

A streaming step can fail a server write while user code is still running. flushablePipe() rejects state.promise, but the step runtime only waits on its collected operation promises after the step body returns. During that gap Node can classify the rejection as unhandled, allowing a transient stream failure to terminate the invocation instead of being handled through the runtime error path.

This PR attaches an immediate no-op rejection observer to the existing promise while leaving that original rejected promise in place for the runtime to await and propagate normally.

Retry behavior

This intentionally does not add automatic retries for failed stream writes. Writes are not currently idempotent, and a response failure after server acceptance cannot be distinguished from a failed write; blind retries can duplicate stream chunks.

Validation

  • pnpm exec biome check --write packages/core/src/flushable-stream.ts packages/core/src/flushable-stream.test.ts packages/world-vercel/src/streamer.ts packages/world-vercel/src/streamer.test.ts
  • pnpm --filter @workflow/core... build
  • WORKFLOW_TARGET_WORLD=local pnpm --filter @workflow/core exec vitest run src (1,034 tests passed)
  • pnpm --filter @workflow/world-vercel exec vitest run src (110 tests passed)
  • pnpm changeset status --since=origin-https/main
  • git diff --check

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 28, 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 May 28, 2026 8:42pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment May 28, 2026 8:42pm
example-workflow Ready Ready Preview, Comment May 28, 2026 8:42pm
workbench-astro-workflow Ready Ready Preview, Comment May 28, 2026 8:42pm
workbench-express-workflow Ready Ready Preview, Comment May 28, 2026 8:42pm
workbench-fastify-workflow Ready Ready Preview, Comment May 28, 2026 8:42pm
workbench-hono-workflow Ready Ready Preview, Comment May 28, 2026 8:42pm
workbench-nitro-workflow Ready Ready Preview, Comment May 28, 2026 8:42pm
workbench-nuxt-workflow Ready Ready Preview, Comment May 28, 2026 8:42pm
workbench-sveltekit-workflow Ready Ready Preview, Comment May 28, 2026 8:42pm
workbench-tanstack-start-workflow Ready Ready Preview, Comment May 28, 2026 8:42pm
workbench-vite-workflow Ready Ready Preview, Comment May 28, 2026 8:42pm
workflow-docs Ready Ready Preview, Comment, Open in v0 May 28, 2026 8:42pm
workflow-swc-playground Ready Ready Preview, Comment May 28, 2026 8:42pm
workflow-tarballs Ready Ready Preview, Comment May 28, 2026 8:42pm
workflow-web Ready Ready Preview, Comment May 28, 2026 8:42pm

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 28, 2026

🦋 Changeset detected

Latest commit: 107df58

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

This PR includes changesets to release 17 packages
Name Type
@workflow/core Patch
@workflow/world-vercel Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing 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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1255 0 219 1474
✅ 💻 Local Development 1657 0 219 1876
✅ 📦 Local Production 1657 0 219 1876
✅ 🐘 Local Postgres 1657 0 219 1876
✅ 🪟 Windows 134 0 0 134
✅ 📋 Other 762 0 176 938
Total 7122 0 1052 8174

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 108 0 26
✅ example 108 0 26
✅ express 108 0 26
✅ fastify 108 0 26
✅ hono 108 0 26
✅ nextjs-turbopack 132 0 2
✅ nextjs-webpack 132 0 2
✅ nitro 108 0 26
✅ nuxt 108 0 26
✅ sveltekit 127 0 7
✅ vite 108 0 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 109 0 25
✅ express-stable 109 0 25
✅ fastify-stable 109 0 25
✅ hono-stable 109 0 25
✅ nextjs-turbopack-canary 115 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 134 0 0
✅ nextjs-webpack-canary 115 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 134 0 0
✅ nitro-stable 109 0 25
✅ nuxt-stable 109 0 25
✅ sveltekit-stable 128 0 6
✅ vite-stable 109 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 109 0 25
✅ express-stable 109 0 25
✅ fastify-stable 109 0 25
✅ hono-stable 109 0 25
✅ nextjs-turbopack-canary 115 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 134 0 0
✅ nextjs-webpack-canary 115 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 134 0 0
✅ nitro-stable 109 0 25
✅ nuxt-stable 109 0 25
✅ sveltekit-stable 128 0 6
✅ vite-stable 109 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 109 0 25
✅ express-stable 109 0 25
✅ fastify-stable 109 0 25
✅ hono-stable 109 0 25
✅ nextjs-turbopack-canary 115 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 134 0 0
✅ nextjs-webpack-canary 115 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 134 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 134 0 0
✅ nitro-stable 109 0 25
✅ nuxt-stable 109 0 25
✅ sveltekit-stable 128 0 6
✅ vite-stable 109 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 134 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 109 0 25
✅ e2e-local-dev-tanstack-start- 109 0 25
✅ e2e-local-postgres-nest-stable 109 0 25
✅ e2e-local-postgres-tanstack-start- 109 0 25
✅ e2e-local-prod-nest-stable 109 0 25
✅ e2e-local-prod-tanstack-start- 109 0 25
✅ e2e-vercel-prod-tanstack-start 108 0 26

📋 View full workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 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.042s (-3.7%) 1.005s (~) 0.963s 10 1.00x
💻 Local Express 0.043s (-1.8%) 1.006s (~) 0.962s 10 1.05x
💻 Local Next.js (Turbopack) 0.054s 1.007s 0.953s 10 1.31x
🐘 Postgres Express 0.064s (+10.3% 🔺) 1.012s (~) 0.948s 10 1.54x
🐘 Postgres Nitro 0.065s (-31.6% 🟢) 1.012s (-3.0%) 0.946s 10 1.57x
🐘 Postgres Next.js (Turbopack) 0.068s 1.012s 0.944s 10 1.63x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 0.327s (-20.1% 🟢) 2.402s (-4.3%) 2.075s 10 1.00x
▲ Vercel Express 0.345s (+46.6% 🔺) 2.398s (+12.3% 🔺) 2.053s 10 1.05x
▲ Vercel Next.js (Turbopack) 0.448s (+78.2% 🔺) 2.414s (+3.4%) 1.965s 10 1.37x

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

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.099s (-2.3%) 2.006s (~) 0.907s 10 1.00x
💻 Local Nitro 1.099s (-2.8%) 2.005s (~) 0.906s 10 1.00x
💻 Local Next.js (Turbopack) 1.105s 2.005s 0.901s 10 1.01x
🐘 Postgres Express 1.105s (-3.6%) 2.009s (~) 0.904s 10 1.01x
🐘 Postgres Nitro 1.112s (-2.5%) 2.010s (~) 0.898s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.148s 2.009s 0.862s 10 1.04x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.555s (-17.1% 🟢) 3.521s (-7.5% 🟢) 1.966s 10 1.00x
▲ Vercel Nitro 1.653s (-57.5% 🟢) 3.300s (-44.1% 🟢) 1.647s 10 1.06x
▲ Vercel Next.js (Turbopack) 1.700s (-16.5% 🟢) 3.620s (-5.5% 🟢) 1.920s 10 1.09x

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

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 10.536s (-3.5%) 11.023s (~) 0.486s 3 1.00x
💻 Local Nitro 10.540s (-3.7%) 11.022s (~) 0.482s 3 1.00x
🐘 Postgres Express 10.549s (-3.8%) 11.023s (~) 0.475s 3 1.00x
🐘 Postgres Nitro 10.590s (-2.6%) 11.022s (~) 0.432s 3 1.01x
💻 Local Next.js (Turbopack) 10.599s 11.021s 0.422s 3 1.01x
🐘 Postgres Next.js (Turbopack) 10.856s 11.018s 0.162s 3 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 14.065s (-40.7% 🟢) 16.481s (-34.4% 🟢) 2.416s 2 1.00x
▲ Vercel Next.js (Turbopack) 14.664s (-15.3% 🟢) 16.439s (-15.3% 🟢) 1.775s 2 1.04x
▲ Vercel Express 14.930s (-12.1% 🟢) 16.836s (-15.9% 🟢) 1.906s 2 1.06x

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

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 13.714s (-8.9% 🟢) 14.027s (-12.5% 🟢) 0.313s 5 1.00x
💻 Local Express 13.781s (-7.9% 🟢) 14.027s (-6.7% 🟢) 0.246s 5 1.00x
🐘 Postgres Express 13.789s (-5.4% 🟢) 14.020s (-6.7% 🟢) 0.231s 5 1.01x
🐘 Postgres Nitro 13.822s (-5.3% 🟢) 14.019s (-6.7% 🟢) 0.197s 5 1.01x
💻 Local Next.js (Turbopack) 14.053s 14.628s 0.576s 5 1.02x
🐘 Postgres Next.js (Turbopack) 14.473s 15.018s 0.545s 4 1.06x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 22.846s (-64.6% 🟢) 24.190s (-63.7% 🟢) 1.343s 3 1.00x
▲ Vercel Express 23.828s (-52.6% 🟢) 26.319s (-49.9% 🟢) 2.491s 3 1.04x
▲ Vercel Next.js (Turbopack) 24.790s (-52.8% 🟢) 25.951s (-52.5% 🟢) 1.161s 3 1.09x

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

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 12.444s (-25.9% 🟢) 13.025s (-23.5% 🟢) 0.582s 7 1.00x
💻 Local Express 12.504s (-24.7% 🟢) 13.025s (-23.5% 🟢) 0.522s 7 1.00x
🐘 Postgres Express 12.510s (-10.7% 🟢) 13.020s (-10.8% 🟢) 0.510s 7 1.01x
🐘 Postgres Nitro 12.633s (-9.5% 🟢) 13.023s (-9.0% 🟢) 0.390s 7 1.02x
💻 Local Next.js (Turbopack) 12.980s 13.311s 0.331s 7 1.04x
🐘 Postgres Next.js (Turbopack) 13.654s 14.023s 0.369s 7 1.10x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 34.478s (-91.8% 🟢) 36.775s (-91.3% 🟢) 2.297s 3 1.00x
▲ Vercel Next.js (Turbopack) 36.647s (-90.7% 🟢) 38.422s (-90.3% 🟢) 1.775s 3 1.06x
▲ Vercel Express 39.195s (-67.7% 🟢) 41.529s (-66.4% 🟢) 2.333s 3 1.14x

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

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.187s (-6.9% 🟢) 2.008s (~) 0.821s 15 1.00x
🐘 Postgres Express 1.190s (-5.6% 🟢) 2.008s (~) 0.818s 15 1.00x
💻 Local Nitro 1.216s (-25.5% 🟢) 2.006s (-3.3%) 0.790s 15 1.02x
💻 Local Express 1.230s (-17.4% 🟢) 2.006s (~) 0.776s 15 1.04x
🐘 Postgres Next.js (Turbopack) 1.239s 2.007s 0.768s 15 1.04x
💻 Local Next.js (Turbopack) 1.278s 2.005s 0.727s 15 1.08x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.770s (-3.2%) 4.874s (+5.4% 🔺) 2.105s 7 1.00x
▲ Vercel Nitro 2.920s (+3.6%) 4.513s (+4.4%) 1.593s 7 1.05x
▲ Vercel Next.js (Turbopack) 53.947s (+1487.7% 🔺) 55.472s (+1024.6% 🔺) 1.526s 6 19.48x

🔍 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 🥇 Nitro 1.260s (-46.4% 🟢) 2.007s (-33.3% 🟢) 0.747s 15 1.00x
🐘 Postgres Express 1.261s (-46.6% 🟢) 2.008s (-33.3% 🟢) 0.747s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.372s 2.007s 0.635s 15 1.09x
💻 Local Nitro 1.631s (-48.1% 🟢) 2.005s (-48.4% 🟢) 0.374s 15 1.29x
💻 Local Next.js (Turbopack) 1.646s 2.005s 0.359s 15 1.31x
💻 Local Express 2.017s (-31.7% 🟢) 2.393s (-30.7% 🟢) 0.376s 13 1.60x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.619s (-10.7% 🟢) 5.094s (-14.0% 🟢) 1.475s 6 1.00x
▲ Vercel Next.js (Turbopack) 3.687s (-48.1% 🟢) 5.348s (-39.9% 🟢) 1.662s 6 1.02x
▲ Vercel Express 4.184s (+15.6% 🔺) 5.985s (+17.1% 🔺) 1.801s 6 1.16x

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

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.398s (-59.9% 🟢) 2.008s (-49.9% 🟢) 0.610s 15 1.00x
🐘 Postgres Nitro 1.400s (-59.8% 🟢) 2.008s (-49.9% 🟢) 0.608s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.725s 2.151s 0.427s 14 1.23x
💻 Local Next.js (Turbopack) 4.041s 4.438s 0.397s 7 2.89x
💻 Local Nitro 4.478s (-46.4% 🟢) 5.012s (-44.4% 🟢) 0.534s 7 3.20x
💻 Local Express 6.102s (-26.8% 🟢) 6.617s (-26.7% 🟢) 0.515s 5 4.37x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.446s (+54.5% 🔺) 7.655s (+38.3% 🔺) 2.209s 4 1.00x
▲ Vercel Next.js (Turbopack) 5.928s (-33.5% 🟢) 7.957s (-27.4% 🟢) 2.029s 5 1.09x
▲ Vercel Express 81.654s (+1825.8% 🔺) 84.029s (+1271.4% 🔺) 2.376s 4 14.99x

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

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.185s (-5.7% 🟢) 2.009s (~) 0.824s 15 1.00x
🐘 Postgres Nitro 1.200s (-4.6%) 2.008s (~) 0.808s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.243s 2.009s 0.766s 15 1.05x
💻 Local Next.js (Turbopack) 1.303s 2.005s 0.702s 15 1.10x
💻 Local Express 1.538s (-18.8% 🟢) 2.007s (-15.1% 🟢) 0.469s 15 1.30x
💻 Local Nitro 1.593s (-14.6% 🟢) 2.006s (-14.3% 🟢) 0.413s 15 1.34x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.683s (+9.1% 🔺) 4.161s (~) 1.478s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.999s (+2.3%) 4.726s (+1.8%) 1.726s 7 1.12x
▲ Vercel Express 3.593s (+39.2% 🔺) 5.054s (+16.2% 🔺) 1.461s 6 1.34x

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

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.269s (-45.8% 🟢) 2.009s (-33.3% 🟢) 0.740s 15 1.00x
🐘 Postgres Nitro 1.270s (-45.7% 🟢) 2.008s (-33.3% 🟢) 0.738s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.382s 2.007s 0.624s 15 1.09x
💻 Local Nitro 1.852s (-39.6% 🟢) 2.316s (-40.4% 🟢) 0.464s 13 1.46x
💻 Local Next.js (Turbopack) 1.889s 2.149s 0.260s 14 1.49x
💻 Local Express 2.130s (-32.0% 🟢) 2.675s (-28.9% 🟢) 0.545s 12 1.68x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.063s (+25.7% 🔺) 5.341s (+5.2% 🔺) 1.278s 6 1.00x
▲ Vercel Next.js (Turbopack) 5.424s (+72.6% 🔺) 7.896s (+74.6% 🔺) 2.471s 4 1.33x
▲ Vercel Express 5.653s (+77.1% 🔺) 7.651s (+59.6% 🔺) 1.998s 5 1.39x

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

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.407s (-59.6% 🟢) 2.009s (-49.9% 🟢) 0.602s 15 1.00x
🐘 Postgres Express 1.433s (-59.1% 🟢) 2.007s (-50.0% 🟢) 0.574s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.652s 2.222s 0.570s 14 1.17x
💻 Local Nitro 4.482s (-51.0% 🟢) 5.014s (-50.0% 🟢) 0.532s 6 3.19x
💻 Local Next.js (Turbopack) 4.677s 5.179s 0.501s 6 3.32x
💻 Local Express 6.254s (-28.9% 🟢) 6.615s (-28.7% 🟢) 0.361s 5 4.44x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.100s (~) 6.970s (+2.2%) 1.870s 5 1.00x
▲ Vercel Express 5.257s (-18.1% 🟢) 6.972s (-14.8% 🟢) 1.715s 5 1.03x
▲ Vercel Next.js (Turbopack) 7.147s (+5.8% 🔺) 9.431s (+10.4% 🔺) 2.284s 4 1.40x

🔍 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 🥇 Nitro 0.569s (-30.6% 🟢) 1.006s (~) 0.437s 60 1.00x
🐘 Postgres Express 0.595s (-29.1% 🟢) 1.024s (~) 0.429s 59 1.04x
💻 Local Express 0.613s (-37.7% 🟢) 1.005s (-6.6% 🟢) 0.391s 60 1.08x
💻 Local Nitro 0.628s (-36.0% 🟢) 1.039s (-5.0%) 0.411s 58 1.10x
💻 Local Next.js (Turbopack) 0.779s 1.076s 0.296s 56 1.37x
🐘 Postgres Next.js (Turbopack) 0.863s 1.042s 0.178s 58 1.52x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.312s (-75.9% 🟢) 6.975s (-71.0% 🟢) 1.664s 9 1.00x
▲ Vercel Express 6.952s (-63.4% 🟢) 8.919s (-58.2% 🟢) 1.968s 7 1.31x
▲ Vercel Next.js (Turbopack) 7.009s (-51.7% 🟢) 8.825s (-45.1% 🟢) 1.816s 7 1.32x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.351s (-29.9% 🟢) 2.008s (-4.4%) 0.656s 45 1.00x
🐘 Postgres Express 1.367s (-30.8% 🟢) 2.008s (-11.1% 🟢) 0.640s 45 1.01x
💻 Local Nitro 1.500s (-50.6% 🟢) 2.006s (-46.6% 🟢) 0.506s 45 1.11x
💻 Local Express 1.521s (-49.6% 🟢) 2.007s (-44.0% 🟢) 0.486s 45 1.13x
💻 Local Next.js (Turbopack) 1.760s 2.028s 0.267s 45 1.30x
🐘 Postgres Next.js (Turbopack) 1.967s 2.203s 0.237s 41 1.46x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 13.225s (-66.5% 🟢) 14.968s (-63.8% 🟢) 1.743s 7 1.00x
▲ Vercel Express 18.195s (-47.3% 🟢) 20.304s (-44.8% 🟢) 2.110s 5 1.38x
▲ Vercel Next.js (Turbopack) 18.273s (-63.3% 🟢) 20.316s (-60.7% 🟢) 2.042s 5 1.38x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.738s (-33.3% 🟢) 3.086s (-33.0% 🟢) 0.349s 39 1.00x
🐘 Postgres Express 2.742s (-31.3% 🟢) 3.086s (-29.4% 🟢) 0.344s 39 1.00x
💻 Local Nitro 3.229s (-65.3% 🟢) 4.010s (-60.0% 🟢) 0.780s 30 1.18x
💻 Local Express 3.271s (-64.5% 🟢) 4.009s (-60.0% 🟢) 0.738s 30 1.19x
🐘 Postgres Next.js (Turbopack) 3.776s 4.043s 0.267s 30 1.38x
💻 Local Next.js (Turbopack) 3.915s 4.181s 0.266s 29 1.43x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 29.188s (-69.9% 🟢) 30.941s (-68.6% 🟢) 1.753s 4 1.00x
▲ Vercel Express 36.047s (-72.3% 🟢) 38.601s (-70.8% 🟢) 2.553s 4 1.23x
▲ Vercel Next.js (Turbopack) 40.090s (-62.6% 🟢) 42.716s (-60.8% 🟢) 2.625s 3 1.37x

🔍 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 🥇 Nitro 0.223s (-21.3% 🟢) 1.006s (~) 0.784s 60 1.00x
🐘 Postgres Next.js (Turbopack) 0.256s 1.006s 0.750s 60 1.15x
🐘 Postgres Express 0.257s (-9.1% 🟢) 1.023s (+1.6%) 0.767s 59 1.15x
💻 Local Express 0.432s (-22.9% 🟢) 1.004s (~) 0.572s 60 1.94x
💻 Local Nitro 0.445s (-26.4% 🟢) 1.004s (-1.7%) 0.559s 60 2.00x
💻 Local Next.js (Turbopack) 0.736s 1.148s 0.412s 53 3.30x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.189s (+31.8% 🔺) 3.630s (+8.3% 🔺) 1.441s 17 1.00x
▲ Vercel Express 2.572s (+31.6% 🔺) 4.332s (+19.1% 🔺) 1.759s 14 1.18x
▲ Vercel Next.js (Turbopack) 2.784s (+37.7% 🔺) 4.168s (+9.9% 🔺) 1.384s 15 1.27x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.352s (-29.1% 🟢) 1.007s (~) 0.655s 90 1.00x
🐘 Postgres Express 0.364s (-28.6% 🟢) 1.006s (~) 0.643s 90 1.03x
🐘 Postgres Next.js (Turbopack) 0.462s 1.006s 0.544s 90 1.31x
💻 Local Nitro 2.061s (-18.8% 🟢) 2.579s (-14.3% 🟢) 0.519s 35 5.86x
💻 Local Next.js (Turbopack) 2.100s 2.737s 0.637s 33 5.97x
💻 Local Express 2.176s (-13.4% 🟢) 2.656s (-11.7% 🟢) 0.480s 34 6.19x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.290s (+73.6% 🔺) 6.829s (+42.0% 🔺) 1.538s 14 1.00x
▲ Vercel Nitro 5.769s (+78.8% 🔺) 7.620s (+58.0% 🔺) 1.850s 12 1.09x
▲ Vercel Next.js (Turbopack) 5.790s (+63.8% 🔺) 7.529s (+45.0% 🔺) 1.739s 12 1.09x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.706s (-10.7% 🟢) 1.008s (~) 0.302s 120 1.00x
🐘 Postgres Express 0.708s (-13.6% 🟢) 1.006s (-1.1%) 0.298s 120 1.00x
🐘 Postgres Next.js (Turbopack) 0.937s 1.326s 0.389s 91 1.33x
💻 Local Nitro 9.339s (-16.5% 🟢) 9.795s (-16.0% 🟢) 0.457s 13 13.23x
💻 Local Next.js (Turbopack) 9.340s 9.874s 0.534s 13 13.23x
💻 Local Express 9.775s (-12.6% 🟢) 10.364s (-13.2% 🟢) 0.589s 12 13.84x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 15.081s (+103.2% 🔺) 17.251s (+86.6% 🔺) 2.171s 7 1.00x
▲ Vercel Nitro 15.642s (+102.5% 🔺) 17.478s (+85.9% 🔺) 1.836s 7 1.04x
▲ Vercel Next.js (Turbopack) 16.640s (+61.1% 🔺) 18.399s (+49.8% 🔺) 1.759s 7 1.10x

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

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.151s (+438.6% 🔺) 2.005s (+99.6% 🔺) 0.010s (-20.8% 🟢) 2.018s (+98.0% 🔺) 0.867s 10 1.00x
💻 Local Next.js (Turbopack) 1.165s 2.003s 0.008s 2.014s 0.849s 10 1.01x
🐘 Postgres Express 1.166s (+468.6% 🔺) 1.999s (+100.2% 🔺) 0.001s (-25.0% 🟢) 2.010s (+98.8% 🔺) 0.844s 10 1.01x
🐘 Postgres Nitro 1.171s (+471.4% 🔺) 1.999s (+100.0% 🔺) 0.002s (+13.3% 🔺) 2.012s (+98.9% 🔺) 0.840s 10 1.02x
💻 Local Express 1.173s (+489.0% 🔺) 2.006s (+99.7% 🔺) 0.013s (+3.3%) 2.021s (+98.5% 🔺) 0.848s 10 1.02x
🐘 Postgres Next.js (Turbopack) 1.220s 2.002s 0.001s 2.011s 0.791s 10 1.06x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.361s (-5.7% 🟢) 3.331s (-18.6% 🟢) 2.020s (+110.2% 🔺) 5.810s (+3.9%) 3.449s 10 1.00x
▲ Vercel Nitro 2.576s (-32.8% 🟢) 3.796s (-28.1% 🟢) 1.993s (+168.6% 🔺) 6.264s (-3.4%) 3.687s 10 1.09x
▲ Vercel Next.js (Turbopack) 2.611s (-61.9% 🟢) 3.485s (-59.7% 🟢) 2.122s (+235.8% 🔺) 6.121s (-37.5% 🟢) 3.510s 10 1.11x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.583s (+88.7% 🔺) 2.012s (+98.8% 🔺) 0.010s (+1.7%) 2.023s (+81.3% 🔺) 0.440s 30 1.00x
💻 Local Express 1.594s (+110.5% 🔺) 2.012s (+95.5% 🔺) 0.009s (+0.9%) 2.023s (+94.6% 🔺) 0.430s 30 1.01x
🐘 Postgres Nitro 1.605s (+157.1% 🔺) 2.005s (+99.2% 🔺) 0.004s (-10.5% 🟢) 2.025s (+98.0% 🔺) 0.420s 30 1.01x
🐘 Postgres Express 1.622s (+157.4% 🔺) 2.006s (+99.3% 🔺) 0.004s (~) 2.025s (+97.9% 🔺) 0.403s 30 1.02x
🐘 Postgres Next.js (Turbopack) 1.747s 2.010s 0.004s 2.026s 0.279s 30 1.10x
💻 Local Next.js (Turbopack) 1.827s 2.010s 0.008s 2.200s 0.373s 28 1.15x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 6.349s (-2.4%) 7.800s (-2.6%) 0.474s (+15.9% 🔺) 8.743s (-1.0%) 2.394s 7 1.00x
▲ Vercel Nitro 6.454s (-78.1% 🟢) 7.709s (-75.0% 🟢) 0.204s (+82.3% 🔺) 8.387s (-73.6% 🟢) 1.934s 8 1.02x
▲ Vercel Next.js (Turbopack) 6.519s (-61.5% 🟢) 7.670s (-58.0% 🟢) 0.202s (-4.6%) 8.282s (-56.3% 🟢) 1.763s 8 1.03x

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

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.723s (-25.4% 🟢) 0.999s (-19.9% 🟢) 0.000s (-18.6% 🟢) 1.023s (-18.7% 🟢) 0.300s 59 1.00x
🐘 Postgres Express 0.760s (-20.9% 🟢) 1.069s (-16.3% 🟢) 0.000s (-20.7% 🟢) 1.084s (-17.0% 🟢) 0.325s 58 1.05x
🐘 Postgres Next.js (Turbopack) 0.831s 1.090s 0.000s 1.100s 0.269s 55 1.15x
💻 Local Next.js (Turbopack) 1.319s 1.979s 0.000s 1.982s 0.663s 31 1.82x
💻 Local Nitro 1.430s (+16.9% 🔺) 2.014s (~) 0.000s (+100.0% 🔺) 2.016s (~) 0.586s 30 1.98x
💻 Local Express 1.448s (+18.2% 🔺) 2.014s (~) 0.000s (-30.0% 🟢) 2.016s (~) 0.568s 30 2.00x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.633s (+19.1% 🔺) 4.574s (+4.1%) 0.001s (+766.7% 🔺) 5.041s (+4.8%) 1.409s 12 1.00x
▲ Vercel Express 3.953s (+5.7% 🔺) 5.235s (+2.6%) 0.000s (-100.0% 🟢) 5.684s (+2.8%) 1.732s 11 1.09x
▲ Vercel Next.js (Turbopack) 3.993s (-60.8% 🟢) 5.121s (-55.5% 🟢) 0.000s (NaN%) 5.530s (-54.1% 🟢) 1.537s 11 1.10x

🔍 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.436s (-19.0% 🟢) 2.140s (-1.7%) 0.000s (NaN%) 2.153s (-2.1%) 0.717s 28 1.00x
🐘 Postgres Nitro 1.502s (-16.2% 🟢) 2.175s (+1.6%) 0.000s (-100.0% 🟢) 2.196s (+1.0%) 0.694s 28 1.05x
🐘 Postgres Next.js (Turbopack) 1.715s 2.311s 0.000s 2.318s 0.603s 26 1.19x
💻 Local Next.js (Turbopack) 2.552s 3.125s 0.001s 3.129s 0.577s 20 1.78x
💻 Local Nitro 3.227s (-4.8%) 3.898s (-3.3%) 0.000s (-53.1% 🟢) 3.905s (-3.2%) 0.679s 16 2.25x
💻 Local Express 3.532s (+1.9%) 3.897s (-3.4%) 0.000s (-41.7% 🟢) 4.235s (+4.9%) 0.703s 15 2.46x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.368s (+31.1% 🔺) 6.435s (+19.7% 🔺) 0.001s (+266.7% 🔺) 6.840s (+18.1% 🔺) 1.471s 9 1.00x
▲ Vercel Express 6.219s (+35.6% 🔺) 7.317s (+21.5% 🔺) 0.000s (+Infinity% 🔺) 7.979s (+23.6% 🔺) 1.760s 8 1.16x
▲ Vercel Next.js (Turbopack) 6.558s (+16.8% 🔺) 8.018s (+14.8% 🔺) 0.001s (+300.0% 🔺) 8.401s (+11.4% 🔺) 1.843s 8 1.22x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 13/21
🐘 Postgres Nitro 11/21
▲ Vercel Nitro 15/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@pranaygp pranaygp marked this pull request as ready for review May 28, 2026 22:49
@pranaygp pranaygp requested a review from a team as a code owner May 28, 2026 22:49
Copilot AI review requested due to automatic review settings May 28, 2026 22:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Prevents stream write failures from surfacing as unhandledRejection before the step runtime collects them via ops, and enriches Vercel stream HTTP errors with the request endpoint and Vercel correlation headers.

Changes:

  • Attach a no-op .catch() to FlushableStreamState.promise at creation so early rejections are observed before the runtime awaits them.
  • Centralize Vercel stream write/close error construction in createStreamRequestError(), including PUT URL plus x-vercel-id / x-vercel-error headers.
  • Add tests for unhandled-rejection behavior and Vercel error diagnostics, plus a patch changeset.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/core/src/flushable-stream.ts Adds no-op rejection observer on state.promise in createFlushableState.
packages/core/src/flushable-stream.test.ts New test verifying no unhandledRejection is emitted on early state rejection.
packages/world-vercel/src/streamer.ts Adds createStreamRequestError helper; uses it in write/writeMulti/close failures.
packages/world-vercel/src/streamer.test.ts New test asserting endpoint and Vercel correlation headers appear in the failure message.
.changeset/stream-failure-diagnostics.md Patch changeset for @workflow/core and @workflow/world-vercel.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

Approve — correct fix for a real bug, with a strong regression test

Two changes:

  1. createFlushableState attaches a no-op .catch(() => {}) to state.promise at creation time, so an early rejection (before the runtime collects state.promise into its ops array and awaits it) doesn't fire unhandledRejection.
  2. world-vercel stream errors now include PUT <url> + x-vercel-id + x-vercel-error headers in the error message — useful for tracing transient stream failures back to a specific Vercel function invocation.

Why this fix is correct

The .catch(() => {}) is a standard Node.js pattern for "this rejection will be observed later." It does not swallow the error:

  • .catch() returns a NEW promise that resolves (because the handler returned undefined).
  • The original state.promise is still rejected and still surfaces the same rejection when later awaited.
  • Both .then()/.catch() chains on the same promise observe the same rejection — they don't compete.

I traced where state.promise is consumed: serialization.ts (×6 sites) and writable-stream.ts all ops.push(state.promise), and the runtime does await Promise.all(ops) after user code returns. So the rejection still propagates through the normal error path. ✓

Test design is excellent

The new test (does not emit an unhandled rejection before the runtime awaits a failed operation):

  1. Listens for process.on('unhandledRejection').
  2. Creates state, rejects it, waits a tick (long enough for Node to fire unhandledRejection if it would).
  3. Asserts no unhandledRejection fired AND state.promise still rejects when awaited.

The second assertion is critical — it proves the fix didn't accidentally swallow the rejection.

I verified the test fails without the fix by temporarily removing the .catch() line:

expected [] to deeply equal [
  Error { "message": "Stream write failed" }
]

So the test correctly catches the regression it's protecting against.

What I verified locally

  • pnpm install --frozen-lockfile
  • pnpm turbo run build --filter @workflow/core --filter @workflow/world-vercel
  • cd packages/core && pnpm exec vitest run src/flushable-stream.test.ts ✓ (10/10)
  • pnpm --filter @workflow/world-vercel test ✓ (110/110)
  • Reverted the .catch(() => {}) line and confirmed the regression test fails as expected — proves the test isn't a vacuous pass.

CI

106 success, 5 failures, all unrelated:

  • E2E Vercel Prod Tests (vite) — single workflow run flake on Vercel infrastructure
  • Benchmark Vercel/Local (nitro-v3) — main also fails these regularly
  • Benchmark Community World (Redis + BullMQ) — unrelated to flushable-stream
  • E2E Required Check — meta-check failing because the above failed

Restraint on retry is the right call

The PR body's note about not adding automatic retries is exactly right. Stream writes aren't idempotent server-side, and a response error after server-accepted bytes can't be distinguished from a pre-accept network failure — blindly retrying could duplicate chunks. The diagnostic improvement (request correlation headers in the error) is the better incremental win: now when a stream write fails, the user has the Vercel request ID to look up server-side state.

Approving.

@pranaygp pranaygp enabled auto-merge (squash) May 28, 2026 23:19
@pranaygp pranaygp merged commit ae37315 into main May 28, 2026
198 of 210 checks passed
@pranaygp pranaygp deleted the pranaygp/codex/stream-write-failure-handling branch May 28, 2026 23:33
github-actions Bot added a commit that referenced this pull request May 28, 2026
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
@github-actions
Copy link
Copy Markdown
Contributor

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

pranaygp pushed a commit that referenced this pull request May 28, 2026
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

3 participants