Skip to content

[core] Fix getWritable() chunk reordering when called repeatedly per step#2086

Draft
VaguelySerious wants to merge 2 commits into
mainfrom
peter/issue-2058
Draft

[core] Fix getWritable() chunk reordering when called repeatedly per step#2086
VaguelySerious wants to merge 2 commits into
mainfrom
peter/issue-2058

Conversation

@VaguelySerious
Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious commented May 22, 2026

Fixes #2058, where calling getWritable multiple times in the same function causes chunk re-ordering without any user-facing warnings. I'm not sure if this is the right approach or we should just throw an error or warn.

The issue

Each call to getWritable() constructed a fresh TransformStream + WorkflowServerWritableStream + background flushablePipe(...), all flushing to the same (runId, name). On Vercel the 50-100ms HTTP write latency turned the inter-pipe race into deterministic reordering — most visibly when callers acquired a fresh writer per chunk (e.g. an AI SDK text-delta loop). Locally world-local made it look fine because filesystem writes are effectively instant.

The fix

  • Share a single TransformStream + flushablePipe per (runId, namespace) within a step context so repeat calls to getWritable() no longer spawn racing pipes.
  • Add per-call guards on ctx.ops so the step still waits for every caller's writes to drain.

Repeat calls to `getWritable()` from the same step previously spawned
independent TransformStream + flushablePipe pairs that all flushed to
the same (runId, name) on the server. On Vercel the 50-100ms HTTP write
latency turned the inter-pipe race into deterministic reordering — most
visibly when callers acquired a fresh writer per chunk (e.g. an AI SDK
text-delta loop). Locally the world-local filesystem path made the race
invisible.

Cache the writable + pipe state per (runId, namespace) in the step
context so repeat calls share one serial sink. Each call still registers
a per-call guard in ctx.ops so the step waits for every caller's writes
to flush before completing, matching the prior multi-pipe semantics.

Closes #2058

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

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

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 22, 2026

🦋 Changeset detected

Latest commit: 40106be

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

This PR includes changesets to release 16 packages
Name Type
@workflow/core Patch
workflow Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web 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 22, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1222 0 219 1441
✅ 💻 Local Development 1615 0 219 1834
✅ 📦 Local Production 1615 0 219 1834
✅ 🐘 Local Postgres 1615 0 219 1834
✅ 🪟 Windows 131 0 0 131
✅ 📋 Other 741 0 176 917
Total 6939 0 1052 7991

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 105 0 26
✅ example 105 0 26
✅ express 105 0 26
✅ fastify 105 0 26
✅ hono 105 0 26
✅ nextjs-turbopack 129 0 2
✅ nextjs-webpack 129 0 2
✅ nitro 105 0 26
✅ nuxt 105 0 26
✅ sveltekit 124 0 7
✅ vite 105 0 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 131 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 106 0 25
✅ e2e-local-dev-tanstack-start- 106 0 25
✅ e2e-local-postgres-nest-stable 106 0 25
✅ e2e-local-postgres-tanstack-start- 106 0 25
✅ e2e-local-prod-nest-stable 106 0 25
✅ e2e-local-prod-tanstack-start- 106 0 25
✅ e2e-vercel-prod-tanstack-start 105 0 26

📋 View full workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 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.029s (-32.7% 🟢) 1.005s (~) 0.976s 10 1.00x
💻 Local Express 0.030s (-32.3% 🟢) 1.006s (~) 0.976s 10 1.03x
💻 Local Next.js (Turbopack) 0.049s 1.006s 0.957s 10 1.69x
🐘 Postgres Nitro 0.050s (-47.6% 🟢) 1.012s (-3.0%) 0.962s 10 1.72x
🐘 Postgres Express 0.051s (-11.9% 🟢) 1.012s (~) 0.961s 10 1.76x
🐘 Postgres Next.js (Turbopack) 0.055s 1.010s 0.955s 10 1.89x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 0.260s (+3.2%) 2.424s (+3.9%) 2.165s 10 1.00x
▲ Vercel Nitro 0.274s (-33.1% 🟢) 2.158s (-14.0% 🟢) 1.885s 10 1.06x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.066s (-5.3% 🟢) 2.006s (~) 0.940s 10 1.00x
💻 Local Nitro 1.069s (-5.5% 🟢) 2.006s (~) 0.937s 10 1.00x
🐘 Postgres Express 1.083s (-5.5% 🟢) 2.009s (~) 0.926s 10 1.02x
🐘 Postgres Nitro 1.086s (-4.7%) 2.009s (~) 0.923s 10 1.02x
💻 Local Next.js (Turbopack) 1.107s 2.006s 0.899s 10 1.04x
🐘 Postgres Next.js (Turbopack) 1.123s 2.009s 0.886s 10 1.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 1.590s (-21.9% 🟢) 3.378s (-11.8% 🟢) 1.788s 10 1.00x
▲ Vercel Nitro 1.608s (-58.7% 🟢) 3.228s (-45.4% 🟢) 1.620s 10 1.01x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 10.397s (-5.0% 🟢) 11.022s (~) 0.625s 3 1.00x
💻 Local Express 10.408s (-4.7%) 11.023s (~) 0.615s 3 1.00x
🐘 Postgres Express 10.413s (-5.0% 🟢) 11.013s (~) 0.600s 3 1.00x
🐘 Postgres Nitro 10.451s (-3.9%) 11.021s (~) 0.570s 3 1.01x
💻 Local Next.js (Turbopack) 10.660s 11.022s 0.363s 3 1.03x
🐘 Postgres Next.js (Turbopack) 10.710s 11.017s 0.307s 3 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 13.267s (-44.1% 🟢) 14.695s (-41.5% 🟢) 1.428s 3 1.00x
▲ Vercel Next.js (Turbopack) 13.473s (-22.2% 🟢) 14.947s (-22.9% 🟢) 1.474s 3 1.02x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 13.429s (-10.8% 🟢) 14.027s (-12.5% 🟢) 0.598s 5 1.00x
💻 Local Express 13.457s (-10.1% 🟢) 14.028s (-6.7% 🟢) 0.571s 5 1.00x
🐘 Postgres Nitro 13.469s (-7.7% 🟢) 14.019s (-6.7% 🟢) 0.550s 5 1.00x
🐘 Postgres Express 13.471s (-7.6% 🟢) 14.018s (-6.7% 🟢) 0.547s 5 1.00x
💻 Local Next.js (Turbopack) 14.075s 15.031s 0.956s 4 1.05x
🐘 Postgres Next.js (Turbopack) 14.118s 15.016s 0.898s 4 1.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 21.328s (-66.9% 🟢) 22.835s (-65.7% 🟢) 1.507s 3 1.00x
▲ Vercel Next.js (Turbopack) 22.554s (-57.1% 🟢) 24.075s (-55.9% 🟢) 1.521s 3 1.06x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 11.788s (-29.8% 🟢) 12.022s (-29.4% 🟢) 0.234s 8 1.00x
🐘 Postgres Express 11.906s (-15.0% 🟢) 12.145s (-16.8% 🟢) 0.239s 8 1.01x
💻 Local Express 11.932s (-28.1% 🟢) 12.150s (-28.7% 🟢) 0.218s 8 1.01x
🐘 Postgres Nitro 11.972s (-14.3% 🟢) 12.268s (-14.3% 🟢) 0.296s 8 1.02x
💻 Local Next.js (Turbopack) 13.074s 14.028s 0.954s 7 1.11x
🐘 Postgres Next.js (Turbopack) 13.117s 13.733s 0.616s 7 1.11x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 31.064s (-92.7% 🟢) 32.780s (-92.3% 🟢) 1.716s 3 1.00x
▲ Vercel Next.js (Turbopack) 34.306s (-91.3% 🟢) 36.360s (-90.8% 🟢) 2.055s 3 1.10x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.137s (-9.8% 🟢) 2.008s (~) 0.871s 15 1.00x
💻 Local Nitro 1.165s (-28.6% 🟢) 2.006s (-3.3%) 0.841s 15 1.02x
💻 Local Express 1.174s (-21.1% 🟢) 2.006s (~) 0.832s 15 1.03x
🐘 Postgres Nitro 1.184s (-7.1% 🟢) 2.007s (~) 0.823s 15 1.04x
🐘 Postgres Next.js (Turbopack) 1.202s 2.007s 0.805s 15 1.06x
💻 Local Next.js (Turbopack) 1.285s 2.006s 0.721s 15 1.13x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.789s (-17.9% 🟢) 4.385s (-11.1% 🟢) 1.596s 7 1.00x
▲ Vercel Nitro 2.886s (+2.4%) 4.249s (-1.7%) 1.363s 8 1.03x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.196s (-49.4% 🟢) 2.007s (-33.3% 🟢) 0.811s 15 1.00x
🐘 Postgres Nitro 1.216s (-48.3% 🟢) 2.007s (-33.3% 🟢) 0.791s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.349s 2.008s 0.659s 15 1.13x
💻 Local Nitro 1.677s (-46.6% 🟢) 2.006s (-48.4% 🟢) 0.328s 15 1.40x
💻 Local Next.js (Turbopack) 1.680s 2.006s 0.326s 15 1.40x
💻 Local Express 1.712s (-42.0% 🟢) 2.006s (-41.9% 🟢) 0.294s 15 1.43x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.701s (-47.9% 🟢) 5.327s (-40.2% 🟢) 1.625s 6 1.00x
▲ Vercel Nitro 3.953s (-2.5%) 5.162s (-12.8% 🟢) 1.210s 6 1.07x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.329s (-61.9% 🟢) 2.008s (-49.9% 🟢) 0.679s 15 1.00x
🐘 Postgres Nitro 1.346s (-61.3% 🟢) 2.006s (-49.9% 🟢) 0.660s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.584s 2.007s 0.423s 15 1.19x
💻 Local Nitro 4.494s (-46.2% 🟢) 5.011s (-44.5% 🟢) 0.516s 7 3.38x
💻 Local Next.js (Turbopack) 4.764s 5.178s 0.414s 6 3.58x
💻 Local Express 4.815s (-42.3% 🟢) 5.179s (-42.6% 🟢) 0.364s 6 3.62x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.312s (-40.4% 🟢) 7.208s (-34.2% 🟢) 1.896s 5 1.00x
▲ Vercel Nitro 6.035s (+71.2% 🔺) 7.638s (+38.0% 🔺) 1.603s 4 1.14x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.145s (-8.9% 🟢) 2.008s (~) 0.863s 15 1.00x
🐘 Postgres Express 1.156s (-8.1% 🟢) 2.008s (~) 0.852s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.203s 2.007s 0.804s 15 1.05x
💻 Local Next.js (Turbopack) 1.324s 2.006s 0.682s 15 1.16x
💻 Local Nitro 1.393s (-25.4% 🟢) 2.006s (-14.3% 🟢) 0.613s 15 1.22x
💻 Local Express 1.405s (-25.8% 🟢) 2.006s (-15.1% 🟢) 0.600s 15 1.23x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.604s (+5.9% 🔺) 4.035s (-3.2%) 1.431s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.746s (-6.3% 🟢) 4.374s (-5.8% 🟢) 1.628s 7 1.05x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.208s (-48.4% 🟢) 2.009s (-33.2% 🟢) 0.802s 15 1.00x
🐘 Postgres Express 1.210s (-48.3% 🟢) 2.009s (-33.3% 🟢) 0.799s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.323s 2.008s 0.685s 15 1.10x
💻 Local Express 2.041s (-34.8% 🟢) 2.469s (-34.4% 🟢) 0.428s 13 1.69x
💻 Local Next.js (Turbopack) 2.061s 2.826s 0.766s 11 1.71x
💻 Local Nitro 2.063s (-32.7% 🟢) 2.593s (-33.3% 🟢) 0.530s 12 1.71x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.297s (+2.0%) 4.707s (-7.3% 🟢) 1.410s 7 1.00x
▲ Vercel Next.js (Turbopack) 4.012s (+27.7% 🔺) 5.468s (+20.9% 🔺) 1.456s 6 1.22x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.318s (-62.3% 🟢) 2.008s (-49.9% 🟢) 0.689s 15 1.00x
🐘 Postgres Nitro 1.326s (-61.9% 🟢) 2.009s (-49.9% 🟢) 0.683s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.567s 2.008s 0.441s 15 1.19x
💻 Local Express 4.705s (-46.5% 🟢) 5.348s (-42.3% 🟢) 0.644s 6 3.57x
💻 Local Nitro 5.421s (-40.7% 🟢) 6.013s (-40.0% 🟢) 0.592s 5 4.11x
💻 Local Next.js (Turbopack) 5.757s 6.215s 0.458s 5 4.37x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.000s (-1.8%) 6.567s (-3.7%) 1.566s 5 1.00x
▲ Vercel Next.js (Turbopack) 6.349s (-6.0% 🟢) 8.030s (-6.0% 🟢) 1.681s 4 1.27x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.438s (-47.8% 🟢) 1.007s (-1.6%) 0.569s 60 1.00x
💻 Local Nitro 0.462s (-52.9% 🟢) 1.004s (-8.2% 🟢) 0.541s 60 1.06x
🐘 Postgres Nitro 0.483s (-41.1% 🟢) 1.024s (+1.8%) 0.541s 59 1.10x
💻 Local Express 0.511s (-48.1% 🟢) 1.039s (-3.5%) 0.527s 58 1.17x
🐘 Postgres Next.js (Turbopack) 0.656s 1.006s 0.350s 60 1.50x
💻 Local Next.js (Turbopack) 0.757s 1.004s 0.247s 60 1.73x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.423s (-75.4% 🟢) 6.932s (-71.1% 🟢) 1.510s 9 1.00x
▲ Vercel Next.js (Turbopack) 5.556s (-61.7% 🟢) 7.267s (-54.8% 🟢) 1.711s 9 1.02x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.023s (-48.2% 🟢) 1.549s (-31.4% 🟢) 0.526s 59 1.00x
🐘 Postgres Nitro 1.091s (-43.4% 🟢) 1.903s (-9.4% 🟢) 0.812s 48 1.07x
💻 Local Nitro 1.166s (-61.6% 🟢) 2.005s (-46.6% 🟢) 0.839s 45 1.14x
💻 Local Express 1.203s (-60.1% 🟢) 2.006s (-44.1% 🟢) 0.802s 45 1.18x
🐘 Postgres Next.js (Turbopack) 1.576s 2.007s 0.431s 45 1.54x
💻 Local Next.js (Turbopack) 1.842s 2.051s 0.209s 44 1.80x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 12.637s (-68.0% 🟢) 14.394s (-65.1% 🟢) 1.757s 7 1.00x
▲ Vercel Next.js (Turbopack) 13.164s (-73.6% 🟢) 15.106s (-70.8% 🟢) 1.942s 7 1.04x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.994s (-50.0% 🟢) 2.335s (-46.6% 🟢) 0.342s 52 1.00x
🐘 Postgres Nitro 2.150s (-47.6% 🟢) 2.799s (-39.2% 🟢) 0.649s 43 1.08x
💻 Local Nitro 2.645s (-71.6% 🟢) 3.007s (-70.0% 🟢) 0.362s 40 1.33x
💻 Local Express 2.743s (-70.2% 🟢) 3.033s (-69.7% 🟢) 0.290s 40 1.38x
🐘 Postgres Next.js (Turbopack) 3.120s 4.010s 0.890s 30 1.56x
💻 Local Next.js (Turbopack) 3.899s 4.215s 0.316s 29 1.96x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 25.288s (-73.9% 🟢) 27.188s (-72.4% 🟢) 1.900s 5 1.00x
▲ Vercel Next.js (Turbopack) 27.396s (-74.4% 🟢) 29.427s (-73.0% 🟢) 2.031s 5 1.08x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | 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.176s (-37.8% 🟢) 1.006s (~) 0.829s 60 1.00x
🐘 Postgres Express 0.180s (-36.1% 🟢) 1.006s (~) 0.825s 60 1.02x
🐘 Postgres Next.js (Turbopack) 0.226s 1.006s 0.780s 60 1.28x
💻 Local Express 0.392s (-30.0% 🟢) 1.004s (~) 0.612s 60 2.22x
💻 Local Nitro 0.395s (-34.7% 🟢) 1.004s (-1.7%) 0.609s 60 2.24x
💻 Local Next.js (Turbopack) 0.549s 1.004s 0.455s 60 3.11x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.378s (+43.2% 🔺) 3.815s (+13.8% 🔺) 1.437s 16 1.00x
▲ Vercel Next.js (Turbopack) 2.893s (+43.0% 🔺) 4.315s (+13.7% 🔺) 1.422s 14 1.22x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: 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 🥇 Express 0.301s (-41.0% 🟢) 1.006s (~) 0.705s 90 1.00x
🐘 Postgres Nitro 0.305s (-38.6% 🟢) 1.006s (~) 0.702s 90 1.01x
🐘 Postgres Next.js (Turbopack) 0.422s 1.006s 0.584s 90 1.40x
💻 Local Nitro 2.135s (-15.9% 🟢) 2.715s (-9.8% 🟢) 0.580s 34 7.09x
💻 Local Express 2.178s (-13.3% 🟢) 2.796s (-7.1% 🟢) 0.618s 33 7.24x
💻 Local Next.js (Turbopack) 2.381s 3.010s 0.630s 30 7.91x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.031s (+56.0% 🔺) 6.621s (+37.3% 🔺) 1.590s 14 1.00x
▲ Vercel Next.js (Turbopack) 5.780s (+63.5% 🔺) 7.417s (+42.8% 🔺) 1.636s 13 1.15x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: 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 🥇 Express 0.617s (-24.7% 🟢) 1.006s (-1.1%) 0.389s 120 1.00x
🐘 Postgres Nitro 0.661s (-16.4% 🟢) 1.006s (~) 0.345s 120 1.07x
🐘 Postgres Next.js (Turbopack) 0.866s 1.023s 0.157s 118 1.40x
💻 Local Nitro 9.855s (-11.9% 🟢) 10.275s (-11.9% 🟢) 0.420s 12 15.98x
💻 Local Express 10.320s (-7.8% 🟢) 10.781s (-9.7% 🟢) 0.461s 12 16.73x
💻 Local Next.js (Turbopack) 11.376s 11.939s 0.563s 11 18.44x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 16.735s (+62.0% 🔺) 18.540s (+50.9% 🔺) 1.805s 7 1.00x
▲ Vercel Nitro 24.069s (+211.7% 🔺) 25.599s (+172.3% 🔺) 1.529s 5 1.44x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

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.130s (+428.7% 🔺) 2.005s (+99.6% 🔺) 0.010s (-18.4% 🟢) 2.017s (+98.0% 🔺) 0.887s 10 1.00x
💻 Local Express 1.142s (+473.5% 🔺) 2.005s (+99.6% 🔺) 0.011s (-13.2% 🟢) 2.018s (+98.2% 🔺) 0.876s 10 1.01x
🐘 Postgres Express 1.145s (+458.5% 🔺) 1.997s (+99.9% 🔺) 0.001s (-31.3% 🟢) 2.010s (+98.7% 🔺) 0.864s 10 1.01x
🐘 Postgres Nitro 1.159s (+465.3% 🔺) 2.002s (+100.2% 🔺) 0.002s (+13.3% 🔺) 2.011s (+98.9% 🔺) 0.852s 10 1.03x
💻 Local Next.js (Turbopack) 1.180s 2.004s 0.011s 2.018s 0.839s 10 1.04x
🐘 Postgres Next.js (Turbopack) 1.196s 2.002s 0.002s 2.010s 0.814s 10 1.06x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.306s (-66.4% 🟢) 3.176s (-63.3% 🟢) 2.309s (+265.5% 🔺) 5.934s (-39.4% 🟢) 3.628s 10 1.00x
▲ Vercel Nitro 2.375s (-38.0% 🟢) 3.202s (-39.3% 🟢) 14.034s (+1791.1% 🔺) 17.640s (+172.1% 🔺) 15.265s 10 1.03x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.519s (+141.2% 🔺) 2.003s (+99.0% 🔺) 0.004s (+1.8%) 2.026s (+98.0% 🔺) 0.506s 30 1.00x
🐘 Postgres Nitro 1.566s (+150.9% 🔺) 2.008s (+99.5% 🔺) 0.004s (-1.7%) 2.025s (+98.1% 🔺) 0.459s 30 1.03x
🐘 Postgres Next.js (Turbopack) 1.652s 2.010s 0.004s 2.026s 0.374s 30 1.09x
💻 Local Nitro 1.694s (+102.0% 🔺) 2.010s (+98.6% 🔺) 0.009s (-2.4%) 2.200s (+97.2% 🔺) 0.507s 28 1.11x
💻 Local Express 1.703s (+125.0% 🔺) 2.010s (+95.4% 🔺) 0.011s (+12.7% 🔺) 2.202s (+111.8% 🔺) 0.499s 28 1.12x
💻 Local Next.js (Turbopack) 1.871s 2.011s 0.009s 2.203s 0.332s 28 1.23x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.720s (-66.2% 🟢) 6.903s (-62.2% 🟢) 0.256s (+20.9% 🔺) 7.611s (-59.8% 🟢) 1.891s 8 1.00x
▲ Vercel Nitro 5.938s (-79.8% 🟢) 7.093s (-77.0% 🟢) 0.210s (+87.5% 🔺) 7.760s (-75.6% 🟢) 1.823s 8 1.04x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.651s (-32.3% 🟢) 1.027s (-19.6% 🟢) 0.000s (-61.0% 🟢) 1.040s (-20.4% 🟢) 0.390s 59 1.00x
🐘 Postgres Nitro 0.674s (-30.4% 🟢) 1.070s (-14.2% 🟢) 0.000s (-15.8% 🟢) 1.077s (-14.3% 🟢) 0.403s 57 1.04x
🐘 Postgres Next.js (Turbopack) 0.750s 1.035s 0.000s 1.042s 0.292s 58 1.15x
💻 Local Nitro 1.341s (+9.7% 🔺) 2.015s (~) 0.001s (+433.3% 🔺) 2.018s (~) 0.677s 30 2.06x
💻 Local Express 1.376s (+12.3% 🔺) 2.015s (~) 0.000s (~) 2.018s (~) 0.642s 30 2.11x
💻 Local Next.js (Turbopack) 1.482s 2.014s 0.000s 2.017s 0.535s 30 2.28x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.886s (-61.8% 🟢) 4.959s (-56.9% 🟢) 0.002s (+Infinity% 🔺) 5.651s (-53.1% 🟢) 1.766s 11 1.00x
▲ Vercel Nitro 4.900s (+60.6% 🔺) 6.154s (+40.1% 🔺) 0.000s (-100.0% 🟢) 6.747s (+40.3% 🔺) 1.847s 9 1.26x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

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.289s (-27.2% 🟢) 2.065s (-5.2% 🟢) 0.000s (+Infinity% 🔺) 2.079s (-5.4% 🟢) 0.790s 29 1.00x
🐘 Postgres Nitro 1.338s (-25.3% 🟢) 2.068s (-3.4%) 0.000s (+189.7% 🔺) 2.079s (-4.4%) 0.742s 29 1.04x
🐘 Postgres Next.js (Turbopack) 1.566s 2.181s 0.000s 2.203s 0.637s 28 1.21x
💻 Local Nitro 3.063s (-9.6% 🟢) 3.677s (-8.8% 🟢) 0.000s (-55.9% 🟢) 3.680s (-8.8% 🟢) 0.617s 17 2.38x
💻 Local Express 3.172s (-8.5% 🟢) 3.904s (-3.2%) 0.001s (-37.5% 🟢) 3.908s (-3.2%) 0.736s 16 2.46x
💻 Local Next.js (Turbopack) 3.190s 3.337s 0.001s 3.612s 0.422s 19 2.47x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.807s (+41.8% 🔺) 7.135s (+32.8% 🔺) 0.000s (+37.5% 🔺) 7.605s (+31.3% 🔺) 1.798s 8 1.00x
▲ Vercel Next.js (Turbopack) 6.464s (+15.1% 🔺) 7.633s (+9.3% 🔺) 0.000s (-100.0% 🟢) 8.268s (+9.6% 🔺) 1.804s 8 1.11x
▲ Vercel Express ⚠️ missing - - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 16/21
🐘 Postgres Express 16/21
▲ Vercel Nitro 12/21
Fastest World by Framework

Winner determined by most benchmark wins

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

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.

Should we memoize the getWritable() return value instead? Seems simpler to me.

@VaguelySerious
Copy link
Copy Markdown
Member Author

@TooTallNate doing now

Drop the per-call guard mechanism and `pollSharedWritableLock` helper.
With memoization, the cached pipe's single `state.promise` is sufficient:
in the loop pattern `await writer.write(chunk)` already blocks until each
chunk is flushed (the sink awaits the scheduled flush before resolving),
so by the time the loop ends `pendingOps === 0` and the final
`releaseLock()` lets `pollWritableLock` resolve the state. One writable,
one pipe, one ops entry per `(runId, namespace)`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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.

getWritable() returns a new TransformStream per call — racing pipes reorder chunks when callers acquire per-write

2 participants