Skip to content

[core] Exclude inline step execution from replay timeout#2013

Open
TooTallNate wants to merge 5 commits into
mainfrom
fix/replay-timeout-step-bodies
Open

[core] Exclude inline step execution from replay timeout#2013
TooTallNate wants to merge 5 commits into
mainfrom
fix/replay-timeout-step-bodies

Conversation

@TooTallNate
Copy link
Copy Markdown
Member

Summary

Fixes #2009.

The v5 combined workflow+step handler (workflowEntrypoint) was wrapping inline step bodies in the same setTimeout(..., REPLAY_TIMEOUT_MS) (240s) guard that, in v4, only bounded the dedicated workflows function's fast deterministic replay. The steps function previously had its own maxDuration: 'max' ceiling (800s on Pro Fluid) and was not subject to the replay timeout.

Net effect on v5: any workflow with a single step exceeding 240s hard-fails after 4 attempts with:

FatalError: Workflow replay exceeded maximum duration (240s) after 4 attempts

…even though the step could legitimately run for the full function maxDuration. This is a regression in apparent behavior from v4.

Changes

  • packages/core/src/runtime.ts — replace the setTimeout guard with a per-invocation budget that only accumulates non-step time. Two small closures (pauseReplayBudget / resumeReplayBudget) bracket every await executeStep(...) call so step duration is excluded. The loop checks isReplayBudgetExhausted() at iteration boundaries; if exhausted, handleReplayBudgetExhausted() runs the same retry-then-fail flow as before (preserving [core] Extend flow route duration to "max" and fail runs where replay takes too long #1567's defense for pathological pure replays).
  • packages/core/src/runtime/constants.ts — keep REPLAY_TIMEOUT_MS = 240_000 as the default. Add getReplayTimeoutMs() which reads WORKFLOW_REPLAY_TIMEOUT_MS (clamped to [30s, 780s]). 780s leaves ≥20s of headroom under Pro Fluid's 800s function ceiling so the handler can still write run_failed before SIGTERM.
  • packages/core/src/describe-error.ts — tighten REPLAY_TIMEOUT_HINT to reflect new semantics ("between step boundaries", "step bodies excluded") and mention the env var override.
  • Testspackages/core/src/runtime/constants.test.ts covers env var parsing, clamping, and fallback behavior. Existing describe-error.test.ts assertions updated for the new hint wording.

Interaction with existing limits

NO_INLINE_REPLAY_AFTER_MS (120s default, WORKFLOW_V2_TIMEOUT_MS env override) is unchanged and continues to re-queue long invocations so they don't exhaust the function's actual maxDuration. For an 8-minute step: the step runs inline to completion (now possible because the replay budget is paused), then the next loop iteration immediately re-queues since 8min > 120s elapsed. Same correctness as today, just no longer false-failing.

Backport

This bug is v5-only — in v4 step bodies ran in the separate steps function which had no replay-timeout wrapper. No backport to stable needed.

Test plan

  • pnpm test passes (956 tests, including 12 new tests in constants.test.ts).
  • pnpm typecheck passes.
  • Local repro (8-minute await new Promise(resolve => setTimeout(resolve, 8 * 60 * 1000)) inside a "use step" function) — to be validated by reviewer against a deployed Pro Fluid project.

The v5 combined workflow+step handler wraps inline step bodies in the
same setTimeout(..., REPLAY_TIMEOUT_MS) guard that previously only
bounded the v4 'workflows' function's fast deterministic replay. As a
result, any workflow with a single step exceeding 240s hard-fails with
FatalError: Workflow replay exceeded maximum duration (240s) after 4
attempts — even though the step could legitimately run for the full
function maxDuration (up to 800s on Pro Fluid).

Replace the setTimeout guard with a per-invocation budget that only
accumulates non-step time. pauseReplayBudget() / resumeReplayBudget()
bracket each executeStep() call, and the loop checks the budget at
iteration boundaries. The retry-then-fail semantics from #1567 are
preserved verbatim for the pure-replay case.

Also adds a WORKFLOW_REPLAY_TIMEOUT_MS env var override (clamped to
30s..780s) so operators can adjust the bare-replay ceiling without
patching @workflow/core.

Fixes #2009.
Copilot AI review requested due to automatic review settings May 18, 2026 21:35
@TooTallNate TooTallNate requested a review from a team as a code owner May 18, 2026 21:35
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 18, 2026

🦋 Changeset detected

Latest commit: ae34ff9

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

This PR includes changesets to release 20 packages
Name Type
@workflow/core Patch
@workflow/world Patch
@workflow/world-vercel Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 1196 4 219 1419
✅ 💻 Local Development 1587 0 219 1806
✅ 📦 Local Production 1587 0 219 1806
✅ 🐘 Local Postgres 1587 0 219 1806
✅ 🪟 Windows 129 0 0 129
✅ 📋 Other 727 0 176 903
Total 6813 4 1052 7869

❌ Failed Tests

▲ Vercel Production (4 failed)

fastify (1 failed):

  • AbortController abortVoidSleepTimeoutWorkflow: documented void sleep().then(abort) pattern works

nextjs-webpack (1 failed):

  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KS2MNZQ1Z80RGZ64C40VGH3H | 🔍 observability

nitro (1 failed):

  • AbortController abortAnyInStepWorkflow: AbortSignal.any inside a step composes deserialized signals

sveltekit (1 failed):

  • AbortController abortAnyInStepWorkflow: AbortSignal.any inside a step composes deserialized signals

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 103 0 26
✅ example 103 0 26
✅ express 103 0 26
❌ fastify 102 1 26
✅ hono 103 0 26
✅ nextjs-turbopack 127 0 2
❌ nextjs-webpack 126 1 2
❌ nitro 102 1 26
✅ nuxt 103 0 26
❌ sveltekit 121 1 7
✅ vite 103 0 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 104 0 25
✅ express-stable 104 0 25
✅ fastify-stable 104 0 25
✅ hono-stable 104 0 25
✅ nextjs-turbopack-canary 110 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 129 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 129 0 0
✅ nextjs-webpack-canary 110 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 129 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 129 0 0
✅ nitro-stable 104 0 25
✅ nuxt-stable 104 0 25
✅ sveltekit-stable 123 0 6
✅ vite-stable 104 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 104 0 25
✅ express-stable 104 0 25
✅ fastify-stable 104 0 25
✅ hono-stable 104 0 25
✅ nextjs-turbopack-canary 110 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 129 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 129 0 0
✅ nextjs-webpack-canary 110 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 129 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 129 0 0
✅ nitro-stable 104 0 25
✅ nuxt-stable 104 0 25
✅ sveltekit-stable 123 0 6
✅ vite-stable 104 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 104 0 25
✅ express-stable 104 0 25
✅ fastify-stable 104 0 25
✅ hono-stable 104 0 25
✅ nextjs-turbopack-canary 110 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 129 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 129 0 0
✅ nextjs-webpack-canary 110 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 129 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 129 0 0
✅ nitro-stable 104 0 25
✅ nuxt-stable 104 0 25
✅ sveltekit-stable 123 0 6
✅ vite-stable 104 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 129 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 104 0 25
✅ e2e-local-dev-tanstack-start- 104 0 25
✅ e2e-local-postgres-nest-stable 104 0 25
✅ e2e-local-postgres-tanstack-start- 104 0 25
✅ e2e-local-prod-nest-stable 104 0 25
✅ e2e-local-prod-tanstack-start- 104 0 25
✅ e2e-vercel-prod-tanstack-start 103 0 26

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 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.030s (-32.1% 🟢) 1.006s (~) 0.976s 10 1.00x
💻 Local Nitro 0.031s (-27.6% 🟢) 1.005s (~) 0.974s 10 1.04x
🐘 Postgres Express 0.045s (-21.7% 🟢) 1.013s (~) 0.967s 10 1.51x
🐘 Postgres Nitro 0.048s (-49.5% 🟢) 1.012s (-2.9%) 0.964s 10 1.60x
💻 Local Next.js (Turbopack) 0.049s 1.006s 0.957s 10 1.64x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 0.297s (-27.4% 🟢) 2.238s (-10.8% 🟢) 1.941s 10 1.00x
▲ Vercel Next.js (Turbopack) 0.334s (+33.0% 🔺) 2.341s (~) 2.007s 10 1.12x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.069s (-5.0%) 2.006s (~) 0.936s 10 1.00x
💻 Local Nitro 1.073s (-5.1% 🟢) 2.006s (~) 0.933s 10 1.00x
🐘 Postgres Nitro 1.082s (-5.1% 🟢) 2.008s (~) 0.926s 10 1.01x
🐘 Postgres Express 1.083s (-5.5% 🟢) 2.010s (~) 0.927s 10 1.01x
💻 Local Next.js (Turbopack) 1.119s 2.006s 0.888s 10 1.05x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 1.688s (-17.0% 🟢) 3.907s (+2.0%) 2.219s 10 1.00x
▲ Vercel Nitro 1.789s (-54.0% 🟢) 3.965s (-32.9% 🟢) 2.176s 10 1.06x
▲ 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
🐘 Postgres 🥇 Nitro 10.399s (-4.3%) 11.015s (~) 0.616s 3 1.00x
💻 Local Nitro 10.416s (-4.8%) 11.022s (~) 0.606s 3 1.00x
💻 Local Express 10.421s (-4.6%) 11.022s (~) 0.601s 3 1.00x
🐘 Postgres Express 10.427s (-4.9%) 11.019s (~) 0.592s 3 1.00x
💻 Local Next.js (Turbopack) 10.654s 11.022s 0.368s 3 1.02x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 14.254s (-39.9% 🟢) 16.153s (-35.7% 🟢) 1.899s 2 1.00x
▲ Vercel Next.js (Turbopack) 14.524s (-16.1% 🟢) 16.341s (-15.8% 🟢) 1.817s 2 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 🥇 Express 13.462s (-10.1% 🟢) 14.027s (-6.7% 🟢) 0.565s 5 1.00x
🐘 Postgres Express 13.466s (-7.7% 🟢) 14.016s (-6.7% 🟢) 0.550s 5 1.00x
💻 Local Nitro 13.491s (-10.4% 🟢) 14.026s (-12.5% 🟢) 0.535s 5 1.00x
🐘 Postgres Nitro 13.491s (-7.6% 🟢) 14.022s (-6.7% 🟢) 0.531s 5 1.00x
💻 Local Next.js (Turbopack) 14.074s 15.029s 0.955s 4 1.05x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 22.609s (-64.9% 🟢) 24.922s (-62.6% 🟢) 2.313s 3 1.00x
▲ Vercel Next.js (Turbopack) 22.871s (-56.5% 🟢) 24.999s (-54.2% 🟢) 2.128s 3 1.01x
▲ 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.931s (-28.9% 🟢) 12.273s (-27.9% 🟢) 0.342s 8 1.00x
💻 Local Express 11.952s (-28.0% 🟢) 12.273s (-27.9% 🟢) 0.321s 8 1.00x
🐘 Postgres Nitro 12.033s (-13.8% 🟢) 12.519s (-12.5% 🟢) 0.486s 8 1.01x
🐘 Postgres Express 12.129s (-13.4% 🟢) 12.874s (-11.8% 🟢) 0.745s 7 1.02x
💻 Local Next.js (Turbopack) 13.153s 14.028s 0.875s 7 1.10x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 37.246s (-91.2% 🟢) 39.974s (-90.6% 🟢) 2.729s 3 1.00x
▲ Vercel Next.js (Turbopack) 39.790s (-89.9% 🟢) 41.875s (-89.4% 🟢) 2.085s 3 1.07x
▲ 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 🥇 Nitro 1.146s (-10.1% 🟢) 2.007s (~) 0.861s 15 1.00x
🐘 Postgres Express 1.148s (-8.9% 🟢) 2.008s (~) 0.860s 15 1.00x
💻 Local Express 1.179s (-20.8% 🟢) 2.006s (~) 0.827s 15 1.03x
💻 Local Nitro 1.196s (-26.7% 🟢) 2.006s (-3.3%) 0.810s 15 1.04x
💻 Local Next.js (Turbopack) 1.318s 2.006s 0.688s 15 1.15x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.124s (+10.8% 🔺) 5.198s (+20.3% 🔺) 2.074s 6 1.00x
▲ Vercel Next.js (Turbopack) 3.436s (+1.1%) 5.304s (+7.5% 🔺) 1.868s 6 1.10x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.197s (-49.1% 🟢) 2.007s (-33.3% 🟢) 0.811s 15 1.00x
🐘 Postgres Express 1.207s (-48.9% 🟢) 2.007s (-33.3% 🟢) 0.800s 15 1.01x
💻 Local Express 1.772s (-40.0% 🟢) 2.006s (-41.9% 🟢) 0.234s 15 1.48x
💻 Local Nitro 1.782s (-43.3% 🟢) 2.008s (-48.3% 🟢) 0.226s 15 1.49x
💻 Local Next.js (Turbopack) 1.957s 2.468s 0.512s 13 1.63x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 4.483s (-36.9% 🟢) 6.394s (-28.2% 🟢) 1.911s 5 1.00x
▲ Vercel Nitro 6.444s (+59.0% 🔺) 8.104s (+36.9% 🔺) 1.660s 4 1.44x
▲ 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.313s (-62.4% 🟢) 2.008s (-49.9% 🟢) 0.696s 15 1.00x
🐘 Postgres Nitro 1.322s (-62.0% 🟢) 2.008s (-49.9% 🟢) 0.687s 15 1.01x
💻 Local Nitro 5.112s (-38.8% 🟢) 5.679s (-37.0% 🟢) 0.566s 6 3.90x
💻 Local Express 5.349s (-35.9% 🟢) 5.846s (-35.2% 🟢) 0.497s 6 4.08x
💻 Local Next.js (Turbopack) 5.639s 6.212s 0.573s 5 4.30x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 7.354s (+108.6% 🔺) 9.195s (+66.2% 🔺) 1.841s 4 1.00x
▲ Vercel Next.js (Turbopack) 10.065s (+12.9% 🔺) 11.733s (+7.0% 🔺) 1.667s 3 1.37x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.144s (-9.0% 🟢) 2.008s (~) 0.864s 15 1.00x
🐘 Postgres Nitro 1.150s (-8.5% 🟢) 2.007s (~) 0.857s 15 1.01x
💻 Local Express 1.406s (-25.7% 🟢) 2.006s (-15.1% 🟢) 0.600s 15 1.23x
💻 Local Nitro 1.411s (-24.4% 🟢) 2.006s (-14.3% 🟢) 0.596s 15 1.23x
💻 Local Next.js (Turbopack) 1.715s 2.392s 0.677s 13 1.50x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.771s (-5.5% 🟢) 4.497s (-3.1%) 1.726s 7 1.00x
▲ Vercel Nitro 3.007s (+22.3% 🔺) 5.136s (+23.2% 🔺) 2.129s 6 1.09x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.198s (-48.9% 🟢) 2.008s (-33.3% 🟢) 0.811s 15 1.00x
🐘 Postgres Nitro 1.204s (-48.5% 🟢) 2.008s (-33.3% 🟢) 0.804s 15 1.01x
💻 Local Nitro 2.008s (-34.5% 🟢) 2.470s (-36.5% 🟢) 0.462s 13 1.68x
💻 Local Express 2.041s (-34.8% 🟢) 2.508s (-33.3% 🟢) 0.467s 12 1.70x
💻 Local Next.js (Turbopack) 2.191s 3.008s 0.817s 10 1.83x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 4.173s (+32.8% 🔺) 6.142s (+35.8% 🔺) 1.969s 5 1.00x
▲ Vercel Nitro 4.290s (+32.7% 🔺) 6.620s (+30.4% 🔺) 2.329s 5 1.03x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.316s (-62.4% 🟢) 2.009s (-49.9% 🟢) 0.693s 15 1.00x
🐘 Postgres Nitro 1.344s (-61.4% 🟢) 2.075s (-48.2% 🟢) 0.731s 15 1.02x
💻 Local Express 5.669s (-35.6% 🟢) 6.013s (-35.2% 🟢) 0.345s 5 4.31x
💻 Local Nitro 5.936s (-35.1% 🟢) 6.614s (-34.0% 🟢) 0.678s 5 4.51x
💻 Local Next.js (Turbopack) 6.212s 6.615s 0.402s 5 4.72x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 6.246s (-7.6% 🟢) 7.551s (-11.6% 🟢) 1.305s 5 1.00x
▲ Vercel Nitro 6.251s (+22.7% 🔺) 8.597s (+26.1% 🔺) 2.347s 4 1.00x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.432s (-47.3% 🟢) 1.007s (~) 0.574s 60 1.00x
🐘 Postgres Express 0.442s (-47.4% 🟢) 1.006s (-1.6%) 0.565s 60 1.02x
💻 Local Nitro 0.468s (-52.3% 🟢) 1.004s (-8.2% 🟢) 0.536s 60 1.08x
💻 Local Express 0.512s (-48.0% 🟢) 1.038s (-3.5%) 0.527s 58 1.18x
💻 Local Next.js (Turbopack) 0.739s 1.005s 0.266s 60 1.71x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 6.198s (-71.9% 🟢) 8.362s (-65.2% 🟢) 2.164s 8 1.00x
▲ Vercel Next.js (Turbopack) 6.463s (-55.4% 🟢) 8.060s (-49.9% 🟢) 1.597s 8 1.04x
▲ 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.073s (-45.7% 🟢) 1.807s (-20.0% 🟢) 0.734s 50 1.00x
🐘 Postgres Nitro 1.108s (-42.5% 🟢) 1.882s (-10.4% 🟢) 0.774s 48 1.03x
💻 Local Nitro 1.196s (-60.6% 🟢) 2.006s (-46.6% 🟢) 0.810s 45 1.11x
💻 Local Express 1.197s (-60.3% 🟢) 2.006s (-44.1% 🟢) 0.809s 45 1.12x
💻 Local Next.js (Turbopack) 1.798s 2.006s 0.208s 45 1.68x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 15.632s (-60.4% 🟢) 17.718s (-57.1% 🟢) 2.087s 6 1.00x
▲ Vercel Next.js (Turbopack) 16.931s (-66.0% 🟢) 18.686s (-63.9% 🟢) 1.755s 5 1.08x
▲ 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 🥇 Nitro 2.040s (-50.3% 🟢) 2.456s (-46.6% 🟢) 0.416s 49 1.00x
🐘 Postgres Express 2.068s (-48.2% 🟢) 2.616s (-40.1% 🟢) 0.548s 46 1.01x
💻 Local Nitro 2.681s (-71.2% 🟢) 3.007s (-70.0% 🟢) 0.326s 40 1.31x
💻 Local Express 2.720s (-70.5% 🟢) 3.058s (-69.5% 🟢) 0.338s 40 1.33x
💻 Local Next.js (Turbopack) 3.874s 4.147s 0.273s 29 1.90x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 44.002s (-54.6% 🟢) 46.250s (-53.0% 🟢) 2.248s 3 1.00x
▲ Vercel Next.js (Turbopack) 45.479s (-57.6% 🟢) 47.366s (-56.5% 🟢) 1.886s 3 1.03x
▲ 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.166s (-41.3% 🟢) 1.006s (~) 0.839s 60 1.00x
🐘 Postgres Express 0.178s (-37.0% 🟢) 1.006s (~) 0.828s 60 1.07x
💻 Local Express 0.446s (-20.4% 🟢) 1.004s (~) 0.558s 60 2.68x
💻 Local Nitro 0.459s (-24.0% 🟢) 1.004s (-1.7%) 0.545s 60 2.76x
💻 Local Next.js (Turbopack) 0.554s 1.005s 0.450s 60 3.33x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.357s (+41.9% 🔺) 4.351s (+29.9% 🔺) 1.994s 14 1.00x
▲ Vercel Next.js (Turbopack) 2.659s (+31.5% 🔺) 4.383s (+15.5% 🔺) 1.723s 14 1.13x
▲ 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 🥇 Nitro 0.284s (-42.7% 🟢) 1.006s (~) 0.721s 90 1.00x
🐘 Postgres Express 0.323s (-36.7% 🟢) 1.017s (+1.1%) 0.695s 89 1.13x
💻 Local Nitro 2.186s (-13.9% 🟢) 2.976s (-1.1%) 0.790s 31 7.69x
💻 Local Express 2.202s (-12.4% 🟢) 2.944s (-2.2%) 0.742s 31 7.74x
💻 Local Next.js (Turbopack) 2.442s 3.044s 0.601s 30 8.59x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 9.742s (+175.6% 🔺) 12.047s (+132.0% 🔺) 2.305s 8 1.00x
▲ Vercel Nitro 10.412s (+222.8% 🔺) 12.617s (+161.7% 🔺) 2.205s 8 1.07x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.599s (-24.2% 🟢) 1.006s (~) 0.407s 120 1.00x
🐘 Postgres Express 0.618s (-24.5% 🟢) 1.006s (-1.1%) 0.388s 120 1.03x
💻 Local Nitro 10.141s (-9.4% 🟢) 10.700s (-8.3% 🟢) 0.558s 12 16.94x
💻 Local Express 10.219s (-8.7% 🟢) 10.860s (-9.1% 🟢) 0.641s 12 17.06x
💻 Local Next.js (Turbopack) 11.520s 12.229s 0.710s 10 19.24x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 22.481s (+191.1% 🔺) 24.606s (+161.8% 🔺) 2.125s 5 1.00x
▲ Vercel Next.js (Turbopack) 22.704s (+119.8% 🔺) 24.349s (+98.2% 🔺) 1.644s 5 1.01x
▲ Vercel Express ⚠️ missing - - - -

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.133s (+430.2% 🔺) 2.005s (+99.6% 🔺) 0.013s (+1.6%) 2.020s (+98.2% 🔺) 0.887s 10 1.00x
💻 Local Express 1.136s (+470.4% 🔺) 2.005s (+99.6% 🔺) 0.013s (+6.6% 🔺) 2.020s (+98.4% 🔺) 0.885s 10 1.00x
🐘 Postgres Express 1.153s (+462.3% 🔺) 2.002s (+100.4% 🔺) 0.002s (-6.3% 🟢) 2.011s (+98.8% 🔺) 0.857s 10 1.02x
🐘 Postgres Nitro 1.159s (+465.2% 🔺) 2.002s (+100.3% 🔺) 0.001s (-26.7% 🟢) 2.010s (+98.8% 🔺) 0.852s 10 1.02x
💻 Local Next.js (Turbopack) 1.185s 2.004s 0.013s 2.020s 0.835s 10 1.05x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.332s (-66.0% 🟢) 3.468s (-59.9% 🟢) 2.263s (+258.2% 🔺) 6.201s (-36.7% 🟢) 3.868s 10 1.00x
▲ Vercel Nitro 2.413s (-37.0% 🟢) 3.682s (-30.2% 🟢) 2.253s (+203.7% 🔺) 6.733s (+3.9%) 4.320s 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 🥇 Nitro 1.503s (+140.8% 🔺) 2.002s (+98.8% 🔺) 0.004s (-0.9%) 2.025s (+98.1% 🔺) 0.523s 30 1.00x
🐘 Postgres Express 1.516s (+140.7% 🔺) 2.001s (+98.8% 🔺) 0.004s (-3.4%) 2.024s (+97.9% 🔺) 0.508s 30 1.01x
💻 Local Nitro 1.528s (+82.2% 🔺) 2.010s (+98.7% 🔺) 0.010s (+2.5%) 2.022s (+81.2% 🔺) 0.493s 30 1.02x
💻 Local Express 1.573s (+107.8% 🔺) 2.011s (+95.5% 🔺) 0.010s (+9.1% 🔺) 2.024s (+94.6% 🔺) 0.450s 30 1.05x
💻 Local Next.js (Turbopack) 1.860s 2.010s 0.011s 2.203s 0.343s 28 1.24x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.761s (-80.4% 🟢) 7.643s (-75.2% 🟢) 0.391s (+249.2% 🔺) 8.599s (-72.9% 🟢) 2.838s 7 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -

🔍 Observability: Nitro

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.643s (-33.6% 🟢) 1.031s (-17.4% 🟢) 0.000s (+68.4% 🔺) 1.062s (-15.6% 🟢) 0.419s 57 1.00x
🐘 Postgres Express 0.661s (-31.2% 🟢) 1.013s (-20.7% 🟢) 0.000s (-23.3% 🟢) 1.030s (-21.1% 🟢) 0.369s 60 1.03x
💻 Local Nitro 1.353s (+10.7% 🔺) 2.016s (~) 0.000s (+366.7% 🔺) 2.018s (~) 0.665s 30 2.11x
💻 Local Express 1.368s (+11.7% 🔺) 2.017s (~) 0.000s (+20.0% 🔺) 2.019s (~) 0.650s 30 2.13x
💻 Local Next.js (Turbopack) 1.434s 2.013s 0.000s 2.016s 0.582s 30 2.23x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.964s (+30.0% 🔺) 5.739s (+30.6% 🔺) 0.000s (-100.0% 🟢) 6.395s (+33.0% 🔺) 2.431s 10 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -

🔍 Observability: 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.214s (-31.5% 🟢) 2.029s (-6.8% 🟢) 0.000s (NaN%) 2.045s (-7.0% 🟢) 0.832s 30 1.00x
🐘 Postgres Nitro 1.221s (-31.9% 🟢) 1.936s (-9.6% 🟢) 0.000s (-9.7% 🟢) 1.975s (-9.2% 🟢) 0.754s 31 1.01x
💻 Local Next.js (Turbopack) 3.056s 3.321s 0.001s 3.620s 0.564s 17 2.52x
💻 Local Express 3.121s (-10.0% 🟢) 3.901s (-3.3%) 0.001s (-21.9% 🟢) 3.905s (-3.3%) 0.784s 16 2.57x
💻 Local Nitro 3.124s (-7.8% 🟢) 3.839s (-4.8%) 0.000s (-53.1% 🟢) 3.844s (-4.8%) 0.720s 16 2.57x
🐘 Postgres Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.940s (+5.8% 🔺) 7.290s (+4.4%) 0.000s (~) 7.744s (+2.7%) 1.805s 8 1.00x
▲ Vercel Express ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

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

Winner determined by most benchmark wins

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a v5 regression where inline step bodies were incorrectly counted toward the 240s replay timeout, causing legitimately long-running steps to fail with REPLAY_TIMEOUT. The PR switches from a whole-handler timeout to a replay “budget” that only measures non-step time between step boundaries, and adds an env var override for the budget.

Changes:

  • Replace the global setTimeout(..., REPLAY_TIMEOUT_MS) guard with replay-budget tracking that pauses during executeStep(...).
  • Add getReplayTimeoutMs() with WORKFLOW_REPLAY_TIMEOUT_MS override (clamped to 30s–780s) plus unit tests.
  • Update REPLAY_TIMEOUT hint messaging and corresponding tests; add a changeset.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/core/src/runtime/constants.ts Adds replay-timeout env override + bounds constants; keeps default at 240s.
packages/core/src/runtime/constants.test.ts Unit tests for parsing/clamping/fallback behavior of getReplayTimeoutMs().
packages/core/src/runtime.ts Implements replay-budget bookkeeping and excludes inline step execution time from the budget.
packages/core/src/describe-error.ts Updates the REPLAY_TIMEOUT hint to reflect new semantics and env override.
packages/core/src/describe-error.test.ts Updates assertions/snapshots for the revised replay-timeout hint.
.changeset/replay-timeout-excludes-step-bodies.md Patch changeset describing the regression fix and new env var.
Comments suppressed due to low confidence (2)

packages/core/src/runtime.ts:274

  • The warning message says "attempt < maxRetries" but the condition is metadata.attempt <= REPLAY_TIMEOUT_MAX_RETRIES. Either adjust the message or the condition so logs accurately reflect the retry semantics.
        const handleReplayBudgetExhausted = async (): Promise<void> => {
          if (metadata.attempt <= REPLAY_TIMEOUT_MAX_RETRIES) {
            runLogger.warn(
              'Workflow replay exceeded timeout but will be re-attempted (attempt < maxRetries)',
              {
                timeoutMs: replayTimeoutMs,
                attempt: metadata.attempt,
                maxRetries: REPLAY_TIMEOUT_MAX_RETRIES,
              }

packages/core/src/runtime.ts:246

  • The comment above pauseReplayBudget says it "reset[s] the start marker", but the function currently only accumulates elapsed time and does not update nonStepStart. Either update the comment or set nonStepStart there to keep the bookkeeping self-consistent and avoid accidentally counting step time if a future call to isReplayBudgetExhausted() occurs before resumeReplayBudget().
        // Accumulate the elapsed delta since the last checkpoint (handler
        // entry or the most recent `resumeReplayBudget`) and reset the
        // start marker. Call this immediately before any inline step body.
        const pauseReplayBudget = (): void => {
          replayElapsedMs += Date.now() - nonStepStart;
        };

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

Comment thread packages/core/src/runtime.ts Outdated
Comment thread .changeset/replay-timeout-excludes-step-bodies.md Outdated
Comment thread packages/core/src/describe-error.test.ts Outdated
Copy link
Copy Markdown
Contributor

@pranaygp pranaygp left a comment

Choose a reason for hiding this comment

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

Thanks for the thorough fix and the detailed PR description — the pause/resume bracketing pattern is clean and the rationale (v4 parity + Fluid headroom) is well laid out. A few inline comments below; the main one I'd want resolved before merge is the loss of the VERCEL_URL gating around process.exit(1).

Comment thread packages/core/src/runtime.ts Outdated
Comment thread packages/core/src/runtime.ts Outdated
Comment thread packages/core/src/runtime.ts
Comment thread packages/core/src/runtime.ts
Comment thread packages/core/src/runtime/constants.ts Outdated
Comment thread packages/core/src/runtime/constants.ts
- Extract budget bookkeeping into ReplayBudget class (replay-budget.ts)
  with sentinel-protected idempotent pause()/resume() to avoid
  double-counting in future refactors that nest step execution
- Restore VERCEL_URL gate around process.exit(1) so a long pure-replay
  in local dev/non-Vercel runtimes can't hard-kill the host process
- Warn (once per distinct raw value) when WORKFLOW_REPLAY_TIMEOUT_MS is
  clamped or rejected, so misconfiguration is observable
- Correct Hobby maxDuration comment (60s standard / 300s Fluid)
- Document budget-check responsiveness trade-off vs. old setTimeout
- Tighten describe-error test assertions to match the full new hint
- Shorten changeset description
- Add ReplayBudget unit tests (9) including 8-minute step regression
- Add warn-once tests for getReplayTimeoutMs (extended)
Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

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

LGTM, something I missed in testing that would be useful to do on this branch: run a workflow with multiple sequential ~4 minute steps, and multiple parallel 4 minute steps, so we can check whether the replay timeout and the max-inline times interact correctly

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.

v5 regression: combined handler subjects step bodies to the 240s REPLAY_TIMEOUT_MS

4 participants