Skip to content

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

Merged
TooTallNate merged 6 commits into
mainfrom
fix/replay-timeout-step-bodies
May 22, 2026
Merged

[core] Exclude inline step execution from replay timeout#2013
TooTallNate merged 6 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.
CopilotAI review requested due to automatic review settings May 18, 2026 21:35
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 18, 2026 21:35
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cf51e9

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-32.5% 🟢)1.005s (~)0.975s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.04x
🐘 PostgresExpress0.044s (-24.0% 🟢)1.011s (~)0.967s101.47x
💻 LocalNext.js (Turbopack)0.044s1.005s0.961s101.48x
🐘 PostgresNitro0.050s (-47.7% 🟢)1.012s (-3.0%)0.962s101.67x
🐘 PostgresNext.js (Turbopack)0.062s1.014s0.952s102.07x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.071s (-5.3% 🟢)2.006s (~)0.935s101.00x
💻 LocalExpress1.073s (-4.7%)2.006s (~)0.934s101.00x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.008s (~)0.928s101.01x
🐘 PostgresNitro1.081s (-5.2% 🟢)2.008s (~)0.927s101.01x
💻 LocalNext.js (Turbopack)1.091s2.005s0.914s101.02x
🐘 PostgresNext.js (Turbopack)1.126s2.009s0.883s101.05x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.415s (-4.9%)11.021s (~)0.606s31.00x
🐘 PostgresNitro10.416s (-4.2%)11.015s (~)0.599s31.00x
🐘 PostgresExpress10.417s (-5.0%)11.014s (~)0.596s31.00x
💻 LocalExpress10.428s (-4.5%)11.022s (~)0.594s31.00x
💻 LocalNext.js (Turbopack)10.551s11.021s0.470s31.01x
🐘 PostgresNext.js (Turbopack)10.723s11.019s0.296s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.443s (-7.9% 🟢)14.014s (-6.7% 🟢)0.571s51.00x
🐘 PostgresExpress13.457s (-7.7% 🟢)14.015s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.468s (-10.0% 🟢)14.026s (-6.7% 🟢)0.558s51.00x
💻 LocalNitro13.479s (-10.5% 🟢)14.026s (-12.5% 🟢)0.547s51.00x
💻 LocalNext.js (Turbopack)13.739s14.028s0.289s51.02x
🐘 PostgresNext.js (Turbopack)14.181s15.021s0.840s41.05x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.897s (-28.3% 🟢)12.146s (-28.7% 🟢)0.249s81.00x
🐘 PostgresExpress11.904s (-15.0% 🟢)12.015s (-17.7% 🟢)0.110s81.00x
💻 LocalNitro11.919s (-29.0% 🟢)12.147s (-28.7% 🟢)0.228s81.00x
🐘 PostgresNitro11.970s (-14.3% 🟢)12.393s (-13.4% 🟢)0.423s81.01x
💻 LocalNext.js (Turbopack)12.433s13.023s0.590s71.05x
🐘 PostgresNext.js (Turbopack)13.115s14.016s0.901s71.10x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (-9.9% 🟢)2.007s (~)0.871s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.008s (~)0.862s151.01x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.175s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.207s2.007s0.799s151.06x
💻 LocalNext.js (Turbopack)1.228s2.005s0.777s151.08x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.816s151.00x
🐘 PostgresExpress1.192s (-49.5% 🟢)2.007s (-33.3% 🟢)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.363s2.074s0.711s151.14x
💻 LocalExpress1.729s (-41.4% 🟢)2.005s (-41.9% 🟢)0.276s151.45x
💻 LocalNext.js (Turbopack)1.762s2.006s0.244s151.48x
💻 LocalNitro1.775s (-43.5% 🟢)2.073s (-46.6% 🟢)0.298s151.49x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.283s (-63.2% 🟢)2.007s (-50.0% 🟢)0.723s151.00x
🐘 PostgresNitro1.320s (-62.1% 🟢)2.010s (-49.9% 🟢)0.690s151.03x
🐘 PostgresNext.js (Turbopack)1.645s2.008s0.363s151.28x
💻 LocalNext.js (Turbopack)4.691s5.010s0.319s63.66x
💻 LocalExpress4.998s (-40.1% 🟢)5.512s (-38.9% 🟢)0.514s63.89x
💻 LocalNitro5.620s (-32.7% 🟢)6.214s (-31.1% 🟢)0.594s54.38x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.135s (-9.7% 🟢)2.008s (~)0.873s151.00x
🐘 PostgresNitro1.143s (-9.1% 🟢)2.007s (~)0.864s151.01x
🐘 PostgresNext.js (Turbopack)1.216s2.007s0.791s151.07x
💻 LocalNext.js (Turbopack)1.306s2.006s0.700s151.15x
💻 LocalNitro1.384s (-25.8% 🟢)2.006s (-14.3% 🟢)0.622s151.22x
💻 LocalExpress1.417s (-25.2% 🟢)2.006s (-15.1% 🟢)0.589s151.25x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.185s (-49.4% 🟢)2.007s (-33.3% 🟢)0.822s151.00x
🐘 PostgresNitro1.194s (-49.0% 🟢)2.009s (-33.3% 🟢)0.815s151.01x
🐘 PostgresNext.js (Turbopack)1.333s2.008s0.675s151.12x
💻 LocalNext.js (Turbopack)1.934s2.315s0.381s131.63x
💻 LocalExpress1.991s (-36.4% 🟢)2.472s (-34.3% 🟢)0.481s131.68x
💻 LocalNitro2.106s (-31.3% 🟢)2.592s (-33.3% 🟢)0.486s121.78x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.273s (-63.6% 🟢)2.006s (-50.0% 🟢)0.733s151.00x
🐘 PostgresNitro1.319s (-62.1% 🟢)2.009s (-49.9% 🟢)0.689s151.04x
🐘 PostgresNext.js (Turbopack)1.631s2.009s0.378s151.28x
💻 LocalNext.js (Turbopack)4.718s5.178s0.460s63.71x
💻 LocalExpress5.503s (-37.5% 🟢)6.014s (-35.1% 🟢)0.511s54.32x
💻 LocalNitro6.005s (-34.3% 🟢)6.614s (-34.0% 🟢)0.609s54.72x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.436s (-46.8% 🟢)1.007s (~)0.570s601.00x
🐘 PostgresExpress0.437s (-47.9% 🟢)1.006s (-1.7%)0.569s601.00x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.6% 🟢)0.526s591.13x
💻 LocalExpress0.502s (-49.0% 🟢)1.004s (-6.7% 🟢)0.502s601.15x
💻 LocalNext.js (Turbopack)0.562s1.004s0.443s601.29x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.330s601.55x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.064s (-46.1% 🟢)1.586s (-29.8% 🟢)0.521s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.844s (-12.2% 🟢)0.750s491.03x
💻 LocalExpress1.196s (-60.3% 🟢)2.005s (-44.1% 🟢)0.809s451.12x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.12x
💻 LocalNext.js (Turbopack)1.384s2.005s0.621s451.30x
🐘 PostgresNext.js (Turbopack)1.624s2.008s0.384s451.53x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.035s (-49.0% 🟢)2.507s (-42.6% 🟢)0.472s481.00x
🐘 PostgresNitro2.100s (-48.8% 🟢)2.616s (-43.2% 🟢)0.516s461.03x
💻 LocalNitro2.665s (-71.3% 🟢)3.007s (-70.0% 🟢)0.343s401.31x
💻 LocalExpress2.712s (-70.6% 🟢)3.057s (-69.5% 🟢)0.346s401.33x
💻 LocalNext.js (Turbopack)3.068s3.644s0.577s331.51x
🐘 PostgresNext.js (Turbopack)3.214s4.010s0.797s301.58x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.166s (-41.3% 🟢)1.005s (~)0.839s601.00x
🐘 PostgresNitro0.174s (-38.4% 🟢)1.006s (~)0.831s601.05x
🐘 PostgresNext.js (Turbopack)0.230s1.006s0.776s601.39x
💻 LocalNitro0.373s (-38.4% 🟢)1.004s (-1.7%)0.632s602.25x
💻 LocalExpress0.389s (-30.5% 🟢)1.004s (~)0.615s602.35x
💻 LocalNext.js (Turbopack)0.478s1.004s0.526s602.88x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-45.4% 🟢)1.006s (~)0.728s901.00x
🐘 PostgresNitro0.291s (-41.4% 🟢)1.006s (~)0.715s901.05x
🐘 PostgresNext.js (Turbopack)0.467s1.007s0.540s901.68x
💻 LocalNitro2.123s (-16.4% 🟢)2.821s (-6.3% 🟢)0.698s327.63x
💻 LocalExpress2.132s (-15.2% 🟢)2.685s (-10.8% 🟢)0.552s347.66x
💻 LocalNext.js (Turbopack)2.211s2.853s0.641s327.95x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.563s (-31.2% 🟢)1.005s (-1.2%)0.442s1201.00x
🐘 PostgresNitro0.608s (-23.0% 🟢)1.006s (~)0.398s1201.08x
🐘 PostgresNext.js (Turbopack)1.011s1.774s0.764s681.79x
💻 LocalExpress9.851s (-12.0% 🟢)10.441s (-12.6% 🟢)0.590s1217.49x
💻 LocalNitro10.119s (-9.6% 🟢)10.609s (-9.0% 🟢)0.491s1217.97x
💻 LocalNext.js (Turbopack)10.406s11.027s0.621s1118.48x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.126s (+449.4% 🔺)2.000s (+100.1% 🔺)0.002s (+6.7% 🔺)2.011s (+98.8% 🔺)0.884s101.00x
🐘 PostgresExpress1.129s (+450.5% 🔺)2.001s (+100.4% 🔺)0.001s (-25.0% 🟢)2.009s (+98.6% 🔺)0.880s101.00x
💻 LocalNitro1.131s (+429.1% 🔺)2.005s (+99.6% 🔺)0.013s (~)2.020s (+98.2% 🔺)0.889s101.00x
💻 LocalExpress1.133s (+469.1% 🔺)2.006s (+99.7% 🔺)0.012s (-1.7%)2.020s (+98.4% 🔺)0.887s101.01x
💻 LocalNext.js (Turbopack)1.153s2.004s0.011s2.018s0.865s101.02x
🐘 PostgresNext.js (Turbopack)1.213s2.002s0.001s2.011s0.798s101.08x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.499s (+140.2% 🔺)2.005s (+99.2% 🔺)0.004s (-0.9%)2.024s (+98.0% 🔺)0.525s301.00x
🐘 PostgresExpress1.499s (+138.0% 🔺)2.005s (+99.2% 🔺)0.003s (-12.1% 🟢)2.023s (+97.7% 🔺)0.523s301.00x
💻 LocalNext.js (Turbopack)1.567s2.009s0.010s2.022s0.454s301.05x
💻 LocalNitro1.700s (+102.7% 🔺)2.011s (+98.7% 🔺)0.011s (+15.1% 🔺)2.203s (+97.4% 🔺)0.503s281.13x
💻 LocalExpress1.706s (+125.3% 🔺)2.010s (+95.4% 🔺)0.010s (+5.8% 🔺)2.201s (+111.7% 🔺)0.495s281.14x
🐘 PostgresNext.js (Turbopack)1.773s2.011s0.004s2.028s0.254s301.18x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.634s (-34.0% 🟢)1.053s (-17.6% 🟢)0.000s (-19.3% 🟢)1.060s (-18.9% 🟢)0.426s571.00x
🐘 PostgresNitro0.637s (-34.2% 🟢)1.025s (-17.8% 🟢)0.000s (+24.1% 🔺)1.048s (-16.7% 🟢)0.410s581.01x
🐘 PostgresNext.js (Turbopack)0.811s1.036s0.000s1.054s0.243s571.28x
💻 LocalNitro1.309s (+7.0% 🔺)2.014s (~)0.000s (~)2.016s (~)0.708s302.06x
💻 LocalExpress1.333s (+8.8% 🔺)2.014s (~)0.000s (-80.0% 🟢)2.016s (~)0.683s302.10x
💻 LocalNext.js (Turbopack)1.383s2.013s0.001s2.017s0.634s302.18x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.254s (-30.0% 🟢)1.994s (-6.9% 🟢)0.000s (+86.7% 🔺)2.042s (-6.1% 🟢)0.788s301.00x
🐘 PostgresExpress1.294s (-27.0% 🟢)2.032s (-6.7% 🟢)0.000s (NaN%)2.041s (-7.2% 🟢)0.747s301.03x
🐘 PostgresNext.js (Turbopack)1.738s2.350s0.000s2.358s0.620s261.39x
💻 LocalNext.js (Turbopack)2.668s3.290s0.000s3.293s0.625s192.13x
💻 LocalExpress3.058s (-11.8% 🟢)3.840s (-4.8%)0.000s (-45.3% 🟢)3.844s (-4.8%)0.786s162.44x
💻 LocalNitro3.065s (-9.5% 🟢)3.676s (-8.8% 🟢)0.001s (+10.3% 🔺)3.679s (-8.8% 🟢)0.614s172.44x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres11/21
Nitro🐘 Postgres17/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.

CopilotAI 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.

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
FileDescription
packages/core/src/runtime/constants.tsAdds replay-timeout env override + bounds constants; keeps default at 240s.
packages/core/src/runtime/constants.test.tsUnit tests for parsing/clamping/fallback behavior of getReplayTimeoutMs().
packages/core/src/runtime.tsImplements replay-budget bookkeeping and excludes inline step execution time from the budget.
packages/core/src/describe-error.tsUpdates the REPLAY_TIMEOUT hint to reflect new semantics and env override.
packages/core/src/describe-error.test.tsUpdates assertions/snapshots for the revised replay-timeout hint.
.changeset/replay-timeout-excludes-step-bodies.mdPatch 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 threadpackages/core/src/runtime.ts Outdated
Comment thread.changeset/replay-timeout-excludes-step-bodies.md Outdated
Comment threadpackages/core/src/describe-error.test.ts Outdated

@pranaygppranaygp 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.

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 threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime/constants.ts Outdated
Comment threadpackages/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)

@VaguelySeriousVaguelySerious 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.

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

…ep-bodies
# Conflicts:
#	packages/core/src/runtime.ts
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2a446af (AI decision).

The PR author explicitly states this bug is v5-only: in v4 (stable) step bodies ran in a separate steps function with its own maxDuration ceiling and were not subject to the replay-timeout wrapper, so the regression being fixed does not exist on stable. Additionally, the fix introduces a new optional World.processExitTriggersQueueRedelivery capability that builds on v5 runtime architecture, making it inappropriate for backport.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2a446af517dbb91ae959adade1d74ef0428a2b09

Sign up for freeto 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

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[core] Exclude inline step execution from replay timeout by TooTallNate · Pull Request #2013 · vercel/workflow · GitHub
Skip to content

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

Merged
TooTallNate merged 6 commits into
mainfrom
fix/replay-timeout-step-bodies
May 22, 2026
Merged

[core] Exclude inline step execution from replay timeout#2013
TooTallNate merged 6 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.
CopilotAI review requested due to automatic review settings May 18, 2026 21:35
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 18, 2026 21:35
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cf51e9

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-32.5% 🟢)1.005s (~)0.975s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.04x
🐘 PostgresExpress0.044s (-24.0% 🟢)1.011s (~)0.967s101.47x
💻 LocalNext.js (Turbopack)0.044s1.005s0.961s101.48x
🐘 PostgresNitro0.050s (-47.7% 🟢)1.012s (-3.0%)0.962s101.67x
🐘 PostgresNext.js (Turbopack)0.062s1.014s0.952s102.07x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.071s (-5.3% 🟢)2.006s (~)0.935s101.00x
💻 LocalExpress1.073s (-4.7%)2.006s (~)0.934s101.00x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.008s (~)0.928s101.01x
🐘 PostgresNitro1.081s (-5.2% 🟢)2.008s (~)0.927s101.01x
💻 LocalNext.js (Turbopack)1.091s2.005s0.914s101.02x
🐘 PostgresNext.js (Turbopack)1.126s2.009s0.883s101.05x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.415s (-4.9%)11.021s (~)0.606s31.00x
🐘 PostgresNitro10.416s (-4.2%)11.015s (~)0.599s31.00x
🐘 PostgresExpress10.417s (-5.0%)11.014s (~)0.596s31.00x
💻 LocalExpress10.428s (-4.5%)11.022s (~)0.594s31.00x
💻 LocalNext.js (Turbopack)10.551s11.021s0.470s31.01x
🐘 PostgresNext.js (Turbopack)10.723s11.019s0.296s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.443s (-7.9% 🟢)14.014s (-6.7% 🟢)0.571s51.00x
🐘 PostgresExpress13.457s (-7.7% 🟢)14.015s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.468s (-10.0% 🟢)14.026s (-6.7% 🟢)0.558s51.00x
💻 LocalNitro13.479s (-10.5% 🟢)14.026s (-12.5% 🟢)0.547s51.00x
💻 LocalNext.js (Turbopack)13.739s14.028s0.289s51.02x
🐘 PostgresNext.js (Turbopack)14.181s15.021s0.840s41.05x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.897s (-28.3% 🟢)12.146s (-28.7% 🟢)0.249s81.00x
🐘 PostgresExpress11.904s (-15.0% 🟢)12.015s (-17.7% 🟢)0.110s81.00x
💻 LocalNitro11.919s (-29.0% 🟢)12.147s (-28.7% 🟢)0.228s81.00x
🐘 PostgresNitro11.970s (-14.3% 🟢)12.393s (-13.4% 🟢)0.423s81.01x
💻 LocalNext.js (Turbopack)12.433s13.023s0.590s71.05x
🐘 PostgresNext.js (Turbopack)13.115s14.016s0.901s71.10x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (-9.9% 🟢)2.007s (~)0.871s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.008s (~)0.862s151.01x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.175s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.207s2.007s0.799s151.06x
💻 LocalNext.js (Turbopack)1.228s2.005s0.777s151.08x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.816s151.00x
🐘 PostgresExpress1.192s (-49.5% 🟢)2.007s (-33.3% 🟢)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.363s2.074s0.711s151.14x
💻 LocalExpress1.729s (-41.4% 🟢)2.005s (-41.9% 🟢)0.276s151.45x
💻 LocalNext.js (Turbopack)1.762s2.006s0.244s151.48x
💻 LocalNitro1.775s (-43.5% 🟢)2.073s (-46.6% 🟢)0.298s151.49x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.283s (-63.2% 🟢)2.007s (-50.0% 🟢)0.723s151.00x
🐘 PostgresNitro1.320s (-62.1% 🟢)2.010s (-49.9% 🟢)0.690s151.03x
🐘 PostgresNext.js (Turbopack)1.645s2.008s0.363s151.28x
💻 LocalNext.js (Turbopack)4.691s5.010s0.319s63.66x
💻 LocalExpress4.998s (-40.1% 🟢)5.512s (-38.9% 🟢)0.514s63.89x
💻 LocalNitro5.620s (-32.7% 🟢)6.214s (-31.1% 🟢)0.594s54.38x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.135s (-9.7% 🟢)2.008s (~)0.873s151.00x
🐘 PostgresNitro1.143s (-9.1% 🟢)2.007s (~)0.864s151.01x
🐘 PostgresNext.js (Turbopack)1.216s2.007s0.791s151.07x
💻 LocalNext.js (Turbopack)1.306s2.006s0.700s151.15x
💻 LocalNitro1.384s (-25.8% 🟢)2.006s (-14.3% 🟢)0.622s151.22x
💻 LocalExpress1.417s (-25.2% 🟢)2.006s (-15.1% 🟢)0.589s151.25x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.185s (-49.4% 🟢)2.007s (-33.3% 🟢)0.822s151.00x
🐘 PostgresNitro1.194s (-49.0% 🟢)2.009s (-33.3% 🟢)0.815s151.01x
🐘 PostgresNext.js (Turbopack)1.333s2.008s0.675s151.12x
💻 LocalNext.js (Turbopack)1.934s2.315s0.381s131.63x
💻 LocalExpress1.991s (-36.4% 🟢)2.472s (-34.3% 🟢)0.481s131.68x
💻 LocalNitro2.106s (-31.3% 🟢)2.592s (-33.3% 🟢)0.486s121.78x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.273s (-63.6% 🟢)2.006s (-50.0% 🟢)0.733s151.00x
🐘 PostgresNitro1.319s (-62.1% 🟢)2.009s (-49.9% 🟢)0.689s151.04x
🐘 PostgresNext.js (Turbopack)1.631s2.009s0.378s151.28x
💻 LocalNext.js (Turbopack)4.718s5.178s0.460s63.71x
💻 LocalExpress5.503s (-37.5% 🟢)6.014s (-35.1% 🟢)0.511s54.32x
💻 LocalNitro6.005s (-34.3% 🟢)6.614s (-34.0% 🟢)0.609s54.72x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.436s (-46.8% 🟢)1.007s (~)0.570s601.00x
🐘 PostgresExpress0.437s (-47.9% 🟢)1.006s (-1.7%)0.569s601.00x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.6% 🟢)0.526s591.13x
💻 LocalExpress0.502s (-49.0% 🟢)1.004s (-6.7% 🟢)0.502s601.15x
💻 LocalNext.js (Turbopack)0.562s1.004s0.443s601.29x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.330s601.55x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.064s (-46.1% 🟢)1.586s (-29.8% 🟢)0.521s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.844s (-12.2% 🟢)0.750s491.03x
💻 LocalExpress1.196s (-60.3% 🟢)2.005s (-44.1% 🟢)0.809s451.12x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.12x
💻 LocalNext.js (Turbopack)1.384s2.005s0.621s451.30x
🐘 PostgresNext.js (Turbopack)1.624s2.008s0.384s451.53x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.035s (-49.0% 🟢)2.507s (-42.6% 🟢)0.472s481.00x
🐘 PostgresNitro2.100s (-48.8% 🟢)2.616s (-43.2% 🟢)0.516s461.03x
💻 LocalNitro2.665s (-71.3% 🟢)3.007s (-70.0% 🟢)0.343s401.31x
💻 LocalExpress2.712s (-70.6% 🟢)3.057s (-69.5% 🟢)0.346s401.33x
💻 LocalNext.js (Turbopack)3.068s3.644s0.577s331.51x
🐘 PostgresNext.js (Turbopack)3.214s4.010s0.797s301.58x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.166s (-41.3% 🟢)1.005s (~)0.839s601.00x
🐘 PostgresNitro0.174s (-38.4% 🟢)1.006s (~)0.831s601.05x
🐘 PostgresNext.js (Turbopack)0.230s1.006s0.776s601.39x
💻 LocalNitro0.373s (-38.4% 🟢)1.004s (-1.7%)0.632s602.25x
💻 LocalExpress0.389s (-30.5% 🟢)1.004s (~)0.615s602.35x
💻 LocalNext.js (Turbopack)0.478s1.004s0.526s602.88x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-45.4% 🟢)1.006s (~)0.728s901.00x
🐘 PostgresNitro0.291s (-41.4% 🟢)1.006s (~)0.715s901.05x
🐘 PostgresNext.js (Turbopack)0.467s1.007s0.540s901.68x
💻 LocalNitro2.123s (-16.4% 🟢)2.821s (-6.3% 🟢)0.698s327.63x
💻 LocalExpress2.132s (-15.2% 🟢)2.685s (-10.8% 🟢)0.552s347.66x
💻 LocalNext.js (Turbopack)2.211s2.853s0.641s327.95x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.563s (-31.2% 🟢)1.005s (-1.2%)0.442s1201.00x
🐘 PostgresNitro0.608s (-23.0% 🟢)1.006s (~)0.398s1201.08x
🐘 PostgresNext.js (Turbopack)1.011s1.774s0.764s681.79x
💻 LocalExpress9.851s (-12.0% 🟢)10.441s (-12.6% 🟢)0.590s1217.49x
💻 LocalNitro10.119s (-9.6% 🟢)10.609s (-9.0% 🟢)0.491s1217.97x
💻 LocalNext.js (Turbopack)10.406s11.027s0.621s1118.48x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.126s (+449.4% 🔺)2.000s (+100.1% 🔺)0.002s (+6.7% 🔺)2.011s (+98.8% 🔺)0.884s101.00x
🐘 PostgresExpress1.129s (+450.5% 🔺)2.001s (+100.4% 🔺)0.001s (-25.0% 🟢)2.009s (+98.6% 🔺)0.880s101.00x
💻 LocalNitro1.131s (+429.1% 🔺)2.005s (+99.6% 🔺)0.013s (~)2.020s (+98.2% 🔺)0.889s101.00x
💻 LocalExpress1.133s (+469.1% 🔺)2.006s (+99.7% 🔺)0.012s (-1.7%)2.020s (+98.4% 🔺)0.887s101.01x
💻 LocalNext.js (Turbopack)1.153s2.004s0.011s2.018s0.865s101.02x
🐘 PostgresNext.js (Turbopack)1.213s2.002s0.001s2.011s0.798s101.08x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.499s (+140.2% 🔺)2.005s (+99.2% 🔺)0.004s (-0.9%)2.024s (+98.0% 🔺)0.525s301.00x
🐘 PostgresExpress1.499s (+138.0% 🔺)2.005s (+99.2% 🔺)0.003s (-12.1% 🟢)2.023s (+97.7% 🔺)0.523s301.00x
💻 LocalNext.js (Turbopack)1.567s2.009s0.010s2.022s0.454s301.05x
💻 LocalNitro1.700s (+102.7% 🔺)2.011s (+98.7% 🔺)0.011s (+15.1% 🔺)2.203s (+97.4% 🔺)0.503s281.13x
💻 LocalExpress1.706s (+125.3% 🔺)2.010s (+95.4% 🔺)0.010s (+5.8% 🔺)2.201s (+111.7% 🔺)0.495s281.14x
🐘 PostgresNext.js (Turbopack)1.773s2.011s0.004s2.028s0.254s301.18x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.634s (-34.0% 🟢)1.053s (-17.6% 🟢)0.000s (-19.3% 🟢)1.060s (-18.9% 🟢)0.426s571.00x
🐘 PostgresNitro0.637s (-34.2% 🟢)1.025s (-17.8% 🟢)0.000s (+24.1% 🔺)1.048s (-16.7% 🟢)0.410s581.01x
🐘 PostgresNext.js (Turbopack)0.811s1.036s0.000s1.054s0.243s571.28x
💻 LocalNitro1.309s (+7.0% 🔺)2.014s (~)0.000s (~)2.016s (~)0.708s302.06x
💻 LocalExpress1.333s (+8.8% 🔺)2.014s (~)0.000s (-80.0% 🟢)2.016s (~)0.683s302.10x
💻 LocalNext.js (Turbopack)1.383s2.013s0.001s2.017s0.634s302.18x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.254s (-30.0% 🟢)1.994s (-6.9% 🟢)0.000s (+86.7% 🔺)2.042s (-6.1% 🟢)0.788s301.00x
🐘 PostgresExpress1.294s (-27.0% 🟢)2.032s (-6.7% 🟢)0.000s (NaN%)2.041s (-7.2% 🟢)0.747s301.03x
🐘 PostgresNext.js (Turbopack)1.738s2.350s0.000s2.358s0.620s261.39x
💻 LocalNext.js (Turbopack)2.668s3.290s0.000s3.293s0.625s192.13x
💻 LocalExpress3.058s (-11.8% 🟢)3.840s (-4.8%)0.000s (-45.3% 🟢)3.844s (-4.8%)0.786s162.44x
💻 LocalNitro3.065s (-9.5% 🟢)3.676s (-8.8% 🟢)0.001s (+10.3% 🔺)3.679s (-8.8% 🟢)0.614s172.44x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres11/21
Nitro🐘 Postgres17/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.

CopilotAI 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.

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
FileDescription
packages/core/src/runtime/constants.tsAdds replay-timeout env override + bounds constants; keeps default at 240s.
packages/core/src/runtime/constants.test.tsUnit tests for parsing/clamping/fallback behavior of getReplayTimeoutMs().
packages/core/src/runtime.tsImplements replay-budget bookkeeping and excludes inline step execution time from the budget.
packages/core/src/describe-error.tsUpdates the REPLAY_TIMEOUT hint to reflect new semantics and env override.
packages/core/src/describe-error.test.tsUpdates assertions/snapshots for the revised replay-timeout hint.
.changeset/replay-timeout-excludes-step-bodies.mdPatch 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 threadpackages/core/src/runtime.ts Outdated
Comment thread.changeset/replay-timeout-excludes-step-bodies.md Outdated
Comment threadpackages/core/src/describe-error.test.ts Outdated

@pranaygppranaygp 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.

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 threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime/constants.ts Outdated
Comment threadpackages/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)

@VaguelySeriousVaguelySerious 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.

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

…ep-bodies
# Conflicts:
#	packages/core/src/runtime.ts
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2a446af (AI decision).

The PR author explicitly states this bug is v5-only: in v4 (stable) step bodies ran in a separate steps function with its own maxDuration ceiling and were not subject to the replay-timeout wrapper, so the regression being fixed does not exist on stable. Additionally, the fix introduces a new optional World.processExitTriggersQueueRedelivery capability that builds on v5 runtime architecture, making it inappropriate for backport.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2a446af517dbb91ae959adade1d74ef0428a2b09

Sign up for freeto 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

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [core] Exclude inline step execution from replay timeout by TooTallNate · Pull Request #2013 · vercel/workflow · GitHub
Skip to content

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

Merged
TooTallNate merged 6 commits into
mainfrom
fix/replay-timeout-step-bodies
May 22, 2026
Merged

[core] Exclude inline step execution from replay timeout#2013
TooTallNate merged 6 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.
CopilotAI review requested due to automatic review settings May 18, 2026 21:35
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 18, 2026 21:35
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cf51e9

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-32.5% 🟢)1.005s (~)0.975s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.04x
🐘 PostgresExpress0.044s (-24.0% 🟢)1.011s (~)0.967s101.47x
💻 LocalNext.js (Turbopack)0.044s1.005s0.961s101.48x
🐘 PostgresNitro0.050s (-47.7% 🟢)1.012s (-3.0%)0.962s101.67x
🐘 PostgresNext.js (Turbopack)0.062s1.014s0.952s102.07x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.071s (-5.3% 🟢)2.006s (~)0.935s101.00x
💻 LocalExpress1.073s (-4.7%)2.006s (~)0.934s101.00x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.008s (~)0.928s101.01x
🐘 PostgresNitro1.081s (-5.2% 🟢)2.008s (~)0.927s101.01x
💻 LocalNext.js (Turbopack)1.091s2.005s0.914s101.02x
🐘 PostgresNext.js (Turbopack)1.126s2.009s0.883s101.05x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.415s (-4.9%)11.021s (~)0.606s31.00x
🐘 PostgresNitro10.416s (-4.2%)11.015s (~)0.599s31.00x
🐘 PostgresExpress10.417s (-5.0%)11.014s (~)0.596s31.00x
💻 LocalExpress10.428s (-4.5%)11.022s (~)0.594s31.00x
💻 LocalNext.js (Turbopack)10.551s11.021s0.470s31.01x
🐘 PostgresNext.js (Turbopack)10.723s11.019s0.296s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.443s (-7.9% 🟢)14.014s (-6.7% 🟢)0.571s51.00x
🐘 PostgresExpress13.457s (-7.7% 🟢)14.015s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.468s (-10.0% 🟢)14.026s (-6.7% 🟢)0.558s51.00x
💻 LocalNitro13.479s (-10.5% 🟢)14.026s (-12.5% 🟢)0.547s51.00x
💻 LocalNext.js (Turbopack)13.739s14.028s0.289s51.02x
🐘 PostgresNext.js (Turbopack)14.181s15.021s0.840s41.05x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.897s (-28.3% 🟢)12.146s (-28.7% 🟢)0.249s81.00x
🐘 PostgresExpress11.904s (-15.0% 🟢)12.015s (-17.7% 🟢)0.110s81.00x
💻 LocalNitro11.919s (-29.0% 🟢)12.147s (-28.7% 🟢)0.228s81.00x
🐘 PostgresNitro11.970s (-14.3% 🟢)12.393s (-13.4% 🟢)0.423s81.01x
💻 LocalNext.js (Turbopack)12.433s13.023s0.590s71.05x
🐘 PostgresNext.js (Turbopack)13.115s14.016s0.901s71.10x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (-9.9% 🟢)2.007s (~)0.871s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.008s (~)0.862s151.01x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.175s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.207s2.007s0.799s151.06x
💻 LocalNext.js (Turbopack)1.228s2.005s0.777s151.08x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.816s151.00x
🐘 PostgresExpress1.192s (-49.5% 🟢)2.007s (-33.3% 🟢)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.363s2.074s0.711s151.14x
💻 LocalExpress1.729s (-41.4% 🟢)2.005s (-41.9% 🟢)0.276s151.45x
💻 LocalNext.js (Turbopack)1.762s2.006s0.244s151.48x
💻 LocalNitro1.775s (-43.5% 🟢)2.073s (-46.6% 🟢)0.298s151.49x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.283s (-63.2% 🟢)2.007s (-50.0% 🟢)0.723s151.00x
🐘 PostgresNitro1.320s (-62.1% 🟢)2.010s (-49.9% 🟢)0.690s151.03x
🐘 PostgresNext.js (Turbopack)1.645s2.008s0.363s151.28x
💻 LocalNext.js (Turbopack)4.691s5.010s0.319s63.66x
💻 LocalExpress4.998s (-40.1% 🟢)5.512s (-38.9% 🟢)0.514s63.89x
💻 LocalNitro5.620s (-32.7% 🟢)6.214s (-31.1% 🟢)0.594s54.38x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.135s (-9.7% 🟢)2.008s (~)0.873s151.00x
🐘 PostgresNitro1.143s (-9.1% 🟢)2.007s (~)0.864s151.01x
🐘 PostgresNext.js (Turbopack)1.216s2.007s0.791s151.07x
💻 LocalNext.js (Turbopack)1.306s2.006s0.700s151.15x
💻 LocalNitro1.384s (-25.8% 🟢)2.006s (-14.3% 🟢)0.622s151.22x
💻 LocalExpress1.417s (-25.2% 🟢)2.006s (-15.1% 🟢)0.589s151.25x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.185s (-49.4% 🟢)2.007s (-33.3% 🟢)0.822s151.00x
🐘 PostgresNitro1.194s (-49.0% 🟢)2.009s (-33.3% 🟢)0.815s151.01x
🐘 PostgresNext.js (Turbopack)1.333s2.008s0.675s151.12x
💻 LocalNext.js (Turbopack)1.934s2.315s0.381s131.63x
💻 LocalExpress1.991s (-36.4% 🟢)2.472s (-34.3% 🟢)0.481s131.68x
💻 LocalNitro2.106s (-31.3% 🟢)2.592s (-33.3% 🟢)0.486s121.78x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.273s (-63.6% 🟢)2.006s (-50.0% 🟢)0.733s151.00x
🐘 PostgresNitro1.319s (-62.1% 🟢)2.009s (-49.9% 🟢)0.689s151.04x
🐘 PostgresNext.js (Turbopack)1.631s2.009s0.378s151.28x
💻 LocalNext.js (Turbopack)4.718s5.178s0.460s63.71x
💻 LocalExpress5.503s (-37.5% 🟢)6.014s (-35.1% 🟢)0.511s54.32x
💻 LocalNitro6.005s (-34.3% 🟢)6.614s (-34.0% 🟢)0.609s54.72x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.436s (-46.8% 🟢)1.007s (~)0.570s601.00x
🐘 PostgresExpress0.437s (-47.9% 🟢)1.006s (-1.7%)0.569s601.00x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.6% 🟢)0.526s591.13x
💻 LocalExpress0.502s (-49.0% 🟢)1.004s (-6.7% 🟢)0.502s601.15x
💻 LocalNext.js (Turbopack)0.562s1.004s0.443s601.29x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.330s601.55x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.064s (-46.1% 🟢)1.586s (-29.8% 🟢)0.521s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.844s (-12.2% 🟢)0.750s491.03x
💻 LocalExpress1.196s (-60.3% 🟢)2.005s (-44.1% 🟢)0.809s451.12x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.12x
💻 LocalNext.js (Turbopack)1.384s2.005s0.621s451.30x
🐘 PostgresNext.js (Turbopack)1.624s2.008s0.384s451.53x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.035s (-49.0% 🟢)2.507s (-42.6% 🟢)0.472s481.00x
🐘 PostgresNitro2.100s (-48.8% 🟢)2.616s (-43.2% 🟢)0.516s461.03x
💻 LocalNitro2.665s (-71.3% 🟢)3.007s (-70.0% 🟢)0.343s401.31x
💻 LocalExpress2.712s (-70.6% 🟢)3.057s (-69.5% 🟢)0.346s401.33x
💻 LocalNext.js (Turbopack)3.068s3.644s0.577s331.51x
🐘 PostgresNext.js (Turbopack)3.214s4.010s0.797s301.58x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.166s (-41.3% 🟢)1.005s (~)0.839s601.00x
🐘 PostgresNitro0.174s (-38.4% 🟢)1.006s (~)0.831s601.05x
🐘 PostgresNext.js (Turbopack)0.230s1.006s0.776s601.39x
💻 LocalNitro0.373s (-38.4% 🟢)1.004s (-1.7%)0.632s602.25x
💻 LocalExpress0.389s (-30.5% 🟢)1.004s (~)0.615s602.35x
💻 LocalNext.js (Turbopack)0.478s1.004s0.526s602.88x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-45.4% 🟢)1.006s (~)0.728s901.00x
🐘 PostgresNitro0.291s (-41.4% 🟢)1.006s (~)0.715s901.05x
🐘 PostgresNext.js (Turbopack)0.467s1.007s0.540s901.68x
💻 LocalNitro2.123s (-16.4% 🟢)2.821s (-6.3% 🟢)0.698s327.63x
💻 LocalExpress2.132s (-15.2% 🟢)2.685s (-10.8% 🟢)0.552s347.66x
💻 LocalNext.js (Turbopack)2.211s2.853s0.641s327.95x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.563s (-31.2% 🟢)1.005s (-1.2%)0.442s1201.00x
🐘 PostgresNitro0.608s (-23.0% 🟢)1.006s (~)0.398s1201.08x
🐘 PostgresNext.js (Turbopack)1.011s1.774s0.764s681.79x
💻 LocalExpress9.851s (-12.0% 🟢)10.441s (-12.6% 🟢)0.590s1217.49x
💻 LocalNitro10.119s (-9.6% 🟢)10.609s (-9.0% 🟢)0.491s1217.97x
💻 LocalNext.js (Turbopack)10.406s11.027s0.621s1118.48x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.126s (+449.4% 🔺)2.000s (+100.1% 🔺)0.002s (+6.7% 🔺)2.011s (+98.8% 🔺)0.884s101.00x
🐘 PostgresExpress1.129s (+450.5% 🔺)2.001s (+100.4% 🔺)0.001s (-25.0% 🟢)2.009s (+98.6% 🔺)0.880s101.00x
💻 LocalNitro1.131s (+429.1% 🔺)2.005s (+99.6% 🔺)0.013s (~)2.020s (+98.2% 🔺)0.889s101.00x
💻 LocalExpress1.133s (+469.1% 🔺)2.006s (+99.7% 🔺)0.012s (-1.7%)2.020s (+98.4% 🔺)0.887s101.01x
💻 LocalNext.js (Turbopack)1.153s2.004s0.011s2.018s0.865s101.02x
🐘 PostgresNext.js (Turbopack)1.213s2.002s0.001s2.011s0.798s101.08x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.499s (+140.2% 🔺)2.005s (+99.2% 🔺)0.004s (-0.9%)2.024s (+98.0% 🔺)0.525s301.00x
🐘 PostgresExpress1.499s (+138.0% 🔺)2.005s (+99.2% 🔺)0.003s (-12.1% 🟢)2.023s (+97.7% 🔺)0.523s301.00x
💻 LocalNext.js (Turbopack)1.567s2.009s0.010s2.022s0.454s301.05x
💻 LocalNitro1.700s (+102.7% 🔺)2.011s (+98.7% 🔺)0.011s (+15.1% 🔺)2.203s (+97.4% 🔺)0.503s281.13x
💻 LocalExpress1.706s (+125.3% 🔺)2.010s (+95.4% 🔺)0.010s (+5.8% 🔺)2.201s (+111.7% 🔺)0.495s281.14x
🐘 PostgresNext.js (Turbopack)1.773s2.011s0.004s2.028s0.254s301.18x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.634s (-34.0% 🟢)1.053s (-17.6% 🟢)0.000s (-19.3% 🟢)1.060s (-18.9% 🟢)0.426s571.00x
🐘 PostgresNitro0.637s (-34.2% 🟢)1.025s (-17.8% 🟢)0.000s (+24.1% 🔺)1.048s (-16.7% 🟢)0.410s581.01x
🐘 PostgresNext.js (Turbopack)0.811s1.036s0.000s1.054s0.243s571.28x
💻 LocalNitro1.309s (+7.0% 🔺)2.014s (~)0.000s (~)2.016s (~)0.708s302.06x
💻 LocalExpress1.333s (+8.8% 🔺)2.014s (~)0.000s (-80.0% 🟢)2.016s (~)0.683s302.10x
💻 LocalNext.js (Turbopack)1.383s2.013s0.001s2.017s0.634s302.18x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.254s (-30.0% 🟢)1.994s (-6.9% 🟢)0.000s (+86.7% 🔺)2.042s (-6.1% 🟢)0.788s301.00x
🐘 PostgresExpress1.294s (-27.0% 🟢)2.032s (-6.7% 🟢)0.000s (NaN%)2.041s (-7.2% 🟢)0.747s301.03x
🐘 PostgresNext.js (Turbopack)1.738s2.350s0.000s2.358s0.620s261.39x
💻 LocalNext.js (Turbopack)2.668s3.290s0.000s3.293s0.625s192.13x
💻 LocalExpress3.058s (-11.8% 🟢)3.840s (-4.8%)0.000s (-45.3% 🟢)3.844s (-4.8%)0.786s162.44x
💻 LocalNitro3.065s (-9.5% 🟢)3.676s (-8.8% 🟢)0.001s (+10.3% 🔺)3.679s (-8.8% 🟢)0.614s172.44x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres11/21
Nitro🐘 Postgres17/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.

CopilotAI 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.

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
FileDescription
packages/core/src/runtime/constants.tsAdds replay-timeout env override + bounds constants; keeps default at 240s.
packages/core/src/runtime/constants.test.tsUnit tests for parsing/clamping/fallback behavior of getReplayTimeoutMs().
packages/core/src/runtime.tsImplements replay-budget bookkeeping and excludes inline step execution time from the budget.
packages/core/src/describe-error.tsUpdates the REPLAY_TIMEOUT hint to reflect new semantics and env override.
packages/core/src/describe-error.test.tsUpdates assertions/snapshots for the revised replay-timeout hint.
.changeset/replay-timeout-excludes-step-bodies.mdPatch 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 threadpackages/core/src/runtime.ts Outdated
Comment thread.changeset/replay-timeout-excludes-step-bodies.md Outdated
Comment threadpackages/core/src/describe-error.test.ts Outdated

@pranaygppranaygp 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.

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 threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime/constants.ts Outdated
Comment threadpackages/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)

@VaguelySeriousVaguelySerious 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.

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

…ep-bodies
# Conflicts:
#	packages/core/src/runtime.ts
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2a446af (AI decision).

The PR author explicitly states this bug is v5-only: in v4 (stable) step bodies ran in a separate steps function with its own maxDuration ceiling and were not subject to the replay-timeout wrapper, so the regression being fixed does not exist on stable. Additionally, the fix introduces a new optional World.processExitTriggersQueueRedelivery capability that builds on v5 runtime architecture, making it inappropriate for backport.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2a446af517dbb91ae959adade1d74ef0428a2b09

Sign up for freeto 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

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [core] Exclude inline step execution from replay timeout by TooTallNate · Pull Request #2013 · vercel/workflow · GitHub
Skip to content

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

Merged
TooTallNate merged 6 commits into
mainfrom
fix/replay-timeout-step-bodies
May 22, 2026
Merged

[core] Exclude inline step execution from replay timeout#2013
TooTallNate merged 6 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.
CopilotAI review requested due to automatic review settings May 18, 2026 21:35
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 18, 2026 21:35
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cf51e9

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-32.5% 🟢)1.005s (~)0.975s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.04x
🐘 PostgresExpress0.044s (-24.0% 🟢)1.011s (~)0.967s101.47x
💻 LocalNext.js (Turbopack)0.044s1.005s0.961s101.48x
🐘 PostgresNitro0.050s (-47.7% 🟢)1.012s (-3.0%)0.962s101.67x
🐘 PostgresNext.js (Turbopack)0.062s1.014s0.952s102.07x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.071s (-5.3% 🟢)2.006s (~)0.935s101.00x
💻 LocalExpress1.073s (-4.7%)2.006s (~)0.934s101.00x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.008s (~)0.928s101.01x
🐘 PostgresNitro1.081s (-5.2% 🟢)2.008s (~)0.927s101.01x
💻 LocalNext.js (Turbopack)1.091s2.005s0.914s101.02x
🐘 PostgresNext.js (Turbopack)1.126s2.009s0.883s101.05x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.415s (-4.9%)11.021s (~)0.606s31.00x
🐘 PostgresNitro10.416s (-4.2%)11.015s (~)0.599s31.00x
🐘 PostgresExpress10.417s (-5.0%)11.014s (~)0.596s31.00x
💻 LocalExpress10.428s (-4.5%)11.022s (~)0.594s31.00x
💻 LocalNext.js (Turbopack)10.551s11.021s0.470s31.01x
🐘 PostgresNext.js (Turbopack)10.723s11.019s0.296s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.443s (-7.9% 🟢)14.014s (-6.7% 🟢)0.571s51.00x
🐘 PostgresExpress13.457s (-7.7% 🟢)14.015s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.468s (-10.0% 🟢)14.026s (-6.7% 🟢)0.558s51.00x
💻 LocalNitro13.479s (-10.5% 🟢)14.026s (-12.5% 🟢)0.547s51.00x
💻 LocalNext.js (Turbopack)13.739s14.028s0.289s51.02x
🐘 PostgresNext.js (Turbopack)14.181s15.021s0.840s41.05x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.897s (-28.3% 🟢)12.146s (-28.7% 🟢)0.249s81.00x
🐘 PostgresExpress11.904s (-15.0% 🟢)12.015s (-17.7% 🟢)0.110s81.00x
💻 LocalNitro11.919s (-29.0% 🟢)12.147s (-28.7% 🟢)0.228s81.00x
🐘 PostgresNitro11.970s (-14.3% 🟢)12.393s (-13.4% 🟢)0.423s81.01x
💻 LocalNext.js (Turbopack)12.433s13.023s0.590s71.05x
🐘 PostgresNext.js (Turbopack)13.115s14.016s0.901s71.10x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (-9.9% 🟢)2.007s (~)0.871s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.008s (~)0.862s151.01x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.175s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.207s2.007s0.799s151.06x
💻 LocalNext.js (Turbopack)1.228s2.005s0.777s151.08x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.816s151.00x
🐘 PostgresExpress1.192s (-49.5% 🟢)2.007s (-33.3% 🟢)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.363s2.074s0.711s151.14x
💻 LocalExpress1.729s (-41.4% 🟢)2.005s (-41.9% 🟢)0.276s151.45x
💻 LocalNext.js (Turbopack)1.762s2.006s0.244s151.48x
💻 LocalNitro1.775s (-43.5% 🟢)2.073s (-46.6% 🟢)0.298s151.49x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.283s (-63.2% 🟢)2.007s (-50.0% 🟢)0.723s151.00x
🐘 PostgresNitro1.320s (-62.1% 🟢)2.010s (-49.9% 🟢)0.690s151.03x
🐘 PostgresNext.js (Turbopack)1.645s2.008s0.363s151.28x
💻 LocalNext.js (Turbopack)4.691s5.010s0.319s63.66x
💻 LocalExpress4.998s (-40.1% 🟢)5.512s (-38.9% 🟢)0.514s63.89x
💻 LocalNitro5.620s (-32.7% 🟢)6.214s (-31.1% 🟢)0.594s54.38x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.135s (-9.7% 🟢)2.008s (~)0.873s151.00x
🐘 PostgresNitro1.143s (-9.1% 🟢)2.007s (~)0.864s151.01x
🐘 PostgresNext.js (Turbopack)1.216s2.007s0.791s151.07x
💻 LocalNext.js (Turbopack)1.306s2.006s0.700s151.15x
💻 LocalNitro1.384s (-25.8% 🟢)2.006s (-14.3% 🟢)0.622s151.22x
💻 LocalExpress1.417s (-25.2% 🟢)2.006s (-15.1% 🟢)0.589s151.25x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.185s (-49.4% 🟢)2.007s (-33.3% 🟢)0.822s151.00x
🐘 PostgresNitro1.194s (-49.0% 🟢)2.009s (-33.3% 🟢)0.815s151.01x
🐘 PostgresNext.js (Turbopack)1.333s2.008s0.675s151.12x
💻 LocalNext.js (Turbopack)1.934s2.315s0.381s131.63x
💻 LocalExpress1.991s (-36.4% 🟢)2.472s (-34.3% 🟢)0.481s131.68x
💻 LocalNitro2.106s (-31.3% 🟢)2.592s (-33.3% 🟢)0.486s121.78x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.273s (-63.6% 🟢)2.006s (-50.0% 🟢)0.733s151.00x
🐘 PostgresNitro1.319s (-62.1% 🟢)2.009s (-49.9% 🟢)0.689s151.04x
🐘 PostgresNext.js (Turbopack)1.631s2.009s0.378s151.28x
💻 LocalNext.js (Turbopack)4.718s5.178s0.460s63.71x
💻 LocalExpress5.503s (-37.5% 🟢)6.014s (-35.1% 🟢)0.511s54.32x
💻 LocalNitro6.005s (-34.3% 🟢)6.614s (-34.0% 🟢)0.609s54.72x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.436s (-46.8% 🟢)1.007s (~)0.570s601.00x
🐘 PostgresExpress0.437s (-47.9% 🟢)1.006s (-1.7%)0.569s601.00x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.6% 🟢)0.526s591.13x
💻 LocalExpress0.502s (-49.0% 🟢)1.004s (-6.7% 🟢)0.502s601.15x
💻 LocalNext.js (Turbopack)0.562s1.004s0.443s601.29x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.330s601.55x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.064s (-46.1% 🟢)1.586s (-29.8% 🟢)0.521s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.844s (-12.2% 🟢)0.750s491.03x
💻 LocalExpress1.196s (-60.3% 🟢)2.005s (-44.1% 🟢)0.809s451.12x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.12x
💻 LocalNext.js (Turbopack)1.384s2.005s0.621s451.30x
🐘 PostgresNext.js (Turbopack)1.624s2.008s0.384s451.53x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.035s (-49.0% 🟢)2.507s (-42.6% 🟢)0.472s481.00x
🐘 PostgresNitro2.100s (-48.8% 🟢)2.616s (-43.2% 🟢)0.516s461.03x
💻 LocalNitro2.665s (-71.3% 🟢)3.007s (-70.0% 🟢)0.343s401.31x
💻 LocalExpress2.712s (-70.6% 🟢)3.057s (-69.5% 🟢)0.346s401.33x
💻 LocalNext.js (Turbopack)3.068s3.644s0.577s331.51x
🐘 PostgresNext.js (Turbopack)3.214s4.010s0.797s301.58x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.166s (-41.3% 🟢)1.005s (~)0.839s601.00x
🐘 PostgresNitro0.174s (-38.4% 🟢)1.006s (~)0.831s601.05x
🐘 PostgresNext.js (Turbopack)0.230s1.006s0.776s601.39x
💻 LocalNitro0.373s (-38.4% 🟢)1.004s (-1.7%)0.632s602.25x
💻 LocalExpress0.389s (-30.5% 🟢)1.004s (~)0.615s602.35x
💻 LocalNext.js (Turbopack)0.478s1.004s0.526s602.88x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-45.4% 🟢)1.006s (~)0.728s901.00x
🐘 PostgresNitro0.291s (-41.4% 🟢)1.006s (~)0.715s901.05x
🐘 PostgresNext.js (Turbopack)0.467s1.007s0.540s901.68x
💻 LocalNitro2.123s (-16.4% 🟢)2.821s (-6.3% 🟢)0.698s327.63x
💻 LocalExpress2.132s (-15.2% 🟢)2.685s (-10.8% 🟢)0.552s347.66x
💻 LocalNext.js (Turbopack)2.211s2.853s0.641s327.95x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.563s (-31.2% 🟢)1.005s (-1.2%)0.442s1201.00x
🐘 PostgresNitro0.608s (-23.0% 🟢)1.006s (~)0.398s1201.08x
🐘 PostgresNext.js (Turbopack)1.011s1.774s0.764s681.79x
💻 LocalExpress9.851s (-12.0% 🟢)10.441s (-12.6% 🟢)0.590s1217.49x
💻 LocalNitro10.119s (-9.6% 🟢)10.609s (-9.0% 🟢)0.491s1217.97x
💻 LocalNext.js (Turbopack)10.406s11.027s0.621s1118.48x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.126s (+449.4% 🔺)2.000s (+100.1% 🔺)0.002s (+6.7% 🔺)2.011s (+98.8% 🔺)0.884s101.00x
🐘 PostgresExpress1.129s (+450.5% 🔺)2.001s (+100.4% 🔺)0.001s (-25.0% 🟢)2.009s (+98.6% 🔺)0.880s101.00x
💻 LocalNitro1.131s (+429.1% 🔺)2.005s (+99.6% 🔺)0.013s (~)2.020s (+98.2% 🔺)0.889s101.00x
💻 LocalExpress1.133s (+469.1% 🔺)2.006s (+99.7% 🔺)0.012s (-1.7%)2.020s (+98.4% 🔺)0.887s101.01x
💻 LocalNext.js (Turbopack)1.153s2.004s0.011s2.018s0.865s101.02x
🐘 PostgresNext.js (Turbopack)1.213s2.002s0.001s2.011s0.798s101.08x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.499s (+140.2% 🔺)2.005s (+99.2% 🔺)0.004s (-0.9%)2.024s (+98.0% 🔺)0.525s301.00x
🐘 PostgresExpress1.499s (+138.0% 🔺)2.005s (+99.2% 🔺)0.003s (-12.1% 🟢)2.023s (+97.7% 🔺)0.523s301.00x
💻 LocalNext.js (Turbopack)1.567s2.009s0.010s2.022s0.454s301.05x
💻 LocalNitro1.700s (+102.7% 🔺)2.011s (+98.7% 🔺)0.011s (+15.1% 🔺)2.203s (+97.4% 🔺)0.503s281.13x
💻 LocalExpress1.706s (+125.3% 🔺)2.010s (+95.4% 🔺)0.010s (+5.8% 🔺)2.201s (+111.7% 🔺)0.495s281.14x
🐘 PostgresNext.js (Turbopack)1.773s2.011s0.004s2.028s0.254s301.18x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.634s (-34.0% 🟢)1.053s (-17.6% 🟢)0.000s (-19.3% 🟢)1.060s (-18.9% 🟢)0.426s571.00x
🐘 PostgresNitro0.637s (-34.2% 🟢)1.025s (-17.8% 🟢)0.000s (+24.1% 🔺)1.048s (-16.7% 🟢)0.410s581.01x
🐘 PostgresNext.js (Turbopack)0.811s1.036s0.000s1.054s0.243s571.28x
💻 LocalNitro1.309s (+7.0% 🔺)2.014s (~)0.000s (~)2.016s (~)0.708s302.06x
💻 LocalExpress1.333s (+8.8% 🔺)2.014s (~)0.000s (-80.0% 🟢)2.016s (~)0.683s302.10x
💻 LocalNext.js (Turbopack)1.383s2.013s0.001s2.017s0.634s302.18x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.254s (-30.0% 🟢)1.994s (-6.9% 🟢)0.000s (+86.7% 🔺)2.042s (-6.1% 🟢)0.788s301.00x
🐘 PostgresExpress1.294s (-27.0% 🟢)2.032s (-6.7% 🟢)0.000s (NaN%)2.041s (-7.2% 🟢)0.747s301.03x
🐘 PostgresNext.js (Turbopack)1.738s2.350s0.000s2.358s0.620s261.39x
💻 LocalNext.js (Turbopack)2.668s3.290s0.000s3.293s0.625s192.13x
💻 LocalExpress3.058s (-11.8% 🟢)3.840s (-4.8%)0.000s (-45.3% 🟢)3.844s (-4.8%)0.786s162.44x
💻 LocalNitro3.065s (-9.5% 🟢)3.676s (-8.8% 🟢)0.001s (+10.3% 🔺)3.679s (-8.8% 🟢)0.614s172.44x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres11/21
Nitro🐘 Postgres17/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.

CopilotAI 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.

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
FileDescription
packages/core/src/runtime/constants.tsAdds replay-timeout env override + bounds constants; keeps default at 240s.
packages/core/src/runtime/constants.test.tsUnit tests for parsing/clamping/fallback behavior of getReplayTimeoutMs().
packages/core/src/runtime.tsImplements replay-budget bookkeeping and excludes inline step execution time from the budget.
packages/core/src/describe-error.tsUpdates the REPLAY_TIMEOUT hint to reflect new semantics and env override.
packages/core/src/describe-error.test.tsUpdates assertions/snapshots for the revised replay-timeout hint.
.changeset/replay-timeout-excludes-step-bodies.mdPatch 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 threadpackages/core/src/runtime.ts Outdated
Comment thread.changeset/replay-timeout-excludes-step-bodies.md Outdated
Comment threadpackages/core/src/describe-error.test.ts Outdated

@pranaygppranaygp 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.

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 threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime/constants.ts Outdated
Comment threadpackages/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)

@VaguelySeriousVaguelySerious 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.

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

…ep-bodies
# Conflicts:
#	packages/core/src/runtime.ts
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2a446af (AI decision).

The PR author explicitly states this bug is v5-only: in v4 (stable) step bodies ran in a separate steps function with its own maxDuration ceiling and were not subject to the replay-timeout wrapper, so the regression being fixed does not exist on stable. Additionally, the fix introduces a new optional World.processExitTriggersQueueRedelivery capability that builds on v5 runtime architecture, making it inappropriate for backport.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2a446af517dbb91ae959adade1d74ef0428a2b09

Sign up for freeto 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

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [core] Exclude inline step execution from replay timeout by TooTallNate · Pull Request #2013 · vercel/workflow · GitHub
Skip to content

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

Merged
TooTallNate merged 6 commits into
mainfrom
fix/replay-timeout-step-bodies
May 22, 2026
Merged

[core] Exclude inline step execution from replay timeout#2013
TooTallNate merged 6 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.
CopilotAI review requested due to automatic review settings May 18, 2026 21:35
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 18, 2026 21:35
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cf51e9

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-32.5% 🟢)1.005s (~)0.975s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.04x
🐘 PostgresExpress0.044s (-24.0% 🟢)1.011s (~)0.967s101.47x
💻 LocalNext.js (Turbopack)0.044s1.005s0.961s101.48x
🐘 PostgresNitro0.050s (-47.7% 🟢)1.012s (-3.0%)0.962s101.67x
🐘 PostgresNext.js (Turbopack)0.062s1.014s0.952s102.07x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.071s (-5.3% 🟢)2.006s (~)0.935s101.00x
💻 LocalExpress1.073s (-4.7%)2.006s (~)0.934s101.00x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.008s (~)0.928s101.01x
🐘 PostgresNitro1.081s (-5.2% 🟢)2.008s (~)0.927s101.01x
💻 LocalNext.js (Turbopack)1.091s2.005s0.914s101.02x
🐘 PostgresNext.js (Turbopack)1.126s2.009s0.883s101.05x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.415s (-4.9%)11.021s (~)0.606s31.00x
🐘 PostgresNitro10.416s (-4.2%)11.015s (~)0.599s31.00x
🐘 PostgresExpress10.417s (-5.0%)11.014s (~)0.596s31.00x
💻 LocalExpress10.428s (-4.5%)11.022s (~)0.594s31.00x
💻 LocalNext.js (Turbopack)10.551s11.021s0.470s31.01x
🐘 PostgresNext.js (Turbopack)10.723s11.019s0.296s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.443s (-7.9% 🟢)14.014s (-6.7% 🟢)0.571s51.00x
🐘 PostgresExpress13.457s (-7.7% 🟢)14.015s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.468s (-10.0% 🟢)14.026s (-6.7% 🟢)0.558s51.00x
💻 LocalNitro13.479s (-10.5% 🟢)14.026s (-12.5% 🟢)0.547s51.00x
💻 LocalNext.js (Turbopack)13.739s14.028s0.289s51.02x
🐘 PostgresNext.js (Turbopack)14.181s15.021s0.840s41.05x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.897s (-28.3% 🟢)12.146s (-28.7% 🟢)0.249s81.00x
🐘 PostgresExpress11.904s (-15.0% 🟢)12.015s (-17.7% 🟢)0.110s81.00x
💻 LocalNitro11.919s (-29.0% 🟢)12.147s (-28.7% 🟢)0.228s81.00x
🐘 PostgresNitro11.970s (-14.3% 🟢)12.393s (-13.4% 🟢)0.423s81.01x
💻 LocalNext.js (Turbopack)12.433s13.023s0.590s71.05x
🐘 PostgresNext.js (Turbopack)13.115s14.016s0.901s71.10x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (-9.9% 🟢)2.007s (~)0.871s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.008s (~)0.862s151.01x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.175s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.207s2.007s0.799s151.06x
💻 LocalNext.js (Turbopack)1.228s2.005s0.777s151.08x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.816s151.00x
🐘 PostgresExpress1.192s (-49.5% 🟢)2.007s (-33.3% 🟢)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.363s2.074s0.711s151.14x
💻 LocalExpress1.729s (-41.4% 🟢)2.005s (-41.9% 🟢)0.276s151.45x
💻 LocalNext.js (Turbopack)1.762s2.006s0.244s151.48x
💻 LocalNitro1.775s (-43.5% 🟢)2.073s (-46.6% 🟢)0.298s151.49x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.283s (-63.2% 🟢)2.007s (-50.0% 🟢)0.723s151.00x
🐘 PostgresNitro1.320s (-62.1% 🟢)2.010s (-49.9% 🟢)0.690s151.03x
🐘 PostgresNext.js (Turbopack)1.645s2.008s0.363s151.28x
💻 LocalNext.js (Turbopack)4.691s5.010s0.319s63.66x
💻 LocalExpress4.998s (-40.1% 🟢)5.512s (-38.9% 🟢)0.514s63.89x
💻 LocalNitro5.620s (-32.7% 🟢)6.214s (-31.1% 🟢)0.594s54.38x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.135s (-9.7% 🟢)2.008s (~)0.873s151.00x
🐘 PostgresNitro1.143s (-9.1% 🟢)2.007s (~)0.864s151.01x
🐘 PostgresNext.js (Turbopack)1.216s2.007s0.791s151.07x
💻 LocalNext.js (Turbopack)1.306s2.006s0.700s151.15x
💻 LocalNitro1.384s (-25.8% 🟢)2.006s (-14.3% 🟢)0.622s151.22x
💻 LocalExpress1.417s (-25.2% 🟢)2.006s (-15.1% 🟢)0.589s151.25x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.185s (-49.4% 🟢)2.007s (-33.3% 🟢)0.822s151.00x
🐘 PostgresNitro1.194s (-49.0% 🟢)2.009s (-33.3% 🟢)0.815s151.01x
🐘 PostgresNext.js (Turbopack)1.333s2.008s0.675s151.12x
💻 LocalNext.js (Turbopack)1.934s2.315s0.381s131.63x
💻 LocalExpress1.991s (-36.4% 🟢)2.472s (-34.3% 🟢)0.481s131.68x
💻 LocalNitro2.106s (-31.3% 🟢)2.592s (-33.3% 🟢)0.486s121.78x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.273s (-63.6% 🟢)2.006s (-50.0% 🟢)0.733s151.00x
🐘 PostgresNitro1.319s (-62.1% 🟢)2.009s (-49.9% 🟢)0.689s151.04x
🐘 PostgresNext.js (Turbopack)1.631s2.009s0.378s151.28x
💻 LocalNext.js (Turbopack)4.718s5.178s0.460s63.71x
💻 LocalExpress5.503s (-37.5% 🟢)6.014s (-35.1% 🟢)0.511s54.32x
💻 LocalNitro6.005s (-34.3% 🟢)6.614s (-34.0% 🟢)0.609s54.72x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.436s (-46.8% 🟢)1.007s (~)0.570s601.00x
🐘 PostgresExpress0.437s (-47.9% 🟢)1.006s (-1.7%)0.569s601.00x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.6% 🟢)0.526s591.13x
💻 LocalExpress0.502s (-49.0% 🟢)1.004s (-6.7% 🟢)0.502s601.15x
💻 LocalNext.js (Turbopack)0.562s1.004s0.443s601.29x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.330s601.55x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.064s (-46.1% 🟢)1.586s (-29.8% 🟢)0.521s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.844s (-12.2% 🟢)0.750s491.03x
💻 LocalExpress1.196s (-60.3% 🟢)2.005s (-44.1% 🟢)0.809s451.12x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.12x
💻 LocalNext.js (Turbopack)1.384s2.005s0.621s451.30x
🐘 PostgresNext.js (Turbopack)1.624s2.008s0.384s451.53x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.035s (-49.0% 🟢)2.507s (-42.6% 🟢)0.472s481.00x
🐘 PostgresNitro2.100s (-48.8% 🟢)2.616s (-43.2% 🟢)0.516s461.03x
💻 LocalNitro2.665s (-71.3% 🟢)3.007s (-70.0% 🟢)0.343s401.31x
💻 LocalExpress2.712s (-70.6% 🟢)3.057s (-69.5% 🟢)0.346s401.33x
💻 LocalNext.js (Turbopack)3.068s3.644s0.577s331.51x
🐘 PostgresNext.js (Turbopack)3.214s4.010s0.797s301.58x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.166s (-41.3% 🟢)1.005s (~)0.839s601.00x
🐘 PostgresNitro0.174s (-38.4% 🟢)1.006s (~)0.831s601.05x
🐘 PostgresNext.js (Turbopack)0.230s1.006s0.776s601.39x
💻 LocalNitro0.373s (-38.4% 🟢)1.004s (-1.7%)0.632s602.25x
💻 LocalExpress0.389s (-30.5% 🟢)1.004s (~)0.615s602.35x
💻 LocalNext.js (Turbopack)0.478s1.004s0.526s602.88x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-45.4% 🟢)1.006s (~)0.728s901.00x
🐘 PostgresNitro0.291s (-41.4% 🟢)1.006s (~)0.715s901.05x
🐘 PostgresNext.js (Turbopack)0.467s1.007s0.540s901.68x
💻 LocalNitro2.123s (-16.4% 🟢)2.821s (-6.3% 🟢)0.698s327.63x
💻 LocalExpress2.132s (-15.2% 🟢)2.685s (-10.8% 🟢)0.552s347.66x
💻 LocalNext.js (Turbopack)2.211s2.853s0.641s327.95x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.563s (-31.2% 🟢)1.005s (-1.2%)0.442s1201.00x
🐘 PostgresNitro0.608s (-23.0% 🟢)1.006s (~)0.398s1201.08x
🐘 PostgresNext.js (Turbopack)1.011s1.774s0.764s681.79x
💻 LocalExpress9.851s (-12.0% 🟢)10.441s (-12.6% 🟢)0.590s1217.49x
💻 LocalNitro10.119s (-9.6% 🟢)10.609s (-9.0% 🟢)0.491s1217.97x
💻 LocalNext.js (Turbopack)10.406s11.027s0.621s1118.48x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.126s (+449.4% 🔺)2.000s (+100.1% 🔺)0.002s (+6.7% 🔺)2.011s (+98.8% 🔺)0.884s101.00x
🐘 PostgresExpress1.129s (+450.5% 🔺)2.001s (+100.4% 🔺)0.001s (-25.0% 🟢)2.009s (+98.6% 🔺)0.880s101.00x
💻 LocalNitro1.131s (+429.1% 🔺)2.005s (+99.6% 🔺)0.013s (~)2.020s (+98.2% 🔺)0.889s101.00x
💻 LocalExpress1.133s (+469.1% 🔺)2.006s (+99.7% 🔺)0.012s (-1.7%)2.020s (+98.4% 🔺)0.887s101.01x
💻 LocalNext.js (Turbopack)1.153s2.004s0.011s2.018s0.865s101.02x
🐘 PostgresNext.js (Turbopack)1.213s2.002s0.001s2.011s0.798s101.08x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.499s (+140.2% 🔺)2.005s (+99.2% 🔺)0.004s (-0.9%)2.024s (+98.0% 🔺)0.525s301.00x
🐘 PostgresExpress1.499s (+138.0% 🔺)2.005s (+99.2% 🔺)0.003s (-12.1% 🟢)2.023s (+97.7% 🔺)0.523s301.00x
💻 LocalNext.js (Turbopack)1.567s2.009s0.010s2.022s0.454s301.05x
💻 LocalNitro1.700s (+102.7% 🔺)2.011s (+98.7% 🔺)0.011s (+15.1% 🔺)2.203s (+97.4% 🔺)0.503s281.13x
💻 LocalExpress1.706s (+125.3% 🔺)2.010s (+95.4% 🔺)0.010s (+5.8% 🔺)2.201s (+111.7% 🔺)0.495s281.14x
🐘 PostgresNext.js (Turbopack)1.773s2.011s0.004s2.028s0.254s301.18x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.634s (-34.0% 🟢)1.053s (-17.6% 🟢)0.000s (-19.3% 🟢)1.060s (-18.9% 🟢)0.426s571.00x
🐘 PostgresNitro0.637s (-34.2% 🟢)1.025s (-17.8% 🟢)0.000s (+24.1% 🔺)1.048s (-16.7% 🟢)0.410s581.01x
🐘 PostgresNext.js (Turbopack)0.811s1.036s0.000s1.054s0.243s571.28x
💻 LocalNitro1.309s (+7.0% 🔺)2.014s (~)0.000s (~)2.016s (~)0.708s302.06x
💻 LocalExpress1.333s (+8.8% 🔺)2.014s (~)0.000s (-80.0% 🟢)2.016s (~)0.683s302.10x
💻 LocalNext.js (Turbopack)1.383s2.013s0.001s2.017s0.634s302.18x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.254s (-30.0% 🟢)1.994s (-6.9% 🟢)0.000s (+86.7% 🔺)2.042s (-6.1% 🟢)0.788s301.00x
🐘 PostgresExpress1.294s (-27.0% 🟢)2.032s (-6.7% 🟢)0.000s (NaN%)2.041s (-7.2% 🟢)0.747s301.03x
🐘 PostgresNext.js (Turbopack)1.738s2.350s0.000s2.358s0.620s261.39x
💻 LocalNext.js (Turbopack)2.668s3.290s0.000s3.293s0.625s192.13x
💻 LocalExpress3.058s (-11.8% 🟢)3.840s (-4.8%)0.000s (-45.3% 🟢)3.844s (-4.8%)0.786s162.44x
💻 LocalNitro3.065s (-9.5% 🟢)3.676s (-8.8% 🟢)0.001s (+10.3% 🔺)3.679s (-8.8% 🟢)0.614s172.44x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres11/21
Nitro🐘 Postgres17/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.

CopilotAI 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.

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
FileDescription
packages/core/src/runtime/constants.tsAdds replay-timeout env override + bounds constants; keeps default at 240s.
packages/core/src/runtime/constants.test.tsUnit tests for parsing/clamping/fallback behavior of getReplayTimeoutMs().
packages/core/src/runtime.tsImplements replay-budget bookkeeping and excludes inline step execution time from the budget.
packages/core/src/describe-error.tsUpdates the REPLAY_TIMEOUT hint to reflect new semantics and env override.
packages/core/src/describe-error.test.tsUpdates assertions/snapshots for the revised replay-timeout hint.
.changeset/replay-timeout-excludes-step-bodies.mdPatch 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 threadpackages/core/src/runtime.ts Outdated
Comment thread.changeset/replay-timeout-excludes-step-bodies.md Outdated
Comment threadpackages/core/src/describe-error.test.ts Outdated

@pranaygppranaygp 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.

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 threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime/constants.ts Outdated
Comment threadpackages/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)

@VaguelySeriousVaguelySerious 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.

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

…ep-bodies
# Conflicts:
#	packages/core/src/runtime.ts
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2a446af (AI decision).

The PR author explicitly states this bug is v5-only: in v4 (stable) step bodies ran in a separate steps function with its own maxDuration ceiling and were not subject to the replay-timeout wrapper, so the regression being fixed does not exist on stable. Additionally, the fix introduces a new optional World.processExitTriggersQueueRedelivery capability that builds on v5 runtime architecture, making it inappropriate for backport.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2a446af517dbb91ae959adade1d74ef0428a2b09

Sign up for freeto 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

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [core] Exclude inline step execution from replay timeout by TooTallNate · Pull Request #2013 · vercel/workflow · GitHub
Skip to content

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

Merged
TooTallNate merged 6 commits into
mainfrom
fix/replay-timeout-step-bodies
May 22, 2026
Merged

[core] Exclude inline step execution from replay timeout#2013
TooTallNate merged 6 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.
CopilotAI review requested due to automatic review settings May 18, 2026 21:35
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 18, 2026 21:35
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cf51e9

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-32.5% 🟢)1.005s (~)0.975s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.04x
🐘 PostgresExpress0.044s (-24.0% 🟢)1.011s (~)0.967s101.47x
💻 LocalNext.js (Turbopack)0.044s1.005s0.961s101.48x
🐘 PostgresNitro0.050s (-47.7% 🟢)1.012s (-3.0%)0.962s101.67x
🐘 PostgresNext.js (Turbopack)0.062s1.014s0.952s102.07x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.071s (-5.3% 🟢)2.006s (~)0.935s101.00x
💻 LocalExpress1.073s (-4.7%)2.006s (~)0.934s101.00x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.008s (~)0.928s101.01x
🐘 PostgresNitro1.081s (-5.2% 🟢)2.008s (~)0.927s101.01x
💻 LocalNext.js (Turbopack)1.091s2.005s0.914s101.02x
🐘 PostgresNext.js (Turbopack)1.126s2.009s0.883s101.05x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.415s (-4.9%)11.021s (~)0.606s31.00x
🐘 PostgresNitro10.416s (-4.2%)11.015s (~)0.599s31.00x
🐘 PostgresExpress10.417s (-5.0%)11.014s (~)0.596s31.00x
💻 LocalExpress10.428s (-4.5%)11.022s (~)0.594s31.00x
💻 LocalNext.js (Turbopack)10.551s11.021s0.470s31.01x
🐘 PostgresNext.js (Turbopack)10.723s11.019s0.296s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.443s (-7.9% 🟢)14.014s (-6.7% 🟢)0.571s51.00x
🐘 PostgresExpress13.457s (-7.7% 🟢)14.015s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.468s (-10.0% 🟢)14.026s (-6.7% 🟢)0.558s51.00x
💻 LocalNitro13.479s (-10.5% 🟢)14.026s (-12.5% 🟢)0.547s51.00x
💻 LocalNext.js (Turbopack)13.739s14.028s0.289s51.02x
🐘 PostgresNext.js (Turbopack)14.181s15.021s0.840s41.05x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.897s (-28.3% 🟢)12.146s (-28.7% 🟢)0.249s81.00x
🐘 PostgresExpress11.904s (-15.0% 🟢)12.015s (-17.7% 🟢)0.110s81.00x
💻 LocalNitro11.919s (-29.0% 🟢)12.147s (-28.7% 🟢)0.228s81.00x
🐘 PostgresNitro11.970s (-14.3% 🟢)12.393s (-13.4% 🟢)0.423s81.01x
💻 LocalNext.js (Turbopack)12.433s13.023s0.590s71.05x
🐘 PostgresNext.js (Turbopack)13.115s14.016s0.901s71.10x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (-9.9% 🟢)2.007s (~)0.871s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.008s (~)0.862s151.01x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.175s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.207s2.007s0.799s151.06x
💻 LocalNext.js (Turbopack)1.228s2.005s0.777s151.08x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.816s151.00x
🐘 PostgresExpress1.192s (-49.5% 🟢)2.007s (-33.3% 🟢)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.363s2.074s0.711s151.14x
💻 LocalExpress1.729s (-41.4% 🟢)2.005s (-41.9% 🟢)0.276s151.45x
💻 LocalNext.js (Turbopack)1.762s2.006s0.244s151.48x
💻 LocalNitro1.775s (-43.5% 🟢)2.073s (-46.6% 🟢)0.298s151.49x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.283s (-63.2% 🟢)2.007s (-50.0% 🟢)0.723s151.00x
🐘 PostgresNitro1.320s (-62.1% 🟢)2.010s (-49.9% 🟢)0.690s151.03x
🐘 PostgresNext.js (Turbopack)1.645s2.008s0.363s151.28x
💻 LocalNext.js (Turbopack)4.691s5.010s0.319s63.66x
💻 LocalExpress4.998s (-40.1% 🟢)5.512s (-38.9% 🟢)0.514s63.89x
💻 LocalNitro5.620s (-32.7% 🟢)6.214s (-31.1% 🟢)0.594s54.38x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.135s (-9.7% 🟢)2.008s (~)0.873s151.00x
🐘 PostgresNitro1.143s (-9.1% 🟢)2.007s (~)0.864s151.01x
🐘 PostgresNext.js (Turbopack)1.216s2.007s0.791s151.07x
💻 LocalNext.js (Turbopack)1.306s2.006s0.700s151.15x
💻 LocalNitro1.384s (-25.8% 🟢)2.006s (-14.3% 🟢)0.622s151.22x
💻 LocalExpress1.417s (-25.2% 🟢)2.006s (-15.1% 🟢)0.589s151.25x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.185s (-49.4% 🟢)2.007s (-33.3% 🟢)0.822s151.00x
🐘 PostgresNitro1.194s (-49.0% 🟢)2.009s (-33.3% 🟢)0.815s151.01x
🐘 PostgresNext.js (Turbopack)1.333s2.008s0.675s151.12x
💻 LocalNext.js (Turbopack)1.934s2.315s0.381s131.63x
💻 LocalExpress1.991s (-36.4% 🟢)2.472s (-34.3% 🟢)0.481s131.68x
💻 LocalNitro2.106s (-31.3% 🟢)2.592s (-33.3% 🟢)0.486s121.78x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.273s (-63.6% 🟢)2.006s (-50.0% 🟢)0.733s151.00x
🐘 PostgresNitro1.319s (-62.1% 🟢)2.009s (-49.9% 🟢)0.689s151.04x
🐘 PostgresNext.js (Turbopack)1.631s2.009s0.378s151.28x
💻 LocalNext.js (Turbopack)4.718s5.178s0.460s63.71x
💻 LocalExpress5.503s (-37.5% 🟢)6.014s (-35.1% 🟢)0.511s54.32x
💻 LocalNitro6.005s (-34.3% 🟢)6.614s (-34.0% 🟢)0.609s54.72x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.436s (-46.8% 🟢)1.007s (~)0.570s601.00x
🐘 PostgresExpress0.437s (-47.9% 🟢)1.006s (-1.7%)0.569s601.00x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.6% 🟢)0.526s591.13x
💻 LocalExpress0.502s (-49.0% 🟢)1.004s (-6.7% 🟢)0.502s601.15x
💻 LocalNext.js (Turbopack)0.562s1.004s0.443s601.29x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.330s601.55x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.064s (-46.1% 🟢)1.586s (-29.8% 🟢)0.521s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.844s (-12.2% 🟢)0.750s491.03x
💻 LocalExpress1.196s (-60.3% 🟢)2.005s (-44.1% 🟢)0.809s451.12x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.12x
💻 LocalNext.js (Turbopack)1.384s2.005s0.621s451.30x
🐘 PostgresNext.js (Turbopack)1.624s2.008s0.384s451.53x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.035s (-49.0% 🟢)2.507s (-42.6% 🟢)0.472s481.00x
🐘 PostgresNitro2.100s (-48.8% 🟢)2.616s (-43.2% 🟢)0.516s461.03x
💻 LocalNitro2.665s (-71.3% 🟢)3.007s (-70.0% 🟢)0.343s401.31x
💻 LocalExpress2.712s (-70.6% 🟢)3.057s (-69.5% 🟢)0.346s401.33x
💻 LocalNext.js (Turbopack)3.068s3.644s0.577s331.51x
🐘 PostgresNext.js (Turbopack)3.214s4.010s0.797s301.58x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.166s (-41.3% 🟢)1.005s (~)0.839s601.00x
🐘 PostgresNitro0.174s (-38.4% 🟢)1.006s (~)0.831s601.05x
🐘 PostgresNext.js (Turbopack)0.230s1.006s0.776s601.39x
💻 LocalNitro0.373s (-38.4% 🟢)1.004s (-1.7%)0.632s602.25x
💻 LocalExpress0.389s (-30.5% 🟢)1.004s (~)0.615s602.35x
💻 LocalNext.js (Turbopack)0.478s1.004s0.526s602.88x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-45.4% 🟢)1.006s (~)0.728s901.00x
🐘 PostgresNitro0.291s (-41.4% 🟢)1.006s (~)0.715s901.05x
🐘 PostgresNext.js (Turbopack)0.467s1.007s0.540s901.68x
💻 LocalNitro2.123s (-16.4% 🟢)2.821s (-6.3% 🟢)0.698s327.63x
💻 LocalExpress2.132s (-15.2% 🟢)2.685s (-10.8% 🟢)0.552s347.66x
💻 LocalNext.js (Turbopack)2.211s2.853s0.641s327.95x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.563s (-31.2% 🟢)1.005s (-1.2%)0.442s1201.00x
🐘 PostgresNitro0.608s (-23.0% 🟢)1.006s (~)0.398s1201.08x
🐘 PostgresNext.js (Turbopack)1.011s1.774s0.764s681.79x
💻 LocalExpress9.851s (-12.0% 🟢)10.441s (-12.6% 🟢)0.590s1217.49x
💻 LocalNitro10.119s (-9.6% 🟢)10.609s (-9.0% 🟢)0.491s1217.97x
💻 LocalNext.js (Turbopack)10.406s11.027s0.621s1118.48x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.126s (+449.4% 🔺)2.000s (+100.1% 🔺)0.002s (+6.7% 🔺)2.011s (+98.8% 🔺)0.884s101.00x
🐘 PostgresExpress1.129s (+450.5% 🔺)2.001s (+100.4% 🔺)0.001s (-25.0% 🟢)2.009s (+98.6% 🔺)0.880s101.00x
💻 LocalNitro1.131s (+429.1% 🔺)2.005s (+99.6% 🔺)0.013s (~)2.020s (+98.2% 🔺)0.889s101.00x
💻 LocalExpress1.133s (+469.1% 🔺)2.006s (+99.7% 🔺)0.012s (-1.7%)2.020s (+98.4% 🔺)0.887s101.01x
💻 LocalNext.js (Turbopack)1.153s2.004s0.011s2.018s0.865s101.02x
🐘 PostgresNext.js (Turbopack)1.213s2.002s0.001s2.011s0.798s101.08x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.499s (+140.2% 🔺)2.005s (+99.2% 🔺)0.004s (-0.9%)2.024s (+98.0% 🔺)0.525s301.00x
🐘 PostgresExpress1.499s (+138.0% 🔺)2.005s (+99.2% 🔺)0.003s (-12.1% 🟢)2.023s (+97.7% 🔺)0.523s301.00x
💻 LocalNext.js (Turbopack)1.567s2.009s0.010s2.022s0.454s301.05x
💻 LocalNitro1.700s (+102.7% 🔺)2.011s (+98.7% 🔺)0.011s (+15.1% 🔺)2.203s (+97.4% 🔺)0.503s281.13x
💻 LocalExpress1.706s (+125.3% 🔺)2.010s (+95.4% 🔺)0.010s (+5.8% 🔺)2.201s (+111.7% 🔺)0.495s281.14x
🐘 PostgresNext.js (Turbopack)1.773s2.011s0.004s2.028s0.254s301.18x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.634s (-34.0% 🟢)1.053s (-17.6% 🟢)0.000s (-19.3% 🟢)1.060s (-18.9% 🟢)0.426s571.00x
🐘 PostgresNitro0.637s (-34.2% 🟢)1.025s (-17.8% 🟢)0.000s (+24.1% 🔺)1.048s (-16.7% 🟢)0.410s581.01x
🐘 PostgresNext.js (Turbopack)0.811s1.036s0.000s1.054s0.243s571.28x
💻 LocalNitro1.309s (+7.0% 🔺)2.014s (~)0.000s (~)2.016s (~)0.708s302.06x
💻 LocalExpress1.333s (+8.8% 🔺)2.014s (~)0.000s (-80.0% 🟢)2.016s (~)0.683s302.10x
💻 LocalNext.js (Turbopack)1.383s2.013s0.001s2.017s0.634s302.18x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.254s (-30.0% 🟢)1.994s (-6.9% 🟢)0.000s (+86.7% 🔺)2.042s (-6.1% 🟢)0.788s301.00x
🐘 PostgresExpress1.294s (-27.0% 🟢)2.032s (-6.7% 🟢)0.000s (NaN%)2.041s (-7.2% 🟢)0.747s301.03x
🐘 PostgresNext.js (Turbopack)1.738s2.350s0.000s2.358s0.620s261.39x
💻 LocalNext.js (Turbopack)2.668s3.290s0.000s3.293s0.625s192.13x
💻 LocalExpress3.058s (-11.8% 🟢)3.840s (-4.8%)0.000s (-45.3% 🟢)3.844s (-4.8%)0.786s162.44x
💻 LocalNitro3.065s (-9.5% 🟢)3.676s (-8.8% 🟢)0.001s (+10.3% 🔺)3.679s (-8.8% 🟢)0.614s172.44x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres11/21
Nitro🐘 Postgres17/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.

CopilotAI 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.

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
FileDescription
packages/core/src/runtime/constants.tsAdds replay-timeout env override + bounds constants; keeps default at 240s.
packages/core/src/runtime/constants.test.tsUnit tests for parsing/clamping/fallback behavior of getReplayTimeoutMs().
packages/core/src/runtime.tsImplements replay-budget bookkeeping and excludes inline step execution time from the budget.
packages/core/src/describe-error.tsUpdates the REPLAY_TIMEOUT hint to reflect new semantics and env override.
packages/core/src/describe-error.test.tsUpdates assertions/snapshots for the revised replay-timeout hint.
.changeset/replay-timeout-excludes-step-bodies.mdPatch 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 threadpackages/core/src/runtime.ts Outdated
Comment thread.changeset/replay-timeout-excludes-step-bodies.md Outdated
Comment threadpackages/core/src/describe-error.test.ts Outdated

@pranaygppranaygp 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.

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 threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime/constants.ts Outdated
Comment threadpackages/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)

@VaguelySeriousVaguelySerious 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.

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

…ep-bodies
# Conflicts:
#	packages/core/src/runtime.ts
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2a446af (AI decision).

The PR author explicitly states this bug is v5-only: in v4 (stable) step bodies ran in a separate steps function with its own maxDuration ceiling and were not subject to the replay-timeout wrapper, so the regression being fixed does not exist on stable. Additionally, the fix introduces a new optional World.processExitTriggersQueueRedelivery capability that builds on v5 runtime architecture, making it inappropriate for backport.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2a446af517dbb91ae959adade1d74ef0428a2b09

Sign up for freeto 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

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [core] Exclude inline step execution from replay timeout by TooTallNate · Pull Request #2013 · vercel/workflow · GitHub
Skip to content

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

Merged
TooTallNate merged 6 commits into
mainfrom
fix/replay-timeout-step-bodies
May 22, 2026
Merged

[core] Exclude inline step execution from replay timeout#2013
TooTallNate merged 6 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.
CopilotAI review requested due to automatic review settings May 18, 2026 21:35
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 18, 2026 21:35
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cf51e9

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-32.5% 🟢)1.005s (~)0.975s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.04x
🐘 PostgresExpress0.044s (-24.0% 🟢)1.011s (~)0.967s101.47x
💻 LocalNext.js (Turbopack)0.044s1.005s0.961s101.48x
🐘 PostgresNitro0.050s (-47.7% 🟢)1.012s (-3.0%)0.962s101.67x
🐘 PostgresNext.js (Turbopack)0.062s1.014s0.952s102.07x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.071s (-5.3% 🟢)2.006s (~)0.935s101.00x
💻 LocalExpress1.073s (-4.7%)2.006s (~)0.934s101.00x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.008s (~)0.928s101.01x
🐘 PostgresNitro1.081s (-5.2% 🟢)2.008s (~)0.927s101.01x
💻 LocalNext.js (Turbopack)1.091s2.005s0.914s101.02x
🐘 PostgresNext.js (Turbopack)1.126s2.009s0.883s101.05x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.415s (-4.9%)11.021s (~)0.606s31.00x
🐘 PostgresNitro10.416s (-4.2%)11.015s (~)0.599s31.00x
🐘 PostgresExpress10.417s (-5.0%)11.014s (~)0.596s31.00x
💻 LocalExpress10.428s (-4.5%)11.022s (~)0.594s31.00x
💻 LocalNext.js (Turbopack)10.551s11.021s0.470s31.01x
🐘 PostgresNext.js (Turbopack)10.723s11.019s0.296s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.443s (-7.9% 🟢)14.014s (-6.7% 🟢)0.571s51.00x
🐘 PostgresExpress13.457s (-7.7% 🟢)14.015s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.468s (-10.0% 🟢)14.026s (-6.7% 🟢)0.558s51.00x
💻 LocalNitro13.479s (-10.5% 🟢)14.026s (-12.5% 🟢)0.547s51.00x
💻 LocalNext.js (Turbopack)13.739s14.028s0.289s51.02x
🐘 PostgresNext.js (Turbopack)14.181s15.021s0.840s41.05x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.897s (-28.3% 🟢)12.146s (-28.7% 🟢)0.249s81.00x
🐘 PostgresExpress11.904s (-15.0% 🟢)12.015s (-17.7% 🟢)0.110s81.00x
💻 LocalNitro11.919s (-29.0% 🟢)12.147s (-28.7% 🟢)0.228s81.00x
🐘 PostgresNitro11.970s (-14.3% 🟢)12.393s (-13.4% 🟢)0.423s81.01x
💻 LocalNext.js (Turbopack)12.433s13.023s0.590s71.05x
🐘 PostgresNext.js (Turbopack)13.115s14.016s0.901s71.10x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (-9.9% 🟢)2.007s (~)0.871s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.008s (~)0.862s151.01x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.175s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.207s2.007s0.799s151.06x
💻 LocalNext.js (Turbopack)1.228s2.005s0.777s151.08x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.816s151.00x
🐘 PostgresExpress1.192s (-49.5% 🟢)2.007s (-33.3% 🟢)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.363s2.074s0.711s151.14x
💻 LocalExpress1.729s (-41.4% 🟢)2.005s (-41.9% 🟢)0.276s151.45x
💻 LocalNext.js (Turbopack)1.762s2.006s0.244s151.48x
💻 LocalNitro1.775s (-43.5% 🟢)2.073s (-46.6% 🟢)0.298s151.49x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.283s (-63.2% 🟢)2.007s (-50.0% 🟢)0.723s151.00x
🐘 PostgresNitro1.320s (-62.1% 🟢)2.010s (-49.9% 🟢)0.690s151.03x
🐘 PostgresNext.js (Turbopack)1.645s2.008s0.363s151.28x
💻 LocalNext.js (Turbopack)4.691s5.010s0.319s63.66x
💻 LocalExpress4.998s (-40.1% 🟢)5.512s (-38.9% 🟢)0.514s63.89x
💻 LocalNitro5.620s (-32.7% 🟢)6.214s (-31.1% 🟢)0.594s54.38x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.135s (-9.7% 🟢)2.008s (~)0.873s151.00x
🐘 PostgresNitro1.143s (-9.1% 🟢)2.007s (~)0.864s151.01x
🐘 PostgresNext.js (Turbopack)1.216s2.007s0.791s151.07x
💻 LocalNext.js (Turbopack)1.306s2.006s0.700s151.15x
💻 LocalNitro1.384s (-25.8% 🟢)2.006s (-14.3% 🟢)0.622s151.22x
💻 LocalExpress1.417s (-25.2% 🟢)2.006s (-15.1% 🟢)0.589s151.25x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.185s (-49.4% 🟢)2.007s (-33.3% 🟢)0.822s151.00x
🐘 PostgresNitro1.194s (-49.0% 🟢)2.009s (-33.3% 🟢)0.815s151.01x
🐘 PostgresNext.js (Turbopack)1.333s2.008s0.675s151.12x
💻 LocalNext.js (Turbopack)1.934s2.315s0.381s131.63x
💻 LocalExpress1.991s (-36.4% 🟢)2.472s (-34.3% 🟢)0.481s131.68x
💻 LocalNitro2.106s (-31.3% 🟢)2.592s (-33.3% 🟢)0.486s121.78x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.273s (-63.6% 🟢)2.006s (-50.0% 🟢)0.733s151.00x
🐘 PostgresNitro1.319s (-62.1% 🟢)2.009s (-49.9% 🟢)0.689s151.04x
🐘 PostgresNext.js (Turbopack)1.631s2.009s0.378s151.28x
💻 LocalNext.js (Turbopack)4.718s5.178s0.460s63.71x
💻 LocalExpress5.503s (-37.5% 🟢)6.014s (-35.1% 🟢)0.511s54.32x
💻 LocalNitro6.005s (-34.3% 🟢)6.614s (-34.0% 🟢)0.609s54.72x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.436s (-46.8% 🟢)1.007s (~)0.570s601.00x
🐘 PostgresExpress0.437s (-47.9% 🟢)1.006s (-1.7%)0.569s601.00x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.6% 🟢)0.526s591.13x
💻 LocalExpress0.502s (-49.0% 🟢)1.004s (-6.7% 🟢)0.502s601.15x
💻 LocalNext.js (Turbopack)0.562s1.004s0.443s601.29x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.330s601.55x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.064s (-46.1% 🟢)1.586s (-29.8% 🟢)0.521s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.844s (-12.2% 🟢)0.750s491.03x
💻 LocalExpress1.196s (-60.3% 🟢)2.005s (-44.1% 🟢)0.809s451.12x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.12x
💻 LocalNext.js (Turbopack)1.384s2.005s0.621s451.30x
🐘 PostgresNext.js (Turbopack)1.624s2.008s0.384s451.53x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.035s (-49.0% 🟢)2.507s (-42.6% 🟢)0.472s481.00x
🐘 PostgresNitro2.100s (-48.8% 🟢)2.616s (-43.2% 🟢)0.516s461.03x
💻 LocalNitro2.665s (-71.3% 🟢)3.007s (-70.0% 🟢)0.343s401.31x
💻 LocalExpress2.712s (-70.6% 🟢)3.057s (-69.5% 🟢)0.346s401.33x
💻 LocalNext.js (Turbopack)3.068s3.644s0.577s331.51x
🐘 PostgresNext.js (Turbopack)3.214s4.010s0.797s301.58x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.166s (-41.3% 🟢)1.005s (~)0.839s601.00x
🐘 PostgresNitro0.174s (-38.4% 🟢)1.006s (~)0.831s601.05x
🐘 PostgresNext.js (Turbopack)0.230s1.006s0.776s601.39x
💻 LocalNitro0.373s (-38.4% 🟢)1.004s (-1.7%)0.632s602.25x
💻 LocalExpress0.389s (-30.5% 🟢)1.004s (~)0.615s602.35x
💻 LocalNext.js (Turbopack)0.478s1.004s0.526s602.88x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-45.4% 🟢)1.006s (~)0.728s901.00x
🐘 PostgresNitro0.291s (-41.4% 🟢)1.006s (~)0.715s901.05x
🐘 PostgresNext.js (Turbopack)0.467s1.007s0.540s901.68x
💻 LocalNitro2.123s (-16.4% 🟢)2.821s (-6.3% 🟢)0.698s327.63x
💻 LocalExpress2.132s (-15.2% 🟢)2.685s (-10.8% 🟢)0.552s347.66x
💻 LocalNext.js (Turbopack)2.211s2.853s0.641s327.95x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.563s (-31.2% 🟢)1.005s (-1.2%)0.442s1201.00x
🐘 PostgresNitro0.608s (-23.0% 🟢)1.006s (~)0.398s1201.08x
🐘 PostgresNext.js (Turbopack)1.011s1.774s0.764s681.79x
💻 LocalExpress9.851s (-12.0% 🟢)10.441s (-12.6% 🟢)0.590s1217.49x
💻 LocalNitro10.119s (-9.6% 🟢)10.609s (-9.0% 🟢)0.491s1217.97x
💻 LocalNext.js (Turbopack)10.406s11.027s0.621s1118.48x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.126s (+449.4% 🔺)2.000s (+100.1% 🔺)0.002s (+6.7% 🔺)2.011s (+98.8% 🔺)0.884s101.00x
🐘 PostgresExpress1.129s (+450.5% 🔺)2.001s (+100.4% 🔺)0.001s (-25.0% 🟢)2.009s (+98.6% 🔺)0.880s101.00x
💻 LocalNitro1.131s (+429.1% 🔺)2.005s (+99.6% 🔺)0.013s (~)2.020s (+98.2% 🔺)0.889s101.00x
💻 LocalExpress1.133s (+469.1% 🔺)2.006s (+99.7% 🔺)0.012s (-1.7%)2.020s (+98.4% 🔺)0.887s101.01x
💻 LocalNext.js (Turbopack)1.153s2.004s0.011s2.018s0.865s101.02x
🐘 PostgresNext.js (Turbopack)1.213s2.002s0.001s2.011s0.798s101.08x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.499s (+140.2% 🔺)2.005s (+99.2% 🔺)0.004s (-0.9%)2.024s (+98.0% 🔺)0.525s301.00x
🐘 PostgresExpress1.499s (+138.0% 🔺)2.005s (+99.2% 🔺)0.003s (-12.1% 🟢)2.023s (+97.7% 🔺)0.523s301.00x
💻 LocalNext.js (Turbopack)1.567s2.009s0.010s2.022s0.454s301.05x
💻 LocalNitro1.700s (+102.7% 🔺)2.011s (+98.7% 🔺)0.011s (+15.1% 🔺)2.203s (+97.4% 🔺)0.503s281.13x
💻 LocalExpress1.706s (+125.3% 🔺)2.010s (+95.4% 🔺)0.010s (+5.8% 🔺)2.201s (+111.7% 🔺)0.495s281.14x
🐘 PostgresNext.js (Turbopack)1.773s2.011s0.004s2.028s0.254s301.18x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.634s (-34.0% 🟢)1.053s (-17.6% 🟢)0.000s (-19.3% 🟢)1.060s (-18.9% 🟢)0.426s571.00x
🐘 PostgresNitro0.637s (-34.2% 🟢)1.025s (-17.8% 🟢)0.000s (+24.1% 🔺)1.048s (-16.7% 🟢)0.410s581.01x
🐘 PostgresNext.js (Turbopack)0.811s1.036s0.000s1.054s0.243s571.28x
💻 LocalNitro1.309s (+7.0% 🔺)2.014s (~)0.000s (~)2.016s (~)0.708s302.06x
💻 LocalExpress1.333s (+8.8% 🔺)2.014s (~)0.000s (-80.0% 🟢)2.016s (~)0.683s302.10x
💻 LocalNext.js (Turbopack)1.383s2.013s0.001s2.017s0.634s302.18x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.254s (-30.0% 🟢)1.994s (-6.9% 🟢)0.000s (+86.7% 🔺)2.042s (-6.1% 🟢)0.788s301.00x
🐘 PostgresExpress1.294s (-27.0% 🟢)2.032s (-6.7% 🟢)0.000s (NaN%)2.041s (-7.2% 🟢)0.747s301.03x
🐘 PostgresNext.js (Turbopack)1.738s2.350s0.000s2.358s0.620s261.39x
💻 LocalNext.js (Turbopack)2.668s3.290s0.000s3.293s0.625s192.13x
💻 LocalExpress3.058s (-11.8% 🟢)3.840s (-4.8%)0.000s (-45.3% 🟢)3.844s (-4.8%)0.786s162.44x
💻 LocalNitro3.065s (-9.5% 🟢)3.676s (-8.8% 🟢)0.001s (+10.3% 🔺)3.679s (-8.8% 🟢)0.614s172.44x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres11/21
Nitro🐘 Postgres17/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.

CopilotAI 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.

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
FileDescription
packages/core/src/runtime/constants.tsAdds replay-timeout env override + bounds constants; keeps default at 240s.
packages/core/src/runtime/constants.test.tsUnit tests for parsing/clamping/fallback behavior of getReplayTimeoutMs().
packages/core/src/runtime.tsImplements replay-budget bookkeeping and excludes inline step execution time from the budget.
packages/core/src/describe-error.tsUpdates the REPLAY_TIMEOUT hint to reflect new semantics and env override.
packages/core/src/describe-error.test.tsUpdates assertions/snapshots for the revised replay-timeout hint.
.changeset/replay-timeout-excludes-step-bodies.mdPatch 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 threadpackages/core/src/runtime.ts Outdated
Comment thread.changeset/replay-timeout-excludes-step-bodies.md Outdated
Comment threadpackages/core/src/describe-error.test.ts Outdated

@pranaygppranaygp 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.

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 threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime/constants.ts Outdated
Comment threadpackages/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)

@VaguelySeriousVaguelySerious 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.

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

…ep-bodies
# Conflicts:
#	packages/core/src/runtime.ts
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2a446af (AI decision).

The PR author explicitly states this bug is v5-only: in v4 (stable) step bodies ran in a separate steps function with its own maxDuration ceiling and were not subject to the replay-timeout wrapper, so the regression being fixed does not exist on stable. Additionally, the fix introduces a new optional World.processExitTriggersQueueRedelivery capability that builds on v5 runtime architecture, making it inappropriate for backport.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2a446af517dbb91ae959adade1d74ef0428a2b09

Sign up for freeto 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

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); [core] Exclude inline step execution from replay timeout by TooTallNate · Pull Request #2013 · vercel/workflow · GitHub
Skip to content

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

Merged
TooTallNate merged 6 commits into
mainfrom
fix/replay-timeout-step-bodies
May 22, 2026
Merged

[core] Exclude inline step execution from replay timeout#2013
TooTallNate merged 6 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.
CopilotAI review requested due to automatic review settings May 18, 2026 21:35
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 18, 2026 21:35
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1cf51e9

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.030s (-32.5% 🟢)1.005s (~)0.975s101.00x
💻 LocalNitro0.031s (-28.1% 🟢)1.006s (~)0.975s101.04x
🐘 PostgresExpress0.044s (-24.0% 🟢)1.011s (~)0.967s101.47x
💻 LocalNext.js (Turbopack)0.044s1.005s0.961s101.48x
🐘 PostgresNitro0.050s (-47.7% 🟢)1.012s (-3.0%)0.962s101.67x
🐘 PostgresNext.js (Turbopack)0.062s1.014s0.952s102.07x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.071s (-5.3% 🟢)2.006s (~)0.935s101.00x
💻 LocalExpress1.073s (-4.7%)2.006s (~)0.934s101.00x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.008s (~)0.928s101.01x
🐘 PostgresNitro1.081s (-5.2% 🟢)2.008s (~)0.927s101.01x
💻 LocalNext.js (Turbopack)1.091s2.005s0.914s101.02x
🐘 PostgresNext.js (Turbopack)1.126s2.009s0.883s101.05x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.415s (-4.9%)11.021s (~)0.606s31.00x
🐘 PostgresNitro10.416s (-4.2%)11.015s (~)0.599s31.00x
🐘 PostgresExpress10.417s (-5.0%)11.014s (~)0.596s31.00x
💻 LocalExpress10.428s (-4.5%)11.022s (~)0.594s31.00x
💻 LocalNext.js (Turbopack)10.551s11.021s0.470s31.01x
🐘 PostgresNext.js (Turbopack)10.723s11.019s0.296s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.443s (-7.9% 🟢)14.014s (-6.7% 🟢)0.571s51.00x
🐘 PostgresExpress13.457s (-7.7% 🟢)14.015s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.468s (-10.0% 🟢)14.026s (-6.7% 🟢)0.558s51.00x
💻 LocalNitro13.479s (-10.5% 🟢)14.026s (-12.5% 🟢)0.547s51.00x
💻 LocalNext.js (Turbopack)13.739s14.028s0.289s51.02x
🐘 PostgresNext.js (Turbopack)14.181s15.021s0.840s41.05x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express11.897s (-28.3% 🟢)12.146s (-28.7% 🟢)0.249s81.00x
🐘 PostgresExpress11.904s (-15.0% 🟢)12.015s (-17.7% 🟢)0.110s81.00x
💻 LocalNitro11.919s (-29.0% 🟢)12.147s (-28.7% 🟢)0.228s81.00x
🐘 PostgresNitro11.970s (-14.3% 🟢)12.393s (-13.4% 🟢)0.423s81.01x
💻 LocalNext.js (Turbopack)12.433s13.023s0.590s71.05x
🐘 PostgresNext.js (Turbopack)13.115s14.016s0.901s71.10x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (-9.9% 🟢)2.007s (~)0.871s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.008s (~)0.862s151.01x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.175s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.207s2.007s0.799s151.06x
💻 LocalNext.js (Turbopack)1.228s2.005s0.777s151.08x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.816s151.00x
🐘 PostgresExpress1.192s (-49.5% 🟢)2.007s (-33.3% 🟢)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.363s2.074s0.711s151.14x
💻 LocalExpress1.729s (-41.4% 🟢)2.005s (-41.9% 🟢)0.276s151.45x
💻 LocalNext.js (Turbopack)1.762s2.006s0.244s151.48x
💻 LocalNitro1.775s (-43.5% 🟢)2.073s (-46.6% 🟢)0.298s151.49x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.283s (-63.2% 🟢)2.007s (-50.0% 🟢)0.723s151.00x
🐘 PostgresNitro1.320s (-62.1% 🟢)2.010s (-49.9% 🟢)0.690s151.03x
🐘 PostgresNext.js (Turbopack)1.645s2.008s0.363s151.28x
💻 LocalNext.js (Turbopack)4.691s5.010s0.319s63.66x
💻 LocalExpress4.998s (-40.1% 🟢)5.512s (-38.9% 🟢)0.514s63.89x
💻 LocalNitro5.620s (-32.7% 🟢)6.214s (-31.1% 🟢)0.594s54.38x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.135s (-9.7% 🟢)2.008s (~)0.873s151.00x
🐘 PostgresNitro1.143s (-9.1% 🟢)2.007s (~)0.864s151.01x
🐘 PostgresNext.js (Turbopack)1.216s2.007s0.791s151.07x
💻 LocalNext.js (Turbopack)1.306s2.006s0.700s151.15x
💻 LocalNitro1.384s (-25.8% 🟢)2.006s (-14.3% 🟢)0.622s151.22x
💻 LocalExpress1.417s (-25.2% 🟢)2.006s (-15.1% 🟢)0.589s151.25x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.185s (-49.4% 🟢)2.007s (-33.3% 🟢)0.822s151.00x
🐘 PostgresNitro1.194s (-49.0% 🟢)2.009s (-33.3% 🟢)0.815s151.01x
🐘 PostgresNext.js (Turbopack)1.333s2.008s0.675s151.12x
💻 LocalNext.js (Turbopack)1.934s2.315s0.381s131.63x
💻 LocalExpress1.991s (-36.4% 🟢)2.472s (-34.3% 🟢)0.481s131.68x
💻 LocalNitro2.106s (-31.3% 🟢)2.592s (-33.3% 🟢)0.486s121.78x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.273s (-63.6% 🟢)2.006s (-50.0% 🟢)0.733s151.00x
🐘 PostgresNitro1.319s (-62.1% 🟢)2.009s (-49.9% 🟢)0.689s151.04x
🐘 PostgresNext.js (Turbopack)1.631s2.009s0.378s151.28x
💻 LocalNext.js (Turbopack)4.718s5.178s0.460s63.71x
💻 LocalExpress5.503s (-37.5% 🟢)6.014s (-35.1% 🟢)0.511s54.32x
💻 LocalNitro6.005s (-34.3% 🟢)6.614s (-34.0% 🟢)0.609s54.72x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.436s (-46.8% 🟢)1.007s (~)0.570s601.00x
🐘 PostgresExpress0.437s (-47.9% 🟢)1.006s (-1.7%)0.569s601.00x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.6% 🟢)0.526s591.13x
💻 LocalExpress0.502s (-49.0% 🟢)1.004s (-6.7% 🟢)0.502s601.15x
💻 LocalNext.js (Turbopack)0.562s1.004s0.443s601.29x
🐘 PostgresNext.js (Turbopack)0.675s1.006s0.330s601.55x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.064s (-46.1% 🟢)1.586s (-29.8% 🟢)0.521s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.844s (-12.2% 🟢)0.750s491.03x
💻 LocalExpress1.196s (-60.3% 🟢)2.005s (-44.1% 🟢)0.809s451.12x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.12x
💻 LocalNext.js (Turbopack)1.384s2.005s0.621s451.30x
🐘 PostgresNext.js (Turbopack)1.624s2.008s0.384s451.53x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.035s (-49.0% 🟢)2.507s (-42.6% 🟢)0.472s481.00x
🐘 PostgresNitro2.100s (-48.8% 🟢)2.616s (-43.2% 🟢)0.516s461.03x
💻 LocalNitro2.665s (-71.3% 🟢)3.007s (-70.0% 🟢)0.343s401.31x
💻 LocalExpress2.712s (-70.6% 🟢)3.057s (-69.5% 🟢)0.346s401.33x
💻 LocalNext.js (Turbopack)3.068s3.644s0.577s331.51x
🐘 PostgresNext.js (Turbopack)3.214s4.010s0.797s301.58x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.166s (-41.3% 🟢)1.005s (~)0.839s601.00x
🐘 PostgresNitro0.174s (-38.4% 🟢)1.006s (~)0.831s601.05x
🐘 PostgresNext.js (Turbopack)0.230s1.006s0.776s601.39x
💻 LocalNitro0.373s (-38.4% 🟢)1.004s (-1.7%)0.632s602.25x
💻 LocalExpress0.389s (-30.5% 🟢)1.004s (~)0.615s602.35x
💻 LocalNext.js (Turbopack)0.478s1.004s0.526s602.88x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.278s (-45.4% 🟢)1.006s (~)0.728s901.00x
🐘 PostgresNitro0.291s (-41.4% 🟢)1.006s (~)0.715s901.05x
🐘 PostgresNext.js (Turbopack)0.467s1.007s0.540s901.68x
💻 LocalNitro2.123s (-16.4% 🟢)2.821s (-6.3% 🟢)0.698s327.63x
💻 LocalExpress2.132s (-15.2% 🟢)2.685s (-10.8% 🟢)0.552s347.66x
💻 LocalNext.js (Turbopack)2.211s2.853s0.641s327.95x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.563s (-31.2% 🟢)1.005s (-1.2%)0.442s1201.00x
🐘 PostgresNitro0.608s (-23.0% 🟢)1.006s (~)0.398s1201.08x
🐘 PostgresNext.js (Turbopack)1.011s1.774s0.764s681.79x
💻 LocalExpress9.851s (-12.0% 🟢)10.441s (-12.6% 🟢)0.590s1217.49x
💻 LocalNitro10.119s (-9.6% 🟢)10.609s (-9.0% 🟢)0.491s1217.97x
💻 LocalNext.js (Turbopack)10.406s11.027s0.621s1118.48x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.126s (+449.4% 🔺)2.000s (+100.1% 🔺)0.002s (+6.7% 🔺)2.011s (+98.8% 🔺)0.884s101.00x
🐘 PostgresExpress1.129s (+450.5% 🔺)2.001s (+100.4% 🔺)0.001s (-25.0% 🟢)2.009s (+98.6% 🔺)0.880s101.00x
💻 LocalNitro1.131s (+429.1% 🔺)2.005s (+99.6% 🔺)0.013s (~)2.020s (+98.2% 🔺)0.889s101.00x
💻 LocalExpress1.133s (+469.1% 🔺)2.006s (+99.7% 🔺)0.012s (-1.7%)2.020s (+98.4% 🔺)0.887s101.01x
💻 LocalNext.js (Turbopack)1.153s2.004s0.011s2.018s0.865s101.02x
🐘 PostgresNext.js (Turbopack)1.213s2.002s0.001s2.011s0.798s101.08x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.499s (+140.2% 🔺)2.005s (+99.2% 🔺)0.004s (-0.9%)2.024s (+98.0% 🔺)0.525s301.00x
🐘 PostgresExpress1.499s (+138.0% 🔺)2.005s (+99.2% 🔺)0.003s (-12.1% 🟢)2.023s (+97.7% 🔺)0.523s301.00x
💻 LocalNext.js (Turbopack)1.567s2.009s0.010s2.022s0.454s301.05x
💻 LocalNitro1.700s (+102.7% 🔺)2.011s (+98.7% 🔺)0.011s (+15.1% 🔺)2.203s (+97.4% 🔺)0.503s281.13x
💻 LocalExpress1.706s (+125.3% 🔺)2.010s (+95.4% 🔺)0.010s (+5.8% 🔺)2.201s (+111.7% 🔺)0.495s281.14x
🐘 PostgresNext.js (Turbopack)1.773s2.011s0.004s2.028s0.254s301.18x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.634s (-34.0% 🟢)1.053s (-17.6% 🟢)0.000s (-19.3% 🟢)1.060s (-18.9% 🟢)0.426s571.00x
🐘 PostgresNitro0.637s (-34.2% 🟢)1.025s (-17.8% 🟢)0.000s (+24.1% 🔺)1.048s (-16.7% 🟢)0.410s581.01x
🐘 PostgresNext.js (Turbopack)0.811s1.036s0.000s1.054s0.243s571.28x
💻 LocalNitro1.309s (+7.0% 🔺)2.014s (~)0.000s (~)2.016s (~)0.708s302.06x
💻 LocalExpress1.333s (+8.8% 🔺)2.014s (~)0.000s (-80.0% 🟢)2.016s (~)0.683s302.10x
💻 LocalNext.js (Turbopack)1.383s2.013s0.001s2.017s0.634s302.18x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.254s (-30.0% 🟢)1.994s (-6.9% 🟢)0.000s (+86.7% 🔺)2.042s (-6.1% 🟢)0.788s301.00x
🐘 PostgresExpress1.294s (-27.0% 🟢)2.032s (-6.7% 🟢)0.000s (NaN%)2.041s (-7.2% 🟢)0.747s301.03x
🐘 PostgresNext.js (Turbopack)1.738s2.350s0.000s2.358s0.620s261.39x
💻 LocalNext.js (Turbopack)2.668s3.290s0.000s3.293s0.625s192.13x
💻 LocalExpress3.058s (-11.8% 🟢)3.840s (-4.8%)0.000s (-45.3% 🟢)3.844s (-4.8%)0.786s162.44x
💻 LocalNitro3.065s (-9.5% 🟢)3.676s (-8.8% 🟢)0.001s (+10.3% 🔺)3.679s (-8.8% 🟢)0.614s172.44x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres11/21
Nitro🐘 Postgres17/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.

CopilotAI 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.

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
FileDescription
packages/core/src/runtime/constants.tsAdds replay-timeout env override + bounds constants; keeps default at 240s.
packages/core/src/runtime/constants.test.tsUnit tests for parsing/clamping/fallback behavior of getReplayTimeoutMs().
packages/core/src/runtime.tsImplements replay-budget bookkeeping and excludes inline step execution time from the budget.
packages/core/src/describe-error.tsUpdates the REPLAY_TIMEOUT hint to reflect new semantics and env override.
packages/core/src/describe-error.test.tsUpdates assertions/snapshots for the revised replay-timeout hint.
.changeset/replay-timeout-excludes-step-bodies.mdPatch 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 threadpackages/core/src/runtime.ts Outdated
Comment thread.changeset/replay-timeout-excludes-step-bodies.md Outdated
Comment threadpackages/core/src/describe-error.test.ts Outdated

@pranaygppranaygp 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.

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 threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime.ts
Comment threadpackages/core/src/runtime/constants.ts Outdated
Comment threadpackages/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)

@VaguelySeriousVaguelySerious 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.

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

…ep-bodies
# Conflicts:
#	packages/core/src/runtime.ts
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2a446af (AI decision).

The PR author explicitly states this bug is v5-only: in v4 (stable) step bodies ran in a separate steps function with its own maxDuration ceiling and were not subject to the replay-timeout wrapper, so the regression being fixed does not exist on stable. Additionally, the fix introduces a new optional World.processExitTriggersQueueRedelivery capability that builds on v5 runtime architecture, making it inappropriate for backport.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2a446af517dbb91ae959adade1d74ef0428a2b09

Sign up for freeto 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

@TooTallNate@pranaygp@VaguelySerious