Skip to content

[world-vercel] Always try JSON queue transport as a fallback#1717

Closed
VaguelySerious wants to merge 2 commits intomainfrom
peter/json-transport-fallback
Closed

[world-vercel] Always try JSON queue transport as a fallback#1717
VaguelySerious wants to merge 2 commits intomainfrom
peter/json-transport-fallback

Conversation

@VaguelySerious
Copy link
Copy Markdown
Member

No description provided.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 13, 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 13, 2026 11:35pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Apr 13, 2026 11:35pm
example-workflow Ready Ready Preview, Comment Apr 13, 2026 11:35pm
workbench-astro-workflow Ready Ready Preview, Comment Apr 13, 2026 11:35pm
workbench-express-workflow Ready Ready Preview, Comment Apr 13, 2026 11:35pm
workbench-fastify-workflow Ready Ready Preview, Comment Apr 13, 2026 11:35pm
workbench-hono-workflow Ready Ready Preview, Comment Apr 13, 2026 11:35pm
workbench-nitro-workflow Ready Ready Preview, Comment Apr 13, 2026 11:35pm
workbench-nuxt-workflow Ready Ready Preview, Comment Apr 13, 2026 11:35pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Apr 13, 2026 11:35pm
workbench-vite-workflow Ready Ready Preview, Comment Apr 13, 2026 11:35pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Apr 13, 2026 11:35pm
workflow-swc-playground Ready Ready Preview, Comment Apr 13, 2026 11:35pm

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 13, 2026

🦋 Changeset detected

Latest commit: 00bea6d

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

This PR includes changesets to release 17 packages
Name Type
@workflow/world-vercel Patch
@workflow/cli Patch
@workflow/core Patch
workflow 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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 13, 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 🥇 Express 0.044s (-1.4%) 1.005s (~) 0.961s 10 1.00x
💻 Local Next.js (Turbopack) 0.048s 1.005s 0.958s 10 1.10x
🐘 Postgres Express 0.058s (-0.7%) 1.010s (~) 0.952s 10 1.32x
🐘 Postgres Next.js (Turbopack) 0.059s 1.010s 0.951s 10 1.35x
🐘 Postgres Nitro 0.061s (-35.7% 🟢) 1.010s (-3.2%) 0.949s 10 1.40x
💻 Local Nitro 0.081s (+87.2% 🔺) 1.008s (~) 0.927s 10 1.85x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 0.297s (+18.1% 🔺) 2.130s (-8.7% 🟢) 1.833s 10 1.00x
▲ Vercel Nitro 0.341s (-16.8% 🟢) 2.609s (+4.0%) 2.268s 10 1.15x
▲ Vercel Express 0.342s (+45.3% 🔺) 2.259s (+5.8% 🔺) 1.917s 10 1.15x

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

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.125s (~) 2.005s (~) 0.880s 10 1.00x
💻 Local Next.js (Turbopack) 1.130s 2.006s 0.876s 10 1.00x
🐘 Postgres Next.js (Turbopack) 1.135s 2.009s 0.874s 10 1.01x
🐘 Postgres Nitro 1.143s (~) 2.010s (~) 0.867s 10 1.02x
🐘 Postgres Express 1.146s (~) 2.009s (~) 0.863s 10 1.02x
💻 Local Nitro 1.262s (+11.6% 🔺) 2.008s (~) 0.746s 10 1.12x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.336s (-40.0% 🟢) 4.061s (-31.3% 🟢) 1.725s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.843s (+39.7% 🔺) 4.353s (+13.6% 🔺) 1.510s 10 1.22x
▲ Vercel Express 3.107s (+65.7% 🔺) 4.974s (+30.7% 🔺) 1.868s 10 1.33x

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

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 10.846s 11.024s 0.177s 3 1.00x
🐘 Postgres Next.js (Turbopack) 10.891s 11.025s 0.134s 3 1.00x
🐘 Postgres Nitro 10.907s (~) 11.021s (~) 0.115s 3 1.01x
🐘 Postgres Express 10.910s (~) 11.019s (~) 0.109s 3 1.01x
💻 Local Express 10.924s (~) 11.024s (~) 0.099s 3 1.01x
💻 Local Nitro 13.744s (+25.6% 🔺) 14.063s (+27.6% 🔺) 0.320s 3 1.27x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 20.357s (+19.9% 🔺) 22.842s (+14.1% 🔺) 2.485s 2 1.00x
▲ Vercel Nitro 21.881s (-7.8% 🟢) 23.283s (-7.3% 🟢) 1.402s 2 1.07x
▲ Vercel Next.js (Turbopack) 23.867s (+37.8% 🔺) 25.037s (+29.1% 🔺) 1.170s 2 1.17x

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

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 14.503s 15.026s 0.523s 4 1.00x
🐘 Postgres Nitro 14.553s (~) 15.019s (~) 0.466s 4 1.00x
🐘 Postgres Express 14.572s (~) 15.024s (~) 0.453s 4 1.00x
💻 Local Next.js (Turbopack) 14.622s 15.030s 0.408s 4 1.01x
💻 Local Express 15.007s (~) 15.281s (+1.7%) 0.274s 4 1.03x
💻 Local Nitro 20.448s (+35.8% 🔺) 21.071s (+31.4% 🔺) 0.623s 4 1.41x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 38.613s (-23.2% 🟢) 40.756s (-22.5% 🟢) 2.143s 2 1.00x
▲ Vercel Nitro 42.900s (-33.4% 🟢) 45.053s (-32.4% 🟢) 2.153s 2 1.11x
▲ Vercel Next.js (Turbopack) 43.361s (-17.5% 🟢) 44.900s (-17.8% 🟢) 1.539s 2 1.12x

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

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 13.792s 14.016s 0.224s 7 1.00x
🐘 Postgres Nitro 13.982s (~) 14.306s (~) 0.324s 7 1.01x
🐘 Postgres Express 14.077s (~) 14.736s (+1.0%) 0.659s 7 1.02x
💻 Local Next.js (Turbopack) 16.085s 16.696s 0.611s 6 1.17x
💻 Local Express 16.683s (~) 17.031s (~) 0.348s 6 1.21x
💻 Local Nitro 34.878s (+107.8% 🔺) 35.468s (+108.3% 🔺) 0.590s 3 2.53x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 74.736s (-38.3% 🟢) 76.583s (-38.1% 🟢) 1.848s 2 1.00x
▲ Vercel Nitro 115.460s (-72.7% 🟢) 116.810s (-72.5% 🟢) 1.350s 1 1.54x
▲ Vercel Next.js (Turbopack) 398.800s (+1.3%) 400.591s (+1.3%) 1.791s 1 5.34x

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

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.238s 2.009s 0.771s 15 1.00x
🐘 Postgres Nitro 1.268s (-0.6%) 2.009s (~) 0.742s 15 1.02x
🐘 Postgres Express 1.270s (+0.8%) 2.010s (~) 0.740s 15 1.03x
💻 Local Express 1.564s (+5.1% 🔺) 2.005s (~) 0.441s 15 1.26x
💻 Local Nitro 1.581s (-3.1%) 2.073s (~) 0.492s 15 1.28x
💻 Local Next.js (Turbopack) 1.595s 2.073s 0.478s 15 1.29x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.520s (-11.9% 🟢) 4.028s (-12.9% 🟢) 1.508s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.865s (-15.7% 🟢) 4.086s (-17.2% 🟢) 1.220s 8 1.14x
▲ Vercel Nitro 2.875s (+2.0%) 4.443s (+2.8%) 1.569s 7 1.14x

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

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.345s (-0.6%) 3.011s (~) 0.666s 10 1.00x
🐘 Postgres Nitro 2.351s (~) 3.011s (~) 0.660s 10 1.00x
🐘 Postgres Next.js (Turbopack) 2.407s 3.011s 0.604s 10 1.03x
💻 Local Next.js (Turbopack) 3.032s 3.760s 0.728s 8 1.29x
💻 Local Express 3.046s (+3.1%) 3.762s (+8.9% 🔺) 0.716s 8 1.30x
💻 Local Nitro 4.885s (+55.4% 🔺) 5.166s (+33.0% 🔺) 0.281s 7 2.08x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.008s (-57.6% 🟢) 4.680s (-47.4% 🟢) 1.672s 7 1.00x
▲ Vercel Nitro 3.479s (-14.1% 🟢) 5.212s (-12.0% 🟢) 1.733s 6 1.16x
▲ Vercel Express 4.435s (+22.5% 🔺) 6.573s (+28.6% 🔺) 2.138s 5 1.47x

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

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.459s (-0.8%) 4.011s (~) 0.551s 8 1.00x
🐘 Postgres Nitro 3.478s (~) 4.011s (~) 0.532s 8 1.01x
🐘 Postgres Next.js (Turbopack) 3.779s 4.136s 0.358s 8 1.09x
💻 Local Next.js (Turbopack) 7.827s 8.518s 0.691s 4 2.26x
💻 Local Express 8.527s (+2.3%) 9.022s (~) 0.495s 4 2.46x
💻 Local Nitro 10.638s (+27.4% 🔺) 11.697s (+29.7% 🔺) 1.060s 3 3.08x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.713s (+5.3% 🔺) 5.216s (-5.8% 🟢) 1.503s 6 1.00x
▲ Vercel Express 3.832s (-9.6% 🟢) 5.729s (-6.5% 🟢) 1.896s 6 1.03x
▲ Vercel Next.js (Turbopack) 4.151s (-53.4% 🟢) 5.646s (-48.5% 🟢) 1.495s 6 1.12x

🔍 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.243s 2.008s 0.766s 15 1.00x
🐘 Postgres Nitro 1.253s (~) 2.010s (~) 0.756s 15 1.01x
🐘 Postgres Express 1.259s (~) 2.008s (~) 0.748s 15 1.01x
💻 Local Next.js (Turbopack) 1.537s 2.005s 0.469s 15 1.24x
💻 Local Express 1.589s (-16.1% 🟢) 2.006s (-15.1% 🟢) 0.417s 15 1.28x
💻 Local Nitro 1.748s (-6.3% 🟢) 2.073s (-11.4% 🟢) 0.325s 15 1.41x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.158s (-16.4% 🟢) 4.355s (~) 2.197s 7 1.00x
▲ Vercel Next.js (Turbopack) 2.637s (-10.1% 🟢) 3.984s (-14.2% 🟢) 1.348s 8 1.22x
▲ Vercel Nitro 2.832s (+15.2% 🔺) 4.455s (+6.8% 🔺) 1.622s 7 1.31x

🔍 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.308s (-1.4%) 3.010s (~) 0.702s 10 1.00x
🐘 Postgres Nitro 2.350s (~) 3.009s (~) 0.659s 10 1.02x
🐘 Postgres Next.js (Turbopack) 2.390s 3.009s 0.620s 10 1.04x
💻 Local Next.js (Turbopack) 3.090s 3.759s 0.669s 8 1.34x
💻 Local Express 3.297s (+5.3% 🔺) 4.135s (+9.9% 🔺) 0.837s 8 1.43x
💻 Local Nitro 3.663s (+19.5% 🔺) 4.262s (+9.7% 🔺) 0.599s 8 1.59x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.490s (+11.1% 🔺) 5.103s (+12.8% 🔺) 1.612s 6 1.00x
▲ Vercel Nitro 3.605s (+11.5% 🔺) 5.546s (+9.2% 🔺) 1.941s 6 1.03x
▲ Vercel Express 4.230s (+32.5% 🔺) 6.120s (+27.7% 🔺) 1.890s 5 1.21x

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

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 3.456s (-0.7%) 4.013s (~) 0.558s 8 1.00x
🐘 Postgres Express 3.462s (-1.0%) 4.010s (~) 0.548s 8 1.00x
🐘 Postgres Next.js (Turbopack) 3.648s 4.010s 0.362s 8 1.06x
💻 Local Express 8.825s (~) 9.525s (+2.7%) 0.700s 4 2.55x
💻 Local Next.js (Turbopack) 8.939s 9.271s 0.332s 4 2.59x
💻 Local Nitro 14.098s (+54.2% 🔺) 15.040s (+50.0% 🔺) 0.942s 2 4.08x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.379s (-47.3% 🟢) 4.958s (-39.4% 🟢) 1.579s 7 1.00x
▲ Vercel Nitro 3.820s (-25.0% 🟢) 5.528s (-18.9% 🟢) 1.707s 6 1.13x
▲ Vercel Next.js (Turbopack) 5.582s (-17.4% 🟢) 7.253s (-15.1% 🟢) 1.671s 5 1.65x

🔍 Observability: Express | 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 🥇 Next.js (Turbopack) 0.781s 1.023s 0.242s 59 1.00x
🐘 Postgres Express 0.809s (-3.6%) 1.006s (-1.7%) 0.197s 60 1.03x
🐘 Postgres Nitro 0.840s (+2.4%) 1.023s (+1.7%) 0.183s 59 1.08x
💻 Local Next.js (Turbopack) 0.855s 1.005s 0.150s 60 1.09x
💻 Local Express 1.091s (+10.9% 🔺) 1.255s (+16.6% 🔺) 0.164s 48 1.40x
💻 Local Nitro 1.339s (+36.6% 🔺) 1.942s (+77.5% 🔺) 0.603s 31 1.71x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 9.918s (-47.8% 🟢) 12.170s (-42.9% 🟢) 2.252s 6 1.00x
▲ Vercel Next.js (Turbopack) 12.706s (-12.4% 🟢) 15.283s (-5.0%) 2.577s 4 1.28x
▲ Vercel Nitro 21.063s (-4.5%) 22.681s (-5.6% 🟢) 1.618s 3 2.12x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.912s (-3.3%) 2.123s (-6.0% 🟢) 0.211s 43 1.00x
🐘 Postgres Next.js (Turbopack) 1.945s 2.124s 0.179s 43 1.02x
🐘 Postgres Nitro 1.956s (+1.5%) 2.175s (+3.5%) 0.219s 42 1.02x
💻 Local Next.js (Turbopack) 2.721s 3.041s 0.320s 30 1.42x
💻 Local Express 2.997s (-0.6%) 3.508s (-2.1%) 0.511s 26 1.57x
💻 Local Nitro 4.313s (+42.1% 🔺) 4.802s (+27.8% 🔺) 0.489s 19 2.26x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 30.922s (-21.7% 🟢) 33.529s (-18.8% 🟢) 2.607s 3 1.00x
▲ Vercel Express 47.200s (+36.7% 🔺) 49.306s (+33.9% 🔺) 2.106s 2 1.53x
▲ Vercel Next.js (Turbopack) 61.995s (+24.5% 🔺) 63.771s (+23.3% 🔺) 1.776s 2 2.00x

🔍 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 🥇 Next.js (Turbopack) 3.865s 4.110s 0.246s 30 1.00x
🐘 Postgres Express 3.948s (-1.1%) 4.182s (-4.3%) 0.235s 29 1.02x
🐘 Postgres Nitro 4.088s (~) 4.780s (+3.8%) 0.692s 26 1.06x
💻 Local Next.js (Turbopack) 8.760s 9.018s 0.258s 14 2.27x
💻 Local Express 9.134s (-0.8%) 9.710s (-3.1%) 0.576s 13 2.36x
💻 Local Nitro 14.804s (+59.2% 🔺) 15.258s (+52.3% 🔺) 0.454s 9 3.83x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 83.722s (-13.6% 🟢) 85.940s (-12.7% 🟢) 2.218s 2 1.00x
▲ Vercel Express 96.317s (-25.9% 🟢) 98.263s (-25.7% 🟢) 1.946s 2 1.15x
▲ Vercel Next.js (Turbopack) 113.963s (+6.4% 🔺) 116.172s (+6.7% 🔺) 2.209s 2 1.36x

🔍 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.246s 1.007s 0.761s 60 1.00x
🐘 Postgres Express 0.280s (-1.0%) 1.007s (~) 0.728s 60 1.14x
🐘 Postgres Nitro 0.284s (~) 1.007s (~) 0.722s 60 1.16x
💻 Local Next.js (Turbopack) 0.556s 1.005s 0.448s 60 2.26x
💻 Local Express 0.604s (+7.7% 🔺) 1.005s (~) 0.401s 60 2.45x
💻 Local Nitro 0.868s (+43.5% 🔺) 1.310s (+28.3% 🔺) 0.443s 46 3.53x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.717s (-12.1% 🟢) 3.432s (-5.6% 🟢) 1.715s 18 1.00x
▲ Vercel Next.js (Turbopack) 1.833s (-9.4% 🟢) 3.470s (-8.5% 🟢) 1.637s 18 1.07x
▲ Vercel Nitro 1.989s (+19.8% 🔺) 3.752s (+12.0% 🔺) 1.762s 16 1.16x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.479s (-6.1% 🟢) 1.006s (~) 0.528s 90 1.00x
🐘 Postgres Next.js (Turbopack) 0.484s 1.007s 0.522s 90 1.01x
🐘 Postgres Nitro 0.500s (+0.8%) 1.007s (~) 0.506s 90 1.05x
💻 Local Express 2.529s (+0.6%) 3.009s (~) 0.480s 30 5.28x
💻 Local Next.js (Turbopack) 2.690s 3.009s 0.319s 30 5.62x
💻 Local Nitro 4.691s (+84.8% 🔺) 5.653s (+87.8% 🔺) 0.961s 16 9.80x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.247s (+0.7%) 5.015s (+4.0%) 1.768s 18 1.00x
▲ Vercel Express 3.282s (+7.7% 🔺) 5.287s (+10.0% 🔺) 2.005s 18 1.01x
▲ Vercel Next.js (Turbopack) 4.025s (+13.9% 🔺) 5.238s (+0.9%) 1.213s 18 1.24x

🔍 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 🥇 Express 0.775s (-5.3% 🟢) 1.007s (-1.0%) 0.232s 120 1.00x
🐘 Postgres Next.js (Turbopack) 0.785s 1.007s 0.222s 120 1.01x
🐘 Postgres Nitro 0.844s (+6.8% 🔺) 1.019s (+1.1%) 0.174s 118 1.09x
💻 Local Next.js (Turbopack) 11.034s 11.483s 0.450s 11 14.23x
💻 Local Express 11.211s (~) 11.758s (-1.5%) 0.547s 11 14.46x
💻 Local Nitro 17.752s (+58.6% 🔺) 19.072s (+63.5% 🔺) 1.321s 7 22.90x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 7.020s (-5.4% 🟢) 8.987s (-2.8%) 1.967s 14 1.00x
▲ Vercel Nitro 8.224s (+6.5% 🔺) 10.101s (+7.5% 🔺) 1.877s 12 1.17x
▲ Vercel Next.js (Turbopack) 10.103s (-2.2%) 11.557s (-5.9% 🟢) 1.454s 11 1.44x

🔍 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 🥇 Next.js (Turbopack) 0.171s 1.003s 0.013s 1.019s 0.847s 10 1.00x
🐘 Postgres Next.js (Turbopack) 0.187s 1.001s 0.002s 1.009s 0.822s 10 1.09x
💻 Local Express 0.203s (+2.2%) 1.004s (~) 0.012s (-0.8%) 1.018s (~) 0.815s 10 1.19x
🐘 Postgres Nitro 0.210s (+2.4%) 0.998s (~) 0.001s (-20.0% 🟢) 1.011s (~) 0.801s 10 1.23x
🐘 Postgres Express 0.213s (+3.9%) 0.995s (~) 0.001s (-43.8% 🟢) 1.009s (~) 0.796s 10 1.24x
💻 Local Nitro 0.302s (+41.3% 🔺) 1.005s (~) 0.014s (+12.8% 🔺) 1.021s (~) 0.719s 10 1.76x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.771s (-29.3% 🟢) 3.071s (-24.9% 🟢) 0.812s (-15.5% 🟢) 4.481s (-19.9% 🟢) 2.710s 10 1.00x
▲ Vercel Nitro 2.745s (-28.4% 🟢) 4.080s (-22.7% 🟢) 0.883s (+19.0% 🔺) 5.503s (-15.1% 🟢) 2.757s 10 1.55x
▲ Vercel Next.js (Turbopack) 2.902s (-57.7% 🟢) 4.059s (-53.1% 🟢) 1.466s (+131.9% 🔺) 6.023s (-38.5% 🟢) 3.121s 10 1.64x

🔍 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
🐘 Postgres 🥇 Next.js (Turbopack) 0.612s 1.009s 0.004s 1.033s 0.421s 59 1.00x
🐘 Postgres Express 0.624s (-0.9%) 1.024s (+1.7%) 0.004s (+4.4%) 1.039s (+1.6%) 0.415s 58 1.02x
🐘 Postgres Nitro 0.645s (+3.4%) 1.006s (~) 0.004s (-7.4% 🟢) 1.022s (~) 0.377s 59 1.05x
💻 Local Next.js (Turbopack) 0.668s 1.011s 0.010s 1.024s 0.356s 59 1.09x
💻 Local Express 0.948s (+25.2% 🔺) 1.012s (-1.6%) 0.010s (+3.6%) 1.228s (+18.1% 🔺) 0.280s 49 1.55x
💻 Local Nitro 1.244s (+48.4% 🔺) 1.364s (+34.8% 🔺) 0.010s (+7.9% 🔺) 1.648s (+47.7% 🔺) 0.404s 37 2.03x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.038s (-70.2% 🟢) 6.321s (-65.3% 🟢) 0.199s (-5.7% 🟢) 6.900s (-63.6% 🟢) 1.861s 9 1.00x
▲ Vercel Express 6.212s (-4.5%) 7.535s (-5.9% 🟢) 0.299s (-26.7% 🟢) 8.276s (-6.3% 🟢) 2.065s 8 1.23x
▲ Vercel Nitro 8.627s (-70.7% 🟢) 9.971s (-67.6% 🟢) 0.235s (+109.8% 🔺) 10.631s (-66.5% 🟢) 2.004s 6 1.71x

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

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.930s (-3.3%) 1.106s (-13.4% 🟢) 0.000s (-14.8% 🟢) 1.119s (-14.4% 🟢) 0.189s 54 1.00x
🐘 Postgres Next.js (Turbopack) 0.933s 1.200s 0.000s 1.208s 0.275s 50 1.00x
🐘 Postgres Nitro 0.960s (-0.9%) 1.172s (-6.0% 🟢) 0.000s (-100.0% 🟢) 1.184s (-5.8% 🟢) 0.224s 51 1.03x
💻 Local Nitro 1.175s (-3.9%) 2.017s (~) 0.000s (+233.3% 🔺) 2.019s (~) 0.844s 30 1.26x
💻 Local Express 1.219s (~) 2.020s (~) 0.000s (-30.0% 🟢) 2.022s (~) 0.803s 30 1.31x
💻 Local Next.js (Turbopack) 1.287s 2.022s 0.001s 2.025s 0.738s 30 1.38x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.986s (-20.2% 🟢) 4.566s (-10.5% 🟢) 0.001s (+661.5% 🔺) 5.079s (-8.2% 🟢) 2.094s 13 1.00x
▲ Vercel Nitro 3.041s (~) 4.438s (+1.0%) 0.000s (~) 4.851s (+0.9%) 1.810s 13 1.02x
▲ Vercel Next.js (Turbopack) 3.596s (-64.7% 🟢) 4.681s (-59.4% 🟢) 0.000s (+Infinity% 🔺) 5.074s (-57.9% 🟢) 1.478s 12 1.20x

🔍 Observability: Express | Nitro | 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.703s (-3.9%) 2.105s (-3.3%) 0.000s (+Infinity% 🔺) 2.149s (-2.3%) 0.446s 28 1.00x
🐘 Postgres Next.js (Turbopack) 1.788s 2.106s 0.000s 2.113s 0.325s 29 1.05x
🐘 Postgres Nitro 1.814s (+1.3%) 2.220s (+3.7%) 0.000s (+3.7%) 2.232s (+2.6%) 0.417s 27 1.07x
💻 Local Nitro 3.308s (-2.3%) 3.795s (-5.9% 🟢) 0.000s (-66.9% 🟢) 3.797s (-5.9% 🟢) 0.489s 17 1.94x
💻 Local Express 3.521s (+1.5%) 4.099s (+1.6%) 0.001s (-16.7% 🟢) 4.102s (+1.6%) 0.581s 15 2.07x
💻 Local Next.js (Turbopack) 3.731s 4.166s 0.001s 4.171s 0.439s 15 2.19x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.336s (+5.9% 🔺) 5.588s (+4.0%) 0.000s (-63.3% 🟢) 6.117s (+5.6% 🔺) 1.781s 10 1.00x
▲ Vercel Express 4.487s (-2.2%) 5.826s (-3.3%) 0.000s (+Infinity% 🔺) 6.325s (-2.0%) 1.839s 10 1.03x
▲ Vercel Next.js (Turbopack) 4.909s (-12.6% 🟢) 6.109s (-12.5% 🟢) 0.002s (+1420.0% 🔺) 6.491s (-13.9% 🟢) 1.582s 10 1.13x

🔍 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) 11/21
▲ Vercel Express 11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 13, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 923 0 67 990
✅ 💻 Local Development 898 0 182 1080
✅ 📦 Local Production 898 0 182 1080
✅ 🐘 Local Postgres 898 0 182 1080
✅ 🪟 Windows 82 0 8 90
❌ 🌍 Community Worlds 133 74 24 231
✅ 📋 Other 228 0 42 270
Total 4060 74 687 4821

❌ Failed Tests

🌍 Community Worlds (74 failed)

mongodb (7 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KP4K2SEHZ56KZ708EP9NWEME
  • webhookWorkflow | wrun_01KP4K31V6J2PNFY1QYSVXSG32
  • fetchWorkflow | wrun_01KP4K69XY4TRWVS5MPACQSRSY
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KP4KA55G0BVHX9JYSSN2044A
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • health check (CLI) - workflow health command reports healthy endpoints
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KP4KG2T5KQ3RZE9EZPTH0B3A

redis (7 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KP4K2SEHZ56KZ708EP9NWEME
  • webhookWorkflow | wrun_01KP4K31V6J2PNFY1QYSVXSG32
  • fetchWorkflow | wrun_01KP4K69XY4TRWVS5MPACQSRSY
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KP4KA55G0BVHX9JYSSN2044A
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • health check (CLI) - workflow health command reports healthy endpoints
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KP4KG2T5KQ3RZE9EZPTH0B3A

turso (60 failed):

  • addTenWorkflow | wrun_01KP4K1QDAHRG6BRZY00GKNT3C
  • addTenWorkflow | wrun_01KP4K1QDAHRG6BRZY00GKNT3C
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KP4K2YQY0DHJT3P1N8Y4TMTT
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KP4K1XK9A3MXEK8BB29MJMN9
  • promiseRaceWorkflow | wrun_01KP4K21Q8QQ0B0KSFJEWG6E8P
  • promiseAnyWorkflow | wrun_01KP4K23M4YJJH5MW0SFCNQPWS
  • importedStepOnlyWorkflow | wrun_01KP4K3940969JJGRC65MFKWRH
  • hookWorkflow | wrun_01KP4K2F8W10VQXEHVRVJ5BYJ8
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KP4K2SEHZ56KZ708EP9NWEME
  • webhookWorkflow | wrun_01KP4K31V6J2PNFY1QYSVXSG32
  • sleepingWorkflow | wrun_01KP4K38EZCQJPQWSSQK4BG3TG
  • parallelSleepWorkflow | wrun_01KP4K3MCPF3PS4BYQZNYETM8X
  • nullByteWorkflow | wrun_01KP4K3QE4JVHD9378ZDCW35ES
  • workflowAndStepMetadataWorkflow | wrun_01KP4K3SAD945QQDCEW9S7ABG1
  • fetchWorkflow | wrun_01KP4K69XY4TRWVS5MPACQSRSY
  • promiseRaceStressTestWorkflow | wrun_01KP4K6CY671Y841JTBNAGHVR4
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KP4K9J5K4AWCGHACB3FJF7Z9
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KP4KA55G0BVHX9JYSSN2044A
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KP4KARD2N0F823BWB9PJXND8
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KP4KBA9BF114V9GBDEM4RCYB
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KP4KBHWKT2BCSYV9G3GA5ZT1
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KP4KBPN9G0BQZHDA3AMJEN6N
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KP4KBRFSBPQSBNDR8PH2H411
  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_01KP4KC16KWG2SQ3XZ6RF44ENE
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KP4KCEKVRKWJVQ8ABB34YA98
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KP4KCKAM44Q83S30NT4K2GTC
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KP4KCS90P0P8CDQ0PH97ADG1
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KP4KCZ31B90FQ1R3KA2AXFQ1
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KP4KD3TCNRBXYJQGYG0FJE5M
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KP4KD9YW2CTJ94SDXP61D3ZK
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KP4KDFSVHTDT9T5GBMVFNQSQ
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KP4KDSHAD80F29FX3G3E7XK0
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KP4KE1SP5R6BD3SX8STWNY56
  • cancelRun - cancelling a running workflow | wrun_01KP4KE7N9WN71Y2V7Q9V7B90H
  • cancelRun via CLI - cancelling a running workflow | wrun_01KP4KEGBT07H5PDWDRBYK9Z18
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KP4KEVHHD6NP6MS7MDWTQ4Q6
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KP4KFE6YM40VYZYVDSDCT288
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KP4KFS82DEKZE5K3CASRYR8B
  • importMetaUrlWorkflow - import.meta.url is available in step bundles | wrun_01KP4KFZ4SWKCTX7A5PHC502R0
  • metadataFromHelperWorkflow - getWorkflowMetadata/getStepMetadata work from module-level helper (#1577) | wrun_01KP4KG10MCEDNHBPKJ2Y2E2GQ
  • resilient start: addTenWorkflow completes when run_created returns 500 | wrun_01KP4KG2T5KQ3RZE9EZPTH0B3A
  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KP4KG5TWENZZYDZJBA3EZWH3

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 83 0 7
✅ example 83 0 7
✅ express 83 0 7
✅ fastify 83 0 7
✅ hono 83 0 7
✅ nextjs-turbopack 88 0 2
✅ nextjs-webpack 88 0 2
✅ nitro 83 0 7
✅ nuxt 83 0 7
✅ sveltekit 83 0 7
✅ vite 83 0 7
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 76 0 14
✅ express-stable 76 0 14
✅ fastify-stable 76 0 14
✅ hono-stable 76 0 14
✅ nextjs-turbopack-canary 63 0 27
✅ nextjs-turbopack-stable 82 0 8
✅ nextjs-webpack-canary 63 0 27
✅ nextjs-webpack-stable 82 0 8
✅ nitro-stable 76 0 14
✅ nuxt-stable 76 0 14
✅ sveltekit-stable 76 0 14
✅ vite-stable 76 0 14
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 76 0 14
✅ express-stable 76 0 14
✅ fastify-stable 76 0 14
✅ hono-stable 76 0 14
✅ nextjs-turbopack-canary 63 0 27
✅ nextjs-turbopack-stable 82 0 8
✅ nextjs-webpack-canary 63 0 27
✅ nextjs-webpack-stable 82 0 8
✅ nitro-stable 76 0 14
✅ nuxt-stable 76 0 14
✅ sveltekit-stable 76 0 14
✅ vite-stable 76 0 14
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 76 0 14
✅ express-stable 76 0 14
✅ fastify-stable 76 0 14
✅ hono-stable 76 0 14
✅ nextjs-turbopack-canary 63 0 27
✅ nextjs-turbopack-stable 82 0 8
✅ nextjs-webpack-canary 63 0 27
✅ nextjs-webpack-stable 82 0 8
✅ nitro-stable 76 0 14
✅ nuxt-stable 76 0 14
✅ sveltekit-stable 76 0 14
✅ vite-stable 76 0 14
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 82 0 8
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 6 0 0
❌ mongodb 56 7 8
✅ redis-dev 6 0 0
❌ redis 56 7 8
✅ turso-dev 6 0 0
❌ turso 3 60 8
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 76 0 14
✅ e2e-local-postgres-nest-stable 76 0 14
✅ e2e-local-prod-nest-stable 76 0 14

📋 View full workflow run

Comment thread packages/world-vercel/src/queue.ts Outdated
…es unhandled to the caller, crashing the workflow instead of being silently handled.

This commit fixes the issue reported at packages/world-vercel/src/queue.ts:260

**Bug Explanation:**

In `packages/world-vercel/src/queue.ts`, the `queue` function sends messages via CBOR transport and falls back to JSON transport on failure. The error handling logic catches `DuplicateMessageError` from the primary CBOR `send()` call, but the JSON fallback `send(jsonTransport)` on line 263 is not wrapped in any error handling.

The scenario where this manifests:
1. `send(cborTransport)` is called with an `idempotencyKey` in `sendOpts`
2. The server successfully receives and processes the message
3. The CBOR response parsing fails (e.g., network issue, malformed response), throwing a non-`DuplicateMessageError`
4. The catch block falls through the `DuplicateMessageError` check and enters the `if (useCbor)` fallback
5. `send(jsonTransport)` is called with the same `sendOpts` including the same `idempotencyKey`
6. The server already has this message, so it throws `DuplicateMessageError`
7. This error propagates completely unhandled to the caller, crashing the workflow

This is a regression from the design intent. The code explicitly handles `DuplicateMessageError` for the primary path with a comment saying "Silently handle idempotency key conflicts - the message was already queued. This matches the behavior of world-local and world-postgres." The JSON fallback should have the same handling.

**Fix Explanation:**

Wrapped the `send(jsonTransport)` fallback call in its own try-catch block that handles `DuplicateMessageError` identically to the primary path — returning a placeholder `messageId` with the `msg_duplicate_` prefix. Any other errors from the fallback are re-thrown as before. This ensures consistent behavior regardless of which transport path encounters a duplicate message.

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <[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

The refactor to extract the send() helper is clean, and the secondary DuplicateMessageError handler in the fallback path is a correct fix for the bug described in commit 2. The title change ("Always try JSON queue transport as a fallback") is where I have concerns.

Concern: silent duplicate enqueue when idempotencyKey is absent

Most world.queue() callers in the runtime don't pass an idempotencyKey. A quick search shows only suspension-handler.ts:275 sets one (for step dispatch). All other sites — workflow re-enqueue from step-handler, start, runs, resume-hook — don't.

For those callers, the fallback path is unsafe when the CBOR send partially succeeded:

  1. send(cborTransport) → server receives message, ingests it, begins returning response
  2. Client fails to parse response (network blip, malformed response, etc.)
  3. CBOR send throws (not DuplicateMessageError)
  4. send(jsonTransport) is called with the same sendOpts, but idempotencyKey is undefined
  5. Server has no way to detect this is a duplicate — it enqueues a second copy

The resulting duplicate workflow re-enqueue would be deduped by the workflow's own event log (concurrent handlers racing on run_started return EntityConflictError), so the impact is just extra queue noise — but it's still strictly worse behavior than before this PR for those callers.

Suggestions

Pick one:

  1. Only fall back when idempotencyKey is set (safest):

    if (useCbor && sendOpts.idempotencyKey) {
      try { return await send(jsonTransport); } catch ...
    }

    Non-idempotent callers continue to get the old behavior (throw). This is a strict improvement — you fix the bug without introducing the duplicate-send footgun.

  2. Auto-generate an idempotencyKey when one isn't provided, used only for this fallback path. Something like a random UUID reused across the CBOR→JSON retry. This preserves the fallback for all callers.

  3. Narrow the fallback to specific error types — only fall back on CBOR-specific errors (decode failures), not generic network errors. This way, "server received but client couldn't parse response" scenarios still throw as before, but actual CBOR incompatibility errors get the fallback. Hard to implement reliably without knowing what errors cbor-x can throw.

I'd go with option 1. The original bug from commit 2 (JSON fallback throwing unhandled DuplicateMessageError) is correctly fixed either way.

Minor nits

  • The changeset message ("Fall back to JSON queue transport if CBOR parsing fails") is misleading — the fallback happens on any non-DuplicateMessageError, not just CBOR parsing failures.
  • fallbackError.idempotencyKey ?? opts?.idempotencyKey ?? 'unknown' — the 'unknown' branch is reachable only when no idempotencyKey was set, which is exactly the concerning case above. A message with ID msg_duplicate_unknown is a code smell.

@TooTallNate
Copy link
Copy Markdown
Member

Follow-up after getting context on the intent: I understand this is meant to address the rolling-deploy scenario where a new sender (CBOR) enqueues a message that might be picked up by an old handler instance (pre-DualTransport, JSON-only).

If that's the goal, this diff doesn't actually achieve it. The send-side try/catch only observes errors from the client.send() call — but:

  • VQS doesn't validate or care about the payload's content-type; a CBOR send to VQS will succeed just like a JSON send
  • The old handler's JSON.parse() failure happens later, asynchronously, on a completely separate request to the handler endpoint
  • That failure surfaces as a handler-side error, never propagates back to the sender's queue() call

So send(cborTransport) will essentially never throw for this reason. The fallback only runs on genuine transport failures (network errors, cbor-x bugs, etc.) — which is a different failure mode than what the PR description implies.

If rolling-deploy compatibility is the actual concern, a better approach would be one of:

  1. Send JSON unconditionally until a deprecation window passes (you pay a small perf cost, but every handler version can read it)
  2. Use specVersion more conservatively — only pick CBOR when you know the target run's deployment has a specVersion >= SUPPORTS_CBOR, and right now start.ts:180 uses world.specVersion or SPEC_VERSION_SUPPORTS_EVENT_SOURCING, which may not reflect the actual deployment's capability
  3. Send both formats in the same request body (unnecessary complexity, probably not worth it)

The existing DuplicateMessageError bug fix from commit 2 is still correct and worth keeping regardless.

That said, if my understanding of the intent is off, let me know. Or if the real goal is just defensive belt-and-suspenders for CBOR encoding failures, the PR does that — in which case the title/changeset message should be reworded.

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

Labels

backport-stable Cherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants