Skip to content

[codex] Forward port stale wait replay fix - #2038

Merged
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix
May 20, 2026
Merged

[codex] Forward port stale wait replay fix#2038
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix

Conversation

@pranaygp

@pranaygppranaygp commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Forward-port the stale wait replay race fix from stable PR #2029 to main.

This keeps workflow replay deterministic when an elapsed wait_completed is committed from a stale event snapshot while a concurrent hook_received event landed durably before it. On main, the fix is adapted to the V2 replay loop/event cache:

  • use cursor / hasMore metadata from preloaded run_started event snapshots
  • refresh events after attempting elapsed wait completions before replaying
  • fetch only the delta after the prior cursor when possible, with a full-reload fallback when cursor metadata is missing or unsafe
  • add the deterministic handler-level regression test for the hook-vs-sleep wait race

Forward-Port Note

This PR is itself a forward port from stable, so it should not be backported.

Validation

  • pnpm --filter @workflow/world build
  • pnpm --filter @workflow/utils build
  • pnpm --filter @workflow/errors build
  • pnpm --filter @workflow/serde build
  • pnpm --filter @workflow/world-local build
  • pnpm --filter @workflow/world-vercel build
  • pnpm --filter @workflow/core build
  • pnpm vitest run packages/core/src/runtime/wait-completion-replay.test.ts
  • pnpm vitest run packages/core/src/hook-sleep-interaction.test.ts
  • pnpm --filter @workflow/core typecheck
  • pnpm --filter @workflow/world-local typecheck
  • pnpm --filter @workflow/world-postgres typecheck
  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm changeset status --since=main
  • pnpm biome check --files-ignore-unknown=true .changeset/bright-waits-refresh.md packages/core/src/runtime.ts packages/core/src/runtime/wait-completion-replay.test.ts packages/world/src/events.ts packages/world-local/src/storage/events-storage.ts packages/world-postgres/src/storage.ts packages/world-vercel/src/events.ts

Note: local commands run under Node v25.2.1, which emits the repo's unsupported-engine warning. Biome exits successfully but reports existing warnings in the large runtime/storage files.

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2f724a

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-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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

@github-actions

github-actionsBot commented May 20, 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 20, 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.031s (-29.8% 🟢)1.006s (~)0.975s101.00x
💻 LocalNitro0.032s (-26.7% 🟢)1.005s (~)0.974s101.02x
🐘 PostgresNitro0.048s (-49.9% 🟢)1.012s (-2.9%)0.965s101.53x
💻 LocalNext.js (Turbopack)0.050s1.006s0.956s101.60x
🐘 PostgresExpress0.053s (-8.3% 🟢)1.013s (~)0.960s101.71x
🐘 PostgresNext.js (Turbopack)0.059s1.012s0.953s101.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.263s (+11.8% 🔺)2.152s (+0.7%)1.888s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (-5.0% 🟢)2.006s (~)0.931s101.00x
🐘 PostgresNitro1.076s (-5.6% 🟢)2.009s (~)0.933s101.00x
💻 LocalExpress1.076s (-4.4%)2.007s (~)0.931s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.010s (~)0.926s101.01x
💻 LocalNext.js (Turbopack)1.107s2.006s0.899s101.03x
🐘 PostgresNext.js (Turbopack)1.132s2.009s0.877s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.542s (-17.8% 🟢)3.515s (-7.7% 🟢)1.973s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.410s (-5.1% 🟢)11.016s (~)0.607s31.00x
🐘 PostgresNitro10.417s (-4.2%)11.016s (~)0.599s31.00x
💻 LocalExpress10.427s (-4.5%)11.023s (~)0.596s31.00x
💻 LocalNitro10.433s (-4.7%)11.023s (~)0.590s31.00x
💻 LocalNext.js (Turbopack)10.647s11.021s0.374s31.02x
🐘 PostgresNext.js (Turbopack)10.698s11.025s0.327s31.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.211s (-22.2% 🟢)14.873s (-25.7% 🟢)1.662s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.423s (-8.0% 🟢)14.015s (-6.7% 🟢)0.593s51.00x
🐘 PostgresExpress13.473s (-7.6% 🟢)14.016s (-6.7% 🟢)0.543s51.00x
💻 LocalNitro13.509s (-10.3% 🟢)14.028s (-12.5% 🟢)0.519s51.01x
💻 LocalExpress13.525s (-9.7% 🟢)14.029s (-6.7% 🟢)0.504s51.01x
💻 LocalNext.js (Turbopack)14.049s14.827s0.778s51.05x
🐘 PostgresNext.js (Turbopack)14.168s15.017s0.849s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.258s (-57.7% 🟢)23.028s (-56.2% 🟢)1.770s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro11.903s (-14.8% 🟢)12.143s (-15.1% 🟢)0.240s81.00x
💻 LocalNitro11.931s (-28.9% 🟢)12.021s (-29.4% 🟢)0.090s81.00x
🐘 PostgresExpress11.984s (-14.4% 🟢)12.515s (-14.2% 🟢)0.531s81.01x
💻 LocalExpress12.022s (-27.6% 🟢)12.398s (-27.2% 🟢)0.377s81.01x
💻 LocalNext.js (Turbopack)13.063s13.596s0.533s71.10x
🐘 PostgresNext.js (Turbopack)13.220s14.017s0.798s71.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.761s (-72.1% 🟢)35.436s (-71.3% 🟢)1.675s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.144s (-10.3% 🟢)2.008s (~)0.865s151.00x
🐘 PostgresExpress1.146s (-9.1% 🟢)2.006s (~)0.861s151.00x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.826s151.03x
💻 LocalExpress1.188s (-20.2% 🟢)2.006s (~)0.818s151.04x
🐘 PostgresNext.js (Turbopack)1.238s2.009s0.771s151.08x
💻 LocalNext.js (Turbopack)1.257s2.005s0.748s151.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.685s (-6.1% 🟢)4.192s (-9.3% 🟢)1.507s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.817s151.00x
🐘 PostgresExpress1.201s (-49.1% 🟢)2.007s (-33.3% 🟢)0.807s151.01x
🐘 PostgresNext.js (Turbopack)1.401s2.007s0.605s151.18x
💻 LocalNitro1.755s (-44.2% 🟢)2.005s (-48.4% 🟢)0.251s151.47x
💻 LocalNext.js (Turbopack)1.789s2.007s0.218s151.50x
💻 LocalExpress1.861s (-37.0% 🟢)2.149s (-37.8% 🟢)0.288s141.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.773s (+31.9% 🔺)6.447s (+26.2% 🔺)1.674s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.311s (-62.3% 🟢)2.008s (-49.9% 🟢)0.698s151.00x
🐘 PostgresExpress1.331s (-61.8% 🟢)2.006s (-50.0% 🟢)0.676s151.02x
🐘 PostgresNext.js (Turbopack)1.722s2.009s0.287s151.31x
💻 LocalNitro4.817s (-42.3% 🟢)5.345s (-40.7% 🟢)0.528s63.68x
💻 LocalExpress5.405s (-35.2% 🟢)6.013s (-33.4% 🟢)0.608s54.12x
💻 LocalNext.js (Turbopack)5.527s6.213s0.686s54.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.418s (+27.8% 🔺)7.587s (+23.8% 🔺)2.169s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.127s (-10.4% 🟢)2.009s (~)0.882s151.00x
🐘 PostgresExpress1.136s (-9.6% 🟢)2.008s (~)0.872s151.01x
🐘 PostgresNext.js (Turbopack)1.221s2.008s0.786s151.08x
💻 LocalNext.js (Turbopack)1.301s2.005s0.704s151.15x
💻 LocalNitro1.385s (-25.8% 🟢)2.006s (-14.3% 🟢)0.621s151.23x
💻 LocalExpress1.388s (-26.7% 🟢)2.006s (-15.1% 🟢)0.618s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.758s (+6.8% 🔺)4.407s (+1.3%)1.649s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNitro1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNext.js (Turbopack)1.369s2.009s0.639s151.14x
💻 LocalExpress2.004s (-36.0% 🟢)2.392s (-36.4% 🟢)0.389s131.66x
💻 LocalNext.js (Turbopack)2.047s2.674s0.627s121.70x
💻 LocalNitro2.070s (-32.5% 🟢)2.507s (-35.5% 🟢)0.438s121.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.677s (+46.5% 🔺)6.286s (+31.2% 🔺)1.609s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.324s (-62.1% 🟢)2.008s (-49.9% 🟢)0.683s151.00x
🐘 PostgresNitro1.329s (-61.8% 🟢)2.007s (-49.9% 🟢)0.678s151.00x
🐘 PostgresNext.js (Turbopack)1.632s2.009s0.376s151.23x
💻 LocalExpress5.661s (-35.7% 🟢)6.415s (-30.8% 🟢)0.754s54.27x
💻 LocalNext.js (Turbopack)5.927s6.213s0.286s54.48x
💻 LocalNitro6.314s (-31.0% 🟢)6.617s (-34.0% 🟢)0.304s54.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.689s (-11.3% 🟢)7.509s (-8.2% 🟢)1.819s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.441s (-46.3% 🟢)1.006s (~)0.565s601.00x
🐘 PostgresExpress0.446s (-46.8% 🟢)1.007s (-1.5%)0.561s601.01x
💻 LocalExpress0.479s (-51.3% 🟢)1.004s (-6.7% 🟢)0.525s601.09x
💻 LocalNitro0.511s (-47.8% 🟢)1.021s (-6.6% 🟢)0.510s591.16x
🐘 PostgresNext.js (Turbopack)0.703s1.006s0.303s601.60x
💻 LocalNext.js (Turbopack)0.708s1.004s0.297s601.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.871s (-74.4% 🟢)6.286s (-70.5% 🟢)1.414s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.011s (-47.6% 🟢)1.435s (-31.7% 🟢)0.424s631.00x
🐘 PostgresExpress1.085s (-45.1% 🟢)1.706s (-24.5% 🟢)0.621s531.07x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.18x
💻 LocalExpress1.243s (-58.8% 🟢)2.006s (-44.0% 🟢)0.763s451.23x
🐘 PostgresNext.js (Turbopack)1.681s2.008s0.327s451.66x
💻 LocalNext.js (Turbopack)1.777s2.006s0.228s451.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express14.195s (-58.9% 🟢)16.255s (-55.8% 🟢)2.060s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.992s (-51.5% 🟢)2.360s (-48.7% 🟢)0.368s511.00x
🐘 PostgresExpress2.147s (-46.2% 🟢)2.719s (-37.8% 🟢)0.572s451.08x
💻 LocalNitro2.762s (-70.3% 🟢)3.032s (-69.7% 🟢)0.271s401.39x
💻 LocalExpress2.777s (-69.8% 🟢)3.008s (-70.0% 🟢)0.231s401.39x
🐘 PostgresNext.js (Turbopack)3.266s4.009s0.743s301.64x
💻 LocalNext.js (Turbopack)3.760s4.041s0.281s301.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express41.820s (-67.8% 🟢)43.762s (-66.9% 🟢)1.942s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.161s (-43.1% 🟢)1.006s (~)0.844s601.00x
🐘 PostgresExpress0.179s (-36.8% 🟢)1.006s (~)0.827s601.11x
🐘 PostgresNext.js (Turbopack)0.229s1.006s0.777s601.42x
💻 LocalExpress0.399s (-28.9% 🟢)1.005s (~)0.606s602.47x
💻 LocalNitro0.405s (-33.0% 🟢)1.005s (-1.7%)0.600s602.51x
💻 LocalNext.js (Turbopack)0.495s1.004s0.510s603.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.638s (+35.0% 🔺)4.119s (+13.2% 🔺)1.481s151.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.300s (-41.2% 🟢)1.006s (~)0.706s901.00x
🐘 PostgresNitro0.311s (-37.3% 🟢)1.007s (~)0.695s901.04x
🐘 PostgresNext.js (Turbopack)0.455s1.007s0.551s901.52x
💻 LocalNext.js (Turbopack)2.122s2.912s0.790s317.07x
💻 LocalNitro2.188s (-13.8% 🟢)2.797s (-7.1% 🟢)0.609s337.29x
💻 LocalExpress2.280s (-9.3% 🟢)2.945s (-2.1%)0.665s317.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.163s (+135.1% 🔺)8.759s (+82.2% 🔺)1.596s111.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.617s (-21.9% 🟢)1.007s (~)0.390s1201.00x
🐘 PostgresExpress0.623s (-23.9% 🟢)1.006s (-1.2%)0.383s1201.01x
🐘 PostgresNext.js (Turbopack)0.943s1.404s0.461s861.53x
💻 LocalNitro10.236s (-8.5% 🟢)10.777s (-7.6% 🟢)0.541s1216.59x
💻 LocalNext.js (Turbopack)10.456s10.937s0.481s1116.94x
💻 LocalExpress10.494s (-6.2% 🟢)11.122s (-6.9% 🟢)0.628s1117.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.378s (+228.5% 🔺)26.033s (+181.6% 🔺)1.655s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (+453.8% 🔺)1.997s (+99.9% 🔺)0.002s (-6.3% 🟢)2.010s (+98.8% 🔺)0.874s101.00x
🐘 PostgresNitro1.137s (+454.7% 🔺)2.003s (+100.3% 🔺)0.001s (-33.3% 🟢)2.009s (+98.7% 🔺)0.872s101.00x
💻 LocalExpress1.140s (+472.8% 🔺)2.005s (+99.6% 🔺)0.012s (+1.7%)2.020s (+98.4% 🔺)0.880s101.00x
💻 LocalNitro1.141s (+433.9% 🔺)2.005s (+99.6% 🔺)0.012s (-1.6%)2.020s (+98.3% 🔺)0.879s101.00x
💻 LocalNext.js (Turbopack)1.172s2.004s0.012s2.019s0.847s101.03x
🐘 PostgresNext.js (Turbopack)1.211s2.002s0.001s2.012s0.801s101.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.140s (-14.6% 🟢)3.359s (-17.9% 🟢)1.604s (+66.9% 🔺)5.401s (-3.4%)3.261s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.492s (+139.0% 🔺)2.003s (+99.0% 🔺)0.004s (-2.5%)2.026s (+98.1% 🔺)0.534s301.00x
🐘 PostgresExpress1.509s (+139.5% 🔺)2.002s (+98.9% 🔺)0.004s (+2.7%)2.026s (+98.0% 🔺)0.517s301.01x
💻 LocalExpress1.543s (+103.9% 🔺)2.012s (+95.5% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.481s301.03x
💻 LocalNext.js (Turbopack)1.666s2.010s0.010s2.023s0.356s301.12x
💻 LocalNitro1.721s (+105.2% 🔺)2.011s (+98.8% 🔺)0.010s (+5.2% 🔺)2.202s (+97.4% 🔺)0.482s281.15x
🐘 PostgresNext.js (Turbopack)1.724s2.010s0.004s2.026s0.302s301.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.884s (-9.5% 🟢)7.238s (-9.6% 🟢)0.364s (-10.9% 🟢)8.088s (-8.4% 🟢)2.204s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.637s (-33.7% 🟢)1.031s (-19.3% 🟢)0.000s (-100.0% 🟢)1.052s (-19.5% 🟢)0.414s581.00x
🐘 PostgresNitro0.640s (-33.9% 🟢)1.031s (-17.3% 🟢)0.000s (+65.5% 🔺)1.044s (-17.0% 🟢)0.403s581.00x
🐘 PostgresNext.js (Turbopack)0.767s1.018s0.000s1.035s0.268s581.20x
💻 LocalExpress1.362s (+11.2% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.655s302.14x
💻 LocalNitro1.366s (+11.7% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.651s302.14x
💻 LocalNext.js (Turbopack)1.377s2.013s0.000s2.016s0.639s302.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.604s (-3.6%)4.822s (-5.5% 🟢)0.000s (+83.3% 🔺)5.353s (-3.2%)1.749s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.246s (-29.7% 🟢)2.033s (-6.7% 🟢)0.000s (NaN%)2.043s (-7.1% 🟢)0.797s301.00x
🐘 PostgresNitro1.276s (-28.8% 🟢)2.066s (-3.5%)0.000s (-3.4%)2.082s (-4.2%)0.807s291.02x
🐘 PostgresNext.js (Turbopack)1.651s2.147s0.000s2.156s0.505s281.33x
💻 LocalNext.js (Turbopack)2.602s3.239s0.000s3.242s0.640s192.09x
💻 LocalNitro3.121s (-7.9% 🟢)3.906s (-3.1%)0.001s (+5.5% 🔺)3.908s (-3.2%)0.787s162.51x
💻 LocalExpress3.179s (-8.3% 🟢)3.967s (-1.7%)0.000s (-53.1% 🟢)3.969s (-1.7%)0.790s162.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.026s (+31.3% 🔺)7.064s (+17.3% 🔺)0.000s (NaN%)7.559s (+17.1% 🔺)1.534s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress9/21
🐘 PostgresNitro14/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@pranaygp
pranaygp marked this pull request as ready for review May 20, 2026 18:27
@pranaygp
pranaygp requested a review from a team as a code ownerMay 20, 2026 18:27
CopilotAI review requested due to automatic review settings May 20, 2026 18:27

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

Forward-ports the “stale wait replay” race fix to main by making the V2 runtime refresh its event snapshot after committing elapsed wait_completed events, ensuring concurrent durable events (e.g. hook_received) are observed in deterministic order during replay.

Changes:

  • Extends EventResult to optionally include cursor/hasMore pagination metadata for preloaded run_started event snapshots.
  • Updates runtime replay to (a) only trust preloaded events when hasMore !== true, and (b) refresh events after elapsed wait completions using cursor-delta fetch with a full-reload fallback.
  • Adds a deterministic handler-level regression test for the hook-vs-sleep interleaving and ships a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world/src/events.tsAdds optional cursor/hasMore fields to EventResult for preloaded event snapshots.
packages/world-vercel/src/events.tsAccepts/forwards cursor and hasMore from the backend in events.create responses.
packages/world-postgres/src/storage.tsPopulates cursor/hasMore for run_started preloaded events.
packages/world-local/src/storage/events-storage.tsReturns preloaded events with pagination metadata (and caps preload size).
packages/core/src/runtime.tsUses preload pagination metadata and refreshes event snapshots after completing elapsed waits to preserve replay determinism.
packages/core/src/runtime/wait-completion-replay.test.tsNew deterministic regression test that simulates the stale snapshot + concurrent hook ordering race.
.changeset/bright-waits-refresh.mdPatch bumps for affected packages.

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

Comment on lines 1107 to 1108
// For run_started: include all events so the runtime can skip
// the initial events.list call and reduce TTFB.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the run_started preload comment to call out the one-page preload and hasMore check.

Comment on lines 748 to +752
for (const waitEvent of waitsToComplete) {
try {
const result = await world.events.create(
runId,
waitEvent,
{ requestId }
);
events.push(result.event!);
await world.events.create(runId, waitEvent, {
requestId,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the dedupe comment to describe the refreshed wait-completion delta path instead of the old manual push behavior.

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

Approve — clean forward-port of #2029, with V2-specific adaptations

Forward-ports the stable wait-replay race fix to main. The diff against current main looks larger than the stable PR because the branch was forked before #2030 (which added optional stepName/token/resumeAt fields to several event schemas). After rebasing onto current main, the actual change is +593/-15 across 7 files, all consistent with what landed on stable.

Why the V2 adaptations are correct

I traced both versions side by side to make sure nothing was lost in translation:

  • Cursor preservation on first iteration (preloadedEventsCursor plumbed through to eventsCursor) — symmetric with stable.
  • Wait-completion refresh has TWO additions on main vs stable:
    • Dedupe by eventId when merging delta back into existing events (line 803-810). On stable this is just events.push(...newEvents.events). On main it's defensive against the V2 multi-iteration loop where events may persist across iterations as cachedEvents.
    • eventsCursor = loaded.cursor ?? eventsCursor (line 811). Critical for the V2 loop — without it, the next iteration's loadWorkflowRunEvents(runId, eventsCursor) would re-fetch the SAME delta. Stable's loop doesn't have this concern because it doesn't iterate.
  • stripEventDataRefs round-trip through resolveData is identical to the existing events.create flow — no new code paths.

Correctness walk-through

Scenario: workflow with Promise.race([hook, sleep]), hook arrives durably just before handler commits wait_completed.

  1. Handler reads events at cursor C. Sees elapsed wait_created, no hook_received.
  2. Before handler commits wait_completed: another writer commits hook_received (durable position > C).
  3. Handler commits wait_completed (durable position > hook_received).
  4. Without fix: in-memory events missing hook_received. Replay takes sleep branch. Wrong.
  5. With fix: refresh from cursor C, get [hook_received, wait_completed]. Append. Replay takes hook branch. Correct.

The sawAllWaitCompletions defensive check (line 795-800) is smart — if the delta-after-cursor doesn't include the wait completion this handler just wrote, the cursor wasn't a safe replay boundary (read replica lag, eventual consistency hiccup, etc.) and we fall back to a full reload from the beginning.

Test coverage

The four scenarios in wait-completion-replay.test.ts map cleanly onto the code paths:

TestPath exercised
loads only events after the preloaded cursorHappy path: preload + cursor + complete
falls back to a full reload when preloaded events do not include a cursorBackward compat: old worlds without cursor
falls back to a full reload when preloaded events are partialhasMore: true → discard preload, full reload
falls back when cursor delta misses the attempted wait completionDefensive: sawAllWaitCompletions fails

Each scenario asserts both the number of listEvents calls AND the cursor used on each call, so the code paths are pinned, not just outcomes.

What I verified

  • After rebasing onto main: 7 files changed, +593/-15. The events.ts schema "removals" in the raw diff against main are artifacts of the branch being behind #2030 — they vanish on rebase.
  • pnpm --filter @workflow/core test ✓ (965/965 including the new 4 in wait-completion-replay.test.ts)
  • pnpm --filter @workflow/core test packages/core/src/hook-sleep-interaction.test.ts ✓ (the seeded-repro test that was deleted on stable still passes here — main keeps both old + new tests)
  • Typecheck for core, world, world-local, world-postgres, world-vercel ✓
  • Build ✓
  • CI: 107 success, 1 failure, 4 in-progress — the failure is Benchmark Vercel (nextjs-turbopack), which is unrelated (benchmark timing) and main is also failing it on recent commits. All E2E + Unit Tests pass.

Two minor nits (non-blocking)

  1. world-postgreshasMore: false is hardcoded (line 1291). The query has no LIMIT, so this is correct today. If someone ever adds a LIMIT N+1 pattern to that query for pagination, the hardcoded false would silently lie. Worth a comment like // hasMore = false: query has no LIMIT, so we always return all events to make the assumption explicit. Compare to the local world, where hasMore is derived from the paginated query.

  2. The dedupe comment in the iteration-N+1 fetch path (line 650-654) reads as if the cursor isn't advanced after the wait-completion refresh. But this PR DOES advance it (line 811: eventsCursor = loaded.cursor ?? eventsCursor). The dedupe is now effectively defensive against worlds that don't always advance cursors (e.g., return null when no new events) — which is what makes it still worthwhile, but the comment could say so. Minor; understandable as-is.

Backport stance

Body says "this PR is itself a forward port from stable, so it should not be backported." Agreed — the backport action's AI should reach the same conclusion (the fix already exists on stable as #2029), and if it doesn't, manual override is fine.

Approving.

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for dc0be50 (AI decision).

This PR is explicitly a forward-port from stable PR #2029, adapted to main's V2 replay loop and event cache. The underlying fix already exists on stable, and the changes here build on main-only APIs (cursor/hasMore metadata in preloaded run_started snapshots), so backporting would be redundant and likely conflict with the existing stable implementation.

To override, re-run the Backport to stable workflow manually via workflow_dispatch with this commit SHA.

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.

3 participants

@pranaygp@TooTallNate
, '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" + '
[codex] Forward port stale wait replay fix by pranaygp · Pull Request #2038 · vercel/workflow · GitHub
Skip to content

[codex] Forward port stale wait replay fix - #2038

Merged
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix
May 20, 2026
Merged

[codex] Forward port stale wait replay fix#2038
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix

Conversation

@pranaygp

@pranaygppranaygp commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Forward-port the stale wait replay race fix from stable PR #2029 to main.

This keeps workflow replay deterministic when an elapsed wait_completed is committed from a stale event snapshot while a concurrent hook_received event landed durably before it. On main, the fix is adapted to the V2 replay loop/event cache:

  • use cursor / hasMore metadata from preloaded run_started event snapshots
  • refresh events after attempting elapsed wait completions before replaying
  • fetch only the delta after the prior cursor when possible, with a full-reload fallback when cursor metadata is missing or unsafe
  • add the deterministic handler-level regression test for the hook-vs-sleep wait race

Forward-Port Note

This PR is itself a forward port from stable, so it should not be backported.

Validation

  • pnpm --filter @workflow/world build
  • pnpm --filter @workflow/utils build
  • pnpm --filter @workflow/errors build
  • pnpm --filter @workflow/serde build
  • pnpm --filter @workflow/world-local build
  • pnpm --filter @workflow/world-vercel build
  • pnpm --filter @workflow/core build
  • pnpm vitest run packages/core/src/runtime/wait-completion-replay.test.ts
  • pnpm vitest run packages/core/src/hook-sleep-interaction.test.ts
  • pnpm --filter @workflow/core typecheck
  • pnpm --filter @workflow/world-local typecheck
  • pnpm --filter @workflow/world-postgres typecheck
  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm changeset status --since=main
  • pnpm biome check --files-ignore-unknown=true .changeset/bright-waits-refresh.md packages/core/src/runtime.ts packages/core/src/runtime/wait-completion-replay.test.ts packages/world/src/events.ts packages/world-local/src/storage/events-storage.ts packages/world-postgres/src/storage.ts packages/world-vercel/src/events.ts

Note: local commands run under Node v25.2.1, which emits the repo's unsupported-engine warning. Biome exits successfully but reports existing warnings in the large runtime/storage files.

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2f724a

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-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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

@github-actions

github-actionsBot commented May 20, 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 20, 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.031s (-29.8% 🟢)1.006s (~)0.975s101.00x
💻 LocalNitro0.032s (-26.7% 🟢)1.005s (~)0.974s101.02x
🐘 PostgresNitro0.048s (-49.9% 🟢)1.012s (-2.9%)0.965s101.53x
💻 LocalNext.js (Turbopack)0.050s1.006s0.956s101.60x
🐘 PostgresExpress0.053s (-8.3% 🟢)1.013s (~)0.960s101.71x
🐘 PostgresNext.js (Turbopack)0.059s1.012s0.953s101.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.263s (+11.8% 🔺)2.152s (+0.7%)1.888s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (-5.0% 🟢)2.006s (~)0.931s101.00x
🐘 PostgresNitro1.076s (-5.6% 🟢)2.009s (~)0.933s101.00x
💻 LocalExpress1.076s (-4.4%)2.007s (~)0.931s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.010s (~)0.926s101.01x
💻 LocalNext.js (Turbopack)1.107s2.006s0.899s101.03x
🐘 PostgresNext.js (Turbopack)1.132s2.009s0.877s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.542s (-17.8% 🟢)3.515s (-7.7% 🟢)1.973s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.410s (-5.1% 🟢)11.016s (~)0.607s31.00x
🐘 PostgresNitro10.417s (-4.2%)11.016s (~)0.599s31.00x
💻 LocalExpress10.427s (-4.5%)11.023s (~)0.596s31.00x
💻 LocalNitro10.433s (-4.7%)11.023s (~)0.590s31.00x
💻 LocalNext.js (Turbopack)10.647s11.021s0.374s31.02x
🐘 PostgresNext.js (Turbopack)10.698s11.025s0.327s31.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.211s (-22.2% 🟢)14.873s (-25.7% 🟢)1.662s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.423s (-8.0% 🟢)14.015s (-6.7% 🟢)0.593s51.00x
🐘 PostgresExpress13.473s (-7.6% 🟢)14.016s (-6.7% 🟢)0.543s51.00x
💻 LocalNitro13.509s (-10.3% 🟢)14.028s (-12.5% 🟢)0.519s51.01x
💻 LocalExpress13.525s (-9.7% 🟢)14.029s (-6.7% 🟢)0.504s51.01x
💻 LocalNext.js (Turbopack)14.049s14.827s0.778s51.05x
🐘 PostgresNext.js (Turbopack)14.168s15.017s0.849s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.258s (-57.7% 🟢)23.028s (-56.2% 🟢)1.770s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro11.903s (-14.8% 🟢)12.143s (-15.1% 🟢)0.240s81.00x
💻 LocalNitro11.931s (-28.9% 🟢)12.021s (-29.4% 🟢)0.090s81.00x
🐘 PostgresExpress11.984s (-14.4% 🟢)12.515s (-14.2% 🟢)0.531s81.01x
💻 LocalExpress12.022s (-27.6% 🟢)12.398s (-27.2% 🟢)0.377s81.01x
💻 LocalNext.js (Turbopack)13.063s13.596s0.533s71.10x
🐘 PostgresNext.js (Turbopack)13.220s14.017s0.798s71.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.761s (-72.1% 🟢)35.436s (-71.3% 🟢)1.675s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.144s (-10.3% 🟢)2.008s (~)0.865s151.00x
🐘 PostgresExpress1.146s (-9.1% 🟢)2.006s (~)0.861s151.00x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.826s151.03x
💻 LocalExpress1.188s (-20.2% 🟢)2.006s (~)0.818s151.04x
🐘 PostgresNext.js (Turbopack)1.238s2.009s0.771s151.08x
💻 LocalNext.js (Turbopack)1.257s2.005s0.748s151.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.685s (-6.1% 🟢)4.192s (-9.3% 🟢)1.507s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.817s151.00x
🐘 PostgresExpress1.201s (-49.1% 🟢)2.007s (-33.3% 🟢)0.807s151.01x
🐘 PostgresNext.js (Turbopack)1.401s2.007s0.605s151.18x
💻 LocalNitro1.755s (-44.2% 🟢)2.005s (-48.4% 🟢)0.251s151.47x
💻 LocalNext.js (Turbopack)1.789s2.007s0.218s151.50x
💻 LocalExpress1.861s (-37.0% 🟢)2.149s (-37.8% 🟢)0.288s141.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.773s (+31.9% 🔺)6.447s (+26.2% 🔺)1.674s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.311s (-62.3% 🟢)2.008s (-49.9% 🟢)0.698s151.00x
🐘 PostgresExpress1.331s (-61.8% 🟢)2.006s (-50.0% 🟢)0.676s151.02x
🐘 PostgresNext.js (Turbopack)1.722s2.009s0.287s151.31x
💻 LocalNitro4.817s (-42.3% 🟢)5.345s (-40.7% 🟢)0.528s63.68x
💻 LocalExpress5.405s (-35.2% 🟢)6.013s (-33.4% 🟢)0.608s54.12x
💻 LocalNext.js (Turbopack)5.527s6.213s0.686s54.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.418s (+27.8% 🔺)7.587s (+23.8% 🔺)2.169s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.127s (-10.4% 🟢)2.009s (~)0.882s151.00x
🐘 PostgresExpress1.136s (-9.6% 🟢)2.008s (~)0.872s151.01x
🐘 PostgresNext.js (Turbopack)1.221s2.008s0.786s151.08x
💻 LocalNext.js (Turbopack)1.301s2.005s0.704s151.15x
💻 LocalNitro1.385s (-25.8% 🟢)2.006s (-14.3% 🟢)0.621s151.23x
💻 LocalExpress1.388s (-26.7% 🟢)2.006s (-15.1% 🟢)0.618s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.758s (+6.8% 🔺)4.407s (+1.3%)1.649s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNitro1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNext.js (Turbopack)1.369s2.009s0.639s151.14x
💻 LocalExpress2.004s (-36.0% 🟢)2.392s (-36.4% 🟢)0.389s131.66x
💻 LocalNext.js (Turbopack)2.047s2.674s0.627s121.70x
💻 LocalNitro2.070s (-32.5% 🟢)2.507s (-35.5% 🟢)0.438s121.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.677s (+46.5% 🔺)6.286s (+31.2% 🔺)1.609s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.324s (-62.1% 🟢)2.008s (-49.9% 🟢)0.683s151.00x
🐘 PostgresNitro1.329s (-61.8% 🟢)2.007s (-49.9% 🟢)0.678s151.00x
🐘 PostgresNext.js (Turbopack)1.632s2.009s0.376s151.23x
💻 LocalExpress5.661s (-35.7% 🟢)6.415s (-30.8% 🟢)0.754s54.27x
💻 LocalNext.js (Turbopack)5.927s6.213s0.286s54.48x
💻 LocalNitro6.314s (-31.0% 🟢)6.617s (-34.0% 🟢)0.304s54.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.689s (-11.3% 🟢)7.509s (-8.2% 🟢)1.819s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.441s (-46.3% 🟢)1.006s (~)0.565s601.00x
🐘 PostgresExpress0.446s (-46.8% 🟢)1.007s (-1.5%)0.561s601.01x
💻 LocalExpress0.479s (-51.3% 🟢)1.004s (-6.7% 🟢)0.525s601.09x
💻 LocalNitro0.511s (-47.8% 🟢)1.021s (-6.6% 🟢)0.510s591.16x
🐘 PostgresNext.js (Turbopack)0.703s1.006s0.303s601.60x
💻 LocalNext.js (Turbopack)0.708s1.004s0.297s601.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.871s (-74.4% 🟢)6.286s (-70.5% 🟢)1.414s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.011s (-47.6% 🟢)1.435s (-31.7% 🟢)0.424s631.00x
🐘 PostgresExpress1.085s (-45.1% 🟢)1.706s (-24.5% 🟢)0.621s531.07x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.18x
💻 LocalExpress1.243s (-58.8% 🟢)2.006s (-44.0% 🟢)0.763s451.23x
🐘 PostgresNext.js (Turbopack)1.681s2.008s0.327s451.66x
💻 LocalNext.js (Turbopack)1.777s2.006s0.228s451.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express14.195s (-58.9% 🟢)16.255s (-55.8% 🟢)2.060s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.992s (-51.5% 🟢)2.360s (-48.7% 🟢)0.368s511.00x
🐘 PostgresExpress2.147s (-46.2% 🟢)2.719s (-37.8% 🟢)0.572s451.08x
💻 LocalNitro2.762s (-70.3% 🟢)3.032s (-69.7% 🟢)0.271s401.39x
💻 LocalExpress2.777s (-69.8% 🟢)3.008s (-70.0% 🟢)0.231s401.39x
🐘 PostgresNext.js (Turbopack)3.266s4.009s0.743s301.64x
💻 LocalNext.js (Turbopack)3.760s4.041s0.281s301.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express41.820s (-67.8% 🟢)43.762s (-66.9% 🟢)1.942s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.161s (-43.1% 🟢)1.006s (~)0.844s601.00x
🐘 PostgresExpress0.179s (-36.8% 🟢)1.006s (~)0.827s601.11x
🐘 PostgresNext.js (Turbopack)0.229s1.006s0.777s601.42x
💻 LocalExpress0.399s (-28.9% 🟢)1.005s (~)0.606s602.47x
💻 LocalNitro0.405s (-33.0% 🟢)1.005s (-1.7%)0.600s602.51x
💻 LocalNext.js (Turbopack)0.495s1.004s0.510s603.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.638s (+35.0% 🔺)4.119s (+13.2% 🔺)1.481s151.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.300s (-41.2% 🟢)1.006s (~)0.706s901.00x
🐘 PostgresNitro0.311s (-37.3% 🟢)1.007s (~)0.695s901.04x
🐘 PostgresNext.js (Turbopack)0.455s1.007s0.551s901.52x
💻 LocalNext.js (Turbopack)2.122s2.912s0.790s317.07x
💻 LocalNitro2.188s (-13.8% 🟢)2.797s (-7.1% 🟢)0.609s337.29x
💻 LocalExpress2.280s (-9.3% 🟢)2.945s (-2.1%)0.665s317.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.163s (+135.1% 🔺)8.759s (+82.2% 🔺)1.596s111.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.617s (-21.9% 🟢)1.007s (~)0.390s1201.00x
🐘 PostgresExpress0.623s (-23.9% 🟢)1.006s (-1.2%)0.383s1201.01x
🐘 PostgresNext.js (Turbopack)0.943s1.404s0.461s861.53x
💻 LocalNitro10.236s (-8.5% 🟢)10.777s (-7.6% 🟢)0.541s1216.59x
💻 LocalNext.js (Turbopack)10.456s10.937s0.481s1116.94x
💻 LocalExpress10.494s (-6.2% 🟢)11.122s (-6.9% 🟢)0.628s1117.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.378s (+228.5% 🔺)26.033s (+181.6% 🔺)1.655s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (+453.8% 🔺)1.997s (+99.9% 🔺)0.002s (-6.3% 🟢)2.010s (+98.8% 🔺)0.874s101.00x
🐘 PostgresNitro1.137s (+454.7% 🔺)2.003s (+100.3% 🔺)0.001s (-33.3% 🟢)2.009s (+98.7% 🔺)0.872s101.00x
💻 LocalExpress1.140s (+472.8% 🔺)2.005s (+99.6% 🔺)0.012s (+1.7%)2.020s (+98.4% 🔺)0.880s101.00x
💻 LocalNitro1.141s (+433.9% 🔺)2.005s (+99.6% 🔺)0.012s (-1.6%)2.020s (+98.3% 🔺)0.879s101.00x
💻 LocalNext.js (Turbopack)1.172s2.004s0.012s2.019s0.847s101.03x
🐘 PostgresNext.js (Turbopack)1.211s2.002s0.001s2.012s0.801s101.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.140s (-14.6% 🟢)3.359s (-17.9% 🟢)1.604s (+66.9% 🔺)5.401s (-3.4%)3.261s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.492s (+139.0% 🔺)2.003s (+99.0% 🔺)0.004s (-2.5%)2.026s (+98.1% 🔺)0.534s301.00x
🐘 PostgresExpress1.509s (+139.5% 🔺)2.002s (+98.9% 🔺)0.004s (+2.7%)2.026s (+98.0% 🔺)0.517s301.01x
💻 LocalExpress1.543s (+103.9% 🔺)2.012s (+95.5% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.481s301.03x
💻 LocalNext.js (Turbopack)1.666s2.010s0.010s2.023s0.356s301.12x
💻 LocalNitro1.721s (+105.2% 🔺)2.011s (+98.8% 🔺)0.010s (+5.2% 🔺)2.202s (+97.4% 🔺)0.482s281.15x
🐘 PostgresNext.js (Turbopack)1.724s2.010s0.004s2.026s0.302s301.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.884s (-9.5% 🟢)7.238s (-9.6% 🟢)0.364s (-10.9% 🟢)8.088s (-8.4% 🟢)2.204s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.637s (-33.7% 🟢)1.031s (-19.3% 🟢)0.000s (-100.0% 🟢)1.052s (-19.5% 🟢)0.414s581.00x
🐘 PostgresNitro0.640s (-33.9% 🟢)1.031s (-17.3% 🟢)0.000s (+65.5% 🔺)1.044s (-17.0% 🟢)0.403s581.00x
🐘 PostgresNext.js (Turbopack)0.767s1.018s0.000s1.035s0.268s581.20x
💻 LocalExpress1.362s (+11.2% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.655s302.14x
💻 LocalNitro1.366s (+11.7% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.651s302.14x
💻 LocalNext.js (Turbopack)1.377s2.013s0.000s2.016s0.639s302.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.604s (-3.6%)4.822s (-5.5% 🟢)0.000s (+83.3% 🔺)5.353s (-3.2%)1.749s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.246s (-29.7% 🟢)2.033s (-6.7% 🟢)0.000s (NaN%)2.043s (-7.1% 🟢)0.797s301.00x
🐘 PostgresNitro1.276s (-28.8% 🟢)2.066s (-3.5%)0.000s (-3.4%)2.082s (-4.2%)0.807s291.02x
🐘 PostgresNext.js (Turbopack)1.651s2.147s0.000s2.156s0.505s281.33x
💻 LocalNext.js (Turbopack)2.602s3.239s0.000s3.242s0.640s192.09x
💻 LocalNitro3.121s (-7.9% 🟢)3.906s (-3.1%)0.001s (+5.5% 🔺)3.908s (-3.2%)0.787s162.51x
💻 LocalExpress3.179s (-8.3% 🟢)3.967s (-1.7%)0.000s (-53.1% 🟢)3.969s (-1.7%)0.790s162.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.026s (+31.3% 🔺)7.064s (+17.3% 🔺)0.000s (NaN%)7.559s (+17.1% 🔺)1.534s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress9/21
🐘 PostgresNitro14/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@pranaygp
pranaygp marked this pull request as ready for review May 20, 2026 18:27
@pranaygp
pranaygp requested a review from a team as a code ownerMay 20, 2026 18:27
CopilotAI review requested due to automatic review settings May 20, 2026 18:27

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

Forward-ports the “stale wait replay” race fix to main by making the V2 runtime refresh its event snapshot after committing elapsed wait_completed events, ensuring concurrent durable events (e.g. hook_received) are observed in deterministic order during replay.

Changes:

  • Extends EventResult to optionally include cursor/hasMore pagination metadata for preloaded run_started event snapshots.
  • Updates runtime replay to (a) only trust preloaded events when hasMore !== true, and (b) refresh events after elapsed wait completions using cursor-delta fetch with a full-reload fallback.
  • Adds a deterministic handler-level regression test for the hook-vs-sleep interleaving and ships a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world/src/events.tsAdds optional cursor/hasMore fields to EventResult for preloaded event snapshots.
packages/world-vercel/src/events.tsAccepts/forwards cursor and hasMore from the backend in events.create responses.
packages/world-postgres/src/storage.tsPopulates cursor/hasMore for run_started preloaded events.
packages/world-local/src/storage/events-storage.tsReturns preloaded events with pagination metadata (and caps preload size).
packages/core/src/runtime.tsUses preload pagination metadata and refreshes event snapshots after completing elapsed waits to preserve replay determinism.
packages/core/src/runtime/wait-completion-replay.test.tsNew deterministic regression test that simulates the stale snapshot + concurrent hook ordering race.
.changeset/bright-waits-refresh.mdPatch bumps for affected packages.

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

Comment on lines 1107 to 1108
// For run_started: include all events so the runtime can skip
// the initial events.list call and reduce TTFB.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the run_started preload comment to call out the one-page preload and hasMore check.

Comment on lines 748 to +752
for (const waitEvent of waitsToComplete) {
try {
const result = await world.events.create(
runId,
waitEvent,
{ requestId }
);
events.push(result.event!);
await world.events.create(runId, waitEvent, {
requestId,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the dedupe comment to describe the refreshed wait-completion delta path instead of the old manual push behavior.

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

Approve — clean forward-port of #2029, with V2-specific adaptations

Forward-ports the stable wait-replay race fix to main. The diff against current main looks larger than the stable PR because the branch was forked before #2030 (which added optional stepName/token/resumeAt fields to several event schemas). After rebasing onto current main, the actual change is +593/-15 across 7 files, all consistent with what landed on stable.

Why the V2 adaptations are correct

I traced both versions side by side to make sure nothing was lost in translation:

  • Cursor preservation on first iteration (preloadedEventsCursor plumbed through to eventsCursor) — symmetric with stable.
  • Wait-completion refresh has TWO additions on main vs stable:
    • Dedupe by eventId when merging delta back into existing events (line 803-810). On stable this is just events.push(...newEvents.events). On main it's defensive against the V2 multi-iteration loop where events may persist across iterations as cachedEvents.
    • eventsCursor = loaded.cursor ?? eventsCursor (line 811). Critical for the V2 loop — without it, the next iteration's loadWorkflowRunEvents(runId, eventsCursor) would re-fetch the SAME delta. Stable's loop doesn't have this concern because it doesn't iterate.
  • stripEventDataRefs round-trip through resolveData is identical to the existing events.create flow — no new code paths.

Correctness walk-through

Scenario: workflow with Promise.race([hook, sleep]), hook arrives durably just before handler commits wait_completed.

  1. Handler reads events at cursor C. Sees elapsed wait_created, no hook_received.
  2. Before handler commits wait_completed: another writer commits hook_received (durable position > C).
  3. Handler commits wait_completed (durable position > hook_received).
  4. Without fix: in-memory events missing hook_received. Replay takes sleep branch. Wrong.
  5. With fix: refresh from cursor C, get [hook_received, wait_completed]. Append. Replay takes hook branch. Correct.

The sawAllWaitCompletions defensive check (line 795-800) is smart — if the delta-after-cursor doesn't include the wait completion this handler just wrote, the cursor wasn't a safe replay boundary (read replica lag, eventual consistency hiccup, etc.) and we fall back to a full reload from the beginning.

Test coverage

The four scenarios in wait-completion-replay.test.ts map cleanly onto the code paths:

TestPath exercised
loads only events after the preloaded cursorHappy path: preload + cursor + complete
falls back to a full reload when preloaded events do not include a cursorBackward compat: old worlds without cursor
falls back to a full reload when preloaded events are partialhasMore: true → discard preload, full reload
falls back when cursor delta misses the attempted wait completionDefensive: sawAllWaitCompletions fails

Each scenario asserts both the number of listEvents calls AND the cursor used on each call, so the code paths are pinned, not just outcomes.

What I verified

  • After rebasing onto main: 7 files changed, +593/-15. The events.ts schema "removals" in the raw diff against main are artifacts of the branch being behind #2030 — they vanish on rebase.
  • pnpm --filter @workflow/core test ✓ (965/965 including the new 4 in wait-completion-replay.test.ts)
  • pnpm --filter @workflow/core test packages/core/src/hook-sleep-interaction.test.ts ✓ (the seeded-repro test that was deleted on stable still passes here — main keeps both old + new tests)
  • Typecheck for core, world, world-local, world-postgres, world-vercel ✓
  • Build ✓
  • CI: 107 success, 1 failure, 4 in-progress — the failure is Benchmark Vercel (nextjs-turbopack), which is unrelated (benchmark timing) and main is also failing it on recent commits. All E2E + Unit Tests pass.

Two minor nits (non-blocking)

  1. world-postgreshasMore: false is hardcoded (line 1291). The query has no LIMIT, so this is correct today. If someone ever adds a LIMIT N+1 pattern to that query for pagination, the hardcoded false would silently lie. Worth a comment like // hasMore = false: query has no LIMIT, so we always return all events to make the assumption explicit. Compare to the local world, where hasMore is derived from the paginated query.

  2. The dedupe comment in the iteration-N+1 fetch path (line 650-654) reads as if the cursor isn't advanced after the wait-completion refresh. But this PR DOES advance it (line 811: eventsCursor = loaded.cursor ?? eventsCursor). The dedupe is now effectively defensive against worlds that don't always advance cursors (e.g., return null when no new events) — which is what makes it still worthwhile, but the comment could say so. Minor; understandable as-is.

Backport stance

Body says "this PR is itself a forward port from stable, so it should not be backported." Agreed — the backport action's AI should reach the same conclusion (the fix already exists on stable as #2029), and if it doesn't, manual override is fine.

Approving.

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for dc0be50 (AI decision).

This PR is explicitly a forward-port from stable PR #2029, adapted to main's V2 replay loop and event cache. The underlying fix already exists on stable, and the changes here build on main-only APIs (cursor/hasMore metadata in preloaded run_started snapshots), so backporting would be redundant and likely conflict with the existing stable implementation.

To override, re-run the Backport to stable workflow manually via workflow_dispatch with this commit SHA.

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.

3 participants

@pranaygp@TooTallNate
, '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('^' + ".*" + ' [codex] Forward port stale wait replay fix by pranaygp · Pull Request #2038 · vercel/workflow · GitHub
Skip to content

[codex] Forward port stale wait replay fix - #2038

Merged
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix
May 20, 2026
Merged

[codex] Forward port stale wait replay fix#2038
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix

Conversation

@pranaygp

@pranaygppranaygp commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Forward-port the stale wait replay race fix from stable PR #2029 to main.

This keeps workflow replay deterministic when an elapsed wait_completed is committed from a stale event snapshot while a concurrent hook_received event landed durably before it. On main, the fix is adapted to the V2 replay loop/event cache:

  • use cursor / hasMore metadata from preloaded run_started event snapshots
  • refresh events after attempting elapsed wait completions before replaying
  • fetch only the delta after the prior cursor when possible, with a full-reload fallback when cursor metadata is missing or unsafe
  • add the deterministic handler-level regression test for the hook-vs-sleep wait race

Forward-Port Note

This PR is itself a forward port from stable, so it should not be backported.

Validation

  • pnpm --filter @workflow/world build
  • pnpm --filter @workflow/utils build
  • pnpm --filter @workflow/errors build
  • pnpm --filter @workflow/serde build
  • pnpm --filter @workflow/world-local build
  • pnpm --filter @workflow/world-vercel build
  • pnpm --filter @workflow/core build
  • pnpm vitest run packages/core/src/runtime/wait-completion-replay.test.ts
  • pnpm vitest run packages/core/src/hook-sleep-interaction.test.ts
  • pnpm --filter @workflow/core typecheck
  • pnpm --filter @workflow/world-local typecheck
  • pnpm --filter @workflow/world-postgres typecheck
  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm changeset status --since=main
  • pnpm biome check --files-ignore-unknown=true .changeset/bright-waits-refresh.md packages/core/src/runtime.ts packages/core/src/runtime/wait-completion-replay.test.ts packages/world/src/events.ts packages/world-local/src/storage/events-storage.ts packages/world-postgres/src/storage.ts packages/world-vercel/src/events.ts

Note: local commands run under Node v25.2.1, which emits the repo's unsupported-engine warning. Biome exits successfully but reports existing warnings in the large runtime/storage files.

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2f724a

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-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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

@github-actions

github-actionsBot commented May 20, 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 20, 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.031s (-29.8% 🟢)1.006s (~)0.975s101.00x
💻 LocalNitro0.032s (-26.7% 🟢)1.005s (~)0.974s101.02x
🐘 PostgresNitro0.048s (-49.9% 🟢)1.012s (-2.9%)0.965s101.53x
💻 LocalNext.js (Turbopack)0.050s1.006s0.956s101.60x
🐘 PostgresExpress0.053s (-8.3% 🟢)1.013s (~)0.960s101.71x
🐘 PostgresNext.js (Turbopack)0.059s1.012s0.953s101.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.263s (+11.8% 🔺)2.152s (+0.7%)1.888s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (-5.0% 🟢)2.006s (~)0.931s101.00x
🐘 PostgresNitro1.076s (-5.6% 🟢)2.009s (~)0.933s101.00x
💻 LocalExpress1.076s (-4.4%)2.007s (~)0.931s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.010s (~)0.926s101.01x
💻 LocalNext.js (Turbopack)1.107s2.006s0.899s101.03x
🐘 PostgresNext.js (Turbopack)1.132s2.009s0.877s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.542s (-17.8% 🟢)3.515s (-7.7% 🟢)1.973s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.410s (-5.1% 🟢)11.016s (~)0.607s31.00x
🐘 PostgresNitro10.417s (-4.2%)11.016s (~)0.599s31.00x
💻 LocalExpress10.427s (-4.5%)11.023s (~)0.596s31.00x
💻 LocalNitro10.433s (-4.7%)11.023s (~)0.590s31.00x
💻 LocalNext.js (Turbopack)10.647s11.021s0.374s31.02x
🐘 PostgresNext.js (Turbopack)10.698s11.025s0.327s31.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.211s (-22.2% 🟢)14.873s (-25.7% 🟢)1.662s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.423s (-8.0% 🟢)14.015s (-6.7% 🟢)0.593s51.00x
🐘 PostgresExpress13.473s (-7.6% 🟢)14.016s (-6.7% 🟢)0.543s51.00x
💻 LocalNitro13.509s (-10.3% 🟢)14.028s (-12.5% 🟢)0.519s51.01x
💻 LocalExpress13.525s (-9.7% 🟢)14.029s (-6.7% 🟢)0.504s51.01x
💻 LocalNext.js (Turbopack)14.049s14.827s0.778s51.05x
🐘 PostgresNext.js (Turbopack)14.168s15.017s0.849s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.258s (-57.7% 🟢)23.028s (-56.2% 🟢)1.770s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro11.903s (-14.8% 🟢)12.143s (-15.1% 🟢)0.240s81.00x
💻 LocalNitro11.931s (-28.9% 🟢)12.021s (-29.4% 🟢)0.090s81.00x
🐘 PostgresExpress11.984s (-14.4% 🟢)12.515s (-14.2% 🟢)0.531s81.01x
💻 LocalExpress12.022s (-27.6% 🟢)12.398s (-27.2% 🟢)0.377s81.01x
💻 LocalNext.js (Turbopack)13.063s13.596s0.533s71.10x
🐘 PostgresNext.js (Turbopack)13.220s14.017s0.798s71.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.761s (-72.1% 🟢)35.436s (-71.3% 🟢)1.675s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.144s (-10.3% 🟢)2.008s (~)0.865s151.00x
🐘 PostgresExpress1.146s (-9.1% 🟢)2.006s (~)0.861s151.00x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.826s151.03x
💻 LocalExpress1.188s (-20.2% 🟢)2.006s (~)0.818s151.04x
🐘 PostgresNext.js (Turbopack)1.238s2.009s0.771s151.08x
💻 LocalNext.js (Turbopack)1.257s2.005s0.748s151.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.685s (-6.1% 🟢)4.192s (-9.3% 🟢)1.507s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.817s151.00x
🐘 PostgresExpress1.201s (-49.1% 🟢)2.007s (-33.3% 🟢)0.807s151.01x
🐘 PostgresNext.js (Turbopack)1.401s2.007s0.605s151.18x
💻 LocalNitro1.755s (-44.2% 🟢)2.005s (-48.4% 🟢)0.251s151.47x
💻 LocalNext.js (Turbopack)1.789s2.007s0.218s151.50x
💻 LocalExpress1.861s (-37.0% 🟢)2.149s (-37.8% 🟢)0.288s141.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.773s (+31.9% 🔺)6.447s (+26.2% 🔺)1.674s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.311s (-62.3% 🟢)2.008s (-49.9% 🟢)0.698s151.00x
🐘 PostgresExpress1.331s (-61.8% 🟢)2.006s (-50.0% 🟢)0.676s151.02x
🐘 PostgresNext.js (Turbopack)1.722s2.009s0.287s151.31x
💻 LocalNitro4.817s (-42.3% 🟢)5.345s (-40.7% 🟢)0.528s63.68x
💻 LocalExpress5.405s (-35.2% 🟢)6.013s (-33.4% 🟢)0.608s54.12x
💻 LocalNext.js (Turbopack)5.527s6.213s0.686s54.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.418s (+27.8% 🔺)7.587s (+23.8% 🔺)2.169s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.127s (-10.4% 🟢)2.009s (~)0.882s151.00x
🐘 PostgresExpress1.136s (-9.6% 🟢)2.008s (~)0.872s151.01x
🐘 PostgresNext.js (Turbopack)1.221s2.008s0.786s151.08x
💻 LocalNext.js (Turbopack)1.301s2.005s0.704s151.15x
💻 LocalNitro1.385s (-25.8% 🟢)2.006s (-14.3% 🟢)0.621s151.23x
💻 LocalExpress1.388s (-26.7% 🟢)2.006s (-15.1% 🟢)0.618s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.758s (+6.8% 🔺)4.407s (+1.3%)1.649s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNitro1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNext.js (Turbopack)1.369s2.009s0.639s151.14x
💻 LocalExpress2.004s (-36.0% 🟢)2.392s (-36.4% 🟢)0.389s131.66x
💻 LocalNext.js (Turbopack)2.047s2.674s0.627s121.70x
💻 LocalNitro2.070s (-32.5% 🟢)2.507s (-35.5% 🟢)0.438s121.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.677s (+46.5% 🔺)6.286s (+31.2% 🔺)1.609s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.324s (-62.1% 🟢)2.008s (-49.9% 🟢)0.683s151.00x
🐘 PostgresNitro1.329s (-61.8% 🟢)2.007s (-49.9% 🟢)0.678s151.00x
🐘 PostgresNext.js (Turbopack)1.632s2.009s0.376s151.23x
💻 LocalExpress5.661s (-35.7% 🟢)6.415s (-30.8% 🟢)0.754s54.27x
💻 LocalNext.js (Turbopack)5.927s6.213s0.286s54.48x
💻 LocalNitro6.314s (-31.0% 🟢)6.617s (-34.0% 🟢)0.304s54.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.689s (-11.3% 🟢)7.509s (-8.2% 🟢)1.819s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.441s (-46.3% 🟢)1.006s (~)0.565s601.00x
🐘 PostgresExpress0.446s (-46.8% 🟢)1.007s (-1.5%)0.561s601.01x
💻 LocalExpress0.479s (-51.3% 🟢)1.004s (-6.7% 🟢)0.525s601.09x
💻 LocalNitro0.511s (-47.8% 🟢)1.021s (-6.6% 🟢)0.510s591.16x
🐘 PostgresNext.js (Turbopack)0.703s1.006s0.303s601.60x
💻 LocalNext.js (Turbopack)0.708s1.004s0.297s601.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.871s (-74.4% 🟢)6.286s (-70.5% 🟢)1.414s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.011s (-47.6% 🟢)1.435s (-31.7% 🟢)0.424s631.00x
🐘 PostgresExpress1.085s (-45.1% 🟢)1.706s (-24.5% 🟢)0.621s531.07x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.18x
💻 LocalExpress1.243s (-58.8% 🟢)2.006s (-44.0% 🟢)0.763s451.23x
🐘 PostgresNext.js (Turbopack)1.681s2.008s0.327s451.66x
💻 LocalNext.js (Turbopack)1.777s2.006s0.228s451.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express14.195s (-58.9% 🟢)16.255s (-55.8% 🟢)2.060s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.992s (-51.5% 🟢)2.360s (-48.7% 🟢)0.368s511.00x
🐘 PostgresExpress2.147s (-46.2% 🟢)2.719s (-37.8% 🟢)0.572s451.08x
💻 LocalNitro2.762s (-70.3% 🟢)3.032s (-69.7% 🟢)0.271s401.39x
💻 LocalExpress2.777s (-69.8% 🟢)3.008s (-70.0% 🟢)0.231s401.39x
🐘 PostgresNext.js (Turbopack)3.266s4.009s0.743s301.64x
💻 LocalNext.js (Turbopack)3.760s4.041s0.281s301.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express41.820s (-67.8% 🟢)43.762s (-66.9% 🟢)1.942s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.161s (-43.1% 🟢)1.006s (~)0.844s601.00x
🐘 PostgresExpress0.179s (-36.8% 🟢)1.006s (~)0.827s601.11x
🐘 PostgresNext.js (Turbopack)0.229s1.006s0.777s601.42x
💻 LocalExpress0.399s (-28.9% 🟢)1.005s (~)0.606s602.47x
💻 LocalNitro0.405s (-33.0% 🟢)1.005s (-1.7%)0.600s602.51x
💻 LocalNext.js (Turbopack)0.495s1.004s0.510s603.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.638s (+35.0% 🔺)4.119s (+13.2% 🔺)1.481s151.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.300s (-41.2% 🟢)1.006s (~)0.706s901.00x
🐘 PostgresNitro0.311s (-37.3% 🟢)1.007s (~)0.695s901.04x
🐘 PostgresNext.js (Turbopack)0.455s1.007s0.551s901.52x
💻 LocalNext.js (Turbopack)2.122s2.912s0.790s317.07x
💻 LocalNitro2.188s (-13.8% 🟢)2.797s (-7.1% 🟢)0.609s337.29x
💻 LocalExpress2.280s (-9.3% 🟢)2.945s (-2.1%)0.665s317.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.163s (+135.1% 🔺)8.759s (+82.2% 🔺)1.596s111.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.617s (-21.9% 🟢)1.007s (~)0.390s1201.00x
🐘 PostgresExpress0.623s (-23.9% 🟢)1.006s (-1.2%)0.383s1201.01x
🐘 PostgresNext.js (Turbopack)0.943s1.404s0.461s861.53x
💻 LocalNitro10.236s (-8.5% 🟢)10.777s (-7.6% 🟢)0.541s1216.59x
💻 LocalNext.js (Turbopack)10.456s10.937s0.481s1116.94x
💻 LocalExpress10.494s (-6.2% 🟢)11.122s (-6.9% 🟢)0.628s1117.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.378s (+228.5% 🔺)26.033s (+181.6% 🔺)1.655s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (+453.8% 🔺)1.997s (+99.9% 🔺)0.002s (-6.3% 🟢)2.010s (+98.8% 🔺)0.874s101.00x
🐘 PostgresNitro1.137s (+454.7% 🔺)2.003s (+100.3% 🔺)0.001s (-33.3% 🟢)2.009s (+98.7% 🔺)0.872s101.00x
💻 LocalExpress1.140s (+472.8% 🔺)2.005s (+99.6% 🔺)0.012s (+1.7%)2.020s (+98.4% 🔺)0.880s101.00x
💻 LocalNitro1.141s (+433.9% 🔺)2.005s (+99.6% 🔺)0.012s (-1.6%)2.020s (+98.3% 🔺)0.879s101.00x
💻 LocalNext.js (Turbopack)1.172s2.004s0.012s2.019s0.847s101.03x
🐘 PostgresNext.js (Turbopack)1.211s2.002s0.001s2.012s0.801s101.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.140s (-14.6% 🟢)3.359s (-17.9% 🟢)1.604s (+66.9% 🔺)5.401s (-3.4%)3.261s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.492s (+139.0% 🔺)2.003s (+99.0% 🔺)0.004s (-2.5%)2.026s (+98.1% 🔺)0.534s301.00x
🐘 PostgresExpress1.509s (+139.5% 🔺)2.002s (+98.9% 🔺)0.004s (+2.7%)2.026s (+98.0% 🔺)0.517s301.01x
💻 LocalExpress1.543s (+103.9% 🔺)2.012s (+95.5% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.481s301.03x
💻 LocalNext.js (Turbopack)1.666s2.010s0.010s2.023s0.356s301.12x
💻 LocalNitro1.721s (+105.2% 🔺)2.011s (+98.8% 🔺)0.010s (+5.2% 🔺)2.202s (+97.4% 🔺)0.482s281.15x
🐘 PostgresNext.js (Turbopack)1.724s2.010s0.004s2.026s0.302s301.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.884s (-9.5% 🟢)7.238s (-9.6% 🟢)0.364s (-10.9% 🟢)8.088s (-8.4% 🟢)2.204s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.637s (-33.7% 🟢)1.031s (-19.3% 🟢)0.000s (-100.0% 🟢)1.052s (-19.5% 🟢)0.414s581.00x
🐘 PostgresNitro0.640s (-33.9% 🟢)1.031s (-17.3% 🟢)0.000s (+65.5% 🔺)1.044s (-17.0% 🟢)0.403s581.00x
🐘 PostgresNext.js (Turbopack)0.767s1.018s0.000s1.035s0.268s581.20x
💻 LocalExpress1.362s (+11.2% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.655s302.14x
💻 LocalNitro1.366s (+11.7% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.651s302.14x
💻 LocalNext.js (Turbopack)1.377s2.013s0.000s2.016s0.639s302.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.604s (-3.6%)4.822s (-5.5% 🟢)0.000s (+83.3% 🔺)5.353s (-3.2%)1.749s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.246s (-29.7% 🟢)2.033s (-6.7% 🟢)0.000s (NaN%)2.043s (-7.1% 🟢)0.797s301.00x
🐘 PostgresNitro1.276s (-28.8% 🟢)2.066s (-3.5%)0.000s (-3.4%)2.082s (-4.2%)0.807s291.02x
🐘 PostgresNext.js (Turbopack)1.651s2.147s0.000s2.156s0.505s281.33x
💻 LocalNext.js (Turbopack)2.602s3.239s0.000s3.242s0.640s192.09x
💻 LocalNitro3.121s (-7.9% 🟢)3.906s (-3.1%)0.001s (+5.5% 🔺)3.908s (-3.2%)0.787s162.51x
💻 LocalExpress3.179s (-8.3% 🟢)3.967s (-1.7%)0.000s (-53.1% 🟢)3.969s (-1.7%)0.790s162.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.026s (+31.3% 🔺)7.064s (+17.3% 🔺)0.000s (NaN%)7.559s (+17.1% 🔺)1.534s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress9/21
🐘 PostgresNitro14/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@pranaygp
pranaygp marked this pull request as ready for review May 20, 2026 18:27
@pranaygp
pranaygp requested a review from a team as a code ownerMay 20, 2026 18:27
CopilotAI review requested due to automatic review settings May 20, 2026 18:27

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

Forward-ports the “stale wait replay” race fix to main by making the V2 runtime refresh its event snapshot after committing elapsed wait_completed events, ensuring concurrent durable events (e.g. hook_received) are observed in deterministic order during replay.

Changes:

  • Extends EventResult to optionally include cursor/hasMore pagination metadata for preloaded run_started event snapshots.
  • Updates runtime replay to (a) only trust preloaded events when hasMore !== true, and (b) refresh events after elapsed wait completions using cursor-delta fetch with a full-reload fallback.
  • Adds a deterministic handler-level regression test for the hook-vs-sleep interleaving and ships a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world/src/events.tsAdds optional cursor/hasMore fields to EventResult for preloaded event snapshots.
packages/world-vercel/src/events.tsAccepts/forwards cursor and hasMore from the backend in events.create responses.
packages/world-postgres/src/storage.tsPopulates cursor/hasMore for run_started preloaded events.
packages/world-local/src/storage/events-storage.tsReturns preloaded events with pagination metadata (and caps preload size).
packages/core/src/runtime.tsUses preload pagination metadata and refreshes event snapshots after completing elapsed waits to preserve replay determinism.
packages/core/src/runtime/wait-completion-replay.test.tsNew deterministic regression test that simulates the stale snapshot + concurrent hook ordering race.
.changeset/bright-waits-refresh.mdPatch bumps for affected packages.

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

Comment on lines 1107 to 1108
// For run_started: include all events so the runtime can skip
// the initial events.list call and reduce TTFB.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the run_started preload comment to call out the one-page preload and hasMore check.

Comment on lines 748 to +752
for (const waitEvent of waitsToComplete) {
try {
const result = await world.events.create(
runId,
waitEvent,
{ requestId }
);
events.push(result.event!);
await world.events.create(runId, waitEvent, {
requestId,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the dedupe comment to describe the refreshed wait-completion delta path instead of the old manual push behavior.

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

Approve — clean forward-port of #2029, with V2-specific adaptations

Forward-ports the stable wait-replay race fix to main. The diff against current main looks larger than the stable PR because the branch was forked before #2030 (which added optional stepName/token/resumeAt fields to several event schemas). After rebasing onto current main, the actual change is +593/-15 across 7 files, all consistent with what landed on stable.

Why the V2 adaptations are correct

I traced both versions side by side to make sure nothing was lost in translation:

  • Cursor preservation on first iteration (preloadedEventsCursor plumbed through to eventsCursor) — symmetric with stable.
  • Wait-completion refresh has TWO additions on main vs stable:
    • Dedupe by eventId when merging delta back into existing events (line 803-810). On stable this is just events.push(...newEvents.events). On main it's defensive against the V2 multi-iteration loop where events may persist across iterations as cachedEvents.
    • eventsCursor = loaded.cursor ?? eventsCursor (line 811). Critical for the V2 loop — without it, the next iteration's loadWorkflowRunEvents(runId, eventsCursor) would re-fetch the SAME delta. Stable's loop doesn't have this concern because it doesn't iterate.
  • stripEventDataRefs round-trip through resolveData is identical to the existing events.create flow — no new code paths.

Correctness walk-through

Scenario: workflow with Promise.race([hook, sleep]), hook arrives durably just before handler commits wait_completed.

  1. Handler reads events at cursor C. Sees elapsed wait_created, no hook_received.
  2. Before handler commits wait_completed: another writer commits hook_received (durable position > C).
  3. Handler commits wait_completed (durable position > hook_received).
  4. Without fix: in-memory events missing hook_received. Replay takes sleep branch. Wrong.
  5. With fix: refresh from cursor C, get [hook_received, wait_completed]. Append. Replay takes hook branch. Correct.

The sawAllWaitCompletions defensive check (line 795-800) is smart — if the delta-after-cursor doesn't include the wait completion this handler just wrote, the cursor wasn't a safe replay boundary (read replica lag, eventual consistency hiccup, etc.) and we fall back to a full reload from the beginning.

Test coverage

The four scenarios in wait-completion-replay.test.ts map cleanly onto the code paths:

TestPath exercised
loads only events after the preloaded cursorHappy path: preload + cursor + complete
falls back to a full reload when preloaded events do not include a cursorBackward compat: old worlds without cursor
falls back to a full reload when preloaded events are partialhasMore: true → discard preload, full reload
falls back when cursor delta misses the attempted wait completionDefensive: sawAllWaitCompletions fails

Each scenario asserts both the number of listEvents calls AND the cursor used on each call, so the code paths are pinned, not just outcomes.

What I verified

  • After rebasing onto main: 7 files changed, +593/-15. The events.ts schema "removals" in the raw diff against main are artifacts of the branch being behind #2030 — they vanish on rebase.
  • pnpm --filter @workflow/core test ✓ (965/965 including the new 4 in wait-completion-replay.test.ts)
  • pnpm --filter @workflow/core test packages/core/src/hook-sleep-interaction.test.ts ✓ (the seeded-repro test that was deleted on stable still passes here — main keeps both old + new tests)
  • Typecheck for core, world, world-local, world-postgres, world-vercel ✓
  • Build ✓
  • CI: 107 success, 1 failure, 4 in-progress — the failure is Benchmark Vercel (nextjs-turbopack), which is unrelated (benchmark timing) and main is also failing it on recent commits. All E2E + Unit Tests pass.

Two minor nits (non-blocking)

  1. world-postgreshasMore: false is hardcoded (line 1291). The query has no LIMIT, so this is correct today. If someone ever adds a LIMIT N+1 pattern to that query for pagination, the hardcoded false would silently lie. Worth a comment like // hasMore = false: query has no LIMIT, so we always return all events to make the assumption explicit. Compare to the local world, where hasMore is derived from the paginated query.

  2. The dedupe comment in the iteration-N+1 fetch path (line 650-654) reads as if the cursor isn't advanced after the wait-completion refresh. But this PR DOES advance it (line 811: eventsCursor = loaded.cursor ?? eventsCursor). The dedupe is now effectively defensive against worlds that don't always advance cursors (e.g., return null when no new events) — which is what makes it still worthwhile, but the comment could say so. Minor; understandable as-is.

Backport stance

Body says "this PR is itself a forward port from stable, so it should not be backported." Agreed — the backport action's AI should reach the same conclusion (the fix already exists on stable as #2029), and if it doesn't, manual override is fine.

Approving.

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for dc0be50 (AI decision).

This PR is explicitly a forward-port from stable PR #2029, adapted to main's V2 replay loop and event cache. The underlying fix already exists on stable, and the changes here build on main-only APIs (cursor/hasMore metadata in preloaded run_started snapshots), so backporting would be redundant and likely conflict with the existing stable implementation.

To override, re-run the Backport to stable workflow manually via workflow_dispatch with this commit SHA.

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.

3 participants

@pranaygp@TooTallNate
, '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('^' + ".*" + ' [codex] Forward port stale wait replay fix by pranaygp · Pull Request #2038 · vercel/workflow · GitHub
Skip to content

[codex] Forward port stale wait replay fix - #2038

Merged
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix
May 20, 2026
Merged

[codex] Forward port stale wait replay fix#2038
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix

Conversation

@pranaygp

@pranaygppranaygp commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Forward-port the stale wait replay race fix from stable PR #2029 to main.

This keeps workflow replay deterministic when an elapsed wait_completed is committed from a stale event snapshot while a concurrent hook_received event landed durably before it. On main, the fix is adapted to the V2 replay loop/event cache:

  • use cursor / hasMore metadata from preloaded run_started event snapshots
  • refresh events after attempting elapsed wait completions before replaying
  • fetch only the delta after the prior cursor when possible, with a full-reload fallback when cursor metadata is missing or unsafe
  • add the deterministic handler-level regression test for the hook-vs-sleep wait race

Forward-Port Note

This PR is itself a forward port from stable, so it should not be backported.

Validation

  • pnpm --filter @workflow/world build
  • pnpm --filter @workflow/utils build
  • pnpm --filter @workflow/errors build
  • pnpm --filter @workflow/serde build
  • pnpm --filter @workflow/world-local build
  • pnpm --filter @workflow/world-vercel build
  • pnpm --filter @workflow/core build
  • pnpm vitest run packages/core/src/runtime/wait-completion-replay.test.ts
  • pnpm vitest run packages/core/src/hook-sleep-interaction.test.ts
  • pnpm --filter @workflow/core typecheck
  • pnpm --filter @workflow/world-local typecheck
  • pnpm --filter @workflow/world-postgres typecheck
  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm changeset status --since=main
  • pnpm biome check --files-ignore-unknown=true .changeset/bright-waits-refresh.md packages/core/src/runtime.ts packages/core/src/runtime/wait-completion-replay.test.ts packages/world/src/events.ts packages/world-local/src/storage/events-storage.ts packages/world-postgres/src/storage.ts packages/world-vercel/src/events.ts

Note: local commands run under Node v25.2.1, which emits the repo's unsupported-engine warning. Biome exits successfully but reports existing warnings in the large runtime/storage files.

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2f724a

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-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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

@github-actions

github-actionsBot commented May 20, 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 20, 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.031s (-29.8% 🟢)1.006s (~)0.975s101.00x
💻 LocalNitro0.032s (-26.7% 🟢)1.005s (~)0.974s101.02x
🐘 PostgresNitro0.048s (-49.9% 🟢)1.012s (-2.9%)0.965s101.53x
💻 LocalNext.js (Turbopack)0.050s1.006s0.956s101.60x
🐘 PostgresExpress0.053s (-8.3% 🟢)1.013s (~)0.960s101.71x
🐘 PostgresNext.js (Turbopack)0.059s1.012s0.953s101.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.263s (+11.8% 🔺)2.152s (+0.7%)1.888s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (-5.0% 🟢)2.006s (~)0.931s101.00x
🐘 PostgresNitro1.076s (-5.6% 🟢)2.009s (~)0.933s101.00x
💻 LocalExpress1.076s (-4.4%)2.007s (~)0.931s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.010s (~)0.926s101.01x
💻 LocalNext.js (Turbopack)1.107s2.006s0.899s101.03x
🐘 PostgresNext.js (Turbopack)1.132s2.009s0.877s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.542s (-17.8% 🟢)3.515s (-7.7% 🟢)1.973s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.410s (-5.1% 🟢)11.016s (~)0.607s31.00x
🐘 PostgresNitro10.417s (-4.2%)11.016s (~)0.599s31.00x
💻 LocalExpress10.427s (-4.5%)11.023s (~)0.596s31.00x
💻 LocalNitro10.433s (-4.7%)11.023s (~)0.590s31.00x
💻 LocalNext.js (Turbopack)10.647s11.021s0.374s31.02x
🐘 PostgresNext.js (Turbopack)10.698s11.025s0.327s31.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.211s (-22.2% 🟢)14.873s (-25.7% 🟢)1.662s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.423s (-8.0% 🟢)14.015s (-6.7% 🟢)0.593s51.00x
🐘 PostgresExpress13.473s (-7.6% 🟢)14.016s (-6.7% 🟢)0.543s51.00x
💻 LocalNitro13.509s (-10.3% 🟢)14.028s (-12.5% 🟢)0.519s51.01x
💻 LocalExpress13.525s (-9.7% 🟢)14.029s (-6.7% 🟢)0.504s51.01x
💻 LocalNext.js (Turbopack)14.049s14.827s0.778s51.05x
🐘 PostgresNext.js (Turbopack)14.168s15.017s0.849s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.258s (-57.7% 🟢)23.028s (-56.2% 🟢)1.770s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro11.903s (-14.8% 🟢)12.143s (-15.1% 🟢)0.240s81.00x
💻 LocalNitro11.931s (-28.9% 🟢)12.021s (-29.4% 🟢)0.090s81.00x
🐘 PostgresExpress11.984s (-14.4% 🟢)12.515s (-14.2% 🟢)0.531s81.01x
💻 LocalExpress12.022s (-27.6% 🟢)12.398s (-27.2% 🟢)0.377s81.01x
💻 LocalNext.js (Turbopack)13.063s13.596s0.533s71.10x
🐘 PostgresNext.js (Turbopack)13.220s14.017s0.798s71.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.761s (-72.1% 🟢)35.436s (-71.3% 🟢)1.675s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.144s (-10.3% 🟢)2.008s (~)0.865s151.00x
🐘 PostgresExpress1.146s (-9.1% 🟢)2.006s (~)0.861s151.00x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.826s151.03x
💻 LocalExpress1.188s (-20.2% 🟢)2.006s (~)0.818s151.04x
🐘 PostgresNext.js (Turbopack)1.238s2.009s0.771s151.08x
💻 LocalNext.js (Turbopack)1.257s2.005s0.748s151.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.685s (-6.1% 🟢)4.192s (-9.3% 🟢)1.507s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.817s151.00x
🐘 PostgresExpress1.201s (-49.1% 🟢)2.007s (-33.3% 🟢)0.807s151.01x
🐘 PostgresNext.js (Turbopack)1.401s2.007s0.605s151.18x
💻 LocalNitro1.755s (-44.2% 🟢)2.005s (-48.4% 🟢)0.251s151.47x
💻 LocalNext.js (Turbopack)1.789s2.007s0.218s151.50x
💻 LocalExpress1.861s (-37.0% 🟢)2.149s (-37.8% 🟢)0.288s141.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.773s (+31.9% 🔺)6.447s (+26.2% 🔺)1.674s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.311s (-62.3% 🟢)2.008s (-49.9% 🟢)0.698s151.00x
🐘 PostgresExpress1.331s (-61.8% 🟢)2.006s (-50.0% 🟢)0.676s151.02x
🐘 PostgresNext.js (Turbopack)1.722s2.009s0.287s151.31x
💻 LocalNitro4.817s (-42.3% 🟢)5.345s (-40.7% 🟢)0.528s63.68x
💻 LocalExpress5.405s (-35.2% 🟢)6.013s (-33.4% 🟢)0.608s54.12x
💻 LocalNext.js (Turbopack)5.527s6.213s0.686s54.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.418s (+27.8% 🔺)7.587s (+23.8% 🔺)2.169s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.127s (-10.4% 🟢)2.009s (~)0.882s151.00x
🐘 PostgresExpress1.136s (-9.6% 🟢)2.008s (~)0.872s151.01x
🐘 PostgresNext.js (Turbopack)1.221s2.008s0.786s151.08x
💻 LocalNext.js (Turbopack)1.301s2.005s0.704s151.15x
💻 LocalNitro1.385s (-25.8% 🟢)2.006s (-14.3% 🟢)0.621s151.23x
💻 LocalExpress1.388s (-26.7% 🟢)2.006s (-15.1% 🟢)0.618s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.758s (+6.8% 🔺)4.407s (+1.3%)1.649s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNitro1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNext.js (Turbopack)1.369s2.009s0.639s151.14x
💻 LocalExpress2.004s (-36.0% 🟢)2.392s (-36.4% 🟢)0.389s131.66x
💻 LocalNext.js (Turbopack)2.047s2.674s0.627s121.70x
💻 LocalNitro2.070s (-32.5% 🟢)2.507s (-35.5% 🟢)0.438s121.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.677s (+46.5% 🔺)6.286s (+31.2% 🔺)1.609s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.324s (-62.1% 🟢)2.008s (-49.9% 🟢)0.683s151.00x
🐘 PostgresNitro1.329s (-61.8% 🟢)2.007s (-49.9% 🟢)0.678s151.00x
🐘 PostgresNext.js (Turbopack)1.632s2.009s0.376s151.23x
💻 LocalExpress5.661s (-35.7% 🟢)6.415s (-30.8% 🟢)0.754s54.27x
💻 LocalNext.js (Turbopack)5.927s6.213s0.286s54.48x
💻 LocalNitro6.314s (-31.0% 🟢)6.617s (-34.0% 🟢)0.304s54.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.689s (-11.3% 🟢)7.509s (-8.2% 🟢)1.819s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.441s (-46.3% 🟢)1.006s (~)0.565s601.00x
🐘 PostgresExpress0.446s (-46.8% 🟢)1.007s (-1.5%)0.561s601.01x
💻 LocalExpress0.479s (-51.3% 🟢)1.004s (-6.7% 🟢)0.525s601.09x
💻 LocalNitro0.511s (-47.8% 🟢)1.021s (-6.6% 🟢)0.510s591.16x
🐘 PostgresNext.js (Turbopack)0.703s1.006s0.303s601.60x
💻 LocalNext.js (Turbopack)0.708s1.004s0.297s601.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.871s (-74.4% 🟢)6.286s (-70.5% 🟢)1.414s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.011s (-47.6% 🟢)1.435s (-31.7% 🟢)0.424s631.00x
🐘 PostgresExpress1.085s (-45.1% 🟢)1.706s (-24.5% 🟢)0.621s531.07x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.18x
💻 LocalExpress1.243s (-58.8% 🟢)2.006s (-44.0% 🟢)0.763s451.23x
🐘 PostgresNext.js (Turbopack)1.681s2.008s0.327s451.66x
💻 LocalNext.js (Turbopack)1.777s2.006s0.228s451.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express14.195s (-58.9% 🟢)16.255s (-55.8% 🟢)2.060s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.992s (-51.5% 🟢)2.360s (-48.7% 🟢)0.368s511.00x
🐘 PostgresExpress2.147s (-46.2% 🟢)2.719s (-37.8% 🟢)0.572s451.08x
💻 LocalNitro2.762s (-70.3% 🟢)3.032s (-69.7% 🟢)0.271s401.39x
💻 LocalExpress2.777s (-69.8% 🟢)3.008s (-70.0% 🟢)0.231s401.39x
🐘 PostgresNext.js (Turbopack)3.266s4.009s0.743s301.64x
💻 LocalNext.js (Turbopack)3.760s4.041s0.281s301.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express41.820s (-67.8% 🟢)43.762s (-66.9% 🟢)1.942s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.161s (-43.1% 🟢)1.006s (~)0.844s601.00x
🐘 PostgresExpress0.179s (-36.8% 🟢)1.006s (~)0.827s601.11x
🐘 PostgresNext.js (Turbopack)0.229s1.006s0.777s601.42x
💻 LocalExpress0.399s (-28.9% 🟢)1.005s (~)0.606s602.47x
💻 LocalNitro0.405s (-33.0% 🟢)1.005s (-1.7%)0.600s602.51x
💻 LocalNext.js (Turbopack)0.495s1.004s0.510s603.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.638s (+35.0% 🔺)4.119s (+13.2% 🔺)1.481s151.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.300s (-41.2% 🟢)1.006s (~)0.706s901.00x
🐘 PostgresNitro0.311s (-37.3% 🟢)1.007s (~)0.695s901.04x
🐘 PostgresNext.js (Turbopack)0.455s1.007s0.551s901.52x
💻 LocalNext.js (Turbopack)2.122s2.912s0.790s317.07x
💻 LocalNitro2.188s (-13.8% 🟢)2.797s (-7.1% 🟢)0.609s337.29x
💻 LocalExpress2.280s (-9.3% 🟢)2.945s (-2.1%)0.665s317.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.163s (+135.1% 🔺)8.759s (+82.2% 🔺)1.596s111.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.617s (-21.9% 🟢)1.007s (~)0.390s1201.00x
🐘 PostgresExpress0.623s (-23.9% 🟢)1.006s (-1.2%)0.383s1201.01x
🐘 PostgresNext.js (Turbopack)0.943s1.404s0.461s861.53x
💻 LocalNitro10.236s (-8.5% 🟢)10.777s (-7.6% 🟢)0.541s1216.59x
💻 LocalNext.js (Turbopack)10.456s10.937s0.481s1116.94x
💻 LocalExpress10.494s (-6.2% 🟢)11.122s (-6.9% 🟢)0.628s1117.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.378s (+228.5% 🔺)26.033s (+181.6% 🔺)1.655s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (+453.8% 🔺)1.997s (+99.9% 🔺)0.002s (-6.3% 🟢)2.010s (+98.8% 🔺)0.874s101.00x
🐘 PostgresNitro1.137s (+454.7% 🔺)2.003s (+100.3% 🔺)0.001s (-33.3% 🟢)2.009s (+98.7% 🔺)0.872s101.00x
💻 LocalExpress1.140s (+472.8% 🔺)2.005s (+99.6% 🔺)0.012s (+1.7%)2.020s (+98.4% 🔺)0.880s101.00x
💻 LocalNitro1.141s (+433.9% 🔺)2.005s (+99.6% 🔺)0.012s (-1.6%)2.020s (+98.3% 🔺)0.879s101.00x
💻 LocalNext.js (Turbopack)1.172s2.004s0.012s2.019s0.847s101.03x
🐘 PostgresNext.js (Turbopack)1.211s2.002s0.001s2.012s0.801s101.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.140s (-14.6% 🟢)3.359s (-17.9% 🟢)1.604s (+66.9% 🔺)5.401s (-3.4%)3.261s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.492s (+139.0% 🔺)2.003s (+99.0% 🔺)0.004s (-2.5%)2.026s (+98.1% 🔺)0.534s301.00x
🐘 PostgresExpress1.509s (+139.5% 🔺)2.002s (+98.9% 🔺)0.004s (+2.7%)2.026s (+98.0% 🔺)0.517s301.01x
💻 LocalExpress1.543s (+103.9% 🔺)2.012s (+95.5% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.481s301.03x
💻 LocalNext.js (Turbopack)1.666s2.010s0.010s2.023s0.356s301.12x
💻 LocalNitro1.721s (+105.2% 🔺)2.011s (+98.8% 🔺)0.010s (+5.2% 🔺)2.202s (+97.4% 🔺)0.482s281.15x
🐘 PostgresNext.js (Turbopack)1.724s2.010s0.004s2.026s0.302s301.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.884s (-9.5% 🟢)7.238s (-9.6% 🟢)0.364s (-10.9% 🟢)8.088s (-8.4% 🟢)2.204s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.637s (-33.7% 🟢)1.031s (-19.3% 🟢)0.000s (-100.0% 🟢)1.052s (-19.5% 🟢)0.414s581.00x
🐘 PostgresNitro0.640s (-33.9% 🟢)1.031s (-17.3% 🟢)0.000s (+65.5% 🔺)1.044s (-17.0% 🟢)0.403s581.00x
🐘 PostgresNext.js (Turbopack)0.767s1.018s0.000s1.035s0.268s581.20x
💻 LocalExpress1.362s (+11.2% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.655s302.14x
💻 LocalNitro1.366s (+11.7% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.651s302.14x
💻 LocalNext.js (Turbopack)1.377s2.013s0.000s2.016s0.639s302.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.604s (-3.6%)4.822s (-5.5% 🟢)0.000s (+83.3% 🔺)5.353s (-3.2%)1.749s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.246s (-29.7% 🟢)2.033s (-6.7% 🟢)0.000s (NaN%)2.043s (-7.1% 🟢)0.797s301.00x
🐘 PostgresNitro1.276s (-28.8% 🟢)2.066s (-3.5%)0.000s (-3.4%)2.082s (-4.2%)0.807s291.02x
🐘 PostgresNext.js (Turbopack)1.651s2.147s0.000s2.156s0.505s281.33x
💻 LocalNext.js (Turbopack)2.602s3.239s0.000s3.242s0.640s192.09x
💻 LocalNitro3.121s (-7.9% 🟢)3.906s (-3.1%)0.001s (+5.5% 🔺)3.908s (-3.2%)0.787s162.51x
💻 LocalExpress3.179s (-8.3% 🟢)3.967s (-1.7%)0.000s (-53.1% 🟢)3.969s (-1.7%)0.790s162.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.026s (+31.3% 🔺)7.064s (+17.3% 🔺)0.000s (NaN%)7.559s (+17.1% 🔺)1.534s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress9/21
🐘 PostgresNitro14/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@pranaygp
pranaygp marked this pull request as ready for review May 20, 2026 18:27
@pranaygp
pranaygp requested a review from a team as a code ownerMay 20, 2026 18:27
CopilotAI review requested due to automatic review settings May 20, 2026 18:27

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

Forward-ports the “stale wait replay” race fix to main by making the V2 runtime refresh its event snapshot after committing elapsed wait_completed events, ensuring concurrent durable events (e.g. hook_received) are observed in deterministic order during replay.

Changes:

  • Extends EventResult to optionally include cursor/hasMore pagination metadata for preloaded run_started event snapshots.
  • Updates runtime replay to (a) only trust preloaded events when hasMore !== true, and (b) refresh events after elapsed wait completions using cursor-delta fetch with a full-reload fallback.
  • Adds a deterministic handler-level regression test for the hook-vs-sleep interleaving and ships a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world/src/events.tsAdds optional cursor/hasMore fields to EventResult for preloaded event snapshots.
packages/world-vercel/src/events.tsAccepts/forwards cursor and hasMore from the backend in events.create responses.
packages/world-postgres/src/storage.tsPopulates cursor/hasMore for run_started preloaded events.
packages/world-local/src/storage/events-storage.tsReturns preloaded events with pagination metadata (and caps preload size).
packages/core/src/runtime.tsUses preload pagination metadata and refreshes event snapshots after completing elapsed waits to preserve replay determinism.
packages/core/src/runtime/wait-completion-replay.test.tsNew deterministic regression test that simulates the stale snapshot + concurrent hook ordering race.
.changeset/bright-waits-refresh.mdPatch bumps for affected packages.

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

Comment on lines 1107 to 1108
// For run_started: include all events so the runtime can skip
// the initial events.list call and reduce TTFB.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the run_started preload comment to call out the one-page preload and hasMore check.

Comment on lines 748 to +752
for (const waitEvent of waitsToComplete) {
try {
const result = await world.events.create(
runId,
waitEvent,
{ requestId }
);
events.push(result.event!);
await world.events.create(runId, waitEvent, {
requestId,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the dedupe comment to describe the refreshed wait-completion delta path instead of the old manual push behavior.

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

Approve — clean forward-port of #2029, with V2-specific adaptations

Forward-ports the stable wait-replay race fix to main. The diff against current main looks larger than the stable PR because the branch was forked before #2030 (which added optional stepName/token/resumeAt fields to several event schemas). After rebasing onto current main, the actual change is +593/-15 across 7 files, all consistent with what landed on stable.

Why the V2 adaptations are correct

I traced both versions side by side to make sure nothing was lost in translation:

  • Cursor preservation on first iteration (preloadedEventsCursor plumbed through to eventsCursor) — symmetric with stable.
  • Wait-completion refresh has TWO additions on main vs stable:
    • Dedupe by eventId when merging delta back into existing events (line 803-810). On stable this is just events.push(...newEvents.events). On main it's defensive against the V2 multi-iteration loop where events may persist across iterations as cachedEvents.
    • eventsCursor = loaded.cursor ?? eventsCursor (line 811). Critical for the V2 loop — without it, the next iteration's loadWorkflowRunEvents(runId, eventsCursor) would re-fetch the SAME delta. Stable's loop doesn't have this concern because it doesn't iterate.
  • stripEventDataRefs round-trip through resolveData is identical to the existing events.create flow — no new code paths.

Correctness walk-through

Scenario: workflow with Promise.race([hook, sleep]), hook arrives durably just before handler commits wait_completed.

  1. Handler reads events at cursor C. Sees elapsed wait_created, no hook_received.
  2. Before handler commits wait_completed: another writer commits hook_received (durable position > C).
  3. Handler commits wait_completed (durable position > hook_received).
  4. Without fix: in-memory events missing hook_received. Replay takes sleep branch. Wrong.
  5. With fix: refresh from cursor C, get [hook_received, wait_completed]. Append. Replay takes hook branch. Correct.

The sawAllWaitCompletions defensive check (line 795-800) is smart — if the delta-after-cursor doesn't include the wait completion this handler just wrote, the cursor wasn't a safe replay boundary (read replica lag, eventual consistency hiccup, etc.) and we fall back to a full reload from the beginning.

Test coverage

The four scenarios in wait-completion-replay.test.ts map cleanly onto the code paths:

TestPath exercised
loads only events after the preloaded cursorHappy path: preload + cursor + complete
falls back to a full reload when preloaded events do not include a cursorBackward compat: old worlds without cursor
falls back to a full reload when preloaded events are partialhasMore: true → discard preload, full reload
falls back when cursor delta misses the attempted wait completionDefensive: sawAllWaitCompletions fails

Each scenario asserts both the number of listEvents calls AND the cursor used on each call, so the code paths are pinned, not just outcomes.

What I verified

  • After rebasing onto main: 7 files changed, +593/-15. The events.ts schema "removals" in the raw diff against main are artifacts of the branch being behind #2030 — they vanish on rebase.
  • pnpm --filter @workflow/core test ✓ (965/965 including the new 4 in wait-completion-replay.test.ts)
  • pnpm --filter @workflow/core test packages/core/src/hook-sleep-interaction.test.ts ✓ (the seeded-repro test that was deleted on stable still passes here — main keeps both old + new tests)
  • Typecheck for core, world, world-local, world-postgres, world-vercel ✓
  • Build ✓
  • CI: 107 success, 1 failure, 4 in-progress — the failure is Benchmark Vercel (nextjs-turbopack), which is unrelated (benchmark timing) and main is also failing it on recent commits. All E2E + Unit Tests pass.

Two minor nits (non-blocking)

  1. world-postgreshasMore: false is hardcoded (line 1291). The query has no LIMIT, so this is correct today. If someone ever adds a LIMIT N+1 pattern to that query for pagination, the hardcoded false would silently lie. Worth a comment like // hasMore = false: query has no LIMIT, so we always return all events to make the assumption explicit. Compare to the local world, where hasMore is derived from the paginated query.

  2. The dedupe comment in the iteration-N+1 fetch path (line 650-654) reads as if the cursor isn't advanced after the wait-completion refresh. But this PR DOES advance it (line 811: eventsCursor = loaded.cursor ?? eventsCursor). The dedupe is now effectively defensive against worlds that don't always advance cursors (e.g., return null when no new events) — which is what makes it still worthwhile, but the comment could say so. Minor; understandable as-is.

Backport stance

Body says "this PR is itself a forward port from stable, so it should not be backported." Agreed — the backport action's AI should reach the same conclusion (the fix already exists on stable as #2029), and if it doesn't, manual override is fine.

Approving.

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for dc0be50 (AI decision).

This PR is explicitly a forward-port from stable PR #2029, adapted to main's V2 replay loop and event cache. The underlying fix already exists on stable, and the changes here build on main-only APIs (cursor/hasMore metadata in preloaded run_started snapshots), so backporting would be redundant and likely conflict with the existing stable implementation.

To override, re-run the Backport to stable workflow manually via workflow_dispatch with this commit SHA.

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.

3 participants

@pranaygp@TooTallNate
, '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" + ' [codex] Forward port stale wait replay fix by pranaygp · Pull Request #2038 · vercel/workflow · GitHub
Skip to content

[codex] Forward port stale wait replay fix - #2038

Merged
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix
May 20, 2026
Merged

[codex] Forward port stale wait replay fix#2038
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix

Conversation

@pranaygp

@pranaygppranaygp commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Forward-port the stale wait replay race fix from stable PR #2029 to main.

This keeps workflow replay deterministic when an elapsed wait_completed is committed from a stale event snapshot while a concurrent hook_received event landed durably before it. On main, the fix is adapted to the V2 replay loop/event cache:

  • use cursor / hasMore metadata from preloaded run_started event snapshots
  • refresh events after attempting elapsed wait completions before replaying
  • fetch only the delta after the prior cursor when possible, with a full-reload fallback when cursor metadata is missing or unsafe
  • add the deterministic handler-level regression test for the hook-vs-sleep wait race

Forward-Port Note

This PR is itself a forward port from stable, so it should not be backported.

Validation

  • pnpm --filter @workflow/world build
  • pnpm --filter @workflow/utils build
  • pnpm --filter @workflow/errors build
  • pnpm --filter @workflow/serde build
  • pnpm --filter @workflow/world-local build
  • pnpm --filter @workflow/world-vercel build
  • pnpm --filter @workflow/core build
  • pnpm vitest run packages/core/src/runtime/wait-completion-replay.test.ts
  • pnpm vitest run packages/core/src/hook-sleep-interaction.test.ts
  • pnpm --filter @workflow/core typecheck
  • pnpm --filter @workflow/world-local typecheck
  • pnpm --filter @workflow/world-postgres typecheck
  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm changeset status --since=main
  • pnpm biome check --files-ignore-unknown=true .changeset/bright-waits-refresh.md packages/core/src/runtime.ts packages/core/src/runtime/wait-completion-replay.test.ts packages/world/src/events.ts packages/world-local/src/storage/events-storage.ts packages/world-postgres/src/storage.ts packages/world-vercel/src/events.ts

Note: local commands run under Node v25.2.1, which emits the repo's unsupported-engine warning. Biome exits successfully but reports existing warnings in the large runtime/storage files.

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2f724a

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-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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

@github-actions

github-actionsBot commented May 20, 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 20, 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.031s (-29.8% 🟢)1.006s (~)0.975s101.00x
💻 LocalNitro0.032s (-26.7% 🟢)1.005s (~)0.974s101.02x
🐘 PostgresNitro0.048s (-49.9% 🟢)1.012s (-2.9%)0.965s101.53x
💻 LocalNext.js (Turbopack)0.050s1.006s0.956s101.60x
🐘 PostgresExpress0.053s (-8.3% 🟢)1.013s (~)0.960s101.71x
🐘 PostgresNext.js (Turbopack)0.059s1.012s0.953s101.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.263s (+11.8% 🔺)2.152s (+0.7%)1.888s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (-5.0% 🟢)2.006s (~)0.931s101.00x
🐘 PostgresNitro1.076s (-5.6% 🟢)2.009s (~)0.933s101.00x
💻 LocalExpress1.076s (-4.4%)2.007s (~)0.931s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.010s (~)0.926s101.01x
💻 LocalNext.js (Turbopack)1.107s2.006s0.899s101.03x
🐘 PostgresNext.js (Turbopack)1.132s2.009s0.877s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.542s (-17.8% 🟢)3.515s (-7.7% 🟢)1.973s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.410s (-5.1% 🟢)11.016s (~)0.607s31.00x
🐘 PostgresNitro10.417s (-4.2%)11.016s (~)0.599s31.00x
💻 LocalExpress10.427s (-4.5%)11.023s (~)0.596s31.00x
💻 LocalNitro10.433s (-4.7%)11.023s (~)0.590s31.00x
💻 LocalNext.js (Turbopack)10.647s11.021s0.374s31.02x
🐘 PostgresNext.js (Turbopack)10.698s11.025s0.327s31.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.211s (-22.2% 🟢)14.873s (-25.7% 🟢)1.662s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.423s (-8.0% 🟢)14.015s (-6.7% 🟢)0.593s51.00x
🐘 PostgresExpress13.473s (-7.6% 🟢)14.016s (-6.7% 🟢)0.543s51.00x
💻 LocalNitro13.509s (-10.3% 🟢)14.028s (-12.5% 🟢)0.519s51.01x
💻 LocalExpress13.525s (-9.7% 🟢)14.029s (-6.7% 🟢)0.504s51.01x
💻 LocalNext.js (Turbopack)14.049s14.827s0.778s51.05x
🐘 PostgresNext.js (Turbopack)14.168s15.017s0.849s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.258s (-57.7% 🟢)23.028s (-56.2% 🟢)1.770s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro11.903s (-14.8% 🟢)12.143s (-15.1% 🟢)0.240s81.00x
💻 LocalNitro11.931s (-28.9% 🟢)12.021s (-29.4% 🟢)0.090s81.00x
🐘 PostgresExpress11.984s (-14.4% 🟢)12.515s (-14.2% 🟢)0.531s81.01x
💻 LocalExpress12.022s (-27.6% 🟢)12.398s (-27.2% 🟢)0.377s81.01x
💻 LocalNext.js (Turbopack)13.063s13.596s0.533s71.10x
🐘 PostgresNext.js (Turbopack)13.220s14.017s0.798s71.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.761s (-72.1% 🟢)35.436s (-71.3% 🟢)1.675s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.144s (-10.3% 🟢)2.008s (~)0.865s151.00x
🐘 PostgresExpress1.146s (-9.1% 🟢)2.006s (~)0.861s151.00x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.826s151.03x
💻 LocalExpress1.188s (-20.2% 🟢)2.006s (~)0.818s151.04x
🐘 PostgresNext.js (Turbopack)1.238s2.009s0.771s151.08x
💻 LocalNext.js (Turbopack)1.257s2.005s0.748s151.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.685s (-6.1% 🟢)4.192s (-9.3% 🟢)1.507s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.817s151.00x
🐘 PostgresExpress1.201s (-49.1% 🟢)2.007s (-33.3% 🟢)0.807s151.01x
🐘 PostgresNext.js (Turbopack)1.401s2.007s0.605s151.18x
💻 LocalNitro1.755s (-44.2% 🟢)2.005s (-48.4% 🟢)0.251s151.47x
💻 LocalNext.js (Turbopack)1.789s2.007s0.218s151.50x
💻 LocalExpress1.861s (-37.0% 🟢)2.149s (-37.8% 🟢)0.288s141.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.773s (+31.9% 🔺)6.447s (+26.2% 🔺)1.674s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.311s (-62.3% 🟢)2.008s (-49.9% 🟢)0.698s151.00x
🐘 PostgresExpress1.331s (-61.8% 🟢)2.006s (-50.0% 🟢)0.676s151.02x
🐘 PostgresNext.js (Turbopack)1.722s2.009s0.287s151.31x
💻 LocalNitro4.817s (-42.3% 🟢)5.345s (-40.7% 🟢)0.528s63.68x
💻 LocalExpress5.405s (-35.2% 🟢)6.013s (-33.4% 🟢)0.608s54.12x
💻 LocalNext.js (Turbopack)5.527s6.213s0.686s54.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.418s (+27.8% 🔺)7.587s (+23.8% 🔺)2.169s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.127s (-10.4% 🟢)2.009s (~)0.882s151.00x
🐘 PostgresExpress1.136s (-9.6% 🟢)2.008s (~)0.872s151.01x
🐘 PostgresNext.js (Turbopack)1.221s2.008s0.786s151.08x
💻 LocalNext.js (Turbopack)1.301s2.005s0.704s151.15x
💻 LocalNitro1.385s (-25.8% 🟢)2.006s (-14.3% 🟢)0.621s151.23x
💻 LocalExpress1.388s (-26.7% 🟢)2.006s (-15.1% 🟢)0.618s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.758s (+6.8% 🔺)4.407s (+1.3%)1.649s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNitro1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNext.js (Turbopack)1.369s2.009s0.639s151.14x
💻 LocalExpress2.004s (-36.0% 🟢)2.392s (-36.4% 🟢)0.389s131.66x
💻 LocalNext.js (Turbopack)2.047s2.674s0.627s121.70x
💻 LocalNitro2.070s (-32.5% 🟢)2.507s (-35.5% 🟢)0.438s121.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.677s (+46.5% 🔺)6.286s (+31.2% 🔺)1.609s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.324s (-62.1% 🟢)2.008s (-49.9% 🟢)0.683s151.00x
🐘 PostgresNitro1.329s (-61.8% 🟢)2.007s (-49.9% 🟢)0.678s151.00x
🐘 PostgresNext.js (Turbopack)1.632s2.009s0.376s151.23x
💻 LocalExpress5.661s (-35.7% 🟢)6.415s (-30.8% 🟢)0.754s54.27x
💻 LocalNext.js (Turbopack)5.927s6.213s0.286s54.48x
💻 LocalNitro6.314s (-31.0% 🟢)6.617s (-34.0% 🟢)0.304s54.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.689s (-11.3% 🟢)7.509s (-8.2% 🟢)1.819s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.441s (-46.3% 🟢)1.006s (~)0.565s601.00x
🐘 PostgresExpress0.446s (-46.8% 🟢)1.007s (-1.5%)0.561s601.01x
💻 LocalExpress0.479s (-51.3% 🟢)1.004s (-6.7% 🟢)0.525s601.09x
💻 LocalNitro0.511s (-47.8% 🟢)1.021s (-6.6% 🟢)0.510s591.16x
🐘 PostgresNext.js (Turbopack)0.703s1.006s0.303s601.60x
💻 LocalNext.js (Turbopack)0.708s1.004s0.297s601.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.871s (-74.4% 🟢)6.286s (-70.5% 🟢)1.414s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.011s (-47.6% 🟢)1.435s (-31.7% 🟢)0.424s631.00x
🐘 PostgresExpress1.085s (-45.1% 🟢)1.706s (-24.5% 🟢)0.621s531.07x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.18x
💻 LocalExpress1.243s (-58.8% 🟢)2.006s (-44.0% 🟢)0.763s451.23x
🐘 PostgresNext.js (Turbopack)1.681s2.008s0.327s451.66x
💻 LocalNext.js (Turbopack)1.777s2.006s0.228s451.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express14.195s (-58.9% 🟢)16.255s (-55.8% 🟢)2.060s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.992s (-51.5% 🟢)2.360s (-48.7% 🟢)0.368s511.00x
🐘 PostgresExpress2.147s (-46.2% 🟢)2.719s (-37.8% 🟢)0.572s451.08x
💻 LocalNitro2.762s (-70.3% 🟢)3.032s (-69.7% 🟢)0.271s401.39x
💻 LocalExpress2.777s (-69.8% 🟢)3.008s (-70.0% 🟢)0.231s401.39x
🐘 PostgresNext.js (Turbopack)3.266s4.009s0.743s301.64x
💻 LocalNext.js (Turbopack)3.760s4.041s0.281s301.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express41.820s (-67.8% 🟢)43.762s (-66.9% 🟢)1.942s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.161s (-43.1% 🟢)1.006s (~)0.844s601.00x
🐘 PostgresExpress0.179s (-36.8% 🟢)1.006s (~)0.827s601.11x
🐘 PostgresNext.js (Turbopack)0.229s1.006s0.777s601.42x
💻 LocalExpress0.399s (-28.9% 🟢)1.005s (~)0.606s602.47x
💻 LocalNitro0.405s (-33.0% 🟢)1.005s (-1.7%)0.600s602.51x
💻 LocalNext.js (Turbopack)0.495s1.004s0.510s603.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.638s (+35.0% 🔺)4.119s (+13.2% 🔺)1.481s151.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.300s (-41.2% 🟢)1.006s (~)0.706s901.00x
🐘 PostgresNitro0.311s (-37.3% 🟢)1.007s (~)0.695s901.04x
🐘 PostgresNext.js (Turbopack)0.455s1.007s0.551s901.52x
💻 LocalNext.js (Turbopack)2.122s2.912s0.790s317.07x
💻 LocalNitro2.188s (-13.8% 🟢)2.797s (-7.1% 🟢)0.609s337.29x
💻 LocalExpress2.280s (-9.3% 🟢)2.945s (-2.1%)0.665s317.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.163s (+135.1% 🔺)8.759s (+82.2% 🔺)1.596s111.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.617s (-21.9% 🟢)1.007s (~)0.390s1201.00x
🐘 PostgresExpress0.623s (-23.9% 🟢)1.006s (-1.2%)0.383s1201.01x
🐘 PostgresNext.js (Turbopack)0.943s1.404s0.461s861.53x
💻 LocalNitro10.236s (-8.5% 🟢)10.777s (-7.6% 🟢)0.541s1216.59x
💻 LocalNext.js (Turbopack)10.456s10.937s0.481s1116.94x
💻 LocalExpress10.494s (-6.2% 🟢)11.122s (-6.9% 🟢)0.628s1117.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.378s (+228.5% 🔺)26.033s (+181.6% 🔺)1.655s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (+453.8% 🔺)1.997s (+99.9% 🔺)0.002s (-6.3% 🟢)2.010s (+98.8% 🔺)0.874s101.00x
🐘 PostgresNitro1.137s (+454.7% 🔺)2.003s (+100.3% 🔺)0.001s (-33.3% 🟢)2.009s (+98.7% 🔺)0.872s101.00x
💻 LocalExpress1.140s (+472.8% 🔺)2.005s (+99.6% 🔺)0.012s (+1.7%)2.020s (+98.4% 🔺)0.880s101.00x
💻 LocalNitro1.141s (+433.9% 🔺)2.005s (+99.6% 🔺)0.012s (-1.6%)2.020s (+98.3% 🔺)0.879s101.00x
💻 LocalNext.js (Turbopack)1.172s2.004s0.012s2.019s0.847s101.03x
🐘 PostgresNext.js (Turbopack)1.211s2.002s0.001s2.012s0.801s101.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.140s (-14.6% 🟢)3.359s (-17.9% 🟢)1.604s (+66.9% 🔺)5.401s (-3.4%)3.261s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.492s (+139.0% 🔺)2.003s (+99.0% 🔺)0.004s (-2.5%)2.026s (+98.1% 🔺)0.534s301.00x
🐘 PostgresExpress1.509s (+139.5% 🔺)2.002s (+98.9% 🔺)0.004s (+2.7%)2.026s (+98.0% 🔺)0.517s301.01x
💻 LocalExpress1.543s (+103.9% 🔺)2.012s (+95.5% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.481s301.03x
💻 LocalNext.js (Turbopack)1.666s2.010s0.010s2.023s0.356s301.12x
💻 LocalNitro1.721s (+105.2% 🔺)2.011s (+98.8% 🔺)0.010s (+5.2% 🔺)2.202s (+97.4% 🔺)0.482s281.15x
🐘 PostgresNext.js (Turbopack)1.724s2.010s0.004s2.026s0.302s301.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.884s (-9.5% 🟢)7.238s (-9.6% 🟢)0.364s (-10.9% 🟢)8.088s (-8.4% 🟢)2.204s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.637s (-33.7% 🟢)1.031s (-19.3% 🟢)0.000s (-100.0% 🟢)1.052s (-19.5% 🟢)0.414s581.00x
🐘 PostgresNitro0.640s (-33.9% 🟢)1.031s (-17.3% 🟢)0.000s (+65.5% 🔺)1.044s (-17.0% 🟢)0.403s581.00x
🐘 PostgresNext.js (Turbopack)0.767s1.018s0.000s1.035s0.268s581.20x
💻 LocalExpress1.362s (+11.2% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.655s302.14x
💻 LocalNitro1.366s (+11.7% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.651s302.14x
💻 LocalNext.js (Turbopack)1.377s2.013s0.000s2.016s0.639s302.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.604s (-3.6%)4.822s (-5.5% 🟢)0.000s (+83.3% 🔺)5.353s (-3.2%)1.749s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.246s (-29.7% 🟢)2.033s (-6.7% 🟢)0.000s (NaN%)2.043s (-7.1% 🟢)0.797s301.00x
🐘 PostgresNitro1.276s (-28.8% 🟢)2.066s (-3.5%)0.000s (-3.4%)2.082s (-4.2%)0.807s291.02x
🐘 PostgresNext.js (Turbopack)1.651s2.147s0.000s2.156s0.505s281.33x
💻 LocalNext.js (Turbopack)2.602s3.239s0.000s3.242s0.640s192.09x
💻 LocalNitro3.121s (-7.9% 🟢)3.906s (-3.1%)0.001s (+5.5% 🔺)3.908s (-3.2%)0.787s162.51x
💻 LocalExpress3.179s (-8.3% 🟢)3.967s (-1.7%)0.000s (-53.1% 🟢)3.969s (-1.7%)0.790s162.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.026s (+31.3% 🔺)7.064s (+17.3% 🔺)0.000s (NaN%)7.559s (+17.1% 🔺)1.534s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress9/21
🐘 PostgresNitro14/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@pranaygp
pranaygp marked this pull request as ready for review May 20, 2026 18:27
@pranaygp
pranaygp requested a review from a team as a code ownerMay 20, 2026 18:27
CopilotAI review requested due to automatic review settings May 20, 2026 18:27

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

Forward-ports the “stale wait replay” race fix to main by making the V2 runtime refresh its event snapshot after committing elapsed wait_completed events, ensuring concurrent durable events (e.g. hook_received) are observed in deterministic order during replay.

Changes:

  • Extends EventResult to optionally include cursor/hasMore pagination metadata for preloaded run_started event snapshots.
  • Updates runtime replay to (a) only trust preloaded events when hasMore !== true, and (b) refresh events after elapsed wait completions using cursor-delta fetch with a full-reload fallback.
  • Adds a deterministic handler-level regression test for the hook-vs-sleep interleaving and ships a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world/src/events.tsAdds optional cursor/hasMore fields to EventResult for preloaded event snapshots.
packages/world-vercel/src/events.tsAccepts/forwards cursor and hasMore from the backend in events.create responses.
packages/world-postgres/src/storage.tsPopulates cursor/hasMore for run_started preloaded events.
packages/world-local/src/storage/events-storage.tsReturns preloaded events with pagination metadata (and caps preload size).
packages/core/src/runtime.tsUses preload pagination metadata and refreshes event snapshots after completing elapsed waits to preserve replay determinism.
packages/core/src/runtime/wait-completion-replay.test.tsNew deterministic regression test that simulates the stale snapshot + concurrent hook ordering race.
.changeset/bright-waits-refresh.mdPatch bumps for affected packages.

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

Comment on lines 1107 to 1108
// For run_started: include all events so the runtime can skip
// the initial events.list call and reduce TTFB.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the run_started preload comment to call out the one-page preload and hasMore check.

Comment on lines 748 to +752
for (const waitEvent of waitsToComplete) {
try {
const result = await world.events.create(
runId,
waitEvent,
{ requestId }
);
events.push(result.event!);
await world.events.create(runId, waitEvent, {
requestId,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the dedupe comment to describe the refreshed wait-completion delta path instead of the old manual push behavior.

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

Approve — clean forward-port of #2029, with V2-specific adaptations

Forward-ports the stable wait-replay race fix to main. The diff against current main looks larger than the stable PR because the branch was forked before #2030 (which added optional stepName/token/resumeAt fields to several event schemas). After rebasing onto current main, the actual change is +593/-15 across 7 files, all consistent with what landed on stable.

Why the V2 adaptations are correct

I traced both versions side by side to make sure nothing was lost in translation:

  • Cursor preservation on first iteration (preloadedEventsCursor plumbed through to eventsCursor) — symmetric with stable.
  • Wait-completion refresh has TWO additions on main vs stable:
    • Dedupe by eventId when merging delta back into existing events (line 803-810). On stable this is just events.push(...newEvents.events). On main it's defensive against the V2 multi-iteration loop where events may persist across iterations as cachedEvents.
    • eventsCursor = loaded.cursor ?? eventsCursor (line 811). Critical for the V2 loop — without it, the next iteration's loadWorkflowRunEvents(runId, eventsCursor) would re-fetch the SAME delta. Stable's loop doesn't have this concern because it doesn't iterate.
  • stripEventDataRefs round-trip through resolveData is identical to the existing events.create flow — no new code paths.

Correctness walk-through

Scenario: workflow with Promise.race([hook, sleep]), hook arrives durably just before handler commits wait_completed.

  1. Handler reads events at cursor C. Sees elapsed wait_created, no hook_received.
  2. Before handler commits wait_completed: another writer commits hook_received (durable position > C).
  3. Handler commits wait_completed (durable position > hook_received).
  4. Without fix: in-memory events missing hook_received. Replay takes sleep branch. Wrong.
  5. With fix: refresh from cursor C, get [hook_received, wait_completed]. Append. Replay takes hook branch. Correct.

The sawAllWaitCompletions defensive check (line 795-800) is smart — if the delta-after-cursor doesn't include the wait completion this handler just wrote, the cursor wasn't a safe replay boundary (read replica lag, eventual consistency hiccup, etc.) and we fall back to a full reload from the beginning.

Test coverage

The four scenarios in wait-completion-replay.test.ts map cleanly onto the code paths:

TestPath exercised
loads only events after the preloaded cursorHappy path: preload + cursor + complete
falls back to a full reload when preloaded events do not include a cursorBackward compat: old worlds without cursor
falls back to a full reload when preloaded events are partialhasMore: true → discard preload, full reload
falls back when cursor delta misses the attempted wait completionDefensive: sawAllWaitCompletions fails

Each scenario asserts both the number of listEvents calls AND the cursor used on each call, so the code paths are pinned, not just outcomes.

What I verified

  • After rebasing onto main: 7 files changed, +593/-15. The events.ts schema "removals" in the raw diff against main are artifacts of the branch being behind #2030 — they vanish on rebase.
  • pnpm --filter @workflow/core test ✓ (965/965 including the new 4 in wait-completion-replay.test.ts)
  • pnpm --filter @workflow/core test packages/core/src/hook-sleep-interaction.test.ts ✓ (the seeded-repro test that was deleted on stable still passes here — main keeps both old + new tests)
  • Typecheck for core, world, world-local, world-postgres, world-vercel ✓
  • Build ✓
  • CI: 107 success, 1 failure, 4 in-progress — the failure is Benchmark Vercel (nextjs-turbopack), which is unrelated (benchmark timing) and main is also failing it on recent commits. All E2E + Unit Tests pass.

Two minor nits (non-blocking)

  1. world-postgreshasMore: false is hardcoded (line 1291). The query has no LIMIT, so this is correct today. If someone ever adds a LIMIT N+1 pattern to that query for pagination, the hardcoded false would silently lie. Worth a comment like // hasMore = false: query has no LIMIT, so we always return all events to make the assumption explicit. Compare to the local world, where hasMore is derived from the paginated query.

  2. The dedupe comment in the iteration-N+1 fetch path (line 650-654) reads as if the cursor isn't advanced after the wait-completion refresh. But this PR DOES advance it (line 811: eventsCursor = loaded.cursor ?? eventsCursor). The dedupe is now effectively defensive against worlds that don't always advance cursors (e.g., return null when no new events) — which is what makes it still worthwhile, but the comment could say so. Minor; understandable as-is.

Backport stance

Body says "this PR is itself a forward port from stable, so it should not be backported." Agreed — the backport action's AI should reach the same conclusion (the fix already exists on stable as #2029), and if it doesn't, manual override is fine.

Approving.

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for dc0be50 (AI decision).

This PR is explicitly a forward-port from stable PR #2029, adapted to main's V2 replay loop and event cache. The underlying fix already exists on stable, and the changes here build on main-only APIs (cursor/hasMore metadata in preloaded run_started snapshots), so backporting would be redundant and likely conflict with the existing stable implementation.

To override, re-run the Backport to stable workflow manually via workflow_dispatch with this commit SHA.

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.

3 participants

@pranaygp@TooTallNate
, '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('^' + ".*" + ' [codex] Forward port stale wait replay fix by pranaygp · Pull Request #2038 · vercel/workflow · GitHub
Skip to content

[codex] Forward port stale wait replay fix - #2038

Merged
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix
May 20, 2026
Merged

[codex] Forward port stale wait replay fix#2038
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix

Conversation

@pranaygp

@pranaygppranaygp commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Forward-port the stale wait replay race fix from stable PR #2029 to main.

This keeps workflow replay deterministic when an elapsed wait_completed is committed from a stale event snapshot while a concurrent hook_received event landed durably before it. On main, the fix is adapted to the V2 replay loop/event cache:

  • use cursor / hasMore metadata from preloaded run_started event snapshots
  • refresh events after attempting elapsed wait completions before replaying
  • fetch only the delta after the prior cursor when possible, with a full-reload fallback when cursor metadata is missing or unsafe
  • add the deterministic handler-level regression test for the hook-vs-sleep wait race

Forward-Port Note

This PR is itself a forward port from stable, so it should not be backported.

Validation

  • pnpm --filter @workflow/world build
  • pnpm --filter @workflow/utils build
  • pnpm --filter @workflow/errors build
  • pnpm --filter @workflow/serde build
  • pnpm --filter @workflow/world-local build
  • pnpm --filter @workflow/world-vercel build
  • pnpm --filter @workflow/core build
  • pnpm vitest run packages/core/src/runtime/wait-completion-replay.test.ts
  • pnpm vitest run packages/core/src/hook-sleep-interaction.test.ts
  • pnpm --filter @workflow/core typecheck
  • pnpm --filter @workflow/world-local typecheck
  • pnpm --filter @workflow/world-postgres typecheck
  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm changeset status --since=main
  • pnpm biome check --files-ignore-unknown=true .changeset/bright-waits-refresh.md packages/core/src/runtime.ts packages/core/src/runtime/wait-completion-replay.test.ts packages/world/src/events.ts packages/world-local/src/storage/events-storage.ts packages/world-postgres/src/storage.ts packages/world-vercel/src/events.ts

Note: local commands run under Node v25.2.1, which emits the repo's unsupported-engine warning. Biome exits successfully but reports existing warnings in the large runtime/storage files.

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2f724a

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-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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

@github-actions

github-actionsBot commented May 20, 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 20, 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.031s (-29.8% 🟢)1.006s (~)0.975s101.00x
💻 LocalNitro0.032s (-26.7% 🟢)1.005s (~)0.974s101.02x
🐘 PostgresNitro0.048s (-49.9% 🟢)1.012s (-2.9%)0.965s101.53x
💻 LocalNext.js (Turbopack)0.050s1.006s0.956s101.60x
🐘 PostgresExpress0.053s (-8.3% 🟢)1.013s (~)0.960s101.71x
🐘 PostgresNext.js (Turbopack)0.059s1.012s0.953s101.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.263s (+11.8% 🔺)2.152s (+0.7%)1.888s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (-5.0% 🟢)2.006s (~)0.931s101.00x
🐘 PostgresNitro1.076s (-5.6% 🟢)2.009s (~)0.933s101.00x
💻 LocalExpress1.076s (-4.4%)2.007s (~)0.931s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.010s (~)0.926s101.01x
💻 LocalNext.js (Turbopack)1.107s2.006s0.899s101.03x
🐘 PostgresNext.js (Turbopack)1.132s2.009s0.877s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.542s (-17.8% 🟢)3.515s (-7.7% 🟢)1.973s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.410s (-5.1% 🟢)11.016s (~)0.607s31.00x
🐘 PostgresNitro10.417s (-4.2%)11.016s (~)0.599s31.00x
💻 LocalExpress10.427s (-4.5%)11.023s (~)0.596s31.00x
💻 LocalNitro10.433s (-4.7%)11.023s (~)0.590s31.00x
💻 LocalNext.js (Turbopack)10.647s11.021s0.374s31.02x
🐘 PostgresNext.js (Turbopack)10.698s11.025s0.327s31.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.211s (-22.2% 🟢)14.873s (-25.7% 🟢)1.662s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.423s (-8.0% 🟢)14.015s (-6.7% 🟢)0.593s51.00x
🐘 PostgresExpress13.473s (-7.6% 🟢)14.016s (-6.7% 🟢)0.543s51.00x
💻 LocalNitro13.509s (-10.3% 🟢)14.028s (-12.5% 🟢)0.519s51.01x
💻 LocalExpress13.525s (-9.7% 🟢)14.029s (-6.7% 🟢)0.504s51.01x
💻 LocalNext.js (Turbopack)14.049s14.827s0.778s51.05x
🐘 PostgresNext.js (Turbopack)14.168s15.017s0.849s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.258s (-57.7% 🟢)23.028s (-56.2% 🟢)1.770s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro11.903s (-14.8% 🟢)12.143s (-15.1% 🟢)0.240s81.00x
💻 LocalNitro11.931s (-28.9% 🟢)12.021s (-29.4% 🟢)0.090s81.00x
🐘 PostgresExpress11.984s (-14.4% 🟢)12.515s (-14.2% 🟢)0.531s81.01x
💻 LocalExpress12.022s (-27.6% 🟢)12.398s (-27.2% 🟢)0.377s81.01x
💻 LocalNext.js (Turbopack)13.063s13.596s0.533s71.10x
🐘 PostgresNext.js (Turbopack)13.220s14.017s0.798s71.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.761s (-72.1% 🟢)35.436s (-71.3% 🟢)1.675s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.144s (-10.3% 🟢)2.008s (~)0.865s151.00x
🐘 PostgresExpress1.146s (-9.1% 🟢)2.006s (~)0.861s151.00x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.826s151.03x
💻 LocalExpress1.188s (-20.2% 🟢)2.006s (~)0.818s151.04x
🐘 PostgresNext.js (Turbopack)1.238s2.009s0.771s151.08x
💻 LocalNext.js (Turbopack)1.257s2.005s0.748s151.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.685s (-6.1% 🟢)4.192s (-9.3% 🟢)1.507s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.817s151.00x
🐘 PostgresExpress1.201s (-49.1% 🟢)2.007s (-33.3% 🟢)0.807s151.01x
🐘 PostgresNext.js (Turbopack)1.401s2.007s0.605s151.18x
💻 LocalNitro1.755s (-44.2% 🟢)2.005s (-48.4% 🟢)0.251s151.47x
💻 LocalNext.js (Turbopack)1.789s2.007s0.218s151.50x
💻 LocalExpress1.861s (-37.0% 🟢)2.149s (-37.8% 🟢)0.288s141.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.773s (+31.9% 🔺)6.447s (+26.2% 🔺)1.674s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.311s (-62.3% 🟢)2.008s (-49.9% 🟢)0.698s151.00x
🐘 PostgresExpress1.331s (-61.8% 🟢)2.006s (-50.0% 🟢)0.676s151.02x
🐘 PostgresNext.js (Turbopack)1.722s2.009s0.287s151.31x
💻 LocalNitro4.817s (-42.3% 🟢)5.345s (-40.7% 🟢)0.528s63.68x
💻 LocalExpress5.405s (-35.2% 🟢)6.013s (-33.4% 🟢)0.608s54.12x
💻 LocalNext.js (Turbopack)5.527s6.213s0.686s54.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.418s (+27.8% 🔺)7.587s (+23.8% 🔺)2.169s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.127s (-10.4% 🟢)2.009s (~)0.882s151.00x
🐘 PostgresExpress1.136s (-9.6% 🟢)2.008s (~)0.872s151.01x
🐘 PostgresNext.js (Turbopack)1.221s2.008s0.786s151.08x
💻 LocalNext.js (Turbopack)1.301s2.005s0.704s151.15x
💻 LocalNitro1.385s (-25.8% 🟢)2.006s (-14.3% 🟢)0.621s151.23x
💻 LocalExpress1.388s (-26.7% 🟢)2.006s (-15.1% 🟢)0.618s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.758s (+6.8% 🔺)4.407s (+1.3%)1.649s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNitro1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNext.js (Turbopack)1.369s2.009s0.639s151.14x
💻 LocalExpress2.004s (-36.0% 🟢)2.392s (-36.4% 🟢)0.389s131.66x
💻 LocalNext.js (Turbopack)2.047s2.674s0.627s121.70x
💻 LocalNitro2.070s (-32.5% 🟢)2.507s (-35.5% 🟢)0.438s121.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.677s (+46.5% 🔺)6.286s (+31.2% 🔺)1.609s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.324s (-62.1% 🟢)2.008s (-49.9% 🟢)0.683s151.00x
🐘 PostgresNitro1.329s (-61.8% 🟢)2.007s (-49.9% 🟢)0.678s151.00x
🐘 PostgresNext.js (Turbopack)1.632s2.009s0.376s151.23x
💻 LocalExpress5.661s (-35.7% 🟢)6.415s (-30.8% 🟢)0.754s54.27x
💻 LocalNext.js (Turbopack)5.927s6.213s0.286s54.48x
💻 LocalNitro6.314s (-31.0% 🟢)6.617s (-34.0% 🟢)0.304s54.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.689s (-11.3% 🟢)7.509s (-8.2% 🟢)1.819s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.441s (-46.3% 🟢)1.006s (~)0.565s601.00x
🐘 PostgresExpress0.446s (-46.8% 🟢)1.007s (-1.5%)0.561s601.01x
💻 LocalExpress0.479s (-51.3% 🟢)1.004s (-6.7% 🟢)0.525s601.09x
💻 LocalNitro0.511s (-47.8% 🟢)1.021s (-6.6% 🟢)0.510s591.16x
🐘 PostgresNext.js (Turbopack)0.703s1.006s0.303s601.60x
💻 LocalNext.js (Turbopack)0.708s1.004s0.297s601.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.871s (-74.4% 🟢)6.286s (-70.5% 🟢)1.414s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.011s (-47.6% 🟢)1.435s (-31.7% 🟢)0.424s631.00x
🐘 PostgresExpress1.085s (-45.1% 🟢)1.706s (-24.5% 🟢)0.621s531.07x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.18x
💻 LocalExpress1.243s (-58.8% 🟢)2.006s (-44.0% 🟢)0.763s451.23x
🐘 PostgresNext.js (Turbopack)1.681s2.008s0.327s451.66x
💻 LocalNext.js (Turbopack)1.777s2.006s0.228s451.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express14.195s (-58.9% 🟢)16.255s (-55.8% 🟢)2.060s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.992s (-51.5% 🟢)2.360s (-48.7% 🟢)0.368s511.00x
🐘 PostgresExpress2.147s (-46.2% 🟢)2.719s (-37.8% 🟢)0.572s451.08x
💻 LocalNitro2.762s (-70.3% 🟢)3.032s (-69.7% 🟢)0.271s401.39x
💻 LocalExpress2.777s (-69.8% 🟢)3.008s (-70.0% 🟢)0.231s401.39x
🐘 PostgresNext.js (Turbopack)3.266s4.009s0.743s301.64x
💻 LocalNext.js (Turbopack)3.760s4.041s0.281s301.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express41.820s (-67.8% 🟢)43.762s (-66.9% 🟢)1.942s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.161s (-43.1% 🟢)1.006s (~)0.844s601.00x
🐘 PostgresExpress0.179s (-36.8% 🟢)1.006s (~)0.827s601.11x
🐘 PostgresNext.js (Turbopack)0.229s1.006s0.777s601.42x
💻 LocalExpress0.399s (-28.9% 🟢)1.005s (~)0.606s602.47x
💻 LocalNitro0.405s (-33.0% 🟢)1.005s (-1.7%)0.600s602.51x
💻 LocalNext.js (Turbopack)0.495s1.004s0.510s603.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.638s (+35.0% 🔺)4.119s (+13.2% 🔺)1.481s151.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.300s (-41.2% 🟢)1.006s (~)0.706s901.00x
🐘 PostgresNitro0.311s (-37.3% 🟢)1.007s (~)0.695s901.04x
🐘 PostgresNext.js (Turbopack)0.455s1.007s0.551s901.52x
💻 LocalNext.js (Turbopack)2.122s2.912s0.790s317.07x
💻 LocalNitro2.188s (-13.8% 🟢)2.797s (-7.1% 🟢)0.609s337.29x
💻 LocalExpress2.280s (-9.3% 🟢)2.945s (-2.1%)0.665s317.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.163s (+135.1% 🔺)8.759s (+82.2% 🔺)1.596s111.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.617s (-21.9% 🟢)1.007s (~)0.390s1201.00x
🐘 PostgresExpress0.623s (-23.9% 🟢)1.006s (-1.2%)0.383s1201.01x
🐘 PostgresNext.js (Turbopack)0.943s1.404s0.461s861.53x
💻 LocalNitro10.236s (-8.5% 🟢)10.777s (-7.6% 🟢)0.541s1216.59x
💻 LocalNext.js (Turbopack)10.456s10.937s0.481s1116.94x
💻 LocalExpress10.494s (-6.2% 🟢)11.122s (-6.9% 🟢)0.628s1117.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.378s (+228.5% 🔺)26.033s (+181.6% 🔺)1.655s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (+453.8% 🔺)1.997s (+99.9% 🔺)0.002s (-6.3% 🟢)2.010s (+98.8% 🔺)0.874s101.00x
🐘 PostgresNitro1.137s (+454.7% 🔺)2.003s (+100.3% 🔺)0.001s (-33.3% 🟢)2.009s (+98.7% 🔺)0.872s101.00x
💻 LocalExpress1.140s (+472.8% 🔺)2.005s (+99.6% 🔺)0.012s (+1.7%)2.020s (+98.4% 🔺)0.880s101.00x
💻 LocalNitro1.141s (+433.9% 🔺)2.005s (+99.6% 🔺)0.012s (-1.6%)2.020s (+98.3% 🔺)0.879s101.00x
💻 LocalNext.js (Turbopack)1.172s2.004s0.012s2.019s0.847s101.03x
🐘 PostgresNext.js (Turbopack)1.211s2.002s0.001s2.012s0.801s101.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.140s (-14.6% 🟢)3.359s (-17.9% 🟢)1.604s (+66.9% 🔺)5.401s (-3.4%)3.261s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.492s (+139.0% 🔺)2.003s (+99.0% 🔺)0.004s (-2.5%)2.026s (+98.1% 🔺)0.534s301.00x
🐘 PostgresExpress1.509s (+139.5% 🔺)2.002s (+98.9% 🔺)0.004s (+2.7%)2.026s (+98.0% 🔺)0.517s301.01x
💻 LocalExpress1.543s (+103.9% 🔺)2.012s (+95.5% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.481s301.03x
💻 LocalNext.js (Turbopack)1.666s2.010s0.010s2.023s0.356s301.12x
💻 LocalNitro1.721s (+105.2% 🔺)2.011s (+98.8% 🔺)0.010s (+5.2% 🔺)2.202s (+97.4% 🔺)0.482s281.15x
🐘 PostgresNext.js (Turbopack)1.724s2.010s0.004s2.026s0.302s301.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.884s (-9.5% 🟢)7.238s (-9.6% 🟢)0.364s (-10.9% 🟢)8.088s (-8.4% 🟢)2.204s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.637s (-33.7% 🟢)1.031s (-19.3% 🟢)0.000s (-100.0% 🟢)1.052s (-19.5% 🟢)0.414s581.00x
🐘 PostgresNitro0.640s (-33.9% 🟢)1.031s (-17.3% 🟢)0.000s (+65.5% 🔺)1.044s (-17.0% 🟢)0.403s581.00x
🐘 PostgresNext.js (Turbopack)0.767s1.018s0.000s1.035s0.268s581.20x
💻 LocalExpress1.362s (+11.2% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.655s302.14x
💻 LocalNitro1.366s (+11.7% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.651s302.14x
💻 LocalNext.js (Turbopack)1.377s2.013s0.000s2.016s0.639s302.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.604s (-3.6%)4.822s (-5.5% 🟢)0.000s (+83.3% 🔺)5.353s (-3.2%)1.749s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.246s (-29.7% 🟢)2.033s (-6.7% 🟢)0.000s (NaN%)2.043s (-7.1% 🟢)0.797s301.00x
🐘 PostgresNitro1.276s (-28.8% 🟢)2.066s (-3.5%)0.000s (-3.4%)2.082s (-4.2%)0.807s291.02x
🐘 PostgresNext.js (Turbopack)1.651s2.147s0.000s2.156s0.505s281.33x
💻 LocalNext.js (Turbopack)2.602s3.239s0.000s3.242s0.640s192.09x
💻 LocalNitro3.121s (-7.9% 🟢)3.906s (-3.1%)0.001s (+5.5% 🔺)3.908s (-3.2%)0.787s162.51x
💻 LocalExpress3.179s (-8.3% 🟢)3.967s (-1.7%)0.000s (-53.1% 🟢)3.969s (-1.7%)0.790s162.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.026s (+31.3% 🔺)7.064s (+17.3% 🔺)0.000s (NaN%)7.559s (+17.1% 🔺)1.534s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress9/21
🐘 PostgresNitro14/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@pranaygp
pranaygp marked this pull request as ready for review May 20, 2026 18:27
@pranaygp
pranaygp requested a review from a team as a code ownerMay 20, 2026 18:27
CopilotAI review requested due to automatic review settings May 20, 2026 18:27

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

Forward-ports the “stale wait replay” race fix to main by making the V2 runtime refresh its event snapshot after committing elapsed wait_completed events, ensuring concurrent durable events (e.g. hook_received) are observed in deterministic order during replay.

Changes:

  • Extends EventResult to optionally include cursor/hasMore pagination metadata for preloaded run_started event snapshots.
  • Updates runtime replay to (a) only trust preloaded events when hasMore !== true, and (b) refresh events after elapsed wait completions using cursor-delta fetch with a full-reload fallback.
  • Adds a deterministic handler-level regression test for the hook-vs-sleep interleaving and ships a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world/src/events.tsAdds optional cursor/hasMore fields to EventResult for preloaded event snapshots.
packages/world-vercel/src/events.tsAccepts/forwards cursor and hasMore from the backend in events.create responses.
packages/world-postgres/src/storage.tsPopulates cursor/hasMore for run_started preloaded events.
packages/world-local/src/storage/events-storage.tsReturns preloaded events with pagination metadata (and caps preload size).
packages/core/src/runtime.tsUses preload pagination metadata and refreshes event snapshots after completing elapsed waits to preserve replay determinism.
packages/core/src/runtime/wait-completion-replay.test.tsNew deterministic regression test that simulates the stale snapshot + concurrent hook ordering race.
.changeset/bright-waits-refresh.mdPatch bumps for affected packages.

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

Comment on lines 1107 to 1108
// For run_started: include all events so the runtime can skip
// the initial events.list call and reduce TTFB.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the run_started preload comment to call out the one-page preload and hasMore check.

Comment on lines 748 to +752
for (const waitEvent of waitsToComplete) {
try {
const result = await world.events.create(
runId,
waitEvent,
{ requestId }
);
events.push(result.event!);
await world.events.create(runId, waitEvent, {
requestId,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the dedupe comment to describe the refreshed wait-completion delta path instead of the old manual push behavior.

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

Approve — clean forward-port of #2029, with V2-specific adaptations

Forward-ports the stable wait-replay race fix to main. The diff against current main looks larger than the stable PR because the branch was forked before #2030 (which added optional stepName/token/resumeAt fields to several event schemas). After rebasing onto current main, the actual change is +593/-15 across 7 files, all consistent with what landed on stable.

Why the V2 adaptations are correct

I traced both versions side by side to make sure nothing was lost in translation:

  • Cursor preservation on first iteration (preloadedEventsCursor plumbed through to eventsCursor) — symmetric with stable.
  • Wait-completion refresh has TWO additions on main vs stable:
    • Dedupe by eventId when merging delta back into existing events (line 803-810). On stable this is just events.push(...newEvents.events). On main it's defensive against the V2 multi-iteration loop where events may persist across iterations as cachedEvents.
    • eventsCursor = loaded.cursor ?? eventsCursor (line 811). Critical for the V2 loop — without it, the next iteration's loadWorkflowRunEvents(runId, eventsCursor) would re-fetch the SAME delta. Stable's loop doesn't have this concern because it doesn't iterate.
  • stripEventDataRefs round-trip through resolveData is identical to the existing events.create flow — no new code paths.

Correctness walk-through

Scenario: workflow with Promise.race([hook, sleep]), hook arrives durably just before handler commits wait_completed.

  1. Handler reads events at cursor C. Sees elapsed wait_created, no hook_received.
  2. Before handler commits wait_completed: another writer commits hook_received (durable position > C).
  3. Handler commits wait_completed (durable position > hook_received).
  4. Without fix: in-memory events missing hook_received. Replay takes sleep branch. Wrong.
  5. With fix: refresh from cursor C, get [hook_received, wait_completed]. Append. Replay takes hook branch. Correct.

The sawAllWaitCompletions defensive check (line 795-800) is smart — if the delta-after-cursor doesn't include the wait completion this handler just wrote, the cursor wasn't a safe replay boundary (read replica lag, eventual consistency hiccup, etc.) and we fall back to a full reload from the beginning.

Test coverage

The four scenarios in wait-completion-replay.test.ts map cleanly onto the code paths:

TestPath exercised
loads only events after the preloaded cursorHappy path: preload + cursor + complete
falls back to a full reload when preloaded events do not include a cursorBackward compat: old worlds without cursor
falls back to a full reload when preloaded events are partialhasMore: true → discard preload, full reload
falls back when cursor delta misses the attempted wait completionDefensive: sawAllWaitCompletions fails

Each scenario asserts both the number of listEvents calls AND the cursor used on each call, so the code paths are pinned, not just outcomes.

What I verified

  • After rebasing onto main: 7 files changed, +593/-15. The events.ts schema "removals" in the raw diff against main are artifacts of the branch being behind #2030 — they vanish on rebase.
  • pnpm --filter @workflow/core test ✓ (965/965 including the new 4 in wait-completion-replay.test.ts)
  • pnpm --filter @workflow/core test packages/core/src/hook-sleep-interaction.test.ts ✓ (the seeded-repro test that was deleted on stable still passes here — main keeps both old + new tests)
  • Typecheck for core, world, world-local, world-postgres, world-vercel ✓
  • Build ✓
  • CI: 107 success, 1 failure, 4 in-progress — the failure is Benchmark Vercel (nextjs-turbopack), which is unrelated (benchmark timing) and main is also failing it on recent commits. All E2E + Unit Tests pass.

Two minor nits (non-blocking)

  1. world-postgreshasMore: false is hardcoded (line 1291). The query has no LIMIT, so this is correct today. If someone ever adds a LIMIT N+1 pattern to that query for pagination, the hardcoded false would silently lie. Worth a comment like // hasMore = false: query has no LIMIT, so we always return all events to make the assumption explicit. Compare to the local world, where hasMore is derived from the paginated query.

  2. The dedupe comment in the iteration-N+1 fetch path (line 650-654) reads as if the cursor isn't advanced after the wait-completion refresh. But this PR DOES advance it (line 811: eventsCursor = loaded.cursor ?? eventsCursor). The dedupe is now effectively defensive against worlds that don't always advance cursors (e.g., return null when no new events) — which is what makes it still worthwhile, but the comment could say so. Minor; understandable as-is.

Backport stance

Body says "this PR is itself a forward port from stable, so it should not be backported." Agreed — the backport action's AI should reach the same conclusion (the fix already exists on stable as #2029), and if it doesn't, manual override is fine.

Approving.

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for dc0be50 (AI decision).

This PR is explicitly a forward-port from stable PR #2029, adapted to main's V2 replay loop and event cache. The underlying fix already exists on stable, and the changes here build on main-only APIs (cursor/hasMore metadata in preloaded run_started snapshots), so backporting would be redundant and likely conflict with the existing stable implementation.

To override, re-run the Backport to stable workflow manually via workflow_dispatch with this commit SHA.

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.

3 participants

@pranaygp@TooTallNate
, '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); } })(); })(); [codex] Forward port stale wait replay fix by pranaygp · Pull Request #2038 · vercel/workflow · GitHub
Skip to content

[codex] Forward port stale wait replay fix - #2038

Merged
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix
May 20, 2026
Merged

[codex] Forward port stale wait replay fix#2038
pranaygp merged 4 commits into
mainfrom
pranaygp/codex/forward-port-sleep-race-fix

Conversation

@pranaygp

@pranaygppranaygp commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Forward-port the stale wait replay race fix from stable PR #2029 to main.

This keeps workflow replay deterministic when an elapsed wait_completed is committed from a stale event snapshot while a concurrent hook_received event landed durably before it. On main, the fix is adapted to the V2 replay loop/event cache:

  • use cursor / hasMore metadata from preloaded run_started event snapshots
  • refresh events after attempting elapsed wait completions before replaying
  • fetch only the delta after the prior cursor when possible, with a full-reload fallback when cursor metadata is missing or unsafe
  • add the deterministic handler-level regression test for the hook-vs-sleep wait race

Forward-Port Note

This PR is itself a forward port from stable, so it should not be backported.

Validation

  • pnpm --filter @workflow/world build
  • pnpm --filter @workflow/utils build
  • pnpm --filter @workflow/errors build
  • pnpm --filter @workflow/serde build
  • pnpm --filter @workflow/world-local build
  • pnpm --filter @workflow/world-vercel build
  • pnpm --filter @workflow/core build
  • pnpm vitest run packages/core/src/runtime/wait-completion-replay.test.ts
  • pnpm vitest run packages/core/src/hook-sleep-interaction.test.ts
  • pnpm --filter @workflow/core typecheck
  • pnpm --filter @workflow/world-local typecheck
  • pnpm --filter @workflow/world-postgres typecheck
  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm changeset status --since=main
  • pnpm biome check --files-ignore-unknown=true .changeset/bright-waits-refresh.md packages/core/src/runtime.ts packages/core/src/runtime/wait-completion-replay.test.ts packages/world/src/events.ts packages/world-local/src/storage/events-storage.ts packages/world-postgres/src/storage.ts packages/world-vercel/src/events.ts

Note: local commands run under Node v25.2.1, which emits the repo's unsupported-engine warning. Biome exits successfully but reports existing warnings in the large runtime/storage files.

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2f724a

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-localPatch
@workflow/world-postgresPatch
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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

@github-actions

github-actionsBot commented May 20, 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 20, 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.031s (-29.8% 🟢)1.006s (~)0.975s101.00x
💻 LocalNitro0.032s (-26.7% 🟢)1.005s (~)0.974s101.02x
🐘 PostgresNitro0.048s (-49.9% 🟢)1.012s (-2.9%)0.965s101.53x
💻 LocalNext.js (Turbopack)0.050s1.006s0.956s101.60x
🐘 PostgresExpress0.053s (-8.3% 🟢)1.013s (~)0.960s101.71x
🐘 PostgresNext.js (Turbopack)0.059s1.012s0.953s101.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.263s (+11.8% 🔺)2.152s (+0.7%)1.888s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.074s (-5.0% 🟢)2.006s (~)0.931s101.00x
🐘 PostgresNitro1.076s (-5.6% 🟢)2.009s (~)0.933s101.00x
💻 LocalExpress1.076s (-4.4%)2.007s (~)0.931s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.010s (~)0.926s101.01x
💻 LocalNext.js (Turbopack)1.107s2.006s0.899s101.03x
🐘 PostgresNext.js (Turbopack)1.132s2.009s0.877s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.542s (-17.8% 🟢)3.515s (-7.7% 🟢)1.973s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express10.410s (-5.1% 🟢)11.016s (~)0.607s31.00x
🐘 PostgresNitro10.417s (-4.2%)11.016s (~)0.599s31.00x
💻 LocalExpress10.427s (-4.5%)11.023s (~)0.596s31.00x
💻 LocalNitro10.433s (-4.7%)11.023s (~)0.590s31.00x
💻 LocalNext.js (Turbopack)10.647s11.021s0.374s31.02x
🐘 PostgresNext.js (Turbopack)10.698s11.025s0.327s31.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.211s (-22.2% 🟢)14.873s (-25.7% 🟢)1.662s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.423s (-8.0% 🟢)14.015s (-6.7% 🟢)0.593s51.00x
🐘 PostgresExpress13.473s (-7.6% 🟢)14.016s (-6.7% 🟢)0.543s51.00x
💻 LocalNitro13.509s (-10.3% 🟢)14.028s (-12.5% 🟢)0.519s51.01x
💻 LocalExpress13.525s (-9.7% 🟢)14.029s (-6.7% 🟢)0.504s51.01x
💻 LocalNext.js (Turbopack)14.049s14.827s0.778s51.05x
🐘 PostgresNext.js (Turbopack)14.168s15.017s0.849s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.258s (-57.7% 🟢)23.028s (-56.2% 🟢)1.770s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro11.903s (-14.8% 🟢)12.143s (-15.1% 🟢)0.240s81.00x
💻 LocalNitro11.931s (-28.9% 🟢)12.021s (-29.4% 🟢)0.090s81.00x
🐘 PostgresExpress11.984s (-14.4% 🟢)12.515s (-14.2% 🟢)0.531s81.01x
💻 LocalExpress12.022s (-27.6% 🟢)12.398s (-27.2% 🟢)0.377s81.01x
💻 LocalNext.js (Turbopack)13.063s13.596s0.533s71.10x
🐘 PostgresNext.js (Turbopack)13.220s14.017s0.798s71.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express33.761s (-72.1% 🟢)35.436s (-71.3% 🟢)1.675s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.144s (-10.3% 🟢)2.008s (~)0.865s151.00x
🐘 PostgresExpress1.146s (-9.1% 🟢)2.006s (~)0.861s151.00x
💻 LocalNitro1.179s (-27.7% 🟢)2.006s (-3.3%)0.826s151.03x
💻 LocalExpress1.188s (-20.2% 🟢)2.006s (~)0.818s151.04x
🐘 PostgresNext.js (Turbopack)1.238s2.009s0.771s151.08x
💻 LocalNext.js (Turbopack)1.257s2.005s0.748s151.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.685s (-6.1% 🟢)4.192s (-9.3% 🟢)1.507s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.190s (-49.4% 🟢)2.007s (-33.3% 🟢)0.817s151.00x
🐘 PostgresExpress1.201s (-49.1% 🟢)2.007s (-33.3% 🟢)0.807s151.01x
🐘 PostgresNext.js (Turbopack)1.401s2.007s0.605s151.18x
💻 LocalNitro1.755s (-44.2% 🟢)2.005s (-48.4% 🟢)0.251s151.47x
💻 LocalNext.js (Turbopack)1.789s2.007s0.218s151.50x
💻 LocalExpress1.861s (-37.0% 🟢)2.149s (-37.8% 🟢)0.288s141.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.773s (+31.9% 🔺)6.447s (+26.2% 🔺)1.674s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.311s (-62.3% 🟢)2.008s (-49.9% 🟢)0.698s151.00x
🐘 PostgresExpress1.331s (-61.8% 🟢)2.006s (-50.0% 🟢)0.676s151.02x
🐘 PostgresNext.js (Turbopack)1.722s2.009s0.287s151.31x
💻 LocalNitro4.817s (-42.3% 🟢)5.345s (-40.7% 🟢)0.528s63.68x
💻 LocalExpress5.405s (-35.2% 🟢)6.013s (-33.4% 🟢)0.608s54.12x
💻 LocalNext.js (Turbopack)5.527s6.213s0.686s54.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.418s (+27.8% 🔺)7.587s (+23.8% 🔺)2.169s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.127s (-10.4% 🟢)2.009s (~)0.882s151.00x
🐘 PostgresExpress1.136s (-9.6% 🟢)2.008s (~)0.872s151.01x
🐘 PostgresNext.js (Turbopack)1.221s2.008s0.786s151.08x
💻 LocalNext.js (Turbopack)1.301s2.005s0.704s151.15x
💻 LocalNitro1.385s (-25.8% 🟢)2.006s (-14.3% 🟢)0.621s151.23x
💻 LocalExpress1.388s (-26.7% 🟢)2.006s (-15.1% 🟢)0.618s151.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.758s (+6.8% 🔺)4.407s (+1.3%)1.649s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNitro1.205s (-48.5% 🟢)2.008s (-33.3% 🟢)0.803s151.00x
🐘 PostgresNext.js (Turbopack)1.369s2.009s0.639s151.14x
💻 LocalExpress2.004s (-36.0% 🟢)2.392s (-36.4% 🟢)0.389s131.66x
💻 LocalNext.js (Turbopack)2.047s2.674s0.627s121.70x
💻 LocalNitro2.070s (-32.5% 🟢)2.507s (-35.5% 🟢)0.438s121.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.677s (+46.5% 🔺)6.286s (+31.2% 🔺)1.609s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.324s (-62.1% 🟢)2.008s (-49.9% 🟢)0.683s151.00x
🐘 PostgresNitro1.329s (-61.8% 🟢)2.007s (-49.9% 🟢)0.678s151.00x
🐘 PostgresNext.js (Turbopack)1.632s2.009s0.376s151.23x
💻 LocalExpress5.661s (-35.7% 🟢)6.415s (-30.8% 🟢)0.754s54.27x
💻 LocalNext.js (Turbopack)5.927s6.213s0.286s54.48x
💻 LocalNitro6.314s (-31.0% 🟢)6.617s (-34.0% 🟢)0.304s54.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.689s (-11.3% 🟢)7.509s (-8.2% 🟢)1.819s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.441s (-46.3% 🟢)1.006s (~)0.565s601.00x
🐘 PostgresExpress0.446s (-46.8% 🟢)1.007s (-1.5%)0.561s601.01x
💻 LocalExpress0.479s (-51.3% 🟢)1.004s (-6.7% 🟢)0.525s601.09x
💻 LocalNitro0.511s (-47.8% 🟢)1.021s (-6.6% 🟢)0.510s591.16x
🐘 PostgresNext.js (Turbopack)0.703s1.006s0.303s601.60x
💻 LocalNext.js (Turbopack)0.708s1.004s0.297s601.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.871s (-74.4% 🟢)6.286s (-70.5% 🟢)1.414s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.011s (-47.6% 🟢)1.435s (-31.7% 🟢)0.424s631.00x
🐘 PostgresExpress1.085s (-45.1% 🟢)1.706s (-24.5% 🟢)0.621s531.07x
💻 LocalNitro1.196s (-60.6% 🟢)2.006s (-46.6% 🟢)0.810s451.18x
💻 LocalExpress1.243s (-58.8% 🟢)2.006s (-44.0% 🟢)0.763s451.23x
🐘 PostgresNext.js (Turbopack)1.681s2.008s0.327s451.66x
💻 LocalNext.js (Turbopack)1.777s2.006s0.228s451.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express14.195s (-58.9% 🟢)16.255s (-55.8% 🟢)2.060s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.992s (-51.5% 🟢)2.360s (-48.7% 🟢)0.368s511.00x
🐘 PostgresExpress2.147s (-46.2% 🟢)2.719s (-37.8% 🟢)0.572s451.08x
💻 LocalNitro2.762s (-70.3% 🟢)3.032s (-69.7% 🟢)0.271s401.39x
💻 LocalExpress2.777s (-69.8% 🟢)3.008s (-70.0% 🟢)0.231s401.39x
🐘 PostgresNext.js (Turbopack)3.266s4.009s0.743s301.64x
💻 LocalNext.js (Turbopack)3.760s4.041s0.281s301.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express41.820s (-67.8% 🟢)43.762s (-66.9% 🟢)1.942s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.161s (-43.1% 🟢)1.006s (~)0.844s601.00x
🐘 PostgresExpress0.179s (-36.8% 🟢)1.006s (~)0.827s601.11x
🐘 PostgresNext.js (Turbopack)0.229s1.006s0.777s601.42x
💻 LocalExpress0.399s (-28.9% 🟢)1.005s (~)0.606s602.47x
💻 LocalNitro0.405s (-33.0% 🟢)1.005s (-1.7%)0.600s602.51x
💻 LocalNext.js (Turbopack)0.495s1.004s0.510s603.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.638s (+35.0% 🔺)4.119s (+13.2% 🔺)1.481s151.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.300s (-41.2% 🟢)1.006s (~)0.706s901.00x
🐘 PostgresNitro0.311s (-37.3% 🟢)1.007s (~)0.695s901.04x
🐘 PostgresNext.js (Turbopack)0.455s1.007s0.551s901.52x
💻 LocalNext.js (Turbopack)2.122s2.912s0.790s317.07x
💻 LocalNitro2.188s (-13.8% 🟢)2.797s (-7.1% 🟢)0.609s337.29x
💻 LocalExpress2.280s (-9.3% 🟢)2.945s (-2.1%)0.665s317.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.163s (+135.1% 🔺)8.759s (+82.2% 🔺)1.596s111.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.617s (-21.9% 🟢)1.007s (~)0.390s1201.00x
🐘 PostgresExpress0.623s (-23.9% 🟢)1.006s (-1.2%)0.383s1201.01x
🐘 PostgresNext.js (Turbopack)0.943s1.404s0.461s861.53x
💻 LocalNitro10.236s (-8.5% 🟢)10.777s (-7.6% 🟢)0.541s1216.59x
💻 LocalNext.js (Turbopack)10.456s10.937s0.481s1116.94x
💻 LocalExpress10.494s (-6.2% 🟢)11.122s (-6.9% 🟢)0.628s1117.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.378s (+228.5% 🔺)26.033s (+181.6% 🔺)1.655s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.136s (+453.8% 🔺)1.997s (+99.9% 🔺)0.002s (-6.3% 🟢)2.010s (+98.8% 🔺)0.874s101.00x
🐘 PostgresNitro1.137s (+454.7% 🔺)2.003s (+100.3% 🔺)0.001s (-33.3% 🟢)2.009s (+98.7% 🔺)0.872s101.00x
💻 LocalExpress1.140s (+472.8% 🔺)2.005s (+99.6% 🔺)0.012s (+1.7%)2.020s (+98.4% 🔺)0.880s101.00x
💻 LocalNitro1.141s (+433.9% 🔺)2.005s (+99.6% 🔺)0.012s (-1.6%)2.020s (+98.3% 🔺)0.879s101.00x
💻 LocalNext.js (Turbopack)1.172s2.004s0.012s2.019s0.847s101.03x
🐘 PostgresNext.js (Turbopack)1.211s2.002s0.001s2.012s0.801s101.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.140s (-14.6% 🟢)3.359s (-17.9% 🟢)1.604s (+66.9% 🔺)5.401s (-3.4%)3.261s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.492s (+139.0% 🔺)2.003s (+99.0% 🔺)0.004s (-2.5%)2.026s (+98.1% 🔺)0.534s301.00x
🐘 PostgresExpress1.509s (+139.5% 🔺)2.002s (+98.9% 🔺)0.004s (+2.7%)2.026s (+98.0% 🔺)0.517s301.01x
💻 LocalExpress1.543s (+103.9% 🔺)2.012s (+95.5% 🔺)0.010s (+7.6% 🔺)2.024s (+94.7% 🔺)0.481s301.03x
💻 LocalNext.js (Turbopack)1.666s2.010s0.010s2.023s0.356s301.12x
💻 LocalNitro1.721s (+105.2% 🔺)2.011s (+98.8% 🔺)0.010s (+5.2% 🔺)2.202s (+97.4% 🔺)0.482s281.15x
🐘 PostgresNext.js (Turbopack)1.724s2.010s0.004s2.026s0.302s301.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.884s (-9.5% 🟢)7.238s (-9.6% 🟢)0.364s (-10.9% 🟢)8.088s (-8.4% 🟢)2.204s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.637s (-33.7% 🟢)1.031s (-19.3% 🟢)0.000s (-100.0% 🟢)1.052s (-19.5% 🟢)0.414s581.00x
🐘 PostgresNitro0.640s (-33.9% 🟢)1.031s (-17.3% 🟢)0.000s (+65.5% 🔺)1.044s (-17.0% 🟢)0.403s581.00x
🐘 PostgresNext.js (Turbopack)0.767s1.018s0.000s1.035s0.268s581.20x
💻 LocalExpress1.362s (+11.2% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.655s302.14x
💻 LocalNitro1.366s (+11.7% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.651s302.14x
💻 LocalNext.js (Turbopack)1.377s2.013s0.000s2.016s0.639s302.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.604s (-3.6%)4.822s (-5.5% 🟢)0.000s (+83.3% 🔺)5.353s (-3.2%)1.749s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.246s (-29.7% 🟢)2.033s (-6.7% 🟢)0.000s (NaN%)2.043s (-7.1% 🟢)0.797s301.00x
🐘 PostgresNitro1.276s (-28.8% 🟢)2.066s (-3.5%)0.000s (-3.4%)2.082s (-4.2%)0.807s291.02x
🐘 PostgresNext.js (Turbopack)1.651s2.147s0.000s2.156s0.505s281.33x
💻 LocalNext.js (Turbopack)2.602s3.239s0.000s3.242s0.640s192.09x
💻 LocalNitro3.121s (-7.9% 🟢)3.906s (-3.1%)0.001s (+5.5% 🔺)3.908s (-3.2%)0.787s162.51x
💻 LocalExpress3.179s (-8.3% 🟢)3.967s (-1.7%)0.000s (-53.1% 🟢)3.969s (-1.7%)0.790s162.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.026s (+31.3% 🔺)7.064s (+17.3% 🔺)0.000s (NaN%)7.559s (+17.1% 🔺)1.534s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress9/21
🐘 PostgresNitro14/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@pranaygp
pranaygp marked this pull request as ready for review May 20, 2026 18:27
@pranaygp
pranaygp requested a review from a team as a code ownerMay 20, 2026 18:27
CopilotAI review requested due to automatic review settings May 20, 2026 18:27

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

Forward-ports the “stale wait replay” race fix to main by making the V2 runtime refresh its event snapshot after committing elapsed wait_completed events, ensuring concurrent durable events (e.g. hook_received) are observed in deterministic order during replay.

Changes:

  • Extends EventResult to optionally include cursor/hasMore pagination metadata for preloaded run_started event snapshots.
  • Updates runtime replay to (a) only trust preloaded events when hasMore !== true, and (b) refresh events after elapsed wait completions using cursor-delta fetch with a full-reload fallback.
  • Adds a deterministic handler-level regression test for the hook-vs-sleep interleaving and ships a patch changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/world/src/events.tsAdds optional cursor/hasMore fields to EventResult for preloaded event snapshots.
packages/world-vercel/src/events.tsAccepts/forwards cursor and hasMore from the backend in events.create responses.
packages/world-postgres/src/storage.tsPopulates cursor/hasMore for run_started preloaded events.
packages/world-local/src/storage/events-storage.tsReturns preloaded events with pagination metadata (and caps preload size).
packages/core/src/runtime.tsUses preload pagination metadata and refreshes event snapshots after completing elapsed waits to preserve replay determinism.
packages/core/src/runtime/wait-completion-replay.test.tsNew deterministic regression test that simulates the stale snapshot + concurrent hook ordering race.
.changeset/bright-waits-refresh.mdPatch bumps for affected packages.

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

Comment on lines 1107 to 1108
// For run_started: include all events so the runtime can skip
// the initial events.list call and reduce TTFB.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the run_started preload comment to call out the one-page preload and hasMore check.

Comment on lines 748 to +752
for (const waitEvent of waitsToComplete) {
try {
const result = await world.events.create(
runId,
waitEvent,
{ requestId }
);
events.push(result.event!);
await world.events.create(runId, waitEvent, {
requestId,
});

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the dedupe comment to describe the refreshed wait-completion delta path instead of the old manual push behavior.

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

Approve — clean forward-port of #2029, with V2-specific adaptations

Forward-ports the stable wait-replay race fix to main. The diff against current main looks larger than the stable PR because the branch was forked before #2030 (which added optional stepName/token/resumeAt fields to several event schemas). After rebasing onto current main, the actual change is +593/-15 across 7 files, all consistent with what landed on stable.

Why the V2 adaptations are correct

I traced both versions side by side to make sure nothing was lost in translation:

  • Cursor preservation on first iteration (preloadedEventsCursor plumbed through to eventsCursor) — symmetric with stable.
  • Wait-completion refresh has TWO additions on main vs stable:
    • Dedupe by eventId when merging delta back into existing events (line 803-810). On stable this is just events.push(...newEvents.events). On main it's defensive against the V2 multi-iteration loop where events may persist across iterations as cachedEvents.
    • eventsCursor = loaded.cursor ?? eventsCursor (line 811). Critical for the V2 loop — without it, the next iteration's loadWorkflowRunEvents(runId, eventsCursor) would re-fetch the SAME delta. Stable's loop doesn't have this concern because it doesn't iterate.
  • stripEventDataRefs round-trip through resolveData is identical to the existing events.create flow — no new code paths.

Correctness walk-through

Scenario: workflow with Promise.race([hook, sleep]), hook arrives durably just before handler commits wait_completed.

  1. Handler reads events at cursor C. Sees elapsed wait_created, no hook_received.
  2. Before handler commits wait_completed: another writer commits hook_received (durable position > C).
  3. Handler commits wait_completed (durable position > hook_received).
  4. Without fix: in-memory events missing hook_received. Replay takes sleep branch. Wrong.
  5. With fix: refresh from cursor C, get [hook_received, wait_completed]. Append. Replay takes hook branch. Correct.

The sawAllWaitCompletions defensive check (line 795-800) is smart — if the delta-after-cursor doesn't include the wait completion this handler just wrote, the cursor wasn't a safe replay boundary (read replica lag, eventual consistency hiccup, etc.) and we fall back to a full reload from the beginning.

Test coverage

The four scenarios in wait-completion-replay.test.ts map cleanly onto the code paths:

TestPath exercised
loads only events after the preloaded cursorHappy path: preload + cursor + complete
falls back to a full reload when preloaded events do not include a cursorBackward compat: old worlds without cursor
falls back to a full reload when preloaded events are partialhasMore: true → discard preload, full reload
falls back when cursor delta misses the attempted wait completionDefensive: sawAllWaitCompletions fails

Each scenario asserts both the number of listEvents calls AND the cursor used on each call, so the code paths are pinned, not just outcomes.

What I verified

  • After rebasing onto main: 7 files changed, +593/-15. The events.ts schema "removals" in the raw diff against main are artifacts of the branch being behind #2030 — they vanish on rebase.
  • pnpm --filter @workflow/core test ✓ (965/965 including the new 4 in wait-completion-replay.test.ts)
  • pnpm --filter @workflow/core test packages/core/src/hook-sleep-interaction.test.ts ✓ (the seeded-repro test that was deleted on stable still passes here — main keeps both old + new tests)
  • Typecheck for core, world, world-local, world-postgres, world-vercel ✓
  • Build ✓
  • CI: 107 success, 1 failure, 4 in-progress — the failure is Benchmark Vercel (nextjs-turbopack), which is unrelated (benchmark timing) and main is also failing it on recent commits. All E2E + Unit Tests pass.

Two minor nits (non-blocking)

  1. world-postgreshasMore: false is hardcoded (line 1291). The query has no LIMIT, so this is correct today. If someone ever adds a LIMIT N+1 pattern to that query for pagination, the hardcoded false would silently lie. Worth a comment like // hasMore = false: query has no LIMIT, so we always return all events to make the assumption explicit. Compare to the local world, where hasMore is derived from the paginated query.

  2. The dedupe comment in the iteration-N+1 fetch path (line 650-654) reads as if the cursor isn't advanced after the wait-completion refresh. But this PR DOES advance it (line 811: eventsCursor = loaded.cursor ?? eventsCursor). The dedupe is now effectively defensive against worlds that don't always advance cursors (e.g., return null when no new events) — which is what makes it still worthwhile, but the comment could say so. Minor; understandable as-is.

Backport stance

Body says "this PR is itself a forward port from stable, so it should not be backported." Agreed — the backport action's AI should reach the same conclusion (the fix already exists on stable as #2029), and if it doesn't, manual override is fine.

Approving.

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for dc0be50 (AI decision).

This PR is explicitly a forward-port from stable PR #2029, adapted to main's V2 replay loop and event cache. The underlying fix already exists on stable, and the changes here build on main-only APIs (cursor/hasMore metadata in preloaded run_started snapshots), so backporting would be redundant and likely conflict with the existing stable implementation.

To override, re-run the Backport to stable workflow manually via workflow_dispatch with this commit SHA.

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.

3 participants

@pranaygp@TooTallNate