Skip to content

Fail steps when step modules fail to load#2709

Open
karthikscale3 wants to merge 8 commits into
mainfrom
kk/lazy-step-loader-failures
Open

Fail steps when step modules fail to load#2709
karthikscale3 wants to merge 8 commits into
mainfrom
kk/lazy-step-loader-failures

Conversation

@karthikscale3

@karthikscale3 karthikscale3 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add lazy step function loaders to the core runtime and expose registerStepFunctionLoader from workflow/runtime.
  • Generate combined routes that register loaders for step IDs instead of eagerly importing the step registration bundle at route initialization.
  • Convert step module loader/import failures into durable step_failed events and requeue the workflow so user code can observe the failure.
  • Add workflow bundle coverage showing top-level bundle evaluation failures are captured as durable run_failed events.
  • Add isolated e2e coverage that builds generated combined routes and drives both failure modes through a local world queue handler.

Root Cause

The combined step/workflow route eagerly imported __step_registrations when the route module loaded. If a step module imported a native dependency that could not load in production, such as sharp missing libvips, the route threw a 500 before the Workflow runtime had claimed the step or written a terminal event. That left the run stuck instead of failing the step.

Workflow Bundle Case

The combined route embeds workflow code as VM input instead of host-importing workflow modules during route initialization. The analogous failure happens while evaluating the workflow bundle in the workflow VM. This PR adds regression coverage that a sharp-like top-level workflow bundle failure is caught by workflowEntrypoint and persisted as run_failed rather than escaping as a route 500.

Local Repro

Added a regression test that creates the old eager route shape and a step registration module throwing Could not load the "sharp" module using the linux-x64 runtime. The legacy route import rejects immediately, matching the reported 500 path. The fixed generated route imports successfully, captures a step loader, and only rejects when that loader is invoked so the runtime can persist step_failed.

E2E Coverage

packages/core/e2e/module-load-failures.test.ts builds temporary combined routes from source, imports the generated route module, registers it with a local-world direct queue handler, then starts real runs through start(). It asserts:

  • step registration module load failures produce step_started followed by step_failed, and the workflow can catch the resulting FatalError
  • workflow bundle evaluation failures produce run_failed with USER_ERROR

Validation

  • pnpm format -- packages/core/e2e/module-load-failures.test.ts packages/core/src/runtime.test.ts
  • pnpm lint -- packages/core/e2e/module-load-failures.test.ts
  • pnpm lint -- packages/core/src/runtime.test.ts exited 0 with existing warning-level findings in unrelated tests in that file
  • pnpm lint -- .changeset/lazy-step-loaders.md packages/workflow/src/runtime.ts packages/workflow/src/runtime.test.ts packages/builders/src/step-source-registration.test.ts
  • pnpm vitest run packages/core/e2e/module-load-failures.test.ts
  • pnpm vitest run packages/core/e2e/module-load-failures.test.ts packages/core/src/runtime.test.ts packages/workflow/src/runtime.test.ts packages/builders/src/step-source-registration.test.ts packages/core/src/private.test.ts packages/core/src/runtime/step-executor.test.ts packages/core/src/runtime/step-handler.test.ts
  • pnpm --filter @workflow/core typecheck currently fails in this checkout with TS2688: Cannot find type definition file for "*" from the shared packages/tsconfig/base.json
  • pnpm --filter @workflow/builders build fails with the same TS2688 issue

@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9ae084b

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/builders Patch
workflow 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

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

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

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

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1442 0 230 1672
✅ 💻 Local Development 1472 0 200 1672
✅ 📦 Local Production 1605 0 219 1824
✅ 🐘 Local Postgres 1593 0 231 1824
✅ 🪟 Windows 152 0 0 152
✅ 📋 Other 885 0 179 1064
Total 7149 0 1059 8208

Details by Category

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

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.042s (-3.9%) 1.006s (~) 0.964s 10 1.00x
💻 Local Express 0.046s (~) 1.006s (~) 0.959s 10 1.09x
💻 Local Next.js (Turbopack) 0.050s (-6.7% 🟢) 1.006s (~) 0.956s 10 1.18x
🐘 Postgres Nitro 0.067s (+5.7% 🔺) 1.012s (~) 0.946s 10 1.57x
🐘 Postgres Express 0.073s (~) 1.012s (~) 0.939s 10 1.72x
🐘 Postgres Next.js (Turbopack) 0.088s (-20.0% 🟢) 1.014s (-2.5%) 0.925s 10 2.08x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 0.200s (-45.2% 🟢) 1.608s (-12.2% 🟢) 1.408s 10 1.00x
▲ Vercel Express 0.219s (+8.2% 🔺) 1.889s (-4.4%) 1.671s 10 1.09x
▲ Vercel Next.js (Turbopack) 0.280s (-39.5% 🟢) 2.019s (-21.4% 🟢) 1.739s 10 1.40x

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

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.081s (~) 2.006s (~) 0.926s 10 1.00x
💻 Local Next.js (Turbopack) 1.081s (~) 2.006s (~) 0.925s 10 1.00x
💻 Local Express 1.085s (~) 2.006s (~) 0.921s 10 1.00x
🐘 Postgres Nitro 1.092s (~) 2.009s (~) 0.917s 10 1.01x
🐘 Postgres Express 1.095s (~) 2.008s (~) 0.914s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.131s (+2.6%) 2.059s (+2.4%) 0.929s 10 1.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.391s (+2.1%) 2.860s (+8.7% 🔺) 1.469s 10 1.00x
▲ Vercel Nitro 1.407s (+4.5%) 2.984s (+5.5% 🔺) 1.578s 10 1.01x
▲ Vercel Next.js (Turbopack) 2.272s (+1.2%) 3.946s (+3.4%) 1.674s 10 1.63x

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

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 10.402s (~) 11.015s (~) 0.612s 3 1.00x
💻 Local Nitro 10.430s (~) 11.022s (~) 0.592s 3 1.00x
💻 Local Express 10.458s (~) 11.022s (~) 0.564s 3 1.01x
🐘 Postgres Nitro 10.465s (~) 11.016s (~) 0.550s 3 1.01x
💻 Local Next.js (Turbopack) 10.467s (~) 11.022s (~) 0.555s 3 1.01x
🐘 Postgres Express 10.527s (~) 11.015s (~) 0.489s 3 1.01x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 11.686s (-1.8%) 12.956s (-5.2% 🟢) 1.270s 3 1.00x
▲ Vercel Express 11.757s (-2.5%) 13.792s (+1.2%) 2.035s 3 1.01x
▲ Vercel Next.js (Turbopack) 12.615s (~) 14.508s (-1.5%) 1.893s 3 1.08x

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

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 13.595s (~) 14.020s (~) 0.425s 5 1.00x
💻 Local Express 13.633s (-0.7%) 14.026s (~) 0.393s 5 1.00x
🐘 Postgres Nitro 13.639s (~) 14.017s (~) 0.379s 5 1.00x
💻 Local Nitro 13.694s (+1.0%) 14.027s (~) 0.333s 5 1.01x
💻 Local Next.js (Turbopack) 13.758s (+0.7%) 14.027s (~) 0.269s 5 1.01x
🐘 Postgres Next.js (Turbopack) 13.762s (+0.8%) 14.021s (~) 0.258s 5 1.01x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 16.806s (~) 18.837s (+2.2%) 2.031s 4 1.00x
▲ Vercel Express 17.596s (+7.1% 🔺) 19.338s (+3.3%) 1.742s 4 1.05x
▲ Vercel Next.js (Turbopack) 18.319s (+6.5% 🔺) 19.674s (+1.9%) 1.355s 4 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.070s (~) 13.025s (~) 0.955s 7 1.00x
🐘 Postgres Nitro 12.083s (-0.9%) 13.021s (~) 0.937s 7 1.00x
💻 Local Express 12.172s (~) 13.026s (~) 0.854s 7 1.01x
💻 Local Next.js (Turbopack) 12.221s (~) 13.027s (~) 0.806s 7 1.01x
🐘 Postgres Next.js (Turbopack) 12.289s (-1.2%) 12.661s (-2.0%) 0.372s 8 1.02x
🐘 Postgres Express 12.394s (-0.7%) 13.022s (~) 0.627s 7 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 20.428s (+17.7% 🔺) 22.186s (+17.0% 🔺) 1.758s 5 1.00x
▲ Vercel Express 20.677s (+17.3% 🔺) 22.340s (+15.6% 🔺) 1.663s 5 1.01x
▲ Vercel Next.js (Turbopack) 22.842s (+15.0% 🔺) 24.387s (+11.5% 🔺) 1.544s 4 1.12x

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

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.182s (-1.7%) 2.007s (~) 0.825s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.188s (-6.7% 🟢) 2.009s (-1.5%) 0.822s 15 1.00x
🐘 Postgres Express 1.208s (+2.1%) 2.008s (~) 0.800s 15 1.02x
💻 Local Nitro 1.370s (-2.2%) 2.007s (~) 0.637s 15 1.16x
💻 Local Express 1.438s (+3.3%) 2.007s (~) 0.569s 15 1.22x
💻 Local Next.js (Turbopack) 1.466s (+3.0%) 2.073s (+3.3%) 0.607s 15 1.24x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.107s (-3.7%) 3.571s (-6.0% 🟢) 1.463s 9 1.00x
▲ Vercel Express 2.182s (+10.6% 🔺) 3.389s (-9.2% 🟢) 1.207s 9 1.04x
▲ Vercel Next.js (Turbopack) 3.120s (-5.9% 🟢) 4.438s (-13.4% 🟢) 1.317s 7 1.48x

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

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.320s (~) 2.592s (-8.3% 🟢) 1.272s 12 1.00x
🐘 Postgres Express 1.326s (-2.1%) 2.737s (+18.2% 🔺) 1.411s 11 1.00x
🐘 Postgres Next.js (Turbopack) 1.345s (+1.0%) 2.735s (+10.3% 🔺) 1.390s 11 1.02x
💻 Local Next.js (Turbopack) 2.348s (-14.3% 🟢) 3.008s (-3.2%) 0.661s 10 1.78x
💻 Local Express 2.420s (-6.3% 🟢) 3.009s (~) 0.589s 10 1.83x
💻 Local Nitro 2.545s (+1.0%) 3.110s (~) 0.565s 10 1.93x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.706s (+13.2% 🔺) 4.307s (+12.5% 🔺) 1.601s 8 1.00x
▲ Vercel Express 2.745s (+3.6%) 4.236s (+5.3% 🔺) 1.492s 8 1.01x
▲ Vercel Next.js (Turbopack) 3.884s (+7.1% 🔺) 5.540s (-4.1%) 1.656s 6 1.44x

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

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.582s (~) 4.010s (~) 2.428s 8 1.00x
🐘 Postgres Express 1.608s (+1.2%) 4.439s (+10.6% 🔺) 2.831s 7 1.02x
🐘 Postgres Next.js (Turbopack) 2.390s (+31.3% 🔺) 4.735s (-3.2%) 2.345s 7 1.51x
💻 Local Nitro 4.217s (-31.4% 🟢) 4.870s (-28.6% 🟢) 0.653s 7 2.67x
💻 Local Express 4.372s (-34.4% 🟢) 5.014s (-32.4% 🟢) 0.642s 6 2.76x
💻 Local Next.js (Turbopack) 4.713s (-39.9% 🟢) 5.517s (-31.2% 🟢) 0.804s 6 2.98x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.478s (+31.5% 🔺) 5.080s (+20.1% 🔺) 1.601s 6 1.00x
▲ Vercel Express 3.880s (+44.7% 🔺) 5.531s (+27.6% 🔺) 1.651s 6 1.12x
▲ Vercel Next.js (Turbopack) 4.794s (+15.5% 🔺) 6.519s (+6.7% 🔺) 1.725s 5 1.38x

🔍 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 🥇 Nitro 1.173s (~) 2.007s (~) 0.833s 15 1.00x
🐘 Postgres Express 1.273s (+7.9% 🔺) 2.075s (+3.4%) 0.802s 15 1.08x
🐘 Postgres Next.js (Turbopack) 1.308s (+10.6% 🔺) 2.017s (~) 0.709s 15 1.11x
💻 Local Nitro 1.411s (+2.9%) 2.007s (~) 0.595s 15 1.20x
💻 Local Next.js (Turbopack) 1.421s (-0.6%) 2.006s (~) 0.585s 15 1.21x
💻 Local Express 1.441s (~) 2.007s (~) 0.566s 15 1.23x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.268s (+19.8% 🔺) 3.497s (-5.8% 🟢) 1.229s 9 1.00x
▲ Vercel Nitro 2.418s (+26.1% 🔺) 3.925s (+11.1% 🔺) 1.508s 8 1.07x
▲ Vercel Next.js (Turbopack) 3.213s (+4.4%) 4.542s (-8.0% 🟢) 1.330s 7 1.42x

🔍 Observability: Express | 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.325s (+0.9%) 2.470s (-4.7%) 1.145s 13 1.00x
🐘 Postgres Express 1.358s (+3.9%) 2.315s (-6.3% 🟢) 0.957s 13 1.03x
🐘 Postgres Next.js (Turbopack) 1.407s (~) 2.318s (-8.5% 🟢) 0.911s 13 1.06x
💻 Local Next.js (Turbopack) 2.515s (-5.8% 🟢) 3.008s (-6.3% 🟢) 0.493s 10 1.90x
💻 Local Express 2.537s (-3.8%) 3.009s (~) 0.472s 10 1.91x
💻 Local Nitro 2.643s (-3.5%) 3.109s (-7.0% 🟢) 0.465s 10 1.99x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.626s (+13.3% 🔺) 4.502s (+19.2% 🔺) 1.876s 7 1.00x
▲ Vercel Express 2.685s (+7.1% 🔺) 4.016s (+1.6%) 1.331s 8 1.02x
▲ Vercel Next.js (Turbopack) 3.766s (+10.5% 🔺) 5.359s (+1.7%) 1.594s 6 1.43x

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

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.627s (-1.1%) 3.885s (-9.6% 🟢) 2.258s 8 1.00x
🐘 Postgres Express 1.673s (+5.0% 🔺) 4.298s (+3.9%) 2.625s 7 1.03x
🐘 Postgres Next.js (Turbopack) 2.333s (-10.5% 🟢) 5.011s (-4.2%) 2.678s 6 1.43x
💻 Local Next.js (Turbopack) 3.762s (-40.9% 🟢) 5.517s (-23.6% 🟢) 1.755s 6 2.31x
💻 Local Nitro 3.817s (-40.7% 🟢) 5.517s (-25.6% 🟢) 1.700s 6 2.35x
💻 Local Express 4.324s (-31.7% 🟢) 5.851s (-14.2% 🟢) 1.527s 6 2.66x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.260s (+14.9% 🔺) 5.103s (+16.4% 🔺) 1.843s 6 1.00x
▲ Vercel Express 3.275s (+15.8% 🔺) 5.189s (+13.9% 🔺) 1.914s 6 1.00x
▲ Vercel Next.js (Turbopack) 4.571s (+4.4%) 6.468s (+4.8%) 1.897s 5 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.551s (-2.2%) 1.007s (-1.6%) 0.455s 60 1.00x
🐘 Postgres Express 0.568s (+2.8%) 1.023s (+1.7%) 0.455s 59 1.03x
💻 Local Nitro 0.603s (~) 1.022s (-1.7%) 0.419s 59 1.09x
💻 Local Express 0.624s (+12.9% 🔺) 1.022s (+1.7%) 0.399s 59 1.13x
💻 Local Next.js (Turbopack) 0.666s (+0.8%) 1.040s (-1.7%) 0.373s 58 1.21x
🐘 Postgres Next.js (Turbopack) 0.679s (+11.9% 🔺) 1.075s (+2.2%) 0.396s 56 1.23x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.871s (+6.7% 🔺) 4.535s (+5.7% 🔺) 1.663s 14 1.00x
▲ Vercel Nitro 3.254s (+18.3% 🔺) 4.982s (+11.8% 🔺) 1.727s 13 1.13x
▲ Vercel Next.js (Turbopack) 4.227s (+17.1% 🔺) 5.702s (+6.0% 🔺) 1.475s 11 1.47x

🔍 Observability: Express | 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 🥇 Nitro 1.258s (-4.2%) 2.007s (-2.3%) 0.749s 45 1.00x
🐘 Postgres Express 1.335s (-1.2%) 2.030s (~) 0.695s 45 1.06x
🐘 Postgres Next.js (Turbopack) 1.472s (-1.1%) 2.118s (-0.9%) 0.645s 43 1.17x
💻 Local Express 1.481s (+5.7% 🔺) 2.006s (~) 0.525s 45 1.18x
💻 Local Nitro 1.486s (+3.9%) 2.006s (~) 0.520s 45 1.18x
💻 Local Next.js (Turbopack) 1.573s (+5.7% 🔺) 2.006s (~) 0.433s 45 1.25x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 7.312s (+18.7% 🔺) 8.872s (+13.0% 🔺) 1.560s 11 1.00x
▲ Vercel Nitro 7.828s (+27.6% 🔺) 9.435s (+24.5% 🔺) 1.607s 10 1.07x
▲ Vercel Next.js (Turbopack) 9.694s (+22.2% 🔺) 11.179s (+14.2% 🔺) 1.485s 9 1.33x

🔍 Observability: Express | 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 🥇 Nitro 2.491s (-4.3%) 3.033s (~) 0.542s 40 1.00x
🐘 Postgres Express 2.616s (-0.6%) 3.111s (+0.8%) 0.495s 39 1.05x
💻 Local Express 3.210s (+7.5% 🔺) 4.009s (+15.7% 🔺) 0.799s 30 1.29x
💻 Local Nitro 3.225s (+1.5%) 4.010s (+1.6%) 0.784s 30 1.29x
🐘 Postgres Next.js (Turbopack) 3.359s (+14.0% 🔺) 3.824s (+10.8% 🔺) 0.465s 32 1.35x
💻 Local Next.js (Turbopack) 3.378s (+5.5% 🔺) 4.009s (~) 0.632s 30 1.36x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 14.393s (+23.0% 🔺) 16.231s (+21.2% 🔺) 1.837s 8 1.00x
▲ Vercel Nitro 14.924s (+25.9% 🔺) 16.987s (+26.7% 🔺) 2.063s 8 1.04x
▲ Vercel Next.js (Turbopack) 20.383s (+20.9% 🔺) 22.323s (+19.3% 🔺) 1.940s 6 1.42x

🔍 Observability: Express | 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.204s (-4.9%) 1.006s (~) 0.802s 60 1.00x
🐘 Postgres Next.js (Turbopack) 0.204s (-11.7% 🟢) 1.014s (~) 0.809s 60 1.00x
🐘 Postgres Express 0.206s (-7.0% 🟢) 1.006s (~) 0.799s 60 1.01x
💻 Local Express 0.523s (+16.2% 🔺) 1.005s (~) 0.482s 60 2.56x
💻 Local Nitro 0.525s (+22.2% 🔺) 1.005s (~) 0.480s 60 2.57x
💻 Local Next.js (Turbopack) 0.628s (~) 1.005s (~) 0.377s 60 3.08x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.309s (+27.7% 🔺) 2.823s (+15.8% 🔺) 1.514s 22 1.00x
▲ Vercel Express 1.315s (+41.1% 🔺) 2.628s (+6.1% 🔺) 1.314s 23 1.00x
▲ Vercel Next.js (Turbopack) 2.661s (+34.2% 🔺) 4.422s (+15.0% 🔺) 1.761s 14 2.03x

🔍 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.326s (-3.1%) 1.006s (~) 0.680s 90 1.00x
🐘 Postgres Express 0.329s (-1.4%) 1.006s (~) 0.677s 90 1.01x
🐘 Postgres Next.js (Turbopack) 0.387s (+21.6% 🔺) 1.038s (+1.7%) 0.651s 87 1.19x
💻 Local Express 2.415s (+11.1% 🔺) 3.010s (+8.8% 🔺) 0.595s 30 7.41x
💻 Local Nitro 2.540s (+32.0% 🔺) 3.009s (+19.9% 🔺) 0.469s 30 7.80x
💻 Local Next.js (Turbopack) 2.659s (-9.1% 🟢) 3.008s (-11.0% 🟢) 0.349s 30 8.16x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.351s (+45.3% 🔺) 4.117s (+37.0% 🔺) 1.766s 23 1.00x
▲ Vercel Express 2.489s (+74.9% 🔺) 4.290s (+42.8% 🔺) 1.801s 21 1.06x
▲ Vercel Next.js (Turbopack) 3.991s (+60.1% 🔺) 5.984s (+45.7% 🔺) 1.993s 16 1.70x

🔍 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 🥇 Nitro 0.526s (-4.3%) 1.050s (-7.0% 🟢) 0.524s 115 1.00x
🐘 Postgres Express 0.537s (-1.1%) 1.128s (+0.9%) 0.591s 107 1.02x
🐘 Postgres Next.js (Turbopack) 0.732s (+46.3% 🔺) 2.198s (-12.6% 🟢) 1.466s 55 1.39x
💻 Local Express 5.258s (-43.1% 🟢) 8.226s (-21.2% 🟢) 2.967s 15 10.00x
💻 Local Nitro 5.620s (-41.7% 🟢) 8.824s (-18.1% 🟢) 3.204s 15 10.69x
💻 Local Next.js (Turbopack) 6.196s (-42.6% 🟢) 9.172s (-22.0% 🟢) 2.975s 14 11.79x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.979s (+67.6% 🔺) 4.650s (+27.1% 🔺) 1.671s 26 1.00x
▲ Vercel Nitro 3.160s (+88.4% 🔺) 4.907s (+42.2% 🔺) 1.747s 25 1.06x
▲ Vercel Next.js (Turbopack) 4.674s (+15.1% 🔺) 6.565s (+11.9% 🔺) 1.890s 19 1.57x

🔍 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) 1.138s (~) 1.968s (~) 0.010s (-17.5% 🟢) 2.018s (~) 0.880s 10 1.00x
💻 Local Nitro 1.151s (-0.5%) 2.004s (~) 0.010s (-7.5% 🟢) 2.017s (~) 0.867s 10 1.01x
💻 Local Express 1.155s (+1.3%) 2.004s (~) 0.013s (+24.5% 🔺) 2.020s (~) 0.865s 10 1.02x
🐘 Postgres Nitro 1.163s (-2.0%) 1.994s (~) 0.001s (+16.7% 🔺) 2.011s (~) 0.847s 10 1.02x
🐘 Postgres Express 1.192s (+3.4%) 1.994s (~) 0.001s (-8.3% 🟢) 2.011s (~) 0.819s 10 1.05x
🐘 Postgres Next.js (Turbopack) 1.233s (+0.9%) 1.998s (~) 0.001s (-11.1% 🟢) 2.012s (~) 0.780s 10 1.08x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.125s (+11.2% 🔺) 3.393s (+9.0% 🔺) 2.610s (+59.4% 🔺) 6.494s (+25.7% 🔺) 4.369s 10 1.00x
▲ Vercel Express 2.177s (+6.8% 🔺) 3.313s (-1.2%) 2.153s (+55.4% 🔺) 5.933s (+13.3% 🔺) 3.756s 10 1.02x
▲ Vercel Next.js (Turbopack) 4.169s (+29.9% 🔺) 4.445s (+11.2% 🔺) 1.537s (+86.1% 🔺) 7.651s (+25.2% 🔺) 3.483s 10 1.96x

🔍 Observability: Nitro | Express | 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 🥇 Nitro 1.547s (-1.7%) 2.003s (~) 0.005s (-8.0% 🟢) 2.026s (~) 0.479s 30 1.00x
💻 Local Nitro 1.553s (~) 2.010s (~) 0.012s (-6.3% 🟢) 2.025s (~) 0.472s 30 1.00x
💻 Local Express 1.575s (+2.3%) 2.007s (~) 0.012s (-10.9% 🟢) 2.024s (~) 0.449s 30 1.02x
🐘 Postgres Express 1.577s (-0.6%) 2.005s (-1.5%) 0.005s (-0.7%) 2.027s (-1.6%) 0.450s 30 1.02x
💻 Local Next.js (Turbopack) 1.595s (+1.0%) 1.972s (~) 0.013s (+12.1% 🔺) 2.027s (~) 0.432s 30 1.03x
🐘 Postgres Next.js (Turbopack) 1.816s (-3.9%) 2.267s (~) 0.004s (-46.2% 🟢) 2.288s (-0.6%) 0.473s 27 1.17x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 6.256s (+10.4% 🔺) 7.607s (+9.2% 🔺) 0.292s (+2.9%) 8.434s (+9.9% 🔺) 2.178s 8 1.00x
▲ Vercel Express 6.627s (+17.5% 🔺) 7.832s (+9.3% 🔺) 0.301s (+47.4% 🔺) 8.692s (+9.7% 🔺) 2.065s 7 1.06x
▲ Vercel Next.js (Turbopack) 10.705s (+13.6% 🔺) 11.223s (+4.4%) 0.412s (+60.7% 🔺) 12.938s (+9.4% 🔺) 2.233s 5 1.71x

🔍 Observability: Nitro | Express | 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.758s (-1.6%) 1.024s (-7.3% 🟢) 0.000s (-62.8% 🟢) 1.048s (-6.3% 🟢) 0.290s 58 1.00x
🐘 Postgres Express 0.807s (+3.7%) 1.101s (+3.9%) 0.000s (+Infinity% 🔺) 1.119s (+3.7%) 0.311s 54 1.06x
🐘 Postgres Next.js (Turbopack) 1.177s (-14.5% 🟢) 1.621s (-17.4% 🟢) 0.000s (NaN%) 1.646s (-17.8% 🟢) 0.469s 37 1.55x
💻 Local Next.js (Turbopack) 1.285s (-7.0% 🟢) 1.980s (~) 0.000s (~) 2.017s (~) 0.732s 30 1.69x
💻 Local Express 1.288s (+1.1%) 1.863s (-7.5% 🟢) 0.000s (-47.0% 🟢) 1.866s (-7.4% 🟢) 0.578s 33 1.70x
💻 Local Nitro 1.312s (+1.1%) 1.919s (-4.7%) 0.000s (+33.9% 🔺) 1.922s (-4.7%) 0.610s 32 1.73x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.221s (-98.3% 🟢) 4.538s (-97.6% 🟢) 0.000s (+Infinity% 🔺) 5.016s (-97.3% 🟢) 1.795s 12 1.00x
▲ Vercel Express 3.425s (+19.0% 🔺) 4.437s (+2.4%) 0.000s (+Infinity% 🔺) 4.947s (+2.6%) 1.522s 13 1.06x
▲ Vercel Next.js (Turbopack) 5.344s (+26.1% 🔺) 6.240s (+14.9% 🔺) 0.000s (+Infinity% 🔺) 7.025s (+13.3% 🔺) 1.681s 9 1.66x

🔍 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 🥇 Nitro 1.810s (-4.3%) 2.393s (-1.7%) 0.000s (~) 2.409s (-1.7%) 0.599s 25 1.00x
🐘 Postgres Express 1.968s (+13.8% 🔺) 2.533s (+10.7% 🔺) 0.000s (-100.0% 🟢) 2.567s (+11.3% 🔺) 0.599s 24 1.09x
🐘 Postgres Next.js (Turbopack) 2.538s (-10.2% 🟢) 3.101s (-7.7% 🟢) 0.000s (NaN%) 3.146s (-7.3% 🟢) 0.607s 20 1.40x
💻 Local Express 3.341s (-1.6%) 4.026s (~) 0.001s (+30.0% 🔺) 4.030s (~) 0.688s 15 1.85x
💻 Local Nitro 3.416s (-7.3% 🟢) 4.027s (-6.7% 🟢) 0.001s (+24.4% 🔺) 4.031s (-6.7% 🟢) 0.616s 15 1.89x
💻 Local Next.js (Turbopack) 3.755s (+7.7% 🔺) 4.197s (+5.2% 🔺) 0.000s (-80.0% 🟢) 4.234s (+5.0%) 0.479s 15 2.07x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.059s (+16.3% 🔺) 6.273s (+6.5% 🔺) 0.000s (+Infinity% 🔺) 6.781s (+5.0% 🔺) 1.723s 9 1.00x
▲ Vercel Express 5.078s (+17.6% 🔺) 6.253s (+10.1% 🔺) 0.000s (+Infinity% 🔺) 6.718s (+9.3% 🔺) 1.639s 9 1.00x
▲ Vercel Next.js (Turbopack) 8.213s (+18.4% 🔺) 8.918s (+12.5% 🔺) 0.012s (+Infinity% 🔺) 10.294s (+16.4% 🔺) 2.082s 6 1.62x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 9/21
🐘 Postgres Nitro 19/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 14/21
Nitro 🐘 Postgres 16/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@karthikscale3 karthikscale3 changed the title [codex] Fail steps when step modules fail to load Fail steps when step modules fail to load Jun 30, 2026

@vercel vercel Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Additional Suggestion:

workflow/runtime does not re-export registerStepFunctionLoader, so generated workflow route bundles fail with "No matching export ... for import registerStepFunctionLoader".

Fix on Vercel

@VaguelySerious VaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: no blocking issues

inputFiles,
stepsOutfile,
flowOutfile,
bundleFinalOutput: false,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

The production build path (bundleFinalOutput: true) is never exercised. Both real builders (standalone.ts and vercel-build-output-api.ts) bundle the generated route through esbuild with bundle: true, but every test here and in step-source-registration.test.ts uses bundleFinalOutput: false (raw Node ESM import).

The entire fix depends on esbuild preserving lazy evaluation of the inlined dynamic import('./__step_registrations.js') — if that import were ever evaluated eagerly (e.g. splitting gets enabled, or the bundler changes), the top-level throw would resurface at route-module init and reintroduce the 500, and no test in this PR would catch it.

I reproduced the esbuild path with the exact build settings used here and confirmed it works today: importing the bundled route does not throw, and the failure is correctly deferred to the loader call. But since that lazy-eval guarantee is the crux of the fix, consider adding a case that drives a bundleFinalOutput: true build and asserts the route imports cleanly while the step still fails at execution.

const normalizedError = await normalizeUnknownError(err);
const normalizedStack = normalizedError.stack || getErrorStack(err) || '';
const wrappedError = new FatalError(
`Failed to load step "${stepName}": ${normalizedError.message}`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

Wrapping the load error in FatalError makes step-module load failures permanently non-retryable: this step_failed is written before stepFn.maxRetries is ever read, so it bypasses the step's normal retry path entirely.

For a deterministic failure (missing native dep like sharp/libvips) that's exactly right. But a transient import failure (isolate OOM, transient FS error) that the previous route-500 → queue-redelivery path might have recovered from on a fresh isolate is now terminal for the step. Worth confirming this fatal-always semantics is intended rather than a side effect of reusing FatalError — if transient recovery matters, a retryable error here would let the queue redeliver.

): Promise<StepFunction | undefined> {
const loader = getStepIdMatch(registeredStepLoaders, stepId);
if (loader) {
await loader();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

loadStepFunction invokes the loader on every step execution with no already-registered short-circuit (deliberate per the "Refresh lazy step loaders before lookup" commit). With the generated () => import(...) loader this is a cached, resolved-promise await, so it's negligible in practice — just noting it adds a microtask to the hot step path where getStepFunction used to be synchronous.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants