[core] Fix hook token reuse after dispose() (same-run and cross-run) - #2779

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777
Jul 7, 2026
Merged

[core] Fix hook token reuse after dispose() (same-run and cross-run)#2779
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 6, 2026

Copy link
Copy Markdown
Member

Closes#2777, closes#2778.

Problem

Reusing a hook token after hook.dispose() deterministically or intermittently failed with a spurious HookConflictError naming an already-disposed hook:

Fix

  • @workflow/core:handleSuspension now processes hook operations grouped per token, in workflow-code order: a dispose of an earlier hook flushes before a later same-token hook's creation is validated, while a hook created and disposed within one suspension is still created first. Different tokens keep processing in parallel, and hooks still process before steps/waits.
  • @workflow/world-local: the hook_created token claim runs in a short bounded loop — it retries when the observed claim vanished mid-check, and treats a claim as vacant when its hook's disposal is committed (dispose lock exists) or its owning run is terminal/missing, releasing the stale claim if the in-flight releaser never finishes. The event-log rebuild (rebuildLiveHookByTokenFromEventLog / hooks.get*) no longer considers a hook live once its dispose lock exists. Live claims still conflict exactly as before.

- core: process hook operations per token in workflow-code order during
suspension handling, so a dispose() of an earlier hook releases the
token before a later same-token hook's creation is validated. A hook
created and disposed within the same suspension is still created
before it is disposed. Fixes the same-run recreate self-conflict
(#2777).
- world-local: retry the token claim when the observed claim vanished
mid-check or is held by a hook whose disposal is committed / a run
that is terminal or missing; never rebuild a claim from the event log
for a hook whose dispose lock exists. Fixes spurious HookConflictError
against an already-disposed hook during fast run-to-run token handoff
(#2778).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 899a288

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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

@vercel

vercelBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 6, 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🥇 Nitro0.048s (-16.4% 🟢)1.006s (-1.3%)0.958s101.00x
💻 LocalExpress0.050s (+3.5%)1.006s (~)0.957s101.03x
💻 LocalNext.js (Turbopack)0.055s (-2.0%)1.005s (~)0.951s101.13x
🐘 PostgresNitro0.055s (-17.7% 🟢)1.012s (~)0.956s101.15x
🐘 PostgresNext.js (Turbopack)0.062s (-1.0%)1.012s (~)0.950s101.28x
🐘 PostgresExpress0.070s (+9.4% 🔺)1.012s (~)0.942s101.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.259s (+43.3% 🔺)2.147s (+31.9% 🔺)1.888s101.00x
▲ VercelExpress0.381s (+69.9% 🔺)2.003s (+12.3% 🔺)1.623s101.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.084s (~)2.007s (~)0.923s101.00x
💻 LocalNext.js (Turbopack)1.085s (-0.7%)2.006s (~)0.922s101.00x
💻 LocalExpress1.091s (+1.3%)2.007s (~)0.916s101.01x
🐘 PostgresNext.js (Turbopack)1.096s (-0.9%)2.010s (~)0.914s101.01x
🐘 PostgresExpress1.102s (+0.6%)2.009s (~)0.907s101.02x
🐘 PostgresNitro1.105s (+0.9%)2.011s (~)0.906s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.408s (-4.8%)2.719s (-18.6% 🟢)1.311s101.00x
▲ VercelNitro1.494s (+8.4% 🔺)3.455s (+4.5%)1.961s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.464s (~)11.023s (~)0.559s31.00x
🐘 PostgresExpress10.472s (~)11.014s (~)0.542s31.00x
🐘 PostgresNitro10.475s (~)11.019s (~)0.545s31.00x
💻 LocalNext.js (Turbopack)10.490s (~)11.022s (~)0.532s31.00x
🐘 PostgresNext.js (Turbopack)10.499s (~)11.014s (~)0.515s31.00x
💻 LocalExpress10.512s (+0.7%)11.023s (~)0.511s31.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.697s (+1.1%)13.812s (+8.1% 🔺)2.116s31.00x
▲ VercelExpress11.771s (+0.9%)13.221s (~)1.450s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)13.613s (-1.1%)14.028s (~)0.415s51.00x
🐘 PostgresExpress13.622s (~)14.020s (~)0.398s51.00x
🐘 PostgresNitro13.671s (~)14.022s (~)0.351s51.00x
💻 LocalNitro13.691s (~)14.026s (~)0.336s51.01x
💻 LocalExpress13.738s (+0.9%)14.029s (~)0.290s51.01x
🐘 PostgresNext.js (Turbopack)13.783s (+0.7%)14.018s (~)0.235s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.359s (-0.6%)18.421s (+2.4%)2.063s41.00x
▲ VercelExpress16.725s (~)18.362s (-0.7%)1.636s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.311s (+0.6%)13.019s (~)0.708s71.00x
💻 LocalNext.js (Turbopack)12.354s (-0.5%)13.026s (~)0.673s71.00x
🐘 PostgresNext.js (Turbopack)12.376s (~)13.016s (~)0.641s71.01x
💻 LocalExpress12.377s (+2.3%)13.027s (~)0.650s71.01x
🐘 PostgresNitro12.385s (~)13.019s (~)0.634s71.01x
💻 LocalNitro12.493s (+1.8%)13.025s (~)0.532s71.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro18.213s (-1.7%)20.197s (+0.7%)1.984s51.00x
▲ VercelExpress19.008s (+8.2% 🔺)20.547s (+9.1% 🔺)1.538s51.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.5%)2.009s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.199s (~)2.008s (~)0.810s151.02x
🐘 PostgresExpress1.221s (+1.1%)2.075s (+3.3%)0.853s151.04x
💻 LocalNext.js (Turbopack)1.383s (-3.5%)2.006s (~)0.623s151.18x
💻 LocalNitro1.398s (~)2.006s (~)0.608s151.19x
💻 LocalExpress1.469s (+6.5% 🔺)2.007s (~)0.538s151.25x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.157s (+2.1%)3.617s (+1.9%)1.460s91.00x
▲ VercelNitro2.165s (-3.6%)3.883s (+6.0% 🔺)1.718s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.315s (-4.1%)2.394s (-4.7%)1.079s131.00x
🐘 PostgresNitro1.319s (-2.5%)2.510s (+1.6%)1.191s121.00x
🐘 PostgresNext.js (Turbopack)1.347s (+1.7%)2.918s (-3.0%)1.571s111.02x
💻 LocalNitro2.390s (-1.0%)3.009s (~)0.619s101.82x
💻 LocalNext.js (Turbopack)2.493s (+4.3%)3.110s (+3.3%)0.617s101.90x
💻 LocalExpress2.505s (+1.1%)3.009s (-3.2%)0.504s101.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.122s (+28.6% 🔺)4.500s (+12.9% 🔺)1.378s71.00x
▲ VercelNitro3.207s (+32.4% 🔺)4.900s (+30.5% 🔺)1.694s71.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.563s (-2.7%)4.138s (-3.9%)2.575s81.00x
🐘 PostgresExpress1.616s (-4.4%)4.021s (-9.5% 🟢)2.405s81.03x
💻 LocalExpress2.767s (-21.7% 🟢)4.298s (-9.1% 🟢)1.531s71.77x
🐘 PostgresNext.js (Turbopack)2.988s (-6.2% 🟢)6.014s (+2.8%)3.026s51.91x
💻 LocalNitro4.589s (+19.5% 🔺)5.013s (+9.3% 🔺)0.424s62.94x
💻 LocalNext.js (Turbopack)4.690s (+24.0% 🔺)5.178s (+6.4% 🔺)0.488s63.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.705s (+19.6% 🔺)5.746s (+21.4% 🔺)2.041s61.00x
▲ VercelExpress4.016s (+34.9% 🔺)5.695s (+27.0% 🔺)1.678s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.8%)2.008s (~)0.838s151.00x
🐘 PostgresNext.js (Turbopack)1.188s (-5.2% 🟢)2.007s (-3.3%)0.819s151.01x
🐘 PostgresExpress1.189s (-3.4%)2.008s (-3.2%)0.819s151.02x
💻 LocalNitro1.441s (+0.6%)2.006s (~)0.565s151.23x
💻 LocalNext.js (Turbopack)1.463s (~)2.006s (~)0.543s151.25x
💻 LocalExpress1.499s (+4.8%)2.007s (~)0.508s151.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.014s (-18.8% 🟢)3.876s (+0.7%)1.861s81.00x
▲ VercelExpress2.130s (~)3.651s (+1.8%)1.521s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.317s (-3.6%)2.150s (-17.1% 🟢)0.834s141.00x
🐘 PostgresNitro1.329s (+0.9%)2.316s (-3.2%)0.987s131.01x
🐘 PostgresNext.js (Turbopack)1.435s (+7.2% 🔺)3.009s (~)1.574s101.09x
💻 LocalExpress2.563s (+4.4%)3.009s (~)0.446s101.95x
💻 LocalNitro2.579s (+4.0%)3.009s (~)0.430s101.96x
💻 LocalNext.js (Turbopack)2.611s (-1.0%)3.109s (+3.3%)0.498s101.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.413s (+2.6%)4.409s (+16.9% 🔺)1.996s71.00x
▲ VercelExpress2.441s (+8.0% 🔺)3.951s (+10.4% 🔺)1.511s81.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.525s (-5.3% 🟢)4.138s (~)2.613s81.00x
🐘 PostgresExpress1.624s (-2.0%)4.583s (+6.6% 🔺)2.959s71.06x
🐘 PostgresNext.js (Turbopack)2.970s (-9.9% 🟢)6.017s (-3.2%)3.048s61.95x
💻 LocalNitro4.840s (-14.1% 🟢)5.850s (-2.7%)1.010s63.17x
💻 LocalNext.js (Turbopack)5.395s (-3.7%)5.847s (-2.8%)0.452s63.54x
💻 LocalExpress5.662s (+3.0%)6.416s (+6.7% 🔺)0.754s53.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.748s (+2.8%)4.556s (+5.8% 🔺)1.808s71.00x
▲ VercelNitro4.034s (+40.6% 🔺)6.082s (+38.0% 🔺)2.048s61.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.525s (-12.6% 🟢)1.006s (-1.7%)0.482s601.00x
🐘 PostgresNitro0.557s (-4.7%)1.041s (~)0.484s581.06x
🐘 PostgresNext.js (Turbopack)0.607s (+7.8% 🔺)1.024s (+1.8%)0.417s591.16x
💻 LocalNitro0.609s (+1.5%)1.005s (~)0.397s601.16x
💻 LocalNext.js (Turbopack)0.619s (-1.9%)1.005s (~)0.386s601.18x
💻 LocalExpress0.645s (+13.6% 🔺)1.005s (~)0.360s601.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-1.6%)4.032s (-1.0%)1.496s151.00x
▲ VercelExpress2.578s (-0.7%)3.948s (+4.3%)1.370s161.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.253s (-11.5% 🟢)2.007s (-2.2%)0.754s451.00x
🐘 PostgresNitro1.296s (-4.3%)2.008s (~)0.712s451.03x
🐘 PostgresNext.js (Turbopack)1.382s (-4.0%)2.008s (-1.1%)0.625s451.10x
💻 LocalNitro1.544s (+1.4%)2.029s (+1.1%)0.485s451.23x
💻 LocalNext.js (Turbopack)1.613s (-2.6%)2.006s (-1.1%)0.393s451.29x
💻 LocalExpress1.632s (+10.2% 🔺)2.007s (-1.0%)0.375s451.30x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.805s (-3.4%)7.644s (+1.0%)1.839s121.00x
▲ VercelExpress6.114s (-1.3%)7.668s (+1.0%)1.554s121.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.595s (-2.5%)3.111s (+2.5%)0.516s391.00x
🐘 PostgresNitro2.618s (-3.2%)3.086s (~)0.468s391.01x
🐘 PostgresNext.js (Turbopack)2.725s (-1.7%)3.033s (+0.8%)0.309s401.05x
💻 LocalNext.js (Turbopack)3.398s (-5.3% 🟢)4.009s (-1.7%)0.611s301.31x
💻 LocalNitro3.416s (+5.2% 🔺)4.009s (~)0.593s301.32x
💻 LocalExpress3.557s (+12.3% 🔺)4.077s (+5.1% 🔺)0.520s301.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.756s (-6.3% 🟢)13.617s (-3.3%)1.861s91.00x
▲ VercelNitro12.119s (+1.1%)14.257s (+6.8% 🔺)2.138s91.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.189s (-9.8% 🟢)1.006s (-1.7%)0.817s601.00x
🐘 PostgresExpress0.208s (-2.5%)1.006s (~)0.798s601.10x
🐘 PostgresNitro0.210s (-5.1% 🟢)1.007s (~)0.796s601.11x
💻 LocalNitro0.532s (+6.7% 🔺)1.005s (~)0.473s602.81x
💻 LocalExpress0.562s (+15.0% 🔺)1.005s (~)0.443s602.97x
💻 LocalNext.js (Turbopack)0.605s (-7.2% 🟢)1.005s (-1.7%)0.399s603.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.091s (+3.3%)2.425s (+6.4% 🔺)1.334s261.00x
▲ VercelNitro1.108s (+12.7% 🔺)2.752s (+24.2% 🔺)1.643s221.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.326s (-2.7%)1.018s (+1.2%)0.693s891.00x
🐘 PostgresExpress0.340s (+4.9%)1.029s (+2.2%)0.689s881.04x
🐘 PostgresNext.js (Turbopack)0.352s (+18.6% 🔺)1.041s (+3.4%)0.689s871.08x
💻 LocalNitro2.465s (-3.3%)3.009s (~)0.545s307.56x
💻 LocalExpress2.503s (+1.2%)3.010s (~)0.507s307.68x
💻 LocalNext.js (Turbopack)2.675s (~)3.075s (+2.2%)0.400s308.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.430s (+7.8% 🔺)2.916s (+17.9% 🔺)1.486s311.00x
▲ VercelNitro1.491s (+7.7% 🔺)3.145s (+14.4% 🔺)1.654s291.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.516s (-3.2%)3.034s (+0.8%)2.518s401.00x
🐘 PostgresNitro0.540s (+6.8% 🔺)1.150s (+11.5% 🔺)0.610s1051.05x
🐘 PostgresExpress0.582s (+11.5% 🔺)1.160s (+7.7% 🔺)0.578s1041.13x
💻 LocalNitro5.602s (+4.4%)7.436s (-11.0% 🟢)1.834s1710.85x
💻 LocalExpress5.988s (+2.9%)8.956s (+0.8%)2.968s1411.60x
💻 LocalNext.js (Turbopack)6.274s (+3.8%)9.172s (+2.4%)2.898s1412.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.941s (+7.2% 🔺)3.965s (+16.7% 🔺)2.024s311.00x
▲ VercelExpress2.171s (+18.6% 🔺)3.898s (+6.5% 🔺)1.727s311.12x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.141s (-1.4%)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.868s101.00x
💻 LocalNext.js (Turbopack)1.151s (~)1.964s (~)0.011s (-19.2% 🟢)2.018s (~)0.867s101.01x
💻 LocalNitro1.161s (~)2.004s (~)0.012s (+3.3%)2.019s (~)0.858s101.02x
🐘 PostgresNext.js (Turbopack)1.165s (~)2.001s (~)0.001s (+8.3% 🔺)2.012s (~)0.847s101.02x
🐘 PostgresExpress1.169s (+0.6%)1.999s (~)0.001s (~)2.010s (~)0.841s101.02x
💻 LocalExpress1.173s (+1.8%)2.005s (~)0.013s (+23.1% 🔺)2.021s (~)0.848s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.962s (-7.4% 🟢)3.736s (+17.4% 🔺)2.143s (+15.8% 🔺)6.372s (+16.3% 🔺)4.410s101.00x
▲ VercelExpress2.103s (-3.2%)3.274s (-4.3%)1.979s (+5.8% 🔺)5.705s (~)3.602s101.07x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.554s (-2.4%)2.002s (~)0.005s (-2.5%)2.028s (~)0.474s301.00x
🐘 PostgresNitro1.585s (+1.1%)2.007s (~)0.005s (-5.4% 🟢)2.025s (~)0.440s301.02x
💻 LocalExpress1.592s (+0.6%)2.010s (~)0.012s (-7.6% 🟢)2.025s (~)0.433s301.02x
💻 LocalNext.js (Turbopack)1.605s (~)1.972s (~)0.014s (+0.7%)2.027s (~)0.422s301.03x
🐘 PostgresNext.js (Turbopack)1.657s (-2.0%)2.010s (-1.6%)0.005s (~)2.027s (-1.6%)0.370s301.07x
💻 LocalNitro1.907s (+21.0% 🔺)2.354s (+17.1% 🔺)0.012s (-4.3%)2.372s (+17.1% 🔺)0.464s261.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.530s (+1.6%)7.432s (+13.4% 🔺)0.328s (+71.3% 🔺)8.232s (+14.6% 🔺)2.702s81.00x
▲ VercelExpress5.684s (+5.8% 🔺)6.911s (+3.9%)0.345s (+53.1% 🔺)7.764s (+5.6% 🔺)2.080s81.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.784s (~)1.081s (+3.0%)0.000s (NaN%)1.099s (+1.7%)0.315s561.00x
🐘 PostgresNitro0.788s (+1.0%)1.032s (-3.1%)0.000s (-67.8% 🟢)1.042s (-4.2%)0.254s581.00x
🐘 PostgresNext.js (Turbopack)0.918s (-5.2% 🟢)1.333s (-7.4% 🟢)0.000s (-8.9% 🟢)1.340s (-8.5% 🟢)0.423s451.17x
💻 LocalNext.js (Turbopack)1.327s (-0.8%)1.978s (~)0.000s (-55.6% 🟢)2.017s (~)0.690s301.69x
💻 LocalNitro1.372s (~)1.919s (-3.1%)0.000s (+23.3% 🔺)1.922s (-3.1%)0.550s321.75x
💻 LocalExpress1.453s (+5.2% 🔺)1.982s (+1.6%)0.000s (+62.5% 🔺)1.985s (+1.6%)0.532s311.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.761s (+23.7% 🔺)5.227s (+25.2% 🔺)0.003s (+Infinity% 🔺)5.792s (+25.6% 🔺)2.032s111.00x
▲ VercelExpress3.837s (+32.6% 🔺)5.303s (+30.4% 🔺)0.000s (+154.5% 🔺)5.759s (+29.4% 🔺)1.923s111.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.640s (-10.8% 🟢)2.306s (-3.1%)0.000s (NaN%)2.317s (-3.4%)0.677s261.00x
🐘 PostgresExpress1.725s (-2.8%)2.301s (-4.0%)0.000s (-100.0% 🟢)2.318s (-3.8%)0.593s261.05x
🐘 PostgresNext.js (Turbopack)2.275s (-17.3% 🟢)2.859s (-14.3% 🟢)0.000s (-100.0% 🟢)2.868s (-14.3% 🟢)0.593s211.39x
💻 LocalNitro3.218s (+1.3%)3.775s (+2.7%)0.001s (+112.5% 🔺)3.779s (+2.7%)0.561s161.96x
💻 LocalExpress3.363s (-2.3%)3.902s (-3.1%)0.000s (-29.7% 🟢)3.905s (-3.1%)0.543s162.05x
💻 LocalNext.js (Turbopack)3.580s (+3.0%)3.992s (~)0.000s (-28.6% 🟢)4.032s (~)0.452s152.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.808s (+15.8% 🔺)5.847s (+10.0% 🔺)0.000s (+10.0% 🔺)6.351s (+10.9% 🔺)1.543s101.00x
▲ VercelExpress5.284s (+24.3% 🔺)6.342s (+20.6% 🔺)0.000s (NaN%)6.789s (+20.3% 🔺)1.505s101.10x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro12/21
🐘 PostgresExpress10/21
▲ VercelNitro14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres19/21
Next.js (Turbopack)🐘 Postgres14/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027

📋 View full workflow run

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 6, 2026 17:04
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 6, 2026 17:04

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

ai review: Approving — no blockers or regressions found.

What I checked:

  • @workflow/core suspension handler: verified the per-token grouping preserves queue-insertion (workflow-code) order, so dispose-before-recreate and create-before-dispose orderings both hold; cross-token parallelism and the hooks-before-steps/waits ordering are unchanged. hasHookEvents (hooksNeedingCreation.length > 0) is equivalent to the old hookEvents.length > 0. Skipping disposal for a conflicted creation is a behavior improvement (previously it attempted the dispose and swallowed HookNotFoundError). Abort processing (hooksNeedingAbort) is unaffected.
  • @workflow/world-local claim loop: the loop is bounded (10 attempts, ≤2×10ms sleeps), a genuinely live claim breaks out on the first iteration into the unchanged conflict path, and the own-claim dedup/recovery path is preserved (existingClaim carried out of the loop matches the old post-failure re-read). On loop exhaustion, behavior degrades to the pre-PR single-attempt semantics rather than something worse.
  • Rebuild/resurrection interaction: findLiveHookCreatedEvent already excludes terminal-run hooks (isTerminalRunCache) and now dispose-committed hooks, so a force-released claim is not resurrected by the event-log rebuild in the disposed-hook and terminal-run cases (matching the two new storage tests).
  • Dispose-lock keying: the lock path/naming is byte-identical to the old inline construction (backward compatible with locks already on disk), and hook correlation IDs are hook_${ulid} so keying by hookId alone is safe — same keying the dispose path already relied on.
  • Tests: the new @workflow/vitest workbench tests are race-free because waitForHook excludes hooks that already have a hook_received event, so each round can only observe the next round's hook; the e2e helper's new excludeHookId handles the same race for the token-lookup-based poll. CI is green including the required E2E check.

Left two non-blocking inline notes on narrow crash/race windows in the world-local force-release path.

// The releaser is not coming. Release the stale claim and
// the hook entity it points at, mirroring the hook_disposed
// cleanup.
await deleteJSON(constraintPath);

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.

ai review (non-blocking): there's a narrow cross-process window where this force-release can lead to a transient token-uniqueness violation. Both in-flight releasers (hook_disposed handler and deleteAllHooksForRun) delete the claim file unconditionally after reading the hook entity. Sequence: releaser writes the dispose lock (or run goes terminal) and reads the hook entity, then stalls; this claimant observes the stale claim releasable 3×, deletes it, and writes its own fresh claim; the stalled releaser then executes its deleteJSON(constraintPath) — deleting the new claimant's live claim, so a third claimant can also claim the token. The 2×10ms grace period narrows this but doesn't close it. A cheap hardening would be for the releasers to re-read the claim and only delete it if it still points at their own (runId, hookId) (still TOCTOU, but the window shrinks from "stall of any length" to adjacent file ops), or to record the claim's identity here and have releasers skip deletion when the hook entity is already gone — which hook_disposed already partially does via the existingHook guard.

WorkflowRunSchema,
tag
);
if (!owningRun) {

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.

ai review (non-blocking): the missing-run semantics here disagree with the event-log rebuild. This treats a claim whose run file is absent as releasable, but findLiveHookCreatedEvent's isTerminalRunCache returns false for a missing run file, i.e. the rebuild treats that same hook as live and re-writes the claim. If a claim + surviving hook_created event ever exist without a run file (crash-lost run cache), the claim loop deletes the claim, the next iteration's rebuild resurrects it, and the loop ping-pongs to exhaustion — ending in the conflict branch with existingClaim = null, i.e. a durable hook_conflict with conflictingRunId: undefined, the exact symptom #2778 fixes. Unreachable today AFAICT (world-local never deletes run files), but worth aligning the two predicates — either both treat a missing run as terminal, or drop this branch.

@VaguelySerious
VaguelySerious merged commit 7637196 into mainJul 7, 2026
180 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/issue-2777 branch July 7, 2026 21:13
@VaguelySerious

Copy link
Copy Markdown
MemberAuthor

Addressing review comment in a follow-up PR

@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 7637196 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

7637196cf0f605ce62243bf8c7762a26153dcd36

@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026

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

Reviewed both halves in depth, ran the full affected suites plus the new regression tests against a live dev server. Approving.

Core (same-run, #2777): The per-token grouping is the right shape. I traced the ordering semantics:

  • Queue-insertion order within a token group = workflow code order, so dispose-of-earlier flushes before create-of-later is validated, while a created-and-disposed-in-one-suspension hook still creates first. Both directions are covered by the new unit tests.
  • The new event-log order (hook_disposed before the successor's hook_created) also matches replay consumption order — under the old code the log recorded them inverted relative to code order, so this is strictly better for replay, not just for conflict avoidance.
  • hasHookConflict / hasAwaitedHookCreation accumulation reaches the same set of creations as before, and hasHookEvents is computed from the same population — no semantic drift in the V2 re-invoke signaling.
  • The creationConflicted gate on dispose is correct belt-and-braces: a conflicted creation has nothing to dispose, and skipping avoids even the benign HookNotFoundError round-trip.
  • Cross-token parallelism is preserved, so unrelated hooks don't serialize.

world-local (cross-run, #2778): The claim loop's force-release is the risky part, so I audited its failure modes:

  • readJSON returns null only on ENOENT (anything else throws), so isHookTokenClaimReleasable can't misclassify a live claim due to a transient read error — a "missing" owning run is genuinely missing, and terminal status is monotonic.
  • Racing force-releasers and the in-flight disposer are all safe: deleteJSON is ENOENT-idempotent, and re-claim arbitration still lands on writeExclusive's EEXIST semantics, so exactly one claimant wins and the loser correctly conflicts against the winner's live claim.
  • The dispose lock as earliest-durable-disposal-marker works because the lock write already preceded the destructive deletes pre-PR — the change formalizes existing ordering rather than introducing new ordering requirements. The findLiveHookCreatedEvent lock check closing the rebuild-resurrection window is a nice catch.
  • I also confirmed the asymmetry with world-postgres is justified: there the hook row is the token claim and hook_disposed releases it via atomic DELETE ... RETURNING, so no separate-release window exists — the core fix alone covers it.

Validation: 1354/1354 core unit tests, 432/432 world-local (after full build — the 17 initial failures were unbuilt-SWC-plugin/e2e-env artifacts), 2/2 new workbench/vitest regression tests, and against a live turbopack dev server: the new hookTokenReuseLoopWorkflow e2e plus all 26 existing hook e2e tests pass. Workbench conventions followed (workflow added to workbench/example and propagated via symlink; the vitest workbench's local workflow matches its local-file convention). Changesets correctly split per package. CI: 105 pass, 1 fail = the known Benchmark Vercel (nextjs-turbopack) flake.

Two non-blocking notes:

  1. If the claim loop exhausts all 10 attempts via repeated vanish races (claim deleted between the exclusive-create attempt and every read), it falls through with existingClaim = null and still records a hook_conflict with no conflictingRunId — the #2778 symptom, now requiring ~10 consecutive lost races to reproduce. Practically unreachable, but a runtimeLogger.warn on loop exhaustion would make it diagnosable if it ever fires.
  2. The dispose locks in .locks/hooks/ are now load-bearing durable state (consulted by the claim path and the rebuild), not just transient mutexes. Worth a note somewhere discoverable that they must never be garbage-collected — a future ".locks cleanup" change would silently reintroduce the resurrection bug. The comment at hookDisposeLockPath covers the what; the hazard is for whoever writes a cleanup without reading it.

VaguelySerious added a commit that referenced this pull request Jul 7, 2026
…tall
Both in-flight releasers (the hook_disposed handler and the terminal-run
deleteAllHooksForRun cleanup) deleted the claim file unconditionally after
reading the hook entity. A releaser stalled between those operations could
outlive a claimant force-releasing its stale claim, and its deferred delete
would then destroy the new claimant's live claim — transiently breaking
token uniqueness (a third claimant could claim the token too).
Releasers now re-read the claim and delete it only if it still points at
their own (runId, hookId). Still TOCTOU, but the window shrinks from "a
stall of any length" to adjacent file ops; a claim owned by someone else
is left for the claimant-side force-release path to reap.
Addresses post-merge feedback on #2779.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@VaguelySerious@TooTallNate@karthikscale3
, '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" + '
Skip to content

[core] Fix hook token reuse after dispose() (same-run and cross-run) - #2779

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777
Jul 7, 2026
Merged

[core] Fix hook token reuse after dispose() (same-run and cross-run)#2779
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 6, 2026

Copy link
Copy Markdown
Member

Closes#2777, closes#2778.

Problem

Reusing a hook token after hook.dispose() deterministically or intermittently failed with a spurious HookConflictError naming an already-disposed hook:

Fix

  • @workflow/core:handleSuspension now processes hook operations grouped per token, in workflow-code order: a dispose of an earlier hook flushes before a later same-token hook's creation is validated, while a hook created and disposed within one suspension is still created first. Different tokens keep processing in parallel, and hooks still process before steps/waits.
  • @workflow/world-local: the hook_created token claim runs in a short bounded loop — it retries when the observed claim vanished mid-check, and treats a claim as vacant when its hook's disposal is committed (dispose lock exists) or its owning run is terminal/missing, releasing the stale claim if the in-flight releaser never finishes. The event-log rebuild (rebuildLiveHookByTokenFromEventLog / hooks.get*) no longer considers a hook live once its dispose lock exists. Live claims still conflict exactly as before.

- core: process hook operations per token in workflow-code order during
suspension handling, so a dispose() of an earlier hook releases the
token before a later same-token hook's creation is validated. A hook
created and disposed within the same suspension is still created
before it is disposed. Fixes the same-run recreate self-conflict
(#2777).
- world-local: retry the token claim when the observed claim vanished
mid-check or is held by a hook whose disposal is committed / a run
that is terminal or missing; never rebuild a claim from the event log
for a hook whose dispose lock exists. Fixes spurious HookConflictError
against an already-disposed hook during fast run-to-run token handoff
(#2778).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 899a288

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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

@vercel

vercelBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 6, 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🥇 Nitro0.048s (-16.4% 🟢)1.006s (-1.3%)0.958s101.00x
💻 LocalExpress0.050s (+3.5%)1.006s (~)0.957s101.03x
💻 LocalNext.js (Turbopack)0.055s (-2.0%)1.005s (~)0.951s101.13x
🐘 PostgresNitro0.055s (-17.7% 🟢)1.012s (~)0.956s101.15x
🐘 PostgresNext.js (Turbopack)0.062s (-1.0%)1.012s (~)0.950s101.28x
🐘 PostgresExpress0.070s (+9.4% 🔺)1.012s (~)0.942s101.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.259s (+43.3% 🔺)2.147s (+31.9% 🔺)1.888s101.00x
▲ VercelExpress0.381s (+69.9% 🔺)2.003s (+12.3% 🔺)1.623s101.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.084s (~)2.007s (~)0.923s101.00x
💻 LocalNext.js (Turbopack)1.085s (-0.7%)2.006s (~)0.922s101.00x
💻 LocalExpress1.091s (+1.3%)2.007s (~)0.916s101.01x
🐘 PostgresNext.js (Turbopack)1.096s (-0.9%)2.010s (~)0.914s101.01x
🐘 PostgresExpress1.102s (+0.6%)2.009s (~)0.907s101.02x
🐘 PostgresNitro1.105s (+0.9%)2.011s (~)0.906s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.408s (-4.8%)2.719s (-18.6% 🟢)1.311s101.00x
▲ VercelNitro1.494s (+8.4% 🔺)3.455s (+4.5%)1.961s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.464s (~)11.023s (~)0.559s31.00x
🐘 PostgresExpress10.472s (~)11.014s (~)0.542s31.00x
🐘 PostgresNitro10.475s (~)11.019s (~)0.545s31.00x
💻 LocalNext.js (Turbopack)10.490s (~)11.022s (~)0.532s31.00x
🐘 PostgresNext.js (Turbopack)10.499s (~)11.014s (~)0.515s31.00x
💻 LocalExpress10.512s (+0.7%)11.023s (~)0.511s31.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.697s (+1.1%)13.812s (+8.1% 🔺)2.116s31.00x
▲ VercelExpress11.771s (+0.9%)13.221s (~)1.450s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)13.613s (-1.1%)14.028s (~)0.415s51.00x
🐘 PostgresExpress13.622s (~)14.020s (~)0.398s51.00x
🐘 PostgresNitro13.671s (~)14.022s (~)0.351s51.00x
💻 LocalNitro13.691s (~)14.026s (~)0.336s51.01x
💻 LocalExpress13.738s (+0.9%)14.029s (~)0.290s51.01x
🐘 PostgresNext.js (Turbopack)13.783s (+0.7%)14.018s (~)0.235s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.359s (-0.6%)18.421s (+2.4%)2.063s41.00x
▲ VercelExpress16.725s (~)18.362s (-0.7%)1.636s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.311s (+0.6%)13.019s (~)0.708s71.00x
💻 LocalNext.js (Turbopack)12.354s (-0.5%)13.026s (~)0.673s71.00x
🐘 PostgresNext.js (Turbopack)12.376s (~)13.016s (~)0.641s71.01x
💻 LocalExpress12.377s (+2.3%)13.027s (~)0.650s71.01x
🐘 PostgresNitro12.385s (~)13.019s (~)0.634s71.01x
💻 LocalNitro12.493s (+1.8%)13.025s (~)0.532s71.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro18.213s (-1.7%)20.197s (+0.7%)1.984s51.00x
▲ VercelExpress19.008s (+8.2% 🔺)20.547s (+9.1% 🔺)1.538s51.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.5%)2.009s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.199s (~)2.008s (~)0.810s151.02x
🐘 PostgresExpress1.221s (+1.1%)2.075s (+3.3%)0.853s151.04x
💻 LocalNext.js (Turbopack)1.383s (-3.5%)2.006s (~)0.623s151.18x
💻 LocalNitro1.398s (~)2.006s (~)0.608s151.19x
💻 LocalExpress1.469s (+6.5% 🔺)2.007s (~)0.538s151.25x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.157s (+2.1%)3.617s (+1.9%)1.460s91.00x
▲ VercelNitro2.165s (-3.6%)3.883s (+6.0% 🔺)1.718s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.315s (-4.1%)2.394s (-4.7%)1.079s131.00x
🐘 PostgresNitro1.319s (-2.5%)2.510s (+1.6%)1.191s121.00x
🐘 PostgresNext.js (Turbopack)1.347s (+1.7%)2.918s (-3.0%)1.571s111.02x
💻 LocalNitro2.390s (-1.0%)3.009s (~)0.619s101.82x
💻 LocalNext.js (Turbopack)2.493s (+4.3%)3.110s (+3.3%)0.617s101.90x
💻 LocalExpress2.505s (+1.1%)3.009s (-3.2%)0.504s101.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.122s (+28.6% 🔺)4.500s (+12.9% 🔺)1.378s71.00x
▲ VercelNitro3.207s (+32.4% 🔺)4.900s (+30.5% 🔺)1.694s71.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.563s (-2.7%)4.138s (-3.9%)2.575s81.00x
🐘 PostgresExpress1.616s (-4.4%)4.021s (-9.5% 🟢)2.405s81.03x
💻 LocalExpress2.767s (-21.7% 🟢)4.298s (-9.1% 🟢)1.531s71.77x
🐘 PostgresNext.js (Turbopack)2.988s (-6.2% 🟢)6.014s (+2.8%)3.026s51.91x
💻 LocalNitro4.589s (+19.5% 🔺)5.013s (+9.3% 🔺)0.424s62.94x
💻 LocalNext.js (Turbopack)4.690s (+24.0% 🔺)5.178s (+6.4% 🔺)0.488s63.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.705s (+19.6% 🔺)5.746s (+21.4% 🔺)2.041s61.00x
▲ VercelExpress4.016s (+34.9% 🔺)5.695s (+27.0% 🔺)1.678s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.8%)2.008s (~)0.838s151.00x
🐘 PostgresNext.js (Turbopack)1.188s (-5.2% 🟢)2.007s (-3.3%)0.819s151.01x
🐘 PostgresExpress1.189s (-3.4%)2.008s (-3.2%)0.819s151.02x
💻 LocalNitro1.441s (+0.6%)2.006s (~)0.565s151.23x
💻 LocalNext.js (Turbopack)1.463s (~)2.006s (~)0.543s151.25x
💻 LocalExpress1.499s (+4.8%)2.007s (~)0.508s151.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.014s (-18.8% 🟢)3.876s (+0.7%)1.861s81.00x
▲ VercelExpress2.130s (~)3.651s (+1.8%)1.521s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.317s (-3.6%)2.150s (-17.1% 🟢)0.834s141.00x
🐘 PostgresNitro1.329s (+0.9%)2.316s (-3.2%)0.987s131.01x
🐘 PostgresNext.js (Turbopack)1.435s (+7.2% 🔺)3.009s (~)1.574s101.09x
💻 LocalExpress2.563s (+4.4%)3.009s (~)0.446s101.95x
💻 LocalNitro2.579s (+4.0%)3.009s (~)0.430s101.96x
💻 LocalNext.js (Turbopack)2.611s (-1.0%)3.109s (+3.3%)0.498s101.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.413s (+2.6%)4.409s (+16.9% 🔺)1.996s71.00x
▲ VercelExpress2.441s (+8.0% 🔺)3.951s (+10.4% 🔺)1.511s81.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.525s (-5.3% 🟢)4.138s (~)2.613s81.00x
🐘 PostgresExpress1.624s (-2.0%)4.583s (+6.6% 🔺)2.959s71.06x
🐘 PostgresNext.js (Turbopack)2.970s (-9.9% 🟢)6.017s (-3.2%)3.048s61.95x
💻 LocalNitro4.840s (-14.1% 🟢)5.850s (-2.7%)1.010s63.17x
💻 LocalNext.js (Turbopack)5.395s (-3.7%)5.847s (-2.8%)0.452s63.54x
💻 LocalExpress5.662s (+3.0%)6.416s (+6.7% 🔺)0.754s53.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.748s (+2.8%)4.556s (+5.8% 🔺)1.808s71.00x
▲ VercelNitro4.034s (+40.6% 🔺)6.082s (+38.0% 🔺)2.048s61.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.525s (-12.6% 🟢)1.006s (-1.7%)0.482s601.00x
🐘 PostgresNitro0.557s (-4.7%)1.041s (~)0.484s581.06x
🐘 PostgresNext.js (Turbopack)0.607s (+7.8% 🔺)1.024s (+1.8%)0.417s591.16x
💻 LocalNitro0.609s (+1.5%)1.005s (~)0.397s601.16x
💻 LocalNext.js (Turbopack)0.619s (-1.9%)1.005s (~)0.386s601.18x
💻 LocalExpress0.645s (+13.6% 🔺)1.005s (~)0.360s601.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-1.6%)4.032s (-1.0%)1.496s151.00x
▲ VercelExpress2.578s (-0.7%)3.948s (+4.3%)1.370s161.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.253s (-11.5% 🟢)2.007s (-2.2%)0.754s451.00x
🐘 PostgresNitro1.296s (-4.3%)2.008s (~)0.712s451.03x
🐘 PostgresNext.js (Turbopack)1.382s (-4.0%)2.008s (-1.1%)0.625s451.10x
💻 LocalNitro1.544s (+1.4%)2.029s (+1.1%)0.485s451.23x
💻 LocalNext.js (Turbopack)1.613s (-2.6%)2.006s (-1.1%)0.393s451.29x
💻 LocalExpress1.632s (+10.2% 🔺)2.007s (-1.0%)0.375s451.30x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.805s (-3.4%)7.644s (+1.0%)1.839s121.00x
▲ VercelExpress6.114s (-1.3%)7.668s (+1.0%)1.554s121.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.595s (-2.5%)3.111s (+2.5%)0.516s391.00x
🐘 PostgresNitro2.618s (-3.2%)3.086s (~)0.468s391.01x
🐘 PostgresNext.js (Turbopack)2.725s (-1.7%)3.033s (+0.8%)0.309s401.05x
💻 LocalNext.js (Turbopack)3.398s (-5.3% 🟢)4.009s (-1.7%)0.611s301.31x
💻 LocalNitro3.416s (+5.2% 🔺)4.009s (~)0.593s301.32x
💻 LocalExpress3.557s (+12.3% 🔺)4.077s (+5.1% 🔺)0.520s301.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.756s (-6.3% 🟢)13.617s (-3.3%)1.861s91.00x
▲ VercelNitro12.119s (+1.1%)14.257s (+6.8% 🔺)2.138s91.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.189s (-9.8% 🟢)1.006s (-1.7%)0.817s601.00x
🐘 PostgresExpress0.208s (-2.5%)1.006s (~)0.798s601.10x
🐘 PostgresNitro0.210s (-5.1% 🟢)1.007s (~)0.796s601.11x
💻 LocalNitro0.532s (+6.7% 🔺)1.005s (~)0.473s602.81x
💻 LocalExpress0.562s (+15.0% 🔺)1.005s (~)0.443s602.97x
💻 LocalNext.js (Turbopack)0.605s (-7.2% 🟢)1.005s (-1.7%)0.399s603.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.091s (+3.3%)2.425s (+6.4% 🔺)1.334s261.00x
▲ VercelNitro1.108s (+12.7% 🔺)2.752s (+24.2% 🔺)1.643s221.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.326s (-2.7%)1.018s (+1.2%)0.693s891.00x
🐘 PostgresExpress0.340s (+4.9%)1.029s (+2.2%)0.689s881.04x
🐘 PostgresNext.js (Turbopack)0.352s (+18.6% 🔺)1.041s (+3.4%)0.689s871.08x
💻 LocalNitro2.465s (-3.3%)3.009s (~)0.545s307.56x
💻 LocalExpress2.503s (+1.2%)3.010s (~)0.507s307.68x
💻 LocalNext.js (Turbopack)2.675s (~)3.075s (+2.2%)0.400s308.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.430s (+7.8% 🔺)2.916s (+17.9% 🔺)1.486s311.00x
▲ VercelNitro1.491s (+7.7% 🔺)3.145s (+14.4% 🔺)1.654s291.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.516s (-3.2%)3.034s (+0.8%)2.518s401.00x
🐘 PostgresNitro0.540s (+6.8% 🔺)1.150s (+11.5% 🔺)0.610s1051.05x
🐘 PostgresExpress0.582s (+11.5% 🔺)1.160s (+7.7% 🔺)0.578s1041.13x
💻 LocalNitro5.602s (+4.4%)7.436s (-11.0% 🟢)1.834s1710.85x
💻 LocalExpress5.988s (+2.9%)8.956s (+0.8%)2.968s1411.60x
💻 LocalNext.js (Turbopack)6.274s (+3.8%)9.172s (+2.4%)2.898s1412.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.941s (+7.2% 🔺)3.965s (+16.7% 🔺)2.024s311.00x
▲ VercelExpress2.171s (+18.6% 🔺)3.898s (+6.5% 🔺)1.727s311.12x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.141s (-1.4%)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.868s101.00x
💻 LocalNext.js (Turbopack)1.151s (~)1.964s (~)0.011s (-19.2% 🟢)2.018s (~)0.867s101.01x
💻 LocalNitro1.161s (~)2.004s (~)0.012s (+3.3%)2.019s (~)0.858s101.02x
🐘 PostgresNext.js (Turbopack)1.165s (~)2.001s (~)0.001s (+8.3% 🔺)2.012s (~)0.847s101.02x
🐘 PostgresExpress1.169s (+0.6%)1.999s (~)0.001s (~)2.010s (~)0.841s101.02x
💻 LocalExpress1.173s (+1.8%)2.005s (~)0.013s (+23.1% 🔺)2.021s (~)0.848s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.962s (-7.4% 🟢)3.736s (+17.4% 🔺)2.143s (+15.8% 🔺)6.372s (+16.3% 🔺)4.410s101.00x
▲ VercelExpress2.103s (-3.2%)3.274s (-4.3%)1.979s (+5.8% 🔺)5.705s (~)3.602s101.07x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.554s (-2.4%)2.002s (~)0.005s (-2.5%)2.028s (~)0.474s301.00x
🐘 PostgresNitro1.585s (+1.1%)2.007s (~)0.005s (-5.4% 🟢)2.025s (~)0.440s301.02x
💻 LocalExpress1.592s (+0.6%)2.010s (~)0.012s (-7.6% 🟢)2.025s (~)0.433s301.02x
💻 LocalNext.js (Turbopack)1.605s (~)1.972s (~)0.014s (+0.7%)2.027s (~)0.422s301.03x
🐘 PostgresNext.js (Turbopack)1.657s (-2.0%)2.010s (-1.6%)0.005s (~)2.027s (-1.6%)0.370s301.07x
💻 LocalNitro1.907s (+21.0% 🔺)2.354s (+17.1% 🔺)0.012s (-4.3%)2.372s (+17.1% 🔺)0.464s261.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.530s (+1.6%)7.432s (+13.4% 🔺)0.328s (+71.3% 🔺)8.232s (+14.6% 🔺)2.702s81.00x
▲ VercelExpress5.684s (+5.8% 🔺)6.911s (+3.9%)0.345s (+53.1% 🔺)7.764s (+5.6% 🔺)2.080s81.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.784s (~)1.081s (+3.0%)0.000s (NaN%)1.099s (+1.7%)0.315s561.00x
🐘 PostgresNitro0.788s (+1.0%)1.032s (-3.1%)0.000s (-67.8% 🟢)1.042s (-4.2%)0.254s581.00x
🐘 PostgresNext.js (Turbopack)0.918s (-5.2% 🟢)1.333s (-7.4% 🟢)0.000s (-8.9% 🟢)1.340s (-8.5% 🟢)0.423s451.17x
💻 LocalNext.js (Turbopack)1.327s (-0.8%)1.978s (~)0.000s (-55.6% 🟢)2.017s (~)0.690s301.69x
💻 LocalNitro1.372s (~)1.919s (-3.1%)0.000s (+23.3% 🔺)1.922s (-3.1%)0.550s321.75x
💻 LocalExpress1.453s (+5.2% 🔺)1.982s (+1.6%)0.000s (+62.5% 🔺)1.985s (+1.6%)0.532s311.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.761s (+23.7% 🔺)5.227s (+25.2% 🔺)0.003s (+Infinity% 🔺)5.792s (+25.6% 🔺)2.032s111.00x
▲ VercelExpress3.837s (+32.6% 🔺)5.303s (+30.4% 🔺)0.000s (+154.5% 🔺)5.759s (+29.4% 🔺)1.923s111.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.640s (-10.8% 🟢)2.306s (-3.1%)0.000s (NaN%)2.317s (-3.4%)0.677s261.00x
🐘 PostgresExpress1.725s (-2.8%)2.301s (-4.0%)0.000s (-100.0% 🟢)2.318s (-3.8%)0.593s261.05x
🐘 PostgresNext.js (Turbopack)2.275s (-17.3% 🟢)2.859s (-14.3% 🟢)0.000s (-100.0% 🟢)2.868s (-14.3% 🟢)0.593s211.39x
💻 LocalNitro3.218s (+1.3%)3.775s (+2.7%)0.001s (+112.5% 🔺)3.779s (+2.7%)0.561s161.96x
💻 LocalExpress3.363s (-2.3%)3.902s (-3.1%)0.000s (-29.7% 🟢)3.905s (-3.1%)0.543s162.05x
💻 LocalNext.js (Turbopack)3.580s (+3.0%)3.992s (~)0.000s (-28.6% 🟢)4.032s (~)0.452s152.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.808s (+15.8% 🔺)5.847s (+10.0% 🔺)0.000s (+10.0% 🔺)6.351s (+10.9% 🔺)1.543s101.00x
▲ VercelExpress5.284s (+24.3% 🔺)6.342s (+20.6% 🔺)0.000s (NaN%)6.789s (+20.3% 🔺)1.505s101.10x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro12/21
🐘 PostgresExpress10/21
▲ VercelNitro14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres19/21
Next.js (Turbopack)🐘 Postgres14/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027

📋 View full workflow run

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 6, 2026 17:04
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 6, 2026 17:04

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

ai review: Approving — no blockers or regressions found.

What I checked:

  • @workflow/core suspension handler: verified the per-token grouping preserves queue-insertion (workflow-code) order, so dispose-before-recreate and create-before-dispose orderings both hold; cross-token parallelism and the hooks-before-steps/waits ordering are unchanged. hasHookEvents (hooksNeedingCreation.length > 0) is equivalent to the old hookEvents.length > 0. Skipping disposal for a conflicted creation is a behavior improvement (previously it attempted the dispose and swallowed HookNotFoundError). Abort processing (hooksNeedingAbort) is unaffected.
  • @workflow/world-local claim loop: the loop is bounded (10 attempts, ≤2×10ms sleeps), a genuinely live claim breaks out on the first iteration into the unchanged conflict path, and the own-claim dedup/recovery path is preserved (existingClaim carried out of the loop matches the old post-failure re-read). On loop exhaustion, behavior degrades to the pre-PR single-attempt semantics rather than something worse.
  • Rebuild/resurrection interaction: findLiveHookCreatedEvent already excludes terminal-run hooks (isTerminalRunCache) and now dispose-committed hooks, so a force-released claim is not resurrected by the event-log rebuild in the disposed-hook and terminal-run cases (matching the two new storage tests).
  • Dispose-lock keying: the lock path/naming is byte-identical to the old inline construction (backward compatible with locks already on disk), and hook correlation IDs are hook_${ulid} so keying by hookId alone is safe — same keying the dispose path already relied on.
  • Tests: the new @workflow/vitest workbench tests are race-free because waitForHook excludes hooks that already have a hook_received event, so each round can only observe the next round's hook; the e2e helper's new excludeHookId handles the same race for the token-lookup-based poll. CI is green including the required E2E check.

Left two non-blocking inline notes on narrow crash/race windows in the world-local force-release path.

// The releaser is not coming. Release the stale claim and
// the hook entity it points at, mirroring the hook_disposed
// cleanup.
await deleteJSON(constraintPath);

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.

ai review (non-blocking): there's a narrow cross-process window where this force-release can lead to a transient token-uniqueness violation. Both in-flight releasers (hook_disposed handler and deleteAllHooksForRun) delete the claim file unconditionally after reading the hook entity. Sequence: releaser writes the dispose lock (or run goes terminal) and reads the hook entity, then stalls; this claimant observes the stale claim releasable 3×, deletes it, and writes its own fresh claim; the stalled releaser then executes its deleteJSON(constraintPath) — deleting the new claimant's live claim, so a third claimant can also claim the token. The 2×10ms grace period narrows this but doesn't close it. A cheap hardening would be for the releasers to re-read the claim and only delete it if it still points at their own (runId, hookId) (still TOCTOU, but the window shrinks from "stall of any length" to adjacent file ops), or to record the claim's identity here and have releasers skip deletion when the hook entity is already gone — which hook_disposed already partially does via the existingHook guard.

WorkflowRunSchema,
tag
);
if (!owningRun) {

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.

ai review (non-blocking): the missing-run semantics here disagree with the event-log rebuild. This treats a claim whose run file is absent as releasable, but findLiveHookCreatedEvent's isTerminalRunCache returns false for a missing run file, i.e. the rebuild treats that same hook as live and re-writes the claim. If a claim + surviving hook_created event ever exist without a run file (crash-lost run cache), the claim loop deletes the claim, the next iteration's rebuild resurrects it, and the loop ping-pongs to exhaustion — ending in the conflict branch with existingClaim = null, i.e. a durable hook_conflict with conflictingRunId: undefined, the exact symptom #2778 fixes. Unreachable today AFAICT (world-local never deletes run files), but worth aligning the two predicates — either both treat a missing run as terminal, or drop this branch.

@VaguelySerious
VaguelySerious merged commit 7637196 into mainJul 7, 2026
180 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/issue-2777 branch July 7, 2026 21:13
@VaguelySerious

Copy link
Copy Markdown
MemberAuthor

Addressing review comment in a follow-up PR

@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 7637196 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

7637196cf0f605ce62243bf8c7762a26153dcd36

@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026

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

Reviewed both halves in depth, ran the full affected suites plus the new regression tests against a live dev server. Approving.

Core (same-run, #2777): The per-token grouping is the right shape. I traced the ordering semantics:

  • Queue-insertion order within a token group = workflow code order, so dispose-of-earlier flushes before create-of-later is validated, while a created-and-disposed-in-one-suspension hook still creates first. Both directions are covered by the new unit tests.
  • The new event-log order (hook_disposed before the successor's hook_created) also matches replay consumption order — under the old code the log recorded them inverted relative to code order, so this is strictly better for replay, not just for conflict avoidance.
  • hasHookConflict / hasAwaitedHookCreation accumulation reaches the same set of creations as before, and hasHookEvents is computed from the same population — no semantic drift in the V2 re-invoke signaling.
  • The creationConflicted gate on dispose is correct belt-and-braces: a conflicted creation has nothing to dispose, and skipping avoids even the benign HookNotFoundError round-trip.
  • Cross-token parallelism is preserved, so unrelated hooks don't serialize.

world-local (cross-run, #2778): The claim loop's force-release is the risky part, so I audited its failure modes:

  • readJSON returns null only on ENOENT (anything else throws), so isHookTokenClaimReleasable can't misclassify a live claim due to a transient read error — a "missing" owning run is genuinely missing, and terminal status is monotonic.
  • Racing force-releasers and the in-flight disposer are all safe: deleteJSON is ENOENT-idempotent, and re-claim arbitration still lands on writeExclusive's EEXIST semantics, so exactly one claimant wins and the loser correctly conflicts against the winner's live claim.
  • The dispose lock as earliest-durable-disposal-marker works because the lock write already preceded the destructive deletes pre-PR — the change formalizes existing ordering rather than introducing new ordering requirements. The findLiveHookCreatedEvent lock check closing the rebuild-resurrection window is a nice catch.
  • I also confirmed the asymmetry with world-postgres is justified: there the hook row is the token claim and hook_disposed releases it via atomic DELETE ... RETURNING, so no separate-release window exists — the core fix alone covers it.

Validation: 1354/1354 core unit tests, 432/432 world-local (after full build — the 17 initial failures were unbuilt-SWC-plugin/e2e-env artifacts), 2/2 new workbench/vitest regression tests, and against a live turbopack dev server: the new hookTokenReuseLoopWorkflow e2e plus all 26 existing hook e2e tests pass. Workbench conventions followed (workflow added to workbench/example and propagated via symlink; the vitest workbench's local workflow matches its local-file convention). Changesets correctly split per package. CI: 105 pass, 1 fail = the known Benchmark Vercel (nextjs-turbopack) flake.

Two non-blocking notes:

  1. If the claim loop exhausts all 10 attempts via repeated vanish races (claim deleted between the exclusive-create attempt and every read), it falls through with existingClaim = null and still records a hook_conflict with no conflictingRunId — the #2778 symptom, now requiring ~10 consecutive lost races to reproduce. Practically unreachable, but a runtimeLogger.warn on loop exhaustion would make it diagnosable if it ever fires.
  2. The dispose locks in .locks/hooks/ are now load-bearing durable state (consulted by the claim path and the rebuild), not just transient mutexes. Worth a note somewhere discoverable that they must never be garbage-collected — a future ".locks cleanup" change would silently reintroduce the resurrection bug. The comment at hookDisposeLockPath covers the what; the hazard is for whoever writes a cleanup without reading it.

VaguelySerious added a commit that referenced this pull request Jul 7, 2026
…tall
Both in-flight releasers (the hook_disposed handler and the terminal-run
deleteAllHooksForRun cleanup) deleted the claim file unconditionally after
reading the hook entity. A releaser stalled between those operations could
outlive a claimant force-releasing its stale claim, and its deferred delete
would then destroy the new claimant's live claim — transiently breaking
token uniqueness (a third claimant could claim the token too).
Releasers now re-read the claim and delete it only if it still points at
their own (runId, hookId). Still TOCTOU, but the window shrinks from "a
stall of any length" to adjacent file ops; a claim owned by someone else
is left for the claimant-side force-release path to reap.
Addresses post-merge feedback on #2779.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@VaguelySerious@TooTallNate@karthikscale3
, '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('^' + ".*" + '
Skip to content

[core] Fix hook token reuse after dispose() (same-run and cross-run) - #2779

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777
Jul 7, 2026
Merged

[core] Fix hook token reuse after dispose() (same-run and cross-run)#2779
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 6, 2026

Copy link
Copy Markdown
Member

Closes#2777, closes#2778.

Problem

Reusing a hook token after hook.dispose() deterministically or intermittently failed with a spurious HookConflictError naming an already-disposed hook:

Fix

  • @workflow/core:handleSuspension now processes hook operations grouped per token, in workflow-code order: a dispose of an earlier hook flushes before a later same-token hook's creation is validated, while a hook created and disposed within one suspension is still created first. Different tokens keep processing in parallel, and hooks still process before steps/waits.
  • @workflow/world-local: the hook_created token claim runs in a short bounded loop — it retries when the observed claim vanished mid-check, and treats a claim as vacant when its hook's disposal is committed (dispose lock exists) or its owning run is terminal/missing, releasing the stale claim if the in-flight releaser never finishes. The event-log rebuild (rebuildLiveHookByTokenFromEventLog / hooks.get*) no longer considers a hook live once its dispose lock exists. Live claims still conflict exactly as before.

- core: process hook operations per token in workflow-code order during
suspension handling, so a dispose() of an earlier hook releases the
token before a later same-token hook's creation is validated. A hook
created and disposed within the same suspension is still created
before it is disposed. Fixes the same-run recreate self-conflict
(#2777).
- world-local: retry the token claim when the observed claim vanished
mid-check or is held by a hook whose disposal is committed / a run
that is terminal or missing; never rebuild a claim from the event log
for a hook whose dispose lock exists. Fixes spurious HookConflictError
against an already-disposed hook during fast run-to-run token handoff
(#2778).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 899a288

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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

@vercel

vercelBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 6, 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🥇 Nitro0.048s (-16.4% 🟢)1.006s (-1.3%)0.958s101.00x
💻 LocalExpress0.050s (+3.5%)1.006s (~)0.957s101.03x
💻 LocalNext.js (Turbopack)0.055s (-2.0%)1.005s (~)0.951s101.13x
🐘 PostgresNitro0.055s (-17.7% 🟢)1.012s (~)0.956s101.15x
🐘 PostgresNext.js (Turbopack)0.062s (-1.0%)1.012s (~)0.950s101.28x
🐘 PostgresExpress0.070s (+9.4% 🔺)1.012s (~)0.942s101.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.259s (+43.3% 🔺)2.147s (+31.9% 🔺)1.888s101.00x
▲ VercelExpress0.381s (+69.9% 🔺)2.003s (+12.3% 🔺)1.623s101.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.084s (~)2.007s (~)0.923s101.00x
💻 LocalNext.js (Turbopack)1.085s (-0.7%)2.006s (~)0.922s101.00x
💻 LocalExpress1.091s (+1.3%)2.007s (~)0.916s101.01x
🐘 PostgresNext.js (Turbopack)1.096s (-0.9%)2.010s (~)0.914s101.01x
🐘 PostgresExpress1.102s (+0.6%)2.009s (~)0.907s101.02x
🐘 PostgresNitro1.105s (+0.9%)2.011s (~)0.906s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.408s (-4.8%)2.719s (-18.6% 🟢)1.311s101.00x
▲ VercelNitro1.494s (+8.4% 🔺)3.455s (+4.5%)1.961s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.464s (~)11.023s (~)0.559s31.00x
🐘 PostgresExpress10.472s (~)11.014s (~)0.542s31.00x
🐘 PostgresNitro10.475s (~)11.019s (~)0.545s31.00x
💻 LocalNext.js (Turbopack)10.490s (~)11.022s (~)0.532s31.00x
🐘 PostgresNext.js (Turbopack)10.499s (~)11.014s (~)0.515s31.00x
💻 LocalExpress10.512s (+0.7%)11.023s (~)0.511s31.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.697s (+1.1%)13.812s (+8.1% 🔺)2.116s31.00x
▲ VercelExpress11.771s (+0.9%)13.221s (~)1.450s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)13.613s (-1.1%)14.028s (~)0.415s51.00x
🐘 PostgresExpress13.622s (~)14.020s (~)0.398s51.00x
🐘 PostgresNitro13.671s (~)14.022s (~)0.351s51.00x
💻 LocalNitro13.691s (~)14.026s (~)0.336s51.01x
💻 LocalExpress13.738s (+0.9%)14.029s (~)0.290s51.01x
🐘 PostgresNext.js (Turbopack)13.783s (+0.7%)14.018s (~)0.235s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.359s (-0.6%)18.421s (+2.4%)2.063s41.00x
▲ VercelExpress16.725s (~)18.362s (-0.7%)1.636s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.311s (+0.6%)13.019s (~)0.708s71.00x
💻 LocalNext.js (Turbopack)12.354s (-0.5%)13.026s (~)0.673s71.00x
🐘 PostgresNext.js (Turbopack)12.376s (~)13.016s (~)0.641s71.01x
💻 LocalExpress12.377s (+2.3%)13.027s (~)0.650s71.01x
🐘 PostgresNitro12.385s (~)13.019s (~)0.634s71.01x
💻 LocalNitro12.493s (+1.8%)13.025s (~)0.532s71.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro18.213s (-1.7%)20.197s (+0.7%)1.984s51.00x
▲ VercelExpress19.008s (+8.2% 🔺)20.547s (+9.1% 🔺)1.538s51.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.5%)2.009s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.199s (~)2.008s (~)0.810s151.02x
🐘 PostgresExpress1.221s (+1.1%)2.075s (+3.3%)0.853s151.04x
💻 LocalNext.js (Turbopack)1.383s (-3.5%)2.006s (~)0.623s151.18x
💻 LocalNitro1.398s (~)2.006s (~)0.608s151.19x
💻 LocalExpress1.469s (+6.5% 🔺)2.007s (~)0.538s151.25x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.157s (+2.1%)3.617s (+1.9%)1.460s91.00x
▲ VercelNitro2.165s (-3.6%)3.883s (+6.0% 🔺)1.718s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.315s (-4.1%)2.394s (-4.7%)1.079s131.00x
🐘 PostgresNitro1.319s (-2.5%)2.510s (+1.6%)1.191s121.00x
🐘 PostgresNext.js (Turbopack)1.347s (+1.7%)2.918s (-3.0%)1.571s111.02x
💻 LocalNitro2.390s (-1.0%)3.009s (~)0.619s101.82x
💻 LocalNext.js (Turbopack)2.493s (+4.3%)3.110s (+3.3%)0.617s101.90x
💻 LocalExpress2.505s (+1.1%)3.009s (-3.2%)0.504s101.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.122s (+28.6% 🔺)4.500s (+12.9% 🔺)1.378s71.00x
▲ VercelNitro3.207s (+32.4% 🔺)4.900s (+30.5% 🔺)1.694s71.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.563s (-2.7%)4.138s (-3.9%)2.575s81.00x
🐘 PostgresExpress1.616s (-4.4%)4.021s (-9.5% 🟢)2.405s81.03x
💻 LocalExpress2.767s (-21.7% 🟢)4.298s (-9.1% 🟢)1.531s71.77x
🐘 PostgresNext.js (Turbopack)2.988s (-6.2% 🟢)6.014s (+2.8%)3.026s51.91x
💻 LocalNitro4.589s (+19.5% 🔺)5.013s (+9.3% 🔺)0.424s62.94x
💻 LocalNext.js (Turbopack)4.690s (+24.0% 🔺)5.178s (+6.4% 🔺)0.488s63.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.705s (+19.6% 🔺)5.746s (+21.4% 🔺)2.041s61.00x
▲ VercelExpress4.016s (+34.9% 🔺)5.695s (+27.0% 🔺)1.678s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.8%)2.008s (~)0.838s151.00x
🐘 PostgresNext.js (Turbopack)1.188s (-5.2% 🟢)2.007s (-3.3%)0.819s151.01x
🐘 PostgresExpress1.189s (-3.4%)2.008s (-3.2%)0.819s151.02x
💻 LocalNitro1.441s (+0.6%)2.006s (~)0.565s151.23x
💻 LocalNext.js (Turbopack)1.463s (~)2.006s (~)0.543s151.25x
💻 LocalExpress1.499s (+4.8%)2.007s (~)0.508s151.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.014s (-18.8% 🟢)3.876s (+0.7%)1.861s81.00x
▲ VercelExpress2.130s (~)3.651s (+1.8%)1.521s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.317s (-3.6%)2.150s (-17.1% 🟢)0.834s141.00x
🐘 PostgresNitro1.329s (+0.9%)2.316s (-3.2%)0.987s131.01x
🐘 PostgresNext.js (Turbopack)1.435s (+7.2% 🔺)3.009s (~)1.574s101.09x
💻 LocalExpress2.563s (+4.4%)3.009s (~)0.446s101.95x
💻 LocalNitro2.579s (+4.0%)3.009s (~)0.430s101.96x
💻 LocalNext.js (Turbopack)2.611s (-1.0%)3.109s (+3.3%)0.498s101.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.413s (+2.6%)4.409s (+16.9% 🔺)1.996s71.00x
▲ VercelExpress2.441s (+8.0% 🔺)3.951s (+10.4% 🔺)1.511s81.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.525s (-5.3% 🟢)4.138s (~)2.613s81.00x
🐘 PostgresExpress1.624s (-2.0%)4.583s (+6.6% 🔺)2.959s71.06x
🐘 PostgresNext.js (Turbopack)2.970s (-9.9% 🟢)6.017s (-3.2%)3.048s61.95x
💻 LocalNitro4.840s (-14.1% 🟢)5.850s (-2.7%)1.010s63.17x
💻 LocalNext.js (Turbopack)5.395s (-3.7%)5.847s (-2.8%)0.452s63.54x
💻 LocalExpress5.662s (+3.0%)6.416s (+6.7% 🔺)0.754s53.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.748s (+2.8%)4.556s (+5.8% 🔺)1.808s71.00x
▲ VercelNitro4.034s (+40.6% 🔺)6.082s (+38.0% 🔺)2.048s61.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.525s (-12.6% 🟢)1.006s (-1.7%)0.482s601.00x
🐘 PostgresNitro0.557s (-4.7%)1.041s (~)0.484s581.06x
🐘 PostgresNext.js (Turbopack)0.607s (+7.8% 🔺)1.024s (+1.8%)0.417s591.16x
💻 LocalNitro0.609s (+1.5%)1.005s (~)0.397s601.16x
💻 LocalNext.js (Turbopack)0.619s (-1.9%)1.005s (~)0.386s601.18x
💻 LocalExpress0.645s (+13.6% 🔺)1.005s (~)0.360s601.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-1.6%)4.032s (-1.0%)1.496s151.00x
▲ VercelExpress2.578s (-0.7%)3.948s (+4.3%)1.370s161.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.253s (-11.5% 🟢)2.007s (-2.2%)0.754s451.00x
🐘 PostgresNitro1.296s (-4.3%)2.008s (~)0.712s451.03x
🐘 PostgresNext.js (Turbopack)1.382s (-4.0%)2.008s (-1.1%)0.625s451.10x
💻 LocalNitro1.544s (+1.4%)2.029s (+1.1%)0.485s451.23x
💻 LocalNext.js (Turbopack)1.613s (-2.6%)2.006s (-1.1%)0.393s451.29x
💻 LocalExpress1.632s (+10.2% 🔺)2.007s (-1.0%)0.375s451.30x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.805s (-3.4%)7.644s (+1.0%)1.839s121.00x
▲ VercelExpress6.114s (-1.3%)7.668s (+1.0%)1.554s121.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.595s (-2.5%)3.111s (+2.5%)0.516s391.00x
🐘 PostgresNitro2.618s (-3.2%)3.086s (~)0.468s391.01x
🐘 PostgresNext.js (Turbopack)2.725s (-1.7%)3.033s (+0.8%)0.309s401.05x
💻 LocalNext.js (Turbopack)3.398s (-5.3% 🟢)4.009s (-1.7%)0.611s301.31x
💻 LocalNitro3.416s (+5.2% 🔺)4.009s (~)0.593s301.32x
💻 LocalExpress3.557s (+12.3% 🔺)4.077s (+5.1% 🔺)0.520s301.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.756s (-6.3% 🟢)13.617s (-3.3%)1.861s91.00x
▲ VercelNitro12.119s (+1.1%)14.257s (+6.8% 🔺)2.138s91.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.189s (-9.8% 🟢)1.006s (-1.7%)0.817s601.00x
🐘 PostgresExpress0.208s (-2.5%)1.006s (~)0.798s601.10x
🐘 PostgresNitro0.210s (-5.1% 🟢)1.007s (~)0.796s601.11x
💻 LocalNitro0.532s (+6.7% 🔺)1.005s (~)0.473s602.81x
💻 LocalExpress0.562s (+15.0% 🔺)1.005s (~)0.443s602.97x
💻 LocalNext.js (Turbopack)0.605s (-7.2% 🟢)1.005s (-1.7%)0.399s603.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.091s (+3.3%)2.425s (+6.4% 🔺)1.334s261.00x
▲ VercelNitro1.108s (+12.7% 🔺)2.752s (+24.2% 🔺)1.643s221.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.326s (-2.7%)1.018s (+1.2%)0.693s891.00x
🐘 PostgresExpress0.340s (+4.9%)1.029s (+2.2%)0.689s881.04x
🐘 PostgresNext.js (Turbopack)0.352s (+18.6% 🔺)1.041s (+3.4%)0.689s871.08x
💻 LocalNitro2.465s (-3.3%)3.009s (~)0.545s307.56x
💻 LocalExpress2.503s (+1.2%)3.010s (~)0.507s307.68x
💻 LocalNext.js (Turbopack)2.675s (~)3.075s (+2.2%)0.400s308.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.430s (+7.8% 🔺)2.916s (+17.9% 🔺)1.486s311.00x
▲ VercelNitro1.491s (+7.7% 🔺)3.145s (+14.4% 🔺)1.654s291.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.516s (-3.2%)3.034s (+0.8%)2.518s401.00x
🐘 PostgresNitro0.540s (+6.8% 🔺)1.150s (+11.5% 🔺)0.610s1051.05x
🐘 PostgresExpress0.582s (+11.5% 🔺)1.160s (+7.7% 🔺)0.578s1041.13x
💻 LocalNitro5.602s (+4.4%)7.436s (-11.0% 🟢)1.834s1710.85x
💻 LocalExpress5.988s (+2.9%)8.956s (+0.8%)2.968s1411.60x
💻 LocalNext.js (Turbopack)6.274s (+3.8%)9.172s (+2.4%)2.898s1412.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.941s (+7.2% 🔺)3.965s (+16.7% 🔺)2.024s311.00x
▲ VercelExpress2.171s (+18.6% 🔺)3.898s (+6.5% 🔺)1.727s311.12x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.141s (-1.4%)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.868s101.00x
💻 LocalNext.js (Turbopack)1.151s (~)1.964s (~)0.011s (-19.2% 🟢)2.018s (~)0.867s101.01x
💻 LocalNitro1.161s (~)2.004s (~)0.012s (+3.3%)2.019s (~)0.858s101.02x
🐘 PostgresNext.js (Turbopack)1.165s (~)2.001s (~)0.001s (+8.3% 🔺)2.012s (~)0.847s101.02x
🐘 PostgresExpress1.169s (+0.6%)1.999s (~)0.001s (~)2.010s (~)0.841s101.02x
💻 LocalExpress1.173s (+1.8%)2.005s (~)0.013s (+23.1% 🔺)2.021s (~)0.848s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.962s (-7.4% 🟢)3.736s (+17.4% 🔺)2.143s (+15.8% 🔺)6.372s (+16.3% 🔺)4.410s101.00x
▲ VercelExpress2.103s (-3.2%)3.274s (-4.3%)1.979s (+5.8% 🔺)5.705s (~)3.602s101.07x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.554s (-2.4%)2.002s (~)0.005s (-2.5%)2.028s (~)0.474s301.00x
🐘 PostgresNitro1.585s (+1.1%)2.007s (~)0.005s (-5.4% 🟢)2.025s (~)0.440s301.02x
💻 LocalExpress1.592s (+0.6%)2.010s (~)0.012s (-7.6% 🟢)2.025s (~)0.433s301.02x
💻 LocalNext.js (Turbopack)1.605s (~)1.972s (~)0.014s (+0.7%)2.027s (~)0.422s301.03x
🐘 PostgresNext.js (Turbopack)1.657s (-2.0%)2.010s (-1.6%)0.005s (~)2.027s (-1.6%)0.370s301.07x
💻 LocalNitro1.907s (+21.0% 🔺)2.354s (+17.1% 🔺)0.012s (-4.3%)2.372s (+17.1% 🔺)0.464s261.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.530s (+1.6%)7.432s (+13.4% 🔺)0.328s (+71.3% 🔺)8.232s (+14.6% 🔺)2.702s81.00x
▲ VercelExpress5.684s (+5.8% 🔺)6.911s (+3.9%)0.345s (+53.1% 🔺)7.764s (+5.6% 🔺)2.080s81.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.784s (~)1.081s (+3.0%)0.000s (NaN%)1.099s (+1.7%)0.315s561.00x
🐘 PostgresNitro0.788s (+1.0%)1.032s (-3.1%)0.000s (-67.8% 🟢)1.042s (-4.2%)0.254s581.00x
🐘 PostgresNext.js (Turbopack)0.918s (-5.2% 🟢)1.333s (-7.4% 🟢)0.000s (-8.9% 🟢)1.340s (-8.5% 🟢)0.423s451.17x
💻 LocalNext.js (Turbopack)1.327s (-0.8%)1.978s (~)0.000s (-55.6% 🟢)2.017s (~)0.690s301.69x
💻 LocalNitro1.372s (~)1.919s (-3.1%)0.000s (+23.3% 🔺)1.922s (-3.1%)0.550s321.75x
💻 LocalExpress1.453s (+5.2% 🔺)1.982s (+1.6%)0.000s (+62.5% 🔺)1.985s (+1.6%)0.532s311.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.761s (+23.7% 🔺)5.227s (+25.2% 🔺)0.003s (+Infinity% 🔺)5.792s (+25.6% 🔺)2.032s111.00x
▲ VercelExpress3.837s (+32.6% 🔺)5.303s (+30.4% 🔺)0.000s (+154.5% 🔺)5.759s (+29.4% 🔺)1.923s111.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.640s (-10.8% 🟢)2.306s (-3.1%)0.000s (NaN%)2.317s (-3.4%)0.677s261.00x
🐘 PostgresExpress1.725s (-2.8%)2.301s (-4.0%)0.000s (-100.0% 🟢)2.318s (-3.8%)0.593s261.05x
🐘 PostgresNext.js (Turbopack)2.275s (-17.3% 🟢)2.859s (-14.3% 🟢)0.000s (-100.0% 🟢)2.868s (-14.3% 🟢)0.593s211.39x
💻 LocalNitro3.218s (+1.3%)3.775s (+2.7%)0.001s (+112.5% 🔺)3.779s (+2.7%)0.561s161.96x
💻 LocalExpress3.363s (-2.3%)3.902s (-3.1%)0.000s (-29.7% 🟢)3.905s (-3.1%)0.543s162.05x
💻 LocalNext.js (Turbopack)3.580s (+3.0%)3.992s (~)0.000s (-28.6% 🟢)4.032s (~)0.452s152.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.808s (+15.8% 🔺)5.847s (+10.0% 🔺)0.000s (+10.0% 🔺)6.351s (+10.9% 🔺)1.543s101.00x
▲ VercelExpress5.284s (+24.3% 🔺)6.342s (+20.6% 🔺)0.000s (NaN%)6.789s (+20.3% 🔺)1.505s101.10x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro12/21
🐘 PostgresExpress10/21
▲ VercelNitro14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres19/21
Next.js (Turbopack)🐘 Postgres14/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027

📋 View full workflow run

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 6, 2026 17:04
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 6, 2026 17:04

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

ai review: Approving — no blockers or regressions found.

What I checked:

  • @workflow/core suspension handler: verified the per-token grouping preserves queue-insertion (workflow-code) order, so dispose-before-recreate and create-before-dispose orderings both hold; cross-token parallelism and the hooks-before-steps/waits ordering are unchanged. hasHookEvents (hooksNeedingCreation.length > 0) is equivalent to the old hookEvents.length > 0. Skipping disposal for a conflicted creation is a behavior improvement (previously it attempted the dispose and swallowed HookNotFoundError). Abort processing (hooksNeedingAbort) is unaffected.
  • @workflow/world-local claim loop: the loop is bounded (10 attempts, ≤2×10ms sleeps), a genuinely live claim breaks out on the first iteration into the unchanged conflict path, and the own-claim dedup/recovery path is preserved (existingClaim carried out of the loop matches the old post-failure re-read). On loop exhaustion, behavior degrades to the pre-PR single-attempt semantics rather than something worse.
  • Rebuild/resurrection interaction: findLiveHookCreatedEvent already excludes terminal-run hooks (isTerminalRunCache) and now dispose-committed hooks, so a force-released claim is not resurrected by the event-log rebuild in the disposed-hook and terminal-run cases (matching the two new storage tests).
  • Dispose-lock keying: the lock path/naming is byte-identical to the old inline construction (backward compatible with locks already on disk), and hook correlation IDs are hook_${ulid} so keying by hookId alone is safe — same keying the dispose path already relied on.
  • Tests: the new @workflow/vitest workbench tests are race-free because waitForHook excludes hooks that already have a hook_received event, so each round can only observe the next round's hook; the e2e helper's new excludeHookId handles the same race for the token-lookup-based poll. CI is green including the required E2E check.

Left two non-blocking inline notes on narrow crash/race windows in the world-local force-release path.

// The releaser is not coming. Release the stale claim and
// the hook entity it points at, mirroring the hook_disposed
// cleanup.
await deleteJSON(constraintPath);

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.

ai review (non-blocking): there's a narrow cross-process window where this force-release can lead to a transient token-uniqueness violation. Both in-flight releasers (hook_disposed handler and deleteAllHooksForRun) delete the claim file unconditionally after reading the hook entity. Sequence: releaser writes the dispose lock (or run goes terminal) and reads the hook entity, then stalls; this claimant observes the stale claim releasable 3×, deletes it, and writes its own fresh claim; the stalled releaser then executes its deleteJSON(constraintPath) — deleting the new claimant's live claim, so a third claimant can also claim the token. The 2×10ms grace period narrows this but doesn't close it. A cheap hardening would be for the releasers to re-read the claim and only delete it if it still points at their own (runId, hookId) (still TOCTOU, but the window shrinks from "stall of any length" to adjacent file ops), or to record the claim's identity here and have releasers skip deletion when the hook entity is already gone — which hook_disposed already partially does via the existingHook guard.

WorkflowRunSchema,
tag
);
if (!owningRun) {

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.

ai review (non-blocking): the missing-run semantics here disagree with the event-log rebuild. This treats a claim whose run file is absent as releasable, but findLiveHookCreatedEvent's isTerminalRunCache returns false for a missing run file, i.e. the rebuild treats that same hook as live and re-writes the claim. If a claim + surviving hook_created event ever exist without a run file (crash-lost run cache), the claim loop deletes the claim, the next iteration's rebuild resurrects it, and the loop ping-pongs to exhaustion — ending in the conflict branch with existingClaim = null, i.e. a durable hook_conflict with conflictingRunId: undefined, the exact symptom #2778 fixes. Unreachable today AFAICT (world-local never deletes run files), but worth aligning the two predicates — either both treat a missing run as terminal, or drop this branch.

@VaguelySerious
VaguelySerious merged commit 7637196 into mainJul 7, 2026
180 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/issue-2777 branch July 7, 2026 21:13
@VaguelySerious

Copy link
Copy Markdown
MemberAuthor

Addressing review comment in a follow-up PR

@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 7637196 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

7637196cf0f605ce62243bf8c7762a26153dcd36

@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026

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

Reviewed both halves in depth, ran the full affected suites plus the new regression tests against a live dev server. Approving.

Core (same-run, #2777): The per-token grouping is the right shape. I traced the ordering semantics:

  • Queue-insertion order within a token group = workflow code order, so dispose-of-earlier flushes before create-of-later is validated, while a created-and-disposed-in-one-suspension hook still creates first. Both directions are covered by the new unit tests.
  • The new event-log order (hook_disposed before the successor's hook_created) also matches replay consumption order — under the old code the log recorded them inverted relative to code order, so this is strictly better for replay, not just for conflict avoidance.
  • hasHookConflict / hasAwaitedHookCreation accumulation reaches the same set of creations as before, and hasHookEvents is computed from the same population — no semantic drift in the V2 re-invoke signaling.
  • The creationConflicted gate on dispose is correct belt-and-braces: a conflicted creation has nothing to dispose, and skipping avoids even the benign HookNotFoundError round-trip.
  • Cross-token parallelism is preserved, so unrelated hooks don't serialize.

world-local (cross-run, #2778): The claim loop's force-release is the risky part, so I audited its failure modes:

  • readJSON returns null only on ENOENT (anything else throws), so isHookTokenClaimReleasable can't misclassify a live claim due to a transient read error — a "missing" owning run is genuinely missing, and terminal status is monotonic.
  • Racing force-releasers and the in-flight disposer are all safe: deleteJSON is ENOENT-idempotent, and re-claim arbitration still lands on writeExclusive's EEXIST semantics, so exactly one claimant wins and the loser correctly conflicts against the winner's live claim.
  • The dispose lock as earliest-durable-disposal-marker works because the lock write already preceded the destructive deletes pre-PR — the change formalizes existing ordering rather than introducing new ordering requirements. The findLiveHookCreatedEvent lock check closing the rebuild-resurrection window is a nice catch.
  • I also confirmed the asymmetry with world-postgres is justified: there the hook row is the token claim and hook_disposed releases it via atomic DELETE ... RETURNING, so no separate-release window exists — the core fix alone covers it.

Validation: 1354/1354 core unit tests, 432/432 world-local (after full build — the 17 initial failures were unbuilt-SWC-plugin/e2e-env artifacts), 2/2 new workbench/vitest regression tests, and against a live turbopack dev server: the new hookTokenReuseLoopWorkflow e2e plus all 26 existing hook e2e tests pass. Workbench conventions followed (workflow added to workbench/example and propagated via symlink; the vitest workbench's local workflow matches its local-file convention). Changesets correctly split per package. CI: 105 pass, 1 fail = the known Benchmark Vercel (nextjs-turbopack) flake.

Two non-blocking notes:

  1. If the claim loop exhausts all 10 attempts via repeated vanish races (claim deleted between the exclusive-create attempt and every read), it falls through with existingClaim = null and still records a hook_conflict with no conflictingRunId — the #2778 symptom, now requiring ~10 consecutive lost races to reproduce. Practically unreachable, but a runtimeLogger.warn on loop exhaustion would make it diagnosable if it ever fires.
  2. The dispose locks in .locks/hooks/ are now load-bearing durable state (consulted by the claim path and the rebuild), not just transient mutexes. Worth a note somewhere discoverable that they must never be garbage-collected — a future ".locks cleanup" change would silently reintroduce the resurrection bug. The comment at hookDisposeLockPath covers the what; the hazard is for whoever writes a cleanup without reading it.

VaguelySerious added a commit that referenced this pull request Jul 7, 2026
…tall
Both in-flight releasers (the hook_disposed handler and the terminal-run
deleteAllHooksForRun cleanup) deleted the claim file unconditionally after
reading the hook entity. A releaser stalled between those operations could
outlive a claimant force-releasing its stale claim, and its deferred delete
would then destroy the new claimant's live claim — transiently breaking
token uniqueness (a third claimant could claim the token too).
Releasers now re-read the claim and delete it only if it still points at
their own (runId, hookId). Still TOCTOU, but the window shrinks from "a
stall of any length" to adjacent file ops; a claim owned by someone else
is left for the claimant-side force-release path to reap.
Addresses post-merge feedback on #2779.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@VaguelySerious@TooTallNate@karthikscale3
, '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('^' + ".*" + '
Skip to content

[core] Fix hook token reuse after dispose() (same-run and cross-run) - #2779

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777
Jul 7, 2026
Merged

[core] Fix hook token reuse after dispose() (same-run and cross-run)#2779
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 6, 2026

Copy link
Copy Markdown
Member

Closes#2777, closes#2778.

Problem

Reusing a hook token after hook.dispose() deterministically or intermittently failed with a spurious HookConflictError naming an already-disposed hook:

Fix

  • @workflow/core:handleSuspension now processes hook operations grouped per token, in workflow-code order: a dispose of an earlier hook flushes before a later same-token hook's creation is validated, while a hook created and disposed within one suspension is still created first. Different tokens keep processing in parallel, and hooks still process before steps/waits.
  • @workflow/world-local: the hook_created token claim runs in a short bounded loop — it retries when the observed claim vanished mid-check, and treats a claim as vacant when its hook's disposal is committed (dispose lock exists) or its owning run is terminal/missing, releasing the stale claim if the in-flight releaser never finishes. The event-log rebuild (rebuildLiveHookByTokenFromEventLog / hooks.get*) no longer considers a hook live once its dispose lock exists. Live claims still conflict exactly as before.

- core: process hook operations per token in workflow-code order during
suspension handling, so a dispose() of an earlier hook releases the
token before a later same-token hook's creation is validated. A hook
created and disposed within the same suspension is still created
before it is disposed. Fixes the same-run recreate self-conflict
(#2777).
- world-local: retry the token claim when the observed claim vanished
mid-check or is held by a hook whose disposal is committed / a run
that is terminal or missing; never rebuild a claim from the event log
for a hook whose dispose lock exists. Fixes spurious HookConflictError
against an already-disposed hook during fast run-to-run token handoff
(#2778).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 899a288

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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

@vercel

vercelBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 6, 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🥇 Nitro0.048s (-16.4% 🟢)1.006s (-1.3%)0.958s101.00x
💻 LocalExpress0.050s (+3.5%)1.006s (~)0.957s101.03x
💻 LocalNext.js (Turbopack)0.055s (-2.0%)1.005s (~)0.951s101.13x
🐘 PostgresNitro0.055s (-17.7% 🟢)1.012s (~)0.956s101.15x
🐘 PostgresNext.js (Turbopack)0.062s (-1.0%)1.012s (~)0.950s101.28x
🐘 PostgresExpress0.070s (+9.4% 🔺)1.012s (~)0.942s101.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.259s (+43.3% 🔺)2.147s (+31.9% 🔺)1.888s101.00x
▲ VercelExpress0.381s (+69.9% 🔺)2.003s (+12.3% 🔺)1.623s101.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.084s (~)2.007s (~)0.923s101.00x
💻 LocalNext.js (Turbopack)1.085s (-0.7%)2.006s (~)0.922s101.00x
💻 LocalExpress1.091s (+1.3%)2.007s (~)0.916s101.01x
🐘 PostgresNext.js (Turbopack)1.096s (-0.9%)2.010s (~)0.914s101.01x
🐘 PostgresExpress1.102s (+0.6%)2.009s (~)0.907s101.02x
🐘 PostgresNitro1.105s (+0.9%)2.011s (~)0.906s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.408s (-4.8%)2.719s (-18.6% 🟢)1.311s101.00x
▲ VercelNitro1.494s (+8.4% 🔺)3.455s (+4.5%)1.961s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.464s (~)11.023s (~)0.559s31.00x
🐘 PostgresExpress10.472s (~)11.014s (~)0.542s31.00x
🐘 PostgresNitro10.475s (~)11.019s (~)0.545s31.00x
💻 LocalNext.js (Turbopack)10.490s (~)11.022s (~)0.532s31.00x
🐘 PostgresNext.js (Turbopack)10.499s (~)11.014s (~)0.515s31.00x
💻 LocalExpress10.512s (+0.7%)11.023s (~)0.511s31.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.697s (+1.1%)13.812s (+8.1% 🔺)2.116s31.00x
▲ VercelExpress11.771s (+0.9%)13.221s (~)1.450s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)13.613s (-1.1%)14.028s (~)0.415s51.00x
🐘 PostgresExpress13.622s (~)14.020s (~)0.398s51.00x
🐘 PostgresNitro13.671s (~)14.022s (~)0.351s51.00x
💻 LocalNitro13.691s (~)14.026s (~)0.336s51.01x
💻 LocalExpress13.738s (+0.9%)14.029s (~)0.290s51.01x
🐘 PostgresNext.js (Turbopack)13.783s (+0.7%)14.018s (~)0.235s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.359s (-0.6%)18.421s (+2.4%)2.063s41.00x
▲ VercelExpress16.725s (~)18.362s (-0.7%)1.636s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.311s (+0.6%)13.019s (~)0.708s71.00x
💻 LocalNext.js (Turbopack)12.354s (-0.5%)13.026s (~)0.673s71.00x
🐘 PostgresNext.js (Turbopack)12.376s (~)13.016s (~)0.641s71.01x
💻 LocalExpress12.377s (+2.3%)13.027s (~)0.650s71.01x
🐘 PostgresNitro12.385s (~)13.019s (~)0.634s71.01x
💻 LocalNitro12.493s (+1.8%)13.025s (~)0.532s71.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro18.213s (-1.7%)20.197s (+0.7%)1.984s51.00x
▲ VercelExpress19.008s (+8.2% 🔺)20.547s (+9.1% 🔺)1.538s51.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.5%)2.009s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.199s (~)2.008s (~)0.810s151.02x
🐘 PostgresExpress1.221s (+1.1%)2.075s (+3.3%)0.853s151.04x
💻 LocalNext.js (Turbopack)1.383s (-3.5%)2.006s (~)0.623s151.18x
💻 LocalNitro1.398s (~)2.006s (~)0.608s151.19x
💻 LocalExpress1.469s (+6.5% 🔺)2.007s (~)0.538s151.25x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.157s (+2.1%)3.617s (+1.9%)1.460s91.00x
▲ VercelNitro2.165s (-3.6%)3.883s (+6.0% 🔺)1.718s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.315s (-4.1%)2.394s (-4.7%)1.079s131.00x
🐘 PostgresNitro1.319s (-2.5%)2.510s (+1.6%)1.191s121.00x
🐘 PostgresNext.js (Turbopack)1.347s (+1.7%)2.918s (-3.0%)1.571s111.02x
💻 LocalNitro2.390s (-1.0%)3.009s (~)0.619s101.82x
💻 LocalNext.js (Turbopack)2.493s (+4.3%)3.110s (+3.3%)0.617s101.90x
💻 LocalExpress2.505s (+1.1%)3.009s (-3.2%)0.504s101.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.122s (+28.6% 🔺)4.500s (+12.9% 🔺)1.378s71.00x
▲ VercelNitro3.207s (+32.4% 🔺)4.900s (+30.5% 🔺)1.694s71.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.563s (-2.7%)4.138s (-3.9%)2.575s81.00x
🐘 PostgresExpress1.616s (-4.4%)4.021s (-9.5% 🟢)2.405s81.03x
💻 LocalExpress2.767s (-21.7% 🟢)4.298s (-9.1% 🟢)1.531s71.77x
🐘 PostgresNext.js (Turbopack)2.988s (-6.2% 🟢)6.014s (+2.8%)3.026s51.91x
💻 LocalNitro4.589s (+19.5% 🔺)5.013s (+9.3% 🔺)0.424s62.94x
💻 LocalNext.js (Turbopack)4.690s (+24.0% 🔺)5.178s (+6.4% 🔺)0.488s63.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.705s (+19.6% 🔺)5.746s (+21.4% 🔺)2.041s61.00x
▲ VercelExpress4.016s (+34.9% 🔺)5.695s (+27.0% 🔺)1.678s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.8%)2.008s (~)0.838s151.00x
🐘 PostgresNext.js (Turbopack)1.188s (-5.2% 🟢)2.007s (-3.3%)0.819s151.01x
🐘 PostgresExpress1.189s (-3.4%)2.008s (-3.2%)0.819s151.02x
💻 LocalNitro1.441s (+0.6%)2.006s (~)0.565s151.23x
💻 LocalNext.js (Turbopack)1.463s (~)2.006s (~)0.543s151.25x
💻 LocalExpress1.499s (+4.8%)2.007s (~)0.508s151.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.014s (-18.8% 🟢)3.876s (+0.7%)1.861s81.00x
▲ VercelExpress2.130s (~)3.651s (+1.8%)1.521s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.317s (-3.6%)2.150s (-17.1% 🟢)0.834s141.00x
🐘 PostgresNitro1.329s (+0.9%)2.316s (-3.2%)0.987s131.01x
🐘 PostgresNext.js (Turbopack)1.435s (+7.2% 🔺)3.009s (~)1.574s101.09x
💻 LocalExpress2.563s (+4.4%)3.009s (~)0.446s101.95x
💻 LocalNitro2.579s (+4.0%)3.009s (~)0.430s101.96x
💻 LocalNext.js (Turbopack)2.611s (-1.0%)3.109s (+3.3%)0.498s101.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.413s (+2.6%)4.409s (+16.9% 🔺)1.996s71.00x
▲ VercelExpress2.441s (+8.0% 🔺)3.951s (+10.4% 🔺)1.511s81.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.525s (-5.3% 🟢)4.138s (~)2.613s81.00x
🐘 PostgresExpress1.624s (-2.0%)4.583s (+6.6% 🔺)2.959s71.06x
🐘 PostgresNext.js (Turbopack)2.970s (-9.9% 🟢)6.017s (-3.2%)3.048s61.95x
💻 LocalNitro4.840s (-14.1% 🟢)5.850s (-2.7%)1.010s63.17x
💻 LocalNext.js (Turbopack)5.395s (-3.7%)5.847s (-2.8%)0.452s63.54x
💻 LocalExpress5.662s (+3.0%)6.416s (+6.7% 🔺)0.754s53.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.748s (+2.8%)4.556s (+5.8% 🔺)1.808s71.00x
▲ VercelNitro4.034s (+40.6% 🔺)6.082s (+38.0% 🔺)2.048s61.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.525s (-12.6% 🟢)1.006s (-1.7%)0.482s601.00x
🐘 PostgresNitro0.557s (-4.7%)1.041s (~)0.484s581.06x
🐘 PostgresNext.js (Turbopack)0.607s (+7.8% 🔺)1.024s (+1.8%)0.417s591.16x
💻 LocalNitro0.609s (+1.5%)1.005s (~)0.397s601.16x
💻 LocalNext.js (Turbopack)0.619s (-1.9%)1.005s (~)0.386s601.18x
💻 LocalExpress0.645s (+13.6% 🔺)1.005s (~)0.360s601.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-1.6%)4.032s (-1.0%)1.496s151.00x
▲ VercelExpress2.578s (-0.7%)3.948s (+4.3%)1.370s161.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.253s (-11.5% 🟢)2.007s (-2.2%)0.754s451.00x
🐘 PostgresNitro1.296s (-4.3%)2.008s (~)0.712s451.03x
🐘 PostgresNext.js (Turbopack)1.382s (-4.0%)2.008s (-1.1%)0.625s451.10x
💻 LocalNitro1.544s (+1.4%)2.029s (+1.1%)0.485s451.23x
💻 LocalNext.js (Turbopack)1.613s (-2.6%)2.006s (-1.1%)0.393s451.29x
💻 LocalExpress1.632s (+10.2% 🔺)2.007s (-1.0%)0.375s451.30x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.805s (-3.4%)7.644s (+1.0%)1.839s121.00x
▲ VercelExpress6.114s (-1.3%)7.668s (+1.0%)1.554s121.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.595s (-2.5%)3.111s (+2.5%)0.516s391.00x
🐘 PostgresNitro2.618s (-3.2%)3.086s (~)0.468s391.01x
🐘 PostgresNext.js (Turbopack)2.725s (-1.7%)3.033s (+0.8%)0.309s401.05x
💻 LocalNext.js (Turbopack)3.398s (-5.3% 🟢)4.009s (-1.7%)0.611s301.31x
💻 LocalNitro3.416s (+5.2% 🔺)4.009s (~)0.593s301.32x
💻 LocalExpress3.557s (+12.3% 🔺)4.077s (+5.1% 🔺)0.520s301.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.756s (-6.3% 🟢)13.617s (-3.3%)1.861s91.00x
▲ VercelNitro12.119s (+1.1%)14.257s (+6.8% 🔺)2.138s91.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.189s (-9.8% 🟢)1.006s (-1.7%)0.817s601.00x
🐘 PostgresExpress0.208s (-2.5%)1.006s (~)0.798s601.10x
🐘 PostgresNitro0.210s (-5.1% 🟢)1.007s (~)0.796s601.11x
💻 LocalNitro0.532s (+6.7% 🔺)1.005s (~)0.473s602.81x
💻 LocalExpress0.562s (+15.0% 🔺)1.005s (~)0.443s602.97x
💻 LocalNext.js (Turbopack)0.605s (-7.2% 🟢)1.005s (-1.7%)0.399s603.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.091s (+3.3%)2.425s (+6.4% 🔺)1.334s261.00x
▲ VercelNitro1.108s (+12.7% 🔺)2.752s (+24.2% 🔺)1.643s221.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.326s (-2.7%)1.018s (+1.2%)0.693s891.00x
🐘 PostgresExpress0.340s (+4.9%)1.029s (+2.2%)0.689s881.04x
🐘 PostgresNext.js (Turbopack)0.352s (+18.6% 🔺)1.041s (+3.4%)0.689s871.08x
💻 LocalNitro2.465s (-3.3%)3.009s (~)0.545s307.56x
💻 LocalExpress2.503s (+1.2%)3.010s (~)0.507s307.68x
💻 LocalNext.js (Turbopack)2.675s (~)3.075s (+2.2%)0.400s308.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.430s (+7.8% 🔺)2.916s (+17.9% 🔺)1.486s311.00x
▲ VercelNitro1.491s (+7.7% 🔺)3.145s (+14.4% 🔺)1.654s291.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.516s (-3.2%)3.034s (+0.8%)2.518s401.00x
🐘 PostgresNitro0.540s (+6.8% 🔺)1.150s (+11.5% 🔺)0.610s1051.05x
🐘 PostgresExpress0.582s (+11.5% 🔺)1.160s (+7.7% 🔺)0.578s1041.13x
💻 LocalNitro5.602s (+4.4%)7.436s (-11.0% 🟢)1.834s1710.85x
💻 LocalExpress5.988s (+2.9%)8.956s (+0.8%)2.968s1411.60x
💻 LocalNext.js (Turbopack)6.274s (+3.8%)9.172s (+2.4%)2.898s1412.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.941s (+7.2% 🔺)3.965s (+16.7% 🔺)2.024s311.00x
▲ VercelExpress2.171s (+18.6% 🔺)3.898s (+6.5% 🔺)1.727s311.12x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.141s (-1.4%)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.868s101.00x
💻 LocalNext.js (Turbopack)1.151s (~)1.964s (~)0.011s (-19.2% 🟢)2.018s (~)0.867s101.01x
💻 LocalNitro1.161s (~)2.004s (~)0.012s (+3.3%)2.019s (~)0.858s101.02x
🐘 PostgresNext.js (Turbopack)1.165s (~)2.001s (~)0.001s (+8.3% 🔺)2.012s (~)0.847s101.02x
🐘 PostgresExpress1.169s (+0.6%)1.999s (~)0.001s (~)2.010s (~)0.841s101.02x
💻 LocalExpress1.173s (+1.8%)2.005s (~)0.013s (+23.1% 🔺)2.021s (~)0.848s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.962s (-7.4% 🟢)3.736s (+17.4% 🔺)2.143s (+15.8% 🔺)6.372s (+16.3% 🔺)4.410s101.00x
▲ VercelExpress2.103s (-3.2%)3.274s (-4.3%)1.979s (+5.8% 🔺)5.705s (~)3.602s101.07x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.554s (-2.4%)2.002s (~)0.005s (-2.5%)2.028s (~)0.474s301.00x
🐘 PostgresNitro1.585s (+1.1%)2.007s (~)0.005s (-5.4% 🟢)2.025s (~)0.440s301.02x
💻 LocalExpress1.592s (+0.6%)2.010s (~)0.012s (-7.6% 🟢)2.025s (~)0.433s301.02x
💻 LocalNext.js (Turbopack)1.605s (~)1.972s (~)0.014s (+0.7%)2.027s (~)0.422s301.03x
🐘 PostgresNext.js (Turbopack)1.657s (-2.0%)2.010s (-1.6%)0.005s (~)2.027s (-1.6%)0.370s301.07x
💻 LocalNitro1.907s (+21.0% 🔺)2.354s (+17.1% 🔺)0.012s (-4.3%)2.372s (+17.1% 🔺)0.464s261.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.530s (+1.6%)7.432s (+13.4% 🔺)0.328s (+71.3% 🔺)8.232s (+14.6% 🔺)2.702s81.00x
▲ VercelExpress5.684s (+5.8% 🔺)6.911s (+3.9%)0.345s (+53.1% 🔺)7.764s (+5.6% 🔺)2.080s81.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.784s (~)1.081s (+3.0%)0.000s (NaN%)1.099s (+1.7%)0.315s561.00x
🐘 PostgresNitro0.788s (+1.0%)1.032s (-3.1%)0.000s (-67.8% 🟢)1.042s (-4.2%)0.254s581.00x
🐘 PostgresNext.js (Turbopack)0.918s (-5.2% 🟢)1.333s (-7.4% 🟢)0.000s (-8.9% 🟢)1.340s (-8.5% 🟢)0.423s451.17x
💻 LocalNext.js (Turbopack)1.327s (-0.8%)1.978s (~)0.000s (-55.6% 🟢)2.017s (~)0.690s301.69x
💻 LocalNitro1.372s (~)1.919s (-3.1%)0.000s (+23.3% 🔺)1.922s (-3.1%)0.550s321.75x
💻 LocalExpress1.453s (+5.2% 🔺)1.982s (+1.6%)0.000s (+62.5% 🔺)1.985s (+1.6%)0.532s311.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.761s (+23.7% 🔺)5.227s (+25.2% 🔺)0.003s (+Infinity% 🔺)5.792s (+25.6% 🔺)2.032s111.00x
▲ VercelExpress3.837s (+32.6% 🔺)5.303s (+30.4% 🔺)0.000s (+154.5% 🔺)5.759s (+29.4% 🔺)1.923s111.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.640s (-10.8% 🟢)2.306s (-3.1%)0.000s (NaN%)2.317s (-3.4%)0.677s261.00x
🐘 PostgresExpress1.725s (-2.8%)2.301s (-4.0%)0.000s (-100.0% 🟢)2.318s (-3.8%)0.593s261.05x
🐘 PostgresNext.js (Turbopack)2.275s (-17.3% 🟢)2.859s (-14.3% 🟢)0.000s (-100.0% 🟢)2.868s (-14.3% 🟢)0.593s211.39x
💻 LocalNitro3.218s (+1.3%)3.775s (+2.7%)0.001s (+112.5% 🔺)3.779s (+2.7%)0.561s161.96x
💻 LocalExpress3.363s (-2.3%)3.902s (-3.1%)0.000s (-29.7% 🟢)3.905s (-3.1%)0.543s162.05x
💻 LocalNext.js (Turbopack)3.580s (+3.0%)3.992s (~)0.000s (-28.6% 🟢)4.032s (~)0.452s152.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.808s (+15.8% 🔺)5.847s (+10.0% 🔺)0.000s (+10.0% 🔺)6.351s (+10.9% 🔺)1.543s101.00x
▲ VercelExpress5.284s (+24.3% 🔺)6.342s (+20.6% 🔺)0.000s (NaN%)6.789s (+20.3% 🔺)1.505s101.10x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro12/21
🐘 PostgresExpress10/21
▲ VercelNitro14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres19/21
Next.js (Turbopack)🐘 Postgres14/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027

📋 View full workflow run

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 6, 2026 17:04
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 6, 2026 17:04

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

ai review: Approving — no blockers or regressions found.

What I checked:

  • @workflow/core suspension handler: verified the per-token grouping preserves queue-insertion (workflow-code) order, so dispose-before-recreate and create-before-dispose orderings both hold; cross-token parallelism and the hooks-before-steps/waits ordering are unchanged. hasHookEvents (hooksNeedingCreation.length > 0) is equivalent to the old hookEvents.length > 0. Skipping disposal for a conflicted creation is a behavior improvement (previously it attempted the dispose and swallowed HookNotFoundError). Abort processing (hooksNeedingAbort) is unaffected.
  • @workflow/world-local claim loop: the loop is bounded (10 attempts, ≤2×10ms sleeps), a genuinely live claim breaks out on the first iteration into the unchanged conflict path, and the own-claim dedup/recovery path is preserved (existingClaim carried out of the loop matches the old post-failure re-read). On loop exhaustion, behavior degrades to the pre-PR single-attempt semantics rather than something worse.
  • Rebuild/resurrection interaction: findLiveHookCreatedEvent already excludes terminal-run hooks (isTerminalRunCache) and now dispose-committed hooks, so a force-released claim is not resurrected by the event-log rebuild in the disposed-hook and terminal-run cases (matching the two new storage tests).
  • Dispose-lock keying: the lock path/naming is byte-identical to the old inline construction (backward compatible with locks already on disk), and hook correlation IDs are hook_${ulid} so keying by hookId alone is safe — same keying the dispose path already relied on.
  • Tests: the new @workflow/vitest workbench tests are race-free because waitForHook excludes hooks that already have a hook_received event, so each round can only observe the next round's hook; the e2e helper's new excludeHookId handles the same race for the token-lookup-based poll. CI is green including the required E2E check.

Left two non-blocking inline notes on narrow crash/race windows in the world-local force-release path.

// The releaser is not coming. Release the stale claim and
// the hook entity it points at, mirroring the hook_disposed
// cleanup.
await deleteJSON(constraintPath);

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.

ai review (non-blocking): there's a narrow cross-process window where this force-release can lead to a transient token-uniqueness violation. Both in-flight releasers (hook_disposed handler and deleteAllHooksForRun) delete the claim file unconditionally after reading the hook entity. Sequence: releaser writes the dispose lock (or run goes terminal) and reads the hook entity, then stalls; this claimant observes the stale claim releasable 3×, deletes it, and writes its own fresh claim; the stalled releaser then executes its deleteJSON(constraintPath) — deleting the new claimant's live claim, so a third claimant can also claim the token. The 2×10ms grace period narrows this but doesn't close it. A cheap hardening would be for the releasers to re-read the claim and only delete it if it still points at their own (runId, hookId) (still TOCTOU, but the window shrinks from "stall of any length" to adjacent file ops), or to record the claim's identity here and have releasers skip deletion when the hook entity is already gone — which hook_disposed already partially does via the existingHook guard.

WorkflowRunSchema,
tag
);
if (!owningRun) {

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.

ai review (non-blocking): the missing-run semantics here disagree with the event-log rebuild. This treats a claim whose run file is absent as releasable, but findLiveHookCreatedEvent's isTerminalRunCache returns false for a missing run file, i.e. the rebuild treats that same hook as live and re-writes the claim. If a claim + surviving hook_created event ever exist without a run file (crash-lost run cache), the claim loop deletes the claim, the next iteration's rebuild resurrects it, and the loop ping-pongs to exhaustion — ending in the conflict branch with existingClaim = null, i.e. a durable hook_conflict with conflictingRunId: undefined, the exact symptom #2778 fixes. Unreachable today AFAICT (world-local never deletes run files), but worth aligning the two predicates — either both treat a missing run as terminal, or drop this branch.

@VaguelySerious
VaguelySerious merged commit 7637196 into mainJul 7, 2026
180 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/issue-2777 branch July 7, 2026 21:13
@VaguelySerious

Copy link
Copy Markdown
MemberAuthor

Addressing review comment in a follow-up PR

@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 7637196 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

7637196cf0f605ce62243bf8c7762a26153dcd36

@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026

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

Reviewed both halves in depth, ran the full affected suites plus the new regression tests against a live dev server. Approving.

Core (same-run, #2777): The per-token grouping is the right shape. I traced the ordering semantics:

  • Queue-insertion order within a token group = workflow code order, so dispose-of-earlier flushes before create-of-later is validated, while a created-and-disposed-in-one-suspension hook still creates first. Both directions are covered by the new unit tests.
  • The new event-log order (hook_disposed before the successor's hook_created) also matches replay consumption order — under the old code the log recorded them inverted relative to code order, so this is strictly better for replay, not just for conflict avoidance.
  • hasHookConflict / hasAwaitedHookCreation accumulation reaches the same set of creations as before, and hasHookEvents is computed from the same population — no semantic drift in the V2 re-invoke signaling.
  • The creationConflicted gate on dispose is correct belt-and-braces: a conflicted creation has nothing to dispose, and skipping avoids even the benign HookNotFoundError round-trip.
  • Cross-token parallelism is preserved, so unrelated hooks don't serialize.

world-local (cross-run, #2778): The claim loop's force-release is the risky part, so I audited its failure modes:

  • readJSON returns null only on ENOENT (anything else throws), so isHookTokenClaimReleasable can't misclassify a live claim due to a transient read error — a "missing" owning run is genuinely missing, and terminal status is monotonic.
  • Racing force-releasers and the in-flight disposer are all safe: deleteJSON is ENOENT-idempotent, and re-claim arbitration still lands on writeExclusive's EEXIST semantics, so exactly one claimant wins and the loser correctly conflicts against the winner's live claim.
  • The dispose lock as earliest-durable-disposal-marker works because the lock write already preceded the destructive deletes pre-PR — the change formalizes existing ordering rather than introducing new ordering requirements. The findLiveHookCreatedEvent lock check closing the rebuild-resurrection window is a nice catch.
  • I also confirmed the asymmetry with world-postgres is justified: there the hook row is the token claim and hook_disposed releases it via atomic DELETE ... RETURNING, so no separate-release window exists — the core fix alone covers it.

Validation: 1354/1354 core unit tests, 432/432 world-local (after full build — the 17 initial failures were unbuilt-SWC-plugin/e2e-env artifacts), 2/2 new workbench/vitest regression tests, and against a live turbopack dev server: the new hookTokenReuseLoopWorkflow e2e plus all 26 existing hook e2e tests pass. Workbench conventions followed (workflow added to workbench/example and propagated via symlink; the vitest workbench's local workflow matches its local-file convention). Changesets correctly split per package. CI: 105 pass, 1 fail = the known Benchmark Vercel (nextjs-turbopack) flake.

Two non-blocking notes:

  1. If the claim loop exhausts all 10 attempts via repeated vanish races (claim deleted between the exclusive-create attempt and every read), it falls through with existingClaim = null and still records a hook_conflict with no conflictingRunId — the #2778 symptom, now requiring ~10 consecutive lost races to reproduce. Practically unreachable, but a runtimeLogger.warn on loop exhaustion would make it diagnosable if it ever fires.
  2. The dispose locks in .locks/hooks/ are now load-bearing durable state (consulted by the claim path and the rebuild), not just transient mutexes. Worth a note somewhere discoverable that they must never be garbage-collected — a future ".locks cleanup" change would silently reintroduce the resurrection bug. The comment at hookDisposeLockPath covers the what; the hazard is for whoever writes a cleanup without reading it.

VaguelySerious added a commit that referenced this pull request Jul 7, 2026
…tall
Both in-flight releasers (the hook_disposed handler and the terminal-run
deleteAllHooksForRun cleanup) deleted the claim file unconditionally after
reading the hook entity. A releaser stalled between those operations could
outlive a claimant force-releasing its stale claim, and its deferred delete
would then destroy the new claimant's live claim — transiently breaking
token uniqueness (a third claimant could claim the token too).
Releasers now re-read the claim and delete it only if it still points at
their own (runId, hookId). Still TOCTOU, but the window shrinks from "a
stall of any length" to adjacent file ops; a claim owned by someone else
is left for the claimant-side force-release path to reap.
Addresses post-merge feedback on #2779.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@VaguelySerious@TooTallNate@karthikscale3
, '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" + '
Skip to content

[core] Fix hook token reuse after dispose() (same-run and cross-run) - #2779

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777
Jul 7, 2026
Merged

[core] Fix hook token reuse after dispose() (same-run and cross-run)#2779
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 6, 2026

Copy link
Copy Markdown
Member

Closes#2777, closes#2778.

Problem

Reusing a hook token after hook.dispose() deterministically or intermittently failed with a spurious HookConflictError naming an already-disposed hook:

Fix

  • @workflow/core:handleSuspension now processes hook operations grouped per token, in workflow-code order: a dispose of an earlier hook flushes before a later same-token hook's creation is validated, while a hook created and disposed within one suspension is still created first. Different tokens keep processing in parallel, and hooks still process before steps/waits.
  • @workflow/world-local: the hook_created token claim runs in a short bounded loop — it retries when the observed claim vanished mid-check, and treats a claim as vacant when its hook's disposal is committed (dispose lock exists) or its owning run is terminal/missing, releasing the stale claim if the in-flight releaser never finishes. The event-log rebuild (rebuildLiveHookByTokenFromEventLog / hooks.get*) no longer considers a hook live once its dispose lock exists. Live claims still conflict exactly as before.

- core: process hook operations per token in workflow-code order during
suspension handling, so a dispose() of an earlier hook releases the
token before a later same-token hook's creation is validated. A hook
created and disposed within the same suspension is still created
before it is disposed. Fixes the same-run recreate self-conflict
(#2777).
- world-local: retry the token claim when the observed claim vanished
mid-check or is held by a hook whose disposal is committed / a run
that is terminal or missing; never rebuild a claim from the event log
for a hook whose dispose lock exists. Fixes spurious HookConflictError
against an already-disposed hook during fast run-to-run token handoff
(#2778).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 899a288

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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

@vercel

vercelBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 6, 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🥇 Nitro0.048s (-16.4% 🟢)1.006s (-1.3%)0.958s101.00x
💻 LocalExpress0.050s (+3.5%)1.006s (~)0.957s101.03x
💻 LocalNext.js (Turbopack)0.055s (-2.0%)1.005s (~)0.951s101.13x
🐘 PostgresNitro0.055s (-17.7% 🟢)1.012s (~)0.956s101.15x
🐘 PostgresNext.js (Turbopack)0.062s (-1.0%)1.012s (~)0.950s101.28x
🐘 PostgresExpress0.070s (+9.4% 🔺)1.012s (~)0.942s101.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.259s (+43.3% 🔺)2.147s (+31.9% 🔺)1.888s101.00x
▲ VercelExpress0.381s (+69.9% 🔺)2.003s (+12.3% 🔺)1.623s101.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.084s (~)2.007s (~)0.923s101.00x
💻 LocalNext.js (Turbopack)1.085s (-0.7%)2.006s (~)0.922s101.00x
💻 LocalExpress1.091s (+1.3%)2.007s (~)0.916s101.01x
🐘 PostgresNext.js (Turbopack)1.096s (-0.9%)2.010s (~)0.914s101.01x
🐘 PostgresExpress1.102s (+0.6%)2.009s (~)0.907s101.02x
🐘 PostgresNitro1.105s (+0.9%)2.011s (~)0.906s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.408s (-4.8%)2.719s (-18.6% 🟢)1.311s101.00x
▲ VercelNitro1.494s (+8.4% 🔺)3.455s (+4.5%)1.961s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.464s (~)11.023s (~)0.559s31.00x
🐘 PostgresExpress10.472s (~)11.014s (~)0.542s31.00x
🐘 PostgresNitro10.475s (~)11.019s (~)0.545s31.00x
💻 LocalNext.js (Turbopack)10.490s (~)11.022s (~)0.532s31.00x
🐘 PostgresNext.js (Turbopack)10.499s (~)11.014s (~)0.515s31.00x
💻 LocalExpress10.512s (+0.7%)11.023s (~)0.511s31.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.697s (+1.1%)13.812s (+8.1% 🔺)2.116s31.00x
▲ VercelExpress11.771s (+0.9%)13.221s (~)1.450s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)13.613s (-1.1%)14.028s (~)0.415s51.00x
🐘 PostgresExpress13.622s (~)14.020s (~)0.398s51.00x
🐘 PostgresNitro13.671s (~)14.022s (~)0.351s51.00x
💻 LocalNitro13.691s (~)14.026s (~)0.336s51.01x
💻 LocalExpress13.738s (+0.9%)14.029s (~)0.290s51.01x
🐘 PostgresNext.js (Turbopack)13.783s (+0.7%)14.018s (~)0.235s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.359s (-0.6%)18.421s (+2.4%)2.063s41.00x
▲ VercelExpress16.725s (~)18.362s (-0.7%)1.636s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.311s (+0.6%)13.019s (~)0.708s71.00x
💻 LocalNext.js (Turbopack)12.354s (-0.5%)13.026s (~)0.673s71.00x
🐘 PostgresNext.js (Turbopack)12.376s (~)13.016s (~)0.641s71.01x
💻 LocalExpress12.377s (+2.3%)13.027s (~)0.650s71.01x
🐘 PostgresNitro12.385s (~)13.019s (~)0.634s71.01x
💻 LocalNitro12.493s (+1.8%)13.025s (~)0.532s71.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro18.213s (-1.7%)20.197s (+0.7%)1.984s51.00x
▲ VercelExpress19.008s (+8.2% 🔺)20.547s (+9.1% 🔺)1.538s51.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.5%)2.009s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.199s (~)2.008s (~)0.810s151.02x
🐘 PostgresExpress1.221s (+1.1%)2.075s (+3.3%)0.853s151.04x
💻 LocalNext.js (Turbopack)1.383s (-3.5%)2.006s (~)0.623s151.18x
💻 LocalNitro1.398s (~)2.006s (~)0.608s151.19x
💻 LocalExpress1.469s (+6.5% 🔺)2.007s (~)0.538s151.25x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.157s (+2.1%)3.617s (+1.9%)1.460s91.00x
▲ VercelNitro2.165s (-3.6%)3.883s (+6.0% 🔺)1.718s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.315s (-4.1%)2.394s (-4.7%)1.079s131.00x
🐘 PostgresNitro1.319s (-2.5%)2.510s (+1.6%)1.191s121.00x
🐘 PostgresNext.js (Turbopack)1.347s (+1.7%)2.918s (-3.0%)1.571s111.02x
💻 LocalNitro2.390s (-1.0%)3.009s (~)0.619s101.82x
💻 LocalNext.js (Turbopack)2.493s (+4.3%)3.110s (+3.3%)0.617s101.90x
💻 LocalExpress2.505s (+1.1%)3.009s (-3.2%)0.504s101.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.122s (+28.6% 🔺)4.500s (+12.9% 🔺)1.378s71.00x
▲ VercelNitro3.207s (+32.4% 🔺)4.900s (+30.5% 🔺)1.694s71.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.563s (-2.7%)4.138s (-3.9%)2.575s81.00x
🐘 PostgresExpress1.616s (-4.4%)4.021s (-9.5% 🟢)2.405s81.03x
💻 LocalExpress2.767s (-21.7% 🟢)4.298s (-9.1% 🟢)1.531s71.77x
🐘 PostgresNext.js (Turbopack)2.988s (-6.2% 🟢)6.014s (+2.8%)3.026s51.91x
💻 LocalNitro4.589s (+19.5% 🔺)5.013s (+9.3% 🔺)0.424s62.94x
💻 LocalNext.js (Turbopack)4.690s (+24.0% 🔺)5.178s (+6.4% 🔺)0.488s63.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.705s (+19.6% 🔺)5.746s (+21.4% 🔺)2.041s61.00x
▲ VercelExpress4.016s (+34.9% 🔺)5.695s (+27.0% 🔺)1.678s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.8%)2.008s (~)0.838s151.00x
🐘 PostgresNext.js (Turbopack)1.188s (-5.2% 🟢)2.007s (-3.3%)0.819s151.01x
🐘 PostgresExpress1.189s (-3.4%)2.008s (-3.2%)0.819s151.02x
💻 LocalNitro1.441s (+0.6%)2.006s (~)0.565s151.23x
💻 LocalNext.js (Turbopack)1.463s (~)2.006s (~)0.543s151.25x
💻 LocalExpress1.499s (+4.8%)2.007s (~)0.508s151.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.014s (-18.8% 🟢)3.876s (+0.7%)1.861s81.00x
▲ VercelExpress2.130s (~)3.651s (+1.8%)1.521s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.317s (-3.6%)2.150s (-17.1% 🟢)0.834s141.00x
🐘 PostgresNitro1.329s (+0.9%)2.316s (-3.2%)0.987s131.01x
🐘 PostgresNext.js (Turbopack)1.435s (+7.2% 🔺)3.009s (~)1.574s101.09x
💻 LocalExpress2.563s (+4.4%)3.009s (~)0.446s101.95x
💻 LocalNitro2.579s (+4.0%)3.009s (~)0.430s101.96x
💻 LocalNext.js (Turbopack)2.611s (-1.0%)3.109s (+3.3%)0.498s101.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.413s (+2.6%)4.409s (+16.9% 🔺)1.996s71.00x
▲ VercelExpress2.441s (+8.0% 🔺)3.951s (+10.4% 🔺)1.511s81.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.525s (-5.3% 🟢)4.138s (~)2.613s81.00x
🐘 PostgresExpress1.624s (-2.0%)4.583s (+6.6% 🔺)2.959s71.06x
🐘 PostgresNext.js (Turbopack)2.970s (-9.9% 🟢)6.017s (-3.2%)3.048s61.95x
💻 LocalNitro4.840s (-14.1% 🟢)5.850s (-2.7%)1.010s63.17x
💻 LocalNext.js (Turbopack)5.395s (-3.7%)5.847s (-2.8%)0.452s63.54x
💻 LocalExpress5.662s (+3.0%)6.416s (+6.7% 🔺)0.754s53.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.748s (+2.8%)4.556s (+5.8% 🔺)1.808s71.00x
▲ VercelNitro4.034s (+40.6% 🔺)6.082s (+38.0% 🔺)2.048s61.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.525s (-12.6% 🟢)1.006s (-1.7%)0.482s601.00x
🐘 PostgresNitro0.557s (-4.7%)1.041s (~)0.484s581.06x
🐘 PostgresNext.js (Turbopack)0.607s (+7.8% 🔺)1.024s (+1.8%)0.417s591.16x
💻 LocalNitro0.609s (+1.5%)1.005s (~)0.397s601.16x
💻 LocalNext.js (Turbopack)0.619s (-1.9%)1.005s (~)0.386s601.18x
💻 LocalExpress0.645s (+13.6% 🔺)1.005s (~)0.360s601.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-1.6%)4.032s (-1.0%)1.496s151.00x
▲ VercelExpress2.578s (-0.7%)3.948s (+4.3%)1.370s161.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.253s (-11.5% 🟢)2.007s (-2.2%)0.754s451.00x
🐘 PostgresNitro1.296s (-4.3%)2.008s (~)0.712s451.03x
🐘 PostgresNext.js (Turbopack)1.382s (-4.0%)2.008s (-1.1%)0.625s451.10x
💻 LocalNitro1.544s (+1.4%)2.029s (+1.1%)0.485s451.23x
💻 LocalNext.js (Turbopack)1.613s (-2.6%)2.006s (-1.1%)0.393s451.29x
💻 LocalExpress1.632s (+10.2% 🔺)2.007s (-1.0%)0.375s451.30x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.805s (-3.4%)7.644s (+1.0%)1.839s121.00x
▲ VercelExpress6.114s (-1.3%)7.668s (+1.0%)1.554s121.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.595s (-2.5%)3.111s (+2.5%)0.516s391.00x
🐘 PostgresNitro2.618s (-3.2%)3.086s (~)0.468s391.01x
🐘 PostgresNext.js (Turbopack)2.725s (-1.7%)3.033s (+0.8%)0.309s401.05x
💻 LocalNext.js (Turbopack)3.398s (-5.3% 🟢)4.009s (-1.7%)0.611s301.31x
💻 LocalNitro3.416s (+5.2% 🔺)4.009s (~)0.593s301.32x
💻 LocalExpress3.557s (+12.3% 🔺)4.077s (+5.1% 🔺)0.520s301.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.756s (-6.3% 🟢)13.617s (-3.3%)1.861s91.00x
▲ VercelNitro12.119s (+1.1%)14.257s (+6.8% 🔺)2.138s91.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.189s (-9.8% 🟢)1.006s (-1.7%)0.817s601.00x
🐘 PostgresExpress0.208s (-2.5%)1.006s (~)0.798s601.10x
🐘 PostgresNitro0.210s (-5.1% 🟢)1.007s (~)0.796s601.11x
💻 LocalNitro0.532s (+6.7% 🔺)1.005s (~)0.473s602.81x
💻 LocalExpress0.562s (+15.0% 🔺)1.005s (~)0.443s602.97x
💻 LocalNext.js (Turbopack)0.605s (-7.2% 🟢)1.005s (-1.7%)0.399s603.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.091s (+3.3%)2.425s (+6.4% 🔺)1.334s261.00x
▲ VercelNitro1.108s (+12.7% 🔺)2.752s (+24.2% 🔺)1.643s221.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.326s (-2.7%)1.018s (+1.2%)0.693s891.00x
🐘 PostgresExpress0.340s (+4.9%)1.029s (+2.2%)0.689s881.04x
🐘 PostgresNext.js (Turbopack)0.352s (+18.6% 🔺)1.041s (+3.4%)0.689s871.08x
💻 LocalNitro2.465s (-3.3%)3.009s (~)0.545s307.56x
💻 LocalExpress2.503s (+1.2%)3.010s (~)0.507s307.68x
💻 LocalNext.js (Turbopack)2.675s (~)3.075s (+2.2%)0.400s308.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.430s (+7.8% 🔺)2.916s (+17.9% 🔺)1.486s311.00x
▲ VercelNitro1.491s (+7.7% 🔺)3.145s (+14.4% 🔺)1.654s291.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.516s (-3.2%)3.034s (+0.8%)2.518s401.00x
🐘 PostgresNitro0.540s (+6.8% 🔺)1.150s (+11.5% 🔺)0.610s1051.05x
🐘 PostgresExpress0.582s (+11.5% 🔺)1.160s (+7.7% 🔺)0.578s1041.13x
💻 LocalNitro5.602s (+4.4%)7.436s (-11.0% 🟢)1.834s1710.85x
💻 LocalExpress5.988s (+2.9%)8.956s (+0.8%)2.968s1411.60x
💻 LocalNext.js (Turbopack)6.274s (+3.8%)9.172s (+2.4%)2.898s1412.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.941s (+7.2% 🔺)3.965s (+16.7% 🔺)2.024s311.00x
▲ VercelExpress2.171s (+18.6% 🔺)3.898s (+6.5% 🔺)1.727s311.12x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.141s (-1.4%)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.868s101.00x
💻 LocalNext.js (Turbopack)1.151s (~)1.964s (~)0.011s (-19.2% 🟢)2.018s (~)0.867s101.01x
💻 LocalNitro1.161s (~)2.004s (~)0.012s (+3.3%)2.019s (~)0.858s101.02x
🐘 PostgresNext.js (Turbopack)1.165s (~)2.001s (~)0.001s (+8.3% 🔺)2.012s (~)0.847s101.02x
🐘 PostgresExpress1.169s (+0.6%)1.999s (~)0.001s (~)2.010s (~)0.841s101.02x
💻 LocalExpress1.173s (+1.8%)2.005s (~)0.013s (+23.1% 🔺)2.021s (~)0.848s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.962s (-7.4% 🟢)3.736s (+17.4% 🔺)2.143s (+15.8% 🔺)6.372s (+16.3% 🔺)4.410s101.00x
▲ VercelExpress2.103s (-3.2%)3.274s (-4.3%)1.979s (+5.8% 🔺)5.705s (~)3.602s101.07x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.554s (-2.4%)2.002s (~)0.005s (-2.5%)2.028s (~)0.474s301.00x
🐘 PostgresNitro1.585s (+1.1%)2.007s (~)0.005s (-5.4% 🟢)2.025s (~)0.440s301.02x
💻 LocalExpress1.592s (+0.6%)2.010s (~)0.012s (-7.6% 🟢)2.025s (~)0.433s301.02x
💻 LocalNext.js (Turbopack)1.605s (~)1.972s (~)0.014s (+0.7%)2.027s (~)0.422s301.03x
🐘 PostgresNext.js (Turbopack)1.657s (-2.0%)2.010s (-1.6%)0.005s (~)2.027s (-1.6%)0.370s301.07x
💻 LocalNitro1.907s (+21.0% 🔺)2.354s (+17.1% 🔺)0.012s (-4.3%)2.372s (+17.1% 🔺)0.464s261.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.530s (+1.6%)7.432s (+13.4% 🔺)0.328s (+71.3% 🔺)8.232s (+14.6% 🔺)2.702s81.00x
▲ VercelExpress5.684s (+5.8% 🔺)6.911s (+3.9%)0.345s (+53.1% 🔺)7.764s (+5.6% 🔺)2.080s81.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.784s (~)1.081s (+3.0%)0.000s (NaN%)1.099s (+1.7%)0.315s561.00x
🐘 PostgresNitro0.788s (+1.0%)1.032s (-3.1%)0.000s (-67.8% 🟢)1.042s (-4.2%)0.254s581.00x
🐘 PostgresNext.js (Turbopack)0.918s (-5.2% 🟢)1.333s (-7.4% 🟢)0.000s (-8.9% 🟢)1.340s (-8.5% 🟢)0.423s451.17x
💻 LocalNext.js (Turbopack)1.327s (-0.8%)1.978s (~)0.000s (-55.6% 🟢)2.017s (~)0.690s301.69x
💻 LocalNitro1.372s (~)1.919s (-3.1%)0.000s (+23.3% 🔺)1.922s (-3.1%)0.550s321.75x
💻 LocalExpress1.453s (+5.2% 🔺)1.982s (+1.6%)0.000s (+62.5% 🔺)1.985s (+1.6%)0.532s311.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.761s (+23.7% 🔺)5.227s (+25.2% 🔺)0.003s (+Infinity% 🔺)5.792s (+25.6% 🔺)2.032s111.00x
▲ VercelExpress3.837s (+32.6% 🔺)5.303s (+30.4% 🔺)0.000s (+154.5% 🔺)5.759s (+29.4% 🔺)1.923s111.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.640s (-10.8% 🟢)2.306s (-3.1%)0.000s (NaN%)2.317s (-3.4%)0.677s261.00x
🐘 PostgresExpress1.725s (-2.8%)2.301s (-4.0%)0.000s (-100.0% 🟢)2.318s (-3.8%)0.593s261.05x
🐘 PostgresNext.js (Turbopack)2.275s (-17.3% 🟢)2.859s (-14.3% 🟢)0.000s (-100.0% 🟢)2.868s (-14.3% 🟢)0.593s211.39x
💻 LocalNitro3.218s (+1.3%)3.775s (+2.7%)0.001s (+112.5% 🔺)3.779s (+2.7%)0.561s161.96x
💻 LocalExpress3.363s (-2.3%)3.902s (-3.1%)0.000s (-29.7% 🟢)3.905s (-3.1%)0.543s162.05x
💻 LocalNext.js (Turbopack)3.580s (+3.0%)3.992s (~)0.000s (-28.6% 🟢)4.032s (~)0.452s152.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.808s (+15.8% 🔺)5.847s (+10.0% 🔺)0.000s (+10.0% 🔺)6.351s (+10.9% 🔺)1.543s101.00x
▲ VercelExpress5.284s (+24.3% 🔺)6.342s (+20.6% 🔺)0.000s (NaN%)6.789s (+20.3% 🔺)1.505s101.10x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro12/21
🐘 PostgresExpress10/21
▲ VercelNitro14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres19/21
Next.js (Turbopack)🐘 Postgres14/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027

📋 View full workflow run

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 6, 2026 17:04
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 6, 2026 17:04

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

ai review: Approving — no blockers or regressions found.

What I checked:

  • @workflow/core suspension handler: verified the per-token grouping preserves queue-insertion (workflow-code) order, so dispose-before-recreate and create-before-dispose orderings both hold; cross-token parallelism and the hooks-before-steps/waits ordering are unchanged. hasHookEvents (hooksNeedingCreation.length > 0) is equivalent to the old hookEvents.length > 0. Skipping disposal for a conflicted creation is a behavior improvement (previously it attempted the dispose and swallowed HookNotFoundError). Abort processing (hooksNeedingAbort) is unaffected.
  • @workflow/world-local claim loop: the loop is bounded (10 attempts, ≤2×10ms sleeps), a genuinely live claim breaks out on the first iteration into the unchanged conflict path, and the own-claim dedup/recovery path is preserved (existingClaim carried out of the loop matches the old post-failure re-read). On loop exhaustion, behavior degrades to the pre-PR single-attempt semantics rather than something worse.
  • Rebuild/resurrection interaction: findLiveHookCreatedEvent already excludes terminal-run hooks (isTerminalRunCache) and now dispose-committed hooks, so a force-released claim is not resurrected by the event-log rebuild in the disposed-hook and terminal-run cases (matching the two new storage tests).
  • Dispose-lock keying: the lock path/naming is byte-identical to the old inline construction (backward compatible with locks already on disk), and hook correlation IDs are hook_${ulid} so keying by hookId alone is safe — same keying the dispose path already relied on.
  • Tests: the new @workflow/vitest workbench tests are race-free because waitForHook excludes hooks that already have a hook_received event, so each round can only observe the next round's hook; the e2e helper's new excludeHookId handles the same race for the token-lookup-based poll. CI is green including the required E2E check.

Left two non-blocking inline notes on narrow crash/race windows in the world-local force-release path.

// The releaser is not coming. Release the stale claim and
// the hook entity it points at, mirroring the hook_disposed
// cleanup.
await deleteJSON(constraintPath);

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.

ai review (non-blocking): there's a narrow cross-process window where this force-release can lead to a transient token-uniqueness violation. Both in-flight releasers (hook_disposed handler and deleteAllHooksForRun) delete the claim file unconditionally after reading the hook entity. Sequence: releaser writes the dispose lock (or run goes terminal) and reads the hook entity, then stalls; this claimant observes the stale claim releasable 3×, deletes it, and writes its own fresh claim; the stalled releaser then executes its deleteJSON(constraintPath) — deleting the new claimant's live claim, so a third claimant can also claim the token. The 2×10ms grace period narrows this but doesn't close it. A cheap hardening would be for the releasers to re-read the claim and only delete it if it still points at their own (runId, hookId) (still TOCTOU, but the window shrinks from "stall of any length" to adjacent file ops), or to record the claim's identity here and have releasers skip deletion when the hook entity is already gone — which hook_disposed already partially does via the existingHook guard.

WorkflowRunSchema,
tag
);
if (!owningRun) {

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.

ai review (non-blocking): the missing-run semantics here disagree with the event-log rebuild. This treats a claim whose run file is absent as releasable, but findLiveHookCreatedEvent's isTerminalRunCache returns false for a missing run file, i.e. the rebuild treats that same hook as live and re-writes the claim. If a claim + surviving hook_created event ever exist without a run file (crash-lost run cache), the claim loop deletes the claim, the next iteration's rebuild resurrects it, and the loop ping-pongs to exhaustion — ending in the conflict branch with existingClaim = null, i.e. a durable hook_conflict with conflictingRunId: undefined, the exact symptom #2778 fixes. Unreachable today AFAICT (world-local never deletes run files), but worth aligning the two predicates — either both treat a missing run as terminal, or drop this branch.

@VaguelySerious
VaguelySerious merged commit 7637196 into mainJul 7, 2026
180 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/issue-2777 branch July 7, 2026 21:13
@VaguelySerious

Copy link
Copy Markdown
MemberAuthor

Addressing review comment in a follow-up PR

@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 7637196 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

7637196cf0f605ce62243bf8c7762a26153dcd36

@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026

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

Reviewed both halves in depth, ran the full affected suites plus the new regression tests against a live dev server. Approving.

Core (same-run, #2777): The per-token grouping is the right shape. I traced the ordering semantics:

  • Queue-insertion order within a token group = workflow code order, so dispose-of-earlier flushes before create-of-later is validated, while a created-and-disposed-in-one-suspension hook still creates first. Both directions are covered by the new unit tests.
  • The new event-log order (hook_disposed before the successor's hook_created) also matches replay consumption order — under the old code the log recorded them inverted relative to code order, so this is strictly better for replay, not just for conflict avoidance.
  • hasHookConflict / hasAwaitedHookCreation accumulation reaches the same set of creations as before, and hasHookEvents is computed from the same population — no semantic drift in the V2 re-invoke signaling.
  • The creationConflicted gate on dispose is correct belt-and-braces: a conflicted creation has nothing to dispose, and skipping avoids even the benign HookNotFoundError round-trip.
  • Cross-token parallelism is preserved, so unrelated hooks don't serialize.

world-local (cross-run, #2778): The claim loop's force-release is the risky part, so I audited its failure modes:

  • readJSON returns null only on ENOENT (anything else throws), so isHookTokenClaimReleasable can't misclassify a live claim due to a transient read error — a "missing" owning run is genuinely missing, and terminal status is monotonic.
  • Racing force-releasers and the in-flight disposer are all safe: deleteJSON is ENOENT-idempotent, and re-claim arbitration still lands on writeExclusive's EEXIST semantics, so exactly one claimant wins and the loser correctly conflicts against the winner's live claim.
  • The dispose lock as earliest-durable-disposal-marker works because the lock write already preceded the destructive deletes pre-PR — the change formalizes existing ordering rather than introducing new ordering requirements. The findLiveHookCreatedEvent lock check closing the rebuild-resurrection window is a nice catch.
  • I also confirmed the asymmetry with world-postgres is justified: there the hook row is the token claim and hook_disposed releases it via atomic DELETE ... RETURNING, so no separate-release window exists — the core fix alone covers it.

Validation: 1354/1354 core unit tests, 432/432 world-local (after full build — the 17 initial failures were unbuilt-SWC-plugin/e2e-env artifacts), 2/2 new workbench/vitest regression tests, and against a live turbopack dev server: the new hookTokenReuseLoopWorkflow e2e plus all 26 existing hook e2e tests pass. Workbench conventions followed (workflow added to workbench/example and propagated via symlink; the vitest workbench's local workflow matches its local-file convention). Changesets correctly split per package. CI: 105 pass, 1 fail = the known Benchmark Vercel (nextjs-turbopack) flake.

Two non-blocking notes:

  1. If the claim loop exhausts all 10 attempts via repeated vanish races (claim deleted between the exclusive-create attempt and every read), it falls through with existingClaim = null and still records a hook_conflict with no conflictingRunId — the #2778 symptom, now requiring ~10 consecutive lost races to reproduce. Practically unreachable, but a runtimeLogger.warn on loop exhaustion would make it diagnosable if it ever fires.
  2. The dispose locks in .locks/hooks/ are now load-bearing durable state (consulted by the claim path and the rebuild), not just transient mutexes. Worth a note somewhere discoverable that they must never be garbage-collected — a future ".locks cleanup" change would silently reintroduce the resurrection bug. The comment at hookDisposeLockPath covers the what; the hazard is for whoever writes a cleanup without reading it.

VaguelySerious added a commit that referenced this pull request Jul 7, 2026
…tall
Both in-flight releasers (the hook_disposed handler and the terminal-run
deleteAllHooksForRun cleanup) deleted the claim file unconditionally after
reading the hook entity. A releaser stalled between those operations could
outlive a claimant force-releasing its stale claim, and its deferred delete
would then destroy the new claimant's live claim — transiently breaking
token uniqueness (a third claimant could claim the token too).
Releasers now re-read the claim and delete it only if it still points at
their own (runId, hookId). Still TOCTOU, but the window shrinks from "a
stall of any length" to adjacent file ops; a claim owned by someone else
is left for the claimant-side force-release path to reap.
Addresses post-merge feedback on #2779.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@VaguelySerious@TooTallNate@karthikscale3
, '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('^' + ".*" + '
Skip to content

[core] Fix hook token reuse after dispose() (same-run and cross-run) - #2779

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777
Jul 7, 2026
Merged

[core] Fix hook token reuse after dispose() (same-run and cross-run)#2779
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 6, 2026

Copy link
Copy Markdown
Member

Closes#2777, closes#2778.

Problem

Reusing a hook token after hook.dispose() deterministically or intermittently failed with a spurious HookConflictError naming an already-disposed hook:

Fix

  • @workflow/core:handleSuspension now processes hook operations grouped per token, in workflow-code order: a dispose of an earlier hook flushes before a later same-token hook's creation is validated, while a hook created and disposed within one suspension is still created first. Different tokens keep processing in parallel, and hooks still process before steps/waits.
  • @workflow/world-local: the hook_created token claim runs in a short bounded loop — it retries when the observed claim vanished mid-check, and treats a claim as vacant when its hook's disposal is committed (dispose lock exists) or its owning run is terminal/missing, releasing the stale claim if the in-flight releaser never finishes. The event-log rebuild (rebuildLiveHookByTokenFromEventLog / hooks.get*) no longer considers a hook live once its dispose lock exists. Live claims still conflict exactly as before.

- core: process hook operations per token in workflow-code order during
suspension handling, so a dispose() of an earlier hook releases the
token before a later same-token hook's creation is validated. A hook
created and disposed within the same suspension is still created
before it is disposed. Fixes the same-run recreate self-conflict
(#2777).
- world-local: retry the token claim when the observed claim vanished
mid-check or is held by a hook whose disposal is committed / a run
that is terminal or missing; never rebuild a claim from the event log
for a hook whose dispose lock exists. Fixes spurious HookConflictError
against an already-disposed hook during fast run-to-run token handoff
(#2778).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 899a288

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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

@vercel

vercelBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 6, 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🥇 Nitro0.048s (-16.4% 🟢)1.006s (-1.3%)0.958s101.00x
💻 LocalExpress0.050s (+3.5%)1.006s (~)0.957s101.03x
💻 LocalNext.js (Turbopack)0.055s (-2.0%)1.005s (~)0.951s101.13x
🐘 PostgresNitro0.055s (-17.7% 🟢)1.012s (~)0.956s101.15x
🐘 PostgresNext.js (Turbopack)0.062s (-1.0%)1.012s (~)0.950s101.28x
🐘 PostgresExpress0.070s (+9.4% 🔺)1.012s (~)0.942s101.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.259s (+43.3% 🔺)2.147s (+31.9% 🔺)1.888s101.00x
▲ VercelExpress0.381s (+69.9% 🔺)2.003s (+12.3% 🔺)1.623s101.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.084s (~)2.007s (~)0.923s101.00x
💻 LocalNext.js (Turbopack)1.085s (-0.7%)2.006s (~)0.922s101.00x
💻 LocalExpress1.091s (+1.3%)2.007s (~)0.916s101.01x
🐘 PostgresNext.js (Turbopack)1.096s (-0.9%)2.010s (~)0.914s101.01x
🐘 PostgresExpress1.102s (+0.6%)2.009s (~)0.907s101.02x
🐘 PostgresNitro1.105s (+0.9%)2.011s (~)0.906s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.408s (-4.8%)2.719s (-18.6% 🟢)1.311s101.00x
▲ VercelNitro1.494s (+8.4% 🔺)3.455s (+4.5%)1.961s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.464s (~)11.023s (~)0.559s31.00x
🐘 PostgresExpress10.472s (~)11.014s (~)0.542s31.00x
🐘 PostgresNitro10.475s (~)11.019s (~)0.545s31.00x
💻 LocalNext.js (Turbopack)10.490s (~)11.022s (~)0.532s31.00x
🐘 PostgresNext.js (Turbopack)10.499s (~)11.014s (~)0.515s31.00x
💻 LocalExpress10.512s (+0.7%)11.023s (~)0.511s31.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.697s (+1.1%)13.812s (+8.1% 🔺)2.116s31.00x
▲ VercelExpress11.771s (+0.9%)13.221s (~)1.450s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)13.613s (-1.1%)14.028s (~)0.415s51.00x
🐘 PostgresExpress13.622s (~)14.020s (~)0.398s51.00x
🐘 PostgresNitro13.671s (~)14.022s (~)0.351s51.00x
💻 LocalNitro13.691s (~)14.026s (~)0.336s51.01x
💻 LocalExpress13.738s (+0.9%)14.029s (~)0.290s51.01x
🐘 PostgresNext.js (Turbopack)13.783s (+0.7%)14.018s (~)0.235s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.359s (-0.6%)18.421s (+2.4%)2.063s41.00x
▲ VercelExpress16.725s (~)18.362s (-0.7%)1.636s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.311s (+0.6%)13.019s (~)0.708s71.00x
💻 LocalNext.js (Turbopack)12.354s (-0.5%)13.026s (~)0.673s71.00x
🐘 PostgresNext.js (Turbopack)12.376s (~)13.016s (~)0.641s71.01x
💻 LocalExpress12.377s (+2.3%)13.027s (~)0.650s71.01x
🐘 PostgresNitro12.385s (~)13.019s (~)0.634s71.01x
💻 LocalNitro12.493s (+1.8%)13.025s (~)0.532s71.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro18.213s (-1.7%)20.197s (+0.7%)1.984s51.00x
▲ VercelExpress19.008s (+8.2% 🔺)20.547s (+9.1% 🔺)1.538s51.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.5%)2.009s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.199s (~)2.008s (~)0.810s151.02x
🐘 PostgresExpress1.221s (+1.1%)2.075s (+3.3%)0.853s151.04x
💻 LocalNext.js (Turbopack)1.383s (-3.5%)2.006s (~)0.623s151.18x
💻 LocalNitro1.398s (~)2.006s (~)0.608s151.19x
💻 LocalExpress1.469s (+6.5% 🔺)2.007s (~)0.538s151.25x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.157s (+2.1%)3.617s (+1.9%)1.460s91.00x
▲ VercelNitro2.165s (-3.6%)3.883s (+6.0% 🔺)1.718s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.315s (-4.1%)2.394s (-4.7%)1.079s131.00x
🐘 PostgresNitro1.319s (-2.5%)2.510s (+1.6%)1.191s121.00x
🐘 PostgresNext.js (Turbopack)1.347s (+1.7%)2.918s (-3.0%)1.571s111.02x
💻 LocalNitro2.390s (-1.0%)3.009s (~)0.619s101.82x
💻 LocalNext.js (Turbopack)2.493s (+4.3%)3.110s (+3.3%)0.617s101.90x
💻 LocalExpress2.505s (+1.1%)3.009s (-3.2%)0.504s101.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.122s (+28.6% 🔺)4.500s (+12.9% 🔺)1.378s71.00x
▲ VercelNitro3.207s (+32.4% 🔺)4.900s (+30.5% 🔺)1.694s71.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.563s (-2.7%)4.138s (-3.9%)2.575s81.00x
🐘 PostgresExpress1.616s (-4.4%)4.021s (-9.5% 🟢)2.405s81.03x
💻 LocalExpress2.767s (-21.7% 🟢)4.298s (-9.1% 🟢)1.531s71.77x
🐘 PostgresNext.js (Turbopack)2.988s (-6.2% 🟢)6.014s (+2.8%)3.026s51.91x
💻 LocalNitro4.589s (+19.5% 🔺)5.013s (+9.3% 🔺)0.424s62.94x
💻 LocalNext.js (Turbopack)4.690s (+24.0% 🔺)5.178s (+6.4% 🔺)0.488s63.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.705s (+19.6% 🔺)5.746s (+21.4% 🔺)2.041s61.00x
▲ VercelExpress4.016s (+34.9% 🔺)5.695s (+27.0% 🔺)1.678s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.8%)2.008s (~)0.838s151.00x
🐘 PostgresNext.js (Turbopack)1.188s (-5.2% 🟢)2.007s (-3.3%)0.819s151.01x
🐘 PostgresExpress1.189s (-3.4%)2.008s (-3.2%)0.819s151.02x
💻 LocalNitro1.441s (+0.6%)2.006s (~)0.565s151.23x
💻 LocalNext.js (Turbopack)1.463s (~)2.006s (~)0.543s151.25x
💻 LocalExpress1.499s (+4.8%)2.007s (~)0.508s151.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.014s (-18.8% 🟢)3.876s (+0.7%)1.861s81.00x
▲ VercelExpress2.130s (~)3.651s (+1.8%)1.521s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.317s (-3.6%)2.150s (-17.1% 🟢)0.834s141.00x
🐘 PostgresNitro1.329s (+0.9%)2.316s (-3.2%)0.987s131.01x
🐘 PostgresNext.js (Turbopack)1.435s (+7.2% 🔺)3.009s (~)1.574s101.09x
💻 LocalExpress2.563s (+4.4%)3.009s (~)0.446s101.95x
💻 LocalNitro2.579s (+4.0%)3.009s (~)0.430s101.96x
💻 LocalNext.js (Turbopack)2.611s (-1.0%)3.109s (+3.3%)0.498s101.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.413s (+2.6%)4.409s (+16.9% 🔺)1.996s71.00x
▲ VercelExpress2.441s (+8.0% 🔺)3.951s (+10.4% 🔺)1.511s81.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.525s (-5.3% 🟢)4.138s (~)2.613s81.00x
🐘 PostgresExpress1.624s (-2.0%)4.583s (+6.6% 🔺)2.959s71.06x
🐘 PostgresNext.js (Turbopack)2.970s (-9.9% 🟢)6.017s (-3.2%)3.048s61.95x
💻 LocalNitro4.840s (-14.1% 🟢)5.850s (-2.7%)1.010s63.17x
💻 LocalNext.js (Turbopack)5.395s (-3.7%)5.847s (-2.8%)0.452s63.54x
💻 LocalExpress5.662s (+3.0%)6.416s (+6.7% 🔺)0.754s53.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.748s (+2.8%)4.556s (+5.8% 🔺)1.808s71.00x
▲ VercelNitro4.034s (+40.6% 🔺)6.082s (+38.0% 🔺)2.048s61.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.525s (-12.6% 🟢)1.006s (-1.7%)0.482s601.00x
🐘 PostgresNitro0.557s (-4.7%)1.041s (~)0.484s581.06x
🐘 PostgresNext.js (Turbopack)0.607s (+7.8% 🔺)1.024s (+1.8%)0.417s591.16x
💻 LocalNitro0.609s (+1.5%)1.005s (~)0.397s601.16x
💻 LocalNext.js (Turbopack)0.619s (-1.9%)1.005s (~)0.386s601.18x
💻 LocalExpress0.645s (+13.6% 🔺)1.005s (~)0.360s601.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-1.6%)4.032s (-1.0%)1.496s151.00x
▲ VercelExpress2.578s (-0.7%)3.948s (+4.3%)1.370s161.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.253s (-11.5% 🟢)2.007s (-2.2%)0.754s451.00x
🐘 PostgresNitro1.296s (-4.3%)2.008s (~)0.712s451.03x
🐘 PostgresNext.js (Turbopack)1.382s (-4.0%)2.008s (-1.1%)0.625s451.10x
💻 LocalNitro1.544s (+1.4%)2.029s (+1.1%)0.485s451.23x
💻 LocalNext.js (Turbopack)1.613s (-2.6%)2.006s (-1.1%)0.393s451.29x
💻 LocalExpress1.632s (+10.2% 🔺)2.007s (-1.0%)0.375s451.30x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.805s (-3.4%)7.644s (+1.0%)1.839s121.00x
▲ VercelExpress6.114s (-1.3%)7.668s (+1.0%)1.554s121.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.595s (-2.5%)3.111s (+2.5%)0.516s391.00x
🐘 PostgresNitro2.618s (-3.2%)3.086s (~)0.468s391.01x
🐘 PostgresNext.js (Turbopack)2.725s (-1.7%)3.033s (+0.8%)0.309s401.05x
💻 LocalNext.js (Turbopack)3.398s (-5.3% 🟢)4.009s (-1.7%)0.611s301.31x
💻 LocalNitro3.416s (+5.2% 🔺)4.009s (~)0.593s301.32x
💻 LocalExpress3.557s (+12.3% 🔺)4.077s (+5.1% 🔺)0.520s301.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.756s (-6.3% 🟢)13.617s (-3.3%)1.861s91.00x
▲ VercelNitro12.119s (+1.1%)14.257s (+6.8% 🔺)2.138s91.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.189s (-9.8% 🟢)1.006s (-1.7%)0.817s601.00x
🐘 PostgresExpress0.208s (-2.5%)1.006s (~)0.798s601.10x
🐘 PostgresNitro0.210s (-5.1% 🟢)1.007s (~)0.796s601.11x
💻 LocalNitro0.532s (+6.7% 🔺)1.005s (~)0.473s602.81x
💻 LocalExpress0.562s (+15.0% 🔺)1.005s (~)0.443s602.97x
💻 LocalNext.js (Turbopack)0.605s (-7.2% 🟢)1.005s (-1.7%)0.399s603.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.091s (+3.3%)2.425s (+6.4% 🔺)1.334s261.00x
▲ VercelNitro1.108s (+12.7% 🔺)2.752s (+24.2% 🔺)1.643s221.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.326s (-2.7%)1.018s (+1.2%)0.693s891.00x
🐘 PostgresExpress0.340s (+4.9%)1.029s (+2.2%)0.689s881.04x
🐘 PostgresNext.js (Turbopack)0.352s (+18.6% 🔺)1.041s (+3.4%)0.689s871.08x
💻 LocalNitro2.465s (-3.3%)3.009s (~)0.545s307.56x
💻 LocalExpress2.503s (+1.2%)3.010s (~)0.507s307.68x
💻 LocalNext.js (Turbopack)2.675s (~)3.075s (+2.2%)0.400s308.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.430s (+7.8% 🔺)2.916s (+17.9% 🔺)1.486s311.00x
▲ VercelNitro1.491s (+7.7% 🔺)3.145s (+14.4% 🔺)1.654s291.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.516s (-3.2%)3.034s (+0.8%)2.518s401.00x
🐘 PostgresNitro0.540s (+6.8% 🔺)1.150s (+11.5% 🔺)0.610s1051.05x
🐘 PostgresExpress0.582s (+11.5% 🔺)1.160s (+7.7% 🔺)0.578s1041.13x
💻 LocalNitro5.602s (+4.4%)7.436s (-11.0% 🟢)1.834s1710.85x
💻 LocalExpress5.988s (+2.9%)8.956s (+0.8%)2.968s1411.60x
💻 LocalNext.js (Turbopack)6.274s (+3.8%)9.172s (+2.4%)2.898s1412.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.941s (+7.2% 🔺)3.965s (+16.7% 🔺)2.024s311.00x
▲ VercelExpress2.171s (+18.6% 🔺)3.898s (+6.5% 🔺)1.727s311.12x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.141s (-1.4%)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.868s101.00x
💻 LocalNext.js (Turbopack)1.151s (~)1.964s (~)0.011s (-19.2% 🟢)2.018s (~)0.867s101.01x
💻 LocalNitro1.161s (~)2.004s (~)0.012s (+3.3%)2.019s (~)0.858s101.02x
🐘 PostgresNext.js (Turbopack)1.165s (~)2.001s (~)0.001s (+8.3% 🔺)2.012s (~)0.847s101.02x
🐘 PostgresExpress1.169s (+0.6%)1.999s (~)0.001s (~)2.010s (~)0.841s101.02x
💻 LocalExpress1.173s (+1.8%)2.005s (~)0.013s (+23.1% 🔺)2.021s (~)0.848s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.962s (-7.4% 🟢)3.736s (+17.4% 🔺)2.143s (+15.8% 🔺)6.372s (+16.3% 🔺)4.410s101.00x
▲ VercelExpress2.103s (-3.2%)3.274s (-4.3%)1.979s (+5.8% 🔺)5.705s (~)3.602s101.07x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.554s (-2.4%)2.002s (~)0.005s (-2.5%)2.028s (~)0.474s301.00x
🐘 PostgresNitro1.585s (+1.1%)2.007s (~)0.005s (-5.4% 🟢)2.025s (~)0.440s301.02x
💻 LocalExpress1.592s (+0.6%)2.010s (~)0.012s (-7.6% 🟢)2.025s (~)0.433s301.02x
💻 LocalNext.js (Turbopack)1.605s (~)1.972s (~)0.014s (+0.7%)2.027s (~)0.422s301.03x
🐘 PostgresNext.js (Turbopack)1.657s (-2.0%)2.010s (-1.6%)0.005s (~)2.027s (-1.6%)0.370s301.07x
💻 LocalNitro1.907s (+21.0% 🔺)2.354s (+17.1% 🔺)0.012s (-4.3%)2.372s (+17.1% 🔺)0.464s261.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.530s (+1.6%)7.432s (+13.4% 🔺)0.328s (+71.3% 🔺)8.232s (+14.6% 🔺)2.702s81.00x
▲ VercelExpress5.684s (+5.8% 🔺)6.911s (+3.9%)0.345s (+53.1% 🔺)7.764s (+5.6% 🔺)2.080s81.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.784s (~)1.081s (+3.0%)0.000s (NaN%)1.099s (+1.7%)0.315s561.00x
🐘 PostgresNitro0.788s (+1.0%)1.032s (-3.1%)0.000s (-67.8% 🟢)1.042s (-4.2%)0.254s581.00x
🐘 PostgresNext.js (Turbopack)0.918s (-5.2% 🟢)1.333s (-7.4% 🟢)0.000s (-8.9% 🟢)1.340s (-8.5% 🟢)0.423s451.17x
💻 LocalNext.js (Turbopack)1.327s (-0.8%)1.978s (~)0.000s (-55.6% 🟢)2.017s (~)0.690s301.69x
💻 LocalNitro1.372s (~)1.919s (-3.1%)0.000s (+23.3% 🔺)1.922s (-3.1%)0.550s321.75x
💻 LocalExpress1.453s (+5.2% 🔺)1.982s (+1.6%)0.000s (+62.5% 🔺)1.985s (+1.6%)0.532s311.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.761s (+23.7% 🔺)5.227s (+25.2% 🔺)0.003s (+Infinity% 🔺)5.792s (+25.6% 🔺)2.032s111.00x
▲ VercelExpress3.837s (+32.6% 🔺)5.303s (+30.4% 🔺)0.000s (+154.5% 🔺)5.759s (+29.4% 🔺)1.923s111.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.640s (-10.8% 🟢)2.306s (-3.1%)0.000s (NaN%)2.317s (-3.4%)0.677s261.00x
🐘 PostgresExpress1.725s (-2.8%)2.301s (-4.0%)0.000s (-100.0% 🟢)2.318s (-3.8%)0.593s261.05x
🐘 PostgresNext.js (Turbopack)2.275s (-17.3% 🟢)2.859s (-14.3% 🟢)0.000s (-100.0% 🟢)2.868s (-14.3% 🟢)0.593s211.39x
💻 LocalNitro3.218s (+1.3%)3.775s (+2.7%)0.001s (+112.5% 🔺)3.779s (+2.7%)0.561s161.96x
💻 LocalExpress3.363s (-2.3%)3.902s (-3.1%)0.000s (-29.7% 🟢)3.905s (-3.1%)0.543s162.05x
💻 LocalNext.js (Turbopack)3.580s (+3.0%)3.992s (~)0.000s (-28.6% 🟢)4.032s (~)0.452s152.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.808s (+15.8% 🔺)5.847s (+10.0% 🔺)0.000s (+10.0% 🔺)6.351s (+10.9% 🔺)1.543s101.00x
▲ VercelExpress5.284s (+24.3% 🔺)6.342s (+20.6% 🔺)0.000s (NaN%)6.789s (+20.3% 🔺)1.505s101.10x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro12/21
🐘 PostgresExpress10/21
▲ VercelNitro14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres19/21
Next.js (Turbopack)🐘 Postgres14/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027

📋 View full workflow run

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 6, 2026 17:04
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 6, 2026 17:04

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

ai review: Approving — no blockers or regressions found.

What I checked:

  • @workflow/core suspension handler: verified the per-token grouping preserves queue-insertion (workflow-code) order, so dispose-before-recreate and create-before-dispose orderings both hold; cross-token parallelism and the hooks-before-steps/waits ordering are unchanged. hasHookEvents (hooksNeedingCreation.length > 0) is equivalent to the old hookEvents.length > 0. Skipping disposal for a conflicted creation is a behavior improvement (previously it attempted the dispose and swallowed HookNotFoundError). Abort processing (hooksNeedingAbort) is unaffected.
  • @workflow/world-local claim loop: the loop is bounded (10 attempts, ≤2×10ms sleeps), a genuinely live claim breaks out on the first iteration into the unchanged conflict path, and the own-claim dedup/recovery path is preserved (existingClaim carried out of the loop matches the old post-failure re-read). On loop exhaustion, behavior degrades to the pre-PR single-attempt semantics rather than something worse.
  • Rebuild/resurrection interaction: findLiveHookCreatedEvent already excludes terminal-run hooks (isTerminalRunCache) and now dispose-committed hooks, so a force-released claim is not resurrected by the event-log rebuild in the disposed-hook and terminal-run cases (matching the two new storage tests).
  • Dispose-lock keying: the lock path/naming is byte-identical to the old inline construction (backward compatible with locks already on disk), and hook correlation IDs are hook_${ulid} so keying by hookId alone is safe — same keying the dispose path already relied on.
  • Tests: the new @workflow/vitest workbench tests are race-free because waitForHook excludes hooks that already have a hook_received event, so each round can only observe the next round's hook; the e2e helper's new excludeHookId handles the same race for the token-lookup-based poll. CI is green including the required E2E check.

Left two non-blocking inline notes on narrow crash/race windows in the world-local force-release path.

// The releaser is not coming. Release the stale claim and
// the hook entity it points at, mirroring the hook_disposed
// cleanup.
await deleteJSON(constraintPath);

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.

ai review (non-blocking): there's a narrow cross-process window where this force-release can lead to a transient token-uniqueness violation. Both in-flight releasers (hook_disposed handler and deleteAllHooksForRun) delete the claim file unconditionally after reading the hook entity. Sequence: releaser writes the dispose lock (or run goes terminal) and reads the hook entity, then stalls; this claimant observes the stale claim releasable 3×, deletes it, and writes its own fresh claim; the stalled releaser then executes its deleteJSON(constraintPath) — deleting the new claimant's live claim, so a third claimant can also claim the token. The 2×10ms grace period narrows this but doesn't close it. A cheap hardening would be for the releasers to re-read the claim and only delete it if it still points at their own (runId, hookId) (still TOCTOU, but the window shrinks from "stall of any length" to adjacent file ops), or to record the claim's identity here and have releasers skip deletion when the hook entity is already gone — which hook_disposed already partially does via the existingHook guard.

WorkflowRunSchema,
tag
);
if (!owningRun) {

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.

ai review (non-blocking): the missing-run semantics here disagree with the event-log rebuild. This treats a claim whose run file is absent as releasable, but findLiveHookCreatedEvent's isTerminalRunCache returns false for a missing run file, i.e. the rebuild treats that same hook as live and re-writes the claim. If a claim + surviving hook_created event ever exist without a run file (crash-lost run cache), the claim loop deletes the claim, the next iteration's rebuild resurrects it, and the loop ping-pongs to exhaustion — ending in the conflict branch with existingClaim = null, i.e. a durable hook_conflict with conflictingRunId: undefined, the exact symptom #2778 fixes. Unreachable today AFAICT (world-local never deletes run files), but worth aligning the two predicates — either both treat a missing run as terminal, or drop this branch.

@VaguelySerious
VaguelySerious merged commit 7637196 into mainJul 7, 2026
180 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/issue-2777 branch July 7, 2026 21:13
@VaguelySerious

Copy link
Copy Markdown
MemberAuthor

Addressing review comment in a follow-up PR

@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 7637196 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

7637196cf0f605ce62243bf8c7762a26153dcd36

@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026

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

Reviewed both halves in depth, ran the full affected suites plus the new regression tests against a live dev server. Approving.

Core (same-run, #2777): The per-token grouping is the right shape. I traced the ordering semantics:

  • Queue-insertion order within a token group = workflow code order, so dispose-of-earlier flushes before create-of-later is validated, while a created-and-disposed-in-one-suspension hook still creates first. Both directions are covered by the new unit tests.
  • The new event-log order (hook_disposed before the successor's hook_created) also matches replay consumption order — under the old code the log recorded them inverted relative to code order, so this is strictly better for replay, not just for conflict avoidance.
  • hasHookConflict / hasAwaitedHookCreation accumulation reaches the same set of creations as before, and hasHookEvents is computed from the same population — no semantic drift in the V2 re-invoke signaling.
  • The creationConflicted gate on dispose is correct belt-and-braces: a conflicted creation has nothing to dispose, and skipping avoids even the benign HookNotFoundError round-trip.
  • Cross-token parallelism is preserved, so unrelated hooks don't serialize.

world-local (cross-run, #2778): The claim loop's force-release is the risky part, so I audited its failure modes:

  • readJSON returns null only on ENOENT (anything else throws), so isHookTokenClaimReleasable can't misclassify a live claim due to a transient read error — a "missing" owning run is genuinely missing, and terminal status is monotonic.
  • Racing force-releasers and the in-flight disposer are all safe: deleteJSON is ENOENT-idempotent, and re-claim arbitration still lands on writeExclusive's EEXIST semantics, so exactly one claimant wins and the loser correctly conflicts against the winner's live claim.
  • The dispose lock as earliest-durable-disposal-marker works because the lock write already preceded the destructive deletes pre-PR — the change formalizes existing ordering rather than introducing new ordering requirements. The findLiveHookCreatedEvent lock check closing the rebuild-resurrection window is a nice catch.
  • I also confirmed the asymmetry with world-postgres is justified: there the hook row is the token claim and hook_disposed releases it via atomic DELETE ... RETURNING, so no separate-release window exists — the core fix alone covers it.

Validation: 1354/1354 core unit tests, 432/432 world-local (after full build — the 17 initial failures were unbuilt-SWC-plugin/e2e-env artifacts), 2/2 new workbench/vitest regression tests, and against a live turbopack dev server: the new hookTokenReuseLoopWorkflow e2e plus all 26 existing hook e2e tests pass. Workbench conventions followed (workflow added to workbench/example and propagated via symlink; the vitest workbench's local workflow matches its local-file convention). Changesets correctly split per package. CI: 105 pass, 1 fail = the known Benchmark Vercel (nextjs-turbopack) flake.

Two non-blocking notes:

  1. If the claim loop exhausts all 10 attempts via repeated vanish races (claim deleted between the exclusive-create attempt and every read), it falls through with existingClaim = null and still records a hook_conflict with no conflictingRunId — the #2778 symptom, now requiring ~10 consecutive lost races to reproduce. Practically unreachable, but a runtimeLogger.warn on loop exhaustion would make it diagnosable if it ever fires.
  2. The dispose locks in .locks/hooks/ are now load-bearing durable state (consulted by the claim path and the rebuild), not just transient mutexes. Worth a note somewhere discoverable that they must never be garbage-collected — a future ".locks cleanup" change would silently reintroduce the resurrection bug. The comment at hookDisposeLockPath covers the what; the hazard is for whoever writes a cleanup without reading it.

VaguelySerious added a commit that referenced this pull request Jul 7, 2026
…tall
Both in-flight releasers (the hook_disposed handler and the terminal-run
deleteAllHooksForRun cleanup) deleted the claim file unconditionally after
reading the hook entity. A releaser stalled between those operations could
outlive a claimant force-releasing its stale claim, and its deferred delete
would then destroy the new claimant's live claim — transiently breaking
token uniqueness (a third claimant could claim the token too).
Releasers now re-read the claim and delete it only if it still points at
their own (runId, hookId). Still TOCTOU, but the window shrinks from "a
stall of any length" to adjacent file ops; a claim owned by someone else
is left for the claimant-side force-release path to reap.
Addresses post-merge feedback on #2779.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

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

[core] Fix hook token reuse after dispose() (same-run and cross-run) - #2779

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777
Jul 7, 2026
Merged

[core] Fix hook token reuse after dispose() (same-run and cross-run)#2779
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 6, 2026

Copy link
Copy Markdown
Member

Closes#2777, closes#2778.

Problem

Reusing a hook token after hook.dispose() deterministically or intermittently failed with a spurious HookConflictError naming an already-disposed hook:

Fix

  • @workflow/core:handleSuspension now processes hook operations grouped per token, in workflow-code order: a dispose of an earlier hook flushes before a later same-token hook's creation is validated, while a hook created and disposed within one suspension is still created first. Different tokens keep processing in parallel, and hooks still process before steps/waits.
  • @workflow/world-local: the hook_created token claim runs in a short bounded loop — it retries when the observed claim vanished mid-check, and treats a claim as vacant when its hook's disposal is committed (dispose lock exists) or its owning run is terminal/missing, releasing the stale claim if the in-flight releaser never finishes. The event-log rebuild (rebuildLiveHookByTokenFromEventLog / hooks.get*) no longer considers a hook live once its dispose lock exists. Live claims still conflict exactly as before.

- core: process hook operations per token in workflow-code order during
suspension handling, so a dispose() of an earlier hook releases the
token before a later same-token hook's creation is validated. A hook
created and disposed within the same suspension is still created
before it is disposed. Fixes the same-run recreate self-conflict
(#2777).
- world-local: retry the token claim when the observed claim vanished
mid-check or is held by a hook whose disposal is committed / a run
that is terminal or missing; never rebuild a claim from the event log
for a hook whose dispose lock exists. Fixes spurious HookConflictError
against an already-disposed hook during fast run-to-run token handoff
(#2778).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 899a288

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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

@vercel

vercelBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 6, 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🥇 Nitro0.048s (-16.4% 🟢)1.006s (-1.3%)0.958s101.00x
💻 LocalExpress0.050s (+3.5%)1.006s (~)0.957s101.03x
💻 LocalNext.js (Turbopack)0.055s (-2.0%)1.005s (~)0.951s101.13x
🐘 PostgresNitro0.055s (-17.7% 🟢)1.012s (~)0.956s101.15x
🐘 PostgresNext.js (Turbopack)0.062s (-1.0%)1.012s (~)0.950s101.28x
🐘 PostgresExpress0.070s (+9.4% 🔺)1.012s (~)0.942s101.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.259s (+43.3% 🔺)2.147s (+31.9% 🔺)1.888s101.00x
▲ VercelExpress0.381s (+69.9% 🔺)2.003s (+12.3% 🔺)1.623s101.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.084s (~)2.007s (~)0.923s101.00x
💻 LocalNext.js (Turbopack)1.085s (-0.7%)2.006s (~)0.922s101.00x
💻 LocalExpress1.091s (+1.3%)2.007s (~)0.916s101.01x
🐘 PostgresNext.js (Turbopack)1.096s (-0.9%)2.010s (~)0.914s101.01x
🐘 PostgresExpress1.102s (+0.6%)2.009s (~)0.907s101.02x
🐘 PostgresNitro1.105s (+0.9%)2.011s (~)0.906s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.408s (-4.8%)2.719s (-18.6% 🟢)1.311s101.00x
▲ VercelNitro1.494s (+8.4% 🔺)3.455s (+4.5%)1.961s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.464s (~)11.023s (~)0.559s31.00x
🐘 PostgresExpress10.472s (~)11.014s (~)0.542s31.00x
🐘 PostgresNitro10.475s (~)11.019s (~)0.545s31.00x
💻 LocalNext.js (Turbopack)10.490s (~)11.022s (~)0.532s31.00x
🐘 PostgresNext.js (Turbopack)10.499s (~)11.014s (~)0.515s31.00x
💻 LocalExpress10.512s (+0.7%)11.023s (~)0.511s31.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.697s (+1.1%)13.812s (+8.1% 🔺)2.116s31.00x
▲ VercelExpress11.771s (+0.9%)13.221s (~)1.450s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)13.613s (-1.1%)14.028s (~)0.415s51.00x
🐘 PostgresExpress13.622s (~)14.020s (~)0.398s51.00x
🐘 PostgresNitro13.671s (~)14.022s (~)0.351s51.00x
💻 LocalNitro13.691s (~)14.026s (~)0.336s51.01x
💻 LocalExpress13.738s (+0.9%)14.029s (~)0.290s51.01x
🐘 PostgresNext.js (Turbopack)13.783s (+0.7%)14.018s (~)0.235s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.359s (-0.6%)18.421s (+2.4%)2.063s41.00x
▲ VercelExpress16.725s (~)18.362s (-0.7%)1.636s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.311s (+0.6%)13.019s (~)0.708s71.00x
💻 LocalNext.js (Turbopack)12.354s (-0.5%)13.026s (~)0.673s71.00x
🐘 PostgresNext.js (Turbopack)12.376s (~)13.016s (~)0.641s71.01x
💻 LocalExpress12.377s (+2.3%)13.027s (~)0.650s71.01x
🐘 PostgresNitro12.385s (~)13.019s (~)0.634s71.01x
💻 LocalNitro12.493s (+1.8%)13.025s (~)0.532s71.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro18.213s (-1.7%)20.197s (+0.7%)1.984s51.00x
▲ VercelExpress19.008s (+8.2% 🔺)20.547s (+9.1% 🔺)1.538s51.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.5%)2.009s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.199s (~)2.008s (~)0.810s151.02x
🐘 PostgresExpress1.221s (+1.1%)2.075s (+3.3%)0.853s151.04x
💻 LocalNext.js (Turbopack)1.383s (-3.5%)2.006s (~)0.623s151.18x
💻 LocalNitro1.398s (~)2.006s (~)0.608s151.19x
💻 LocalExpress1.469s (+6.5% 🔺)2.007s (~)0.538s151.25x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.157s (+2.1%)3.617s (+1.9%)1.460s91.00x
▲ VercelNitro2.165s (-3.6%)3.883s (+6.0% 🔺)1.718s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.315s (-4.1%)2.394s (-4.7%)1.079s131.00x
🐘 PostgresNitro1.319s (-2.5%)2.510s (+1.6%)1.191s121.00x
🐘 PostgresNext.js (Turbopack)1.347s (+1.7%)2.918s (-3.0%)1.571s111.02x
💻 LocalNitro2.390s (-1.0%)3.009s (~)0.619s101.82x
💻 LocalNext.js (Turbopack)2.493s (+4.3%)3.110s (+3.3%)0.617s101.90x
💻 LocalExpress2.505s (+1.1%)3.009s (-3.2%)0.504s101.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.122s (+28.6% 🔺)4.500s (+12.9% 🔺)1.378s71.00x
▲ VercelNitro3.207s (+32.4% 🔺)4.900s (+30.5% 🔺)1.694s71.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.563s (-2.7%)4.138s (-3.9%)2.575s81.00x
🐘 PostgresExpress1.616s (-4.4%)4.021s (-9.5% 🟢)2.405s81.03x
💻 LocalExpress2.767s (-21.7% 🟢)4.298s (-9.1% 🟢)1.531s71.77x
🐘 PostgresNext.js (Turbopack)2.988s (-6.2% 🟢)6.014s (+2.8%)3.026s51.91x
💻 LocalNitro4.589s (+19.5% 🔺)5.013s (+9.3% 🔺)0.424s62.94x
💻 LocalNext.js (Turbopack)4.690s (+24.0% 🔺)5.178s (+6.4% 🔺)0.488s63.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.705s (+19.6% 🔺)5.746s (+21.4% 🔺)2.041s61.00x
▲ VercelExpress4.016s (+34.9% 🔺)5.695s (+27.0% 🔺)1.678s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.8%)2.008s (~)0.838s151.00x
🐘 PostgresNext.js (Turbopack)1.188s (-5.2% 🟢)2.007s (-3.3%)0.819s151.01x
🐘 PostgresExpress1.189s (-3.4%)2.008s (-3.2%)0.819s151.02x
💻 LocalNitro1.441s (+0.6%)2.006s (~)0.565s151.23x
💻 LocalNext.js (Turbopack)1.463s (~)2.006s (~)0.543s151.25x
💻 LocalExpress1.499s (+4.8%)2.007s (~)0.508s151.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.014s (-18.8% 🟢)3.876s (+0.7%)1.861s81.00x
▲ VercelExpress2.130s (~)3.651s (+1.8%)1.521s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.317s (-3.6%)2.150s (-17.1% 🟢)0.834s141.00x
🐘 PostgresNitro1.329s (+0.9%)2.316s (-3.2%)0.987s131.01x
🐘 PostgresNext.js (Turbopack)1.435s (+7.2% 🔺)3.009s (~)1.574s101.09x
💻 LocalExpress2.563s (+4.4%)3.009s (~)0.446s101.95x
💻 LocalNitro2.579s (+4.0%)3.009s (~)0.430s101.96x
💻 LocalNext.js (Turbopack)2.611s (-1.0%)3.109s (+3.3%)0.498s101.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.413s (+2.6%)4.409s (+16.9% 🔺)1.996s71.00x
▲ VercelExpress2.441s (+8.0% 🔺)3.951s (+10.4% 🔺)1.511s81.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.525s (-5.3% 🟢)4.138s (~)2.613s81.00x
🐘 PostgresExpress1.624s (-2.0%)4.583s (+6.6% 🔺)2.959s71.06x
🐘 PostgresNext.js (Turbopack)2.970s (-9.9% 🟢)6.017s (-3.2%)3.048s61.95x
💻 LocalNitro4.840s (-14.1% 🟢)5.850s (-2.7%)1.010s63.17x
💻 LocalNext.js (Turbopack)5.395s (-3.7%)5.847s (-2.8%)0.452s63.54x
💻 LocalExpress5.662s (+3.0%)6.416s (+6.7% 🔺)0.754s53.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.748s (+2.8%)4.556s (+5.8% 🔺)1.808s71.00x
▲ VercelNitro4.034s (+40.6% 🔺)6.082s (+38.0% 🔺)2.048s61.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.525s (-12.6% 🟢)1.006s (-1.7%)0.482s601.00x
🐘 PostgresNitro0.557s (-4.7%)1.041s (~)0.484s581.06x
🐘 PostgresNext.js (Turbopack)0.607s (+7.8% 🔺)1.024s (+1.8%)0.417s591.16x
💻 LocalNitro0.609s (+1.5%)1.005s (~)0.397s601.16x
💻 LocalNext.js (Turbopack)0.619s (-1.9%)1.005s (~)0.386s601.18x
💻 LocalExpress0.645s (+13.6% 🔺)1.005s (~)0.360s601.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-1.6%)4.032s (-1.0%)1.496s151.00x
▲ VercelExpress2.578s (-0.7%)3.948s (+4.3%)1.370s161.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.253s (-11.5% 🟢)2.007s (-2.2%)0.754s451.00x
🐘 PostgresNitro1.296s (-4.3%)2.008s (~)0.712s451.03x
🐘 PostgresNext.js (Turbopack)1.382s (-4.0%)2.008s (-1.1%)0.625s451.10x
💻 LocalNitro1.544s (+1.4%)2.029s (+1.1%)0.485s451.23x
💻 LocalNext.js (Turbopack)1.613s (-2.6%)2.006s (-1.1%)0.393s451.29x
💻 LocalExpress1.632s (+10.2% 🔺)2.007s (-1.0%)0.375s451.30x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.805s (-3.4%)7.644s (+1.0%)1.839s121.00x
▲ VercelExpress6.114s (-1.3%)7.668s (+1.0%)1.554s121.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.595s (-2.5%)3.111s (+2.5%)0.516s391.00x
🐘 PostgresNitro2.618s (-3.2%)3.086s (~)0.468s391.01x
🐘 PostgresNext.js (Turbopack)2.725s (-1.7%)3.033s (+0.8%)0.309s401.05x
💻 LocalNext.js (Turbopack)3.398s (-5.3% 🟢)4.009s (-1.7%)0.611s301.31x
💻 LocalNitro3.416s (+5.2% 🔺)4.009s (~)0.593s301.32x
💻 LocalExpress3.557s (+12.3% 🔺)4.077s (+5.1% 🔺)0.520s301.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.756s (-6.3% 🟢)13.617s (-3.3%)1.861s91.00x
▲ VercelNitro12.119s (+1.1%)14.257s (+6.8% 🔺)2.138s91.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.189s (-9.8% 🟢)1.006s (-1.7%)0.817s601.00x
🐘 PostgresExpress0.208s (-2.5%)1.006s (~)0.798s601.10x
🐘 PostgresNitro0.210s (-5.1% 🟢)1.007s (~)0.796s601.11x
💻 LocalNitro0.532s (+6.7% 🔺)1.005s (~)0.473s602.81x
💻 LocalExpress0.562s (+15.0% 🔺)1.005s (~)0.443s602.97x
💻 LocalNext.js (Turbopack)0.605s (-7.2% 🟢)1.005s (-1.7%)0.399s603.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.091s (+3.3%)2.425s (+6.4% 🔺)1.334s261.00x
▲ VercelNitro1.108s (+12.7% 🔺)2.752s (+24.2% 🔺)1.643s221.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.326s (-2.7%)1.018s (+1.2%)0.693s891.00x
🐘 PostgresExpress0.340s (+4.9%)1.029s (+2.2%)0.689s881.04x
🐘 PostgresNext.js (Turbopack)0.352s (+18.6% 🔺)1.041s (+3.4%)0.689s871.08x
💻 LocalNitro2.465s (-3.3%)3.009s (~)0.545s307.56x
💻 LocalExpress2.503s (+1.2%)3.010s (~)0.507s307.68x
💻 LocalNext.js (Turbopack)2.675s (~)3.075s (+2.2%)0.400s308.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.430s (+7.8% 🔺)2.916s (+17.9% 🔺)1.486s311.00x
▲ VercelNitro1.491s (+7.7% 🔺)3.145s (+14.4% 🔺)1.654s291.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.516s (-3.2%)3.034s (+0.8%)2.518s401.00x
🐘 PostgresNitro0.540s (+6.8% 🔺)1.150s (+11.5% 🔺)0.610s1051.05x
🐘 PostgresExpress0.582s (+11.5% 🔺)1.160s (+7.7% 🔺)0.578s1041.13x
💻 LocalNitro5.602s (+4.4%)7.436s (-11.0% 🟢)1.834s1710.85x
💻 LocalExpress5.988s (+2.9%)8.956s (+0.8%)2.968s1411.60x
💻 LocalNext.js (Turbopack)6.274s (+3.8%)9.172s (+2.4%)2.898s1412.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.941s (+7.2% 🔺)3.965s (+16.7% 🔺)2.024s311.00x
▲ VercelExpress2.171s (+18.6% 🔺)3.898s (+6.5% 🔺)1.727s311.12x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.141s (-1.4%)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.868s101.00x
💻 LocalNext.js (Turbopack)1.151s (~)1.964s (~)0.011s (-19.2% 🟢)2.018s (~)0.867s101.01x
💻 LocalNitro1.161s (~)2.004s (~)0.012s (+3.3%)2.019s (~)0.858s101.02x
🐘 PostgresNext.js (Turbopack)1.165s (~)2.001s (~)0.001s (+8.3% 🔺)2.012s (~)0.847s101.02x
🐘 PostgresExpress1.169s (+0.6%)1.999s (~)0.001s (~)2.010s (~)0.841s101.02x
💻 LocalExpress1.173s (+1.8%)2.005s (~)0.013s (+23.1% 🔺)2.021s (~)0.848s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.962s (-7.4% 🟢)3.736s (+17.4% 🔺)2.143s (+15.8% 🔺)6.372s (+16.3% 🔺)4.410s101.00x
▲ VercelExpress2.103s (-3.2%)3.274s (-4.3%)1.979s (+5.8% 🔺)5.705s (~)3.602s101.07x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.554s (-2.4%)2.002s (~)0.005s (-2.5%)2.028s (~)0.474s301.00x
🐘 PostgresNitro1.585s (+1.1%)2.007s (~)0.005s (-5.4% 🟢)2.025s (~)0.440s301.02x
💻 LocalExpress1.592s (+0.6%)2.010s (~)0.012s (-7.6% 🟢)2.025s (~)0.433s301.02x
💻 LocalNext.js (Turbopack)1.605s (~)1.972s (~)0.014s (+0.7%)2.027s (~)0.422s301.03x
🐘 PostgresNext.js (Turbopack)1.657s (-2.0%)2.010s (-1.6%)0.005s (~)2.027s (-1.6%)0.370s301.07x
💻 LocalNitro1.907s (+21.0% 🔺)2.354s (+17.1% 🔺)0.012s (-4.3%)2.372s (+17.1% 🔺)0.464s261.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.530s (+1.6%)7.432s (+13.4% 🔺)0.328s (+71.3% 🔺)8.232s (+14.6% 🔺)2.702s81.00x
▲ VercelExpress5.684s (+5.8% 🔺)6.911s (+3.9%)0.345s (+53.1% 🔺)7.764s (+5.6% 🔺)2.080s81.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.784s (~)1.081s (+3.0%)0.000s (NaN%)1.099s (+1.7%)0.315s561.00x
🐘 PostgresNitro0.788s (+1.0%)1.032s (-3.1%)0.000s (-67.8% 🟢)1.042s (-4.2%)0.254s581.00x
🐘 PostgresNext.js (Turbopack)0.918s (-5.2% 🟢)1.333s (-7.4% 🟢)0.000s (-8.9% 🟢)1.340s (-8.5% 🟢)0.423s451.17x
💻 LocalNext.js (Turbopack)1.327s (-0.8%)1.978s (~)0.000s (-55.6% 🟢)2.017s (~)0.690s301.69x
💻 LocalNitro1.372s (~)1.919s (-3.1%)0.000s (+23.3% 🔺)1.922s (-3.1%)0.550s321.75x
💻 LocalExpress1.453s (+5.2% 🔺)1.982s (+1.6%)0.000s (+62.5% 🔺)1.985s (+1.6%)0.532s311.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.761s (+23.7% 🔺)5.227s (+25.2% 🔺)0.003s (+Infinity% 🔺)5.792s (+25.6% 🔺)2.032s111.00x
▲ VercelExpress3.837s (+32.6% 🔺)5.303s (+30.4% 🔺)0.000s (+154.5% 🔺)5.759s (+29.4% 🔺)1.923s111.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.640s (-10.8% 🟢)2.306s (-3.1%)0.000s (NaN%)2.317s (-3.4%)0.677s261.00x
🐘 PostgresExpress1.725s (-2.8%)2.301s (-4.0%)0.000s (-100.0% 🟢)2.318s (-3.8%)0.593s261.05x
🐘 PostgresNext.js (Turbopack)2.275s (-17.3% 🟢)2.859s (-14.3% 🟢)0.000s (-100.0% 🟢)2.868s (-14.3% 🟢)0.593s211.39x
💻 LocalNitro3.218s (+1.3%)3.775s (+2.7%)0.001s (+112.5% 🔺)3.779s (+2.7%)0.561s161.96x
💻 LocalExpress3.363s (-2.3%)3.902s (-3.1%)0.000s (-29.7% 🟢)3.905s (-3.1%)0.543s162.05x
💻 LocalNext.js (Turbopack)3.580s (+3.0%)3.992s (~)0.000s (-28.6% 🟢)4.032s (~)0.452s152.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.808s (+15.8% 🔺)5.847s (+10.0% 🔺)0.000s (+10.0% 🔺)6.351s (+10.9% 🔺)1.543s101.00x
▲ VercelExpress5.284s (+24.3% 🔺)6.342s (+20.6% 🔺)0.000s (NaN%)6.789s (+20.3% 🔺)1.505s101.10x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro12/21
🐘 PostgresExpress10/21
▲ VercelNitro14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres19/21
Next.js (Turbopack)🐘 Postgres14/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027

📋 View full workflow run

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 6, 2026 17:04
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 6, 2026 17:04

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

ai review: Approving — no blockers or regressions found.

What I checked:

  • @workflow/core suspension handler: verified the per-token grouping preserves queue-insertion (workflow-code) order, so dispose-before-recreate and create-before-dispose orderings both hold; cross-token parallelism and the hooks-before-steps/waits ordering are unchanged. hasHookEvents (hooksNeedingCreation.length > 0) is equivalent to the old hookEvents.length > 0. Skipping disposal for a conflicted creation is a behavior improvement (previously it attempted the dispose and swallowed HookNotFoundError). Abort processing (hooksNeedingAbort) is unaffected.
  • @workflow/world-local claim loop: the loop is bounded (10 attempts, ≤2×10ms sleeps), a genuinely live claim breaks out on the first iteration into the unchanged conflict path, and the own-claim dedup/recovery path is preserved (existingClaim carried out of the loop matches the old post-failure re-read). On loop exhaustion, behavior degrades to the pre-PR single-attempt semantics rather than something worse.
  • Rebuild/resurrection interaction: findLiveHookCreatedEvent already excludes terminal-run hooks (isTerminalRunCache) and now dispose-committed hooks, so a force-released claim is not resurrected by the event-log rebuild in the disposed-hook and terminal-run cases (matching the two new storage tests).
  • Dispose-lock keying: the lock path/naming is byte-identical to the old inline construction (backward compatible with locks already on disk), and hook correlation IDs are hook_${ulid} so keying by hookId alone is safe — same keying the dispose path already relied on.
  • Tests: the new @workflow/vitest workbench tests are race-free because waitForHook excludes hooks that already have a hook_received event, so each round can only observe the next round's hook; the e2e helper's new excludeHookId handles the same race for the token-lookup-based poll. CI is green including the required E2E check.

Left two non-blocking inline notes on narrow crash/race windows in the world-local force-release path.

// The releaser is not coming. Release the stale claim and
// the hook entity it points at, mirroring the hook_disposed
// cleanup.
await deleteJSON(constraintPath);

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.

ai review (non-blocking): there's a narrow cross-process window where this force-release can lead to a transient token-uniqueness violation. Both in-flight releasers (hook_disposed handler and deleteAllHooksForRun) delete the claim file unconditionally after reading the hook entity. Sequence: releaser writes the dispose lock (or run goes terminal) and reads the hook entity, then stalls; this claimant observes the stale claim releasable 3×, deletes it, and writes its own fresh claim; the stalled releaser then executes its deleteJSON(constraintPath) — deleting the new claimant's live claim, so a third claimant can also claim the token. The 2×10ms grace period narrows this but doesn't close it. A cheap hardening would be for the releasers to re-read the claim and only delete it if it still points at their own (runId, hookId) (still TOCTOU, but the window shrinks from "stall of any length" to adjacent file ops), or to record the claim's identity here and have releasers skip deletion when the hook entity is already gone — which hook_disposed already partially does via the existingHook guard.

WorkflowRunSchema,
tag
);
if (!owningRun) {

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.

ai review (non-blocking): the missing-run semantics here disagree with the event-log rebuild. This treats a claim whose run file is absent as releasable, but findLiveHookCreatedEvent's isTerminalRunCache returns false for a missing run file, i.e. the rebuild treats that same hook as live and re-writes the claim. If a claim + surviving hook_created event ever exist without a run file (crash-lost run cache), the claim loop deletes the claim, the next iteration's rebuild resurrects it, and the loop ping-pongs to exhaustion — ending in the conflict branch with existingClaim = null, i.e. a durable hook_conflict with conflictingRunId: undefined, the exact symptom #2778 fixes. Unreachable today AFAICT (world-local never deletes run files), but worth aligning the two predicates — either both treat a missing run as terminal, or drop this branch.

@VaguelySerious
VaguelySerious merged commit 7637196 into mainJul 7, 2026
180 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/issue-2777 branch July 7, 2026 21:13
@VaguelySerious

Copy link
Copy Markdown
MemberAuthor

Addressing review comment in a follow-up PR

@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 7637196 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

7637196cf0f605ce62243bf8c7762a26153dcd36

@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026

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

Reviewed both halves in depth, ran the full affected suites plus the new regression tests against a live dev server. Approving.

Core (same-run, #2777): The per-token grouping is the right shape. I traced the ordering semantics:

  • Queue-insertion order within a token group = workflow code order, so dispose-of-earlier flushes before create-of-later is validated, while a created-and-disposed-in-one-suspension hook still creates first. Both directions are covered by the new unit tests.
  • The new event-log order (hook_disposed before the successor's hook_created) also matches replay consumption order — under the old code the log recorded them inverted relative to code order, so this is strictly better for replay, not just for conflict avoidance.
  • hasHookConflict / hasAwaitedHookCreation accumulation reaches the same set of creations as before, and hasHookEvents is computed from the same population — no semantic drift in the V2 re-invoke signaling.
  • The creationConflicted gate on dispose is correct belt-and-braces: a conflicted creation has nothing to dispose, and skipping avoids even the benign HookNotFoundError round-trip.
  • Cross-token parallelism is preserved, so unrelated hooks don't serialize.

world-local (cross-run, #2778): The claim loop's force-release is the risky part, so I audited its failure modes:

  • readJSON returns null only on ENOENT (anything else throws), so isHookTokenClaimReleasable can't misclassify a live claim due to a transient read error — a "missing" owning run is genuinely missing, and terminal status is monotonic.
  • Racing force-releasers and the in-flight disposer are all safe: deleteJSON is ENOENT-idempotent, and re-claim arbitration still lands on writeExclusive's EEXIST semantics, so exactly one claimant wins and the loser correctly conflicts against the winner's live claim.
  • The dispose lock as earliest-durable-disposal-marker works because the lock write already preceded the destructive deletes pre-PR — the change formalizes existing ordering rather than introducing new ordering requirements. The findLiveHookCreatedEvent lock check closing the rebuild-resurrection window is a nice catch.
  • I also confirmed the asymmetry with world-postgres is justified: there the hook row is the token claim and hook_disposed releases it via atomic DELETE ... RETURNING, so no separate-release window exists — the core fix alone covers it.

Validation: 1354/1354 core unit tests, 432/432 world-local (after full build — the 17 initial failures were unbuilt-SWC-plugin/e2e-env artifacts), 2/2 new workbench/vitest regression tests, and against a live turbopack dev server: the new hookTokenReuseLoopWorkflow e2e plus all 26 existing hook e2e tests pass. Workbench conventions followed (workflow added to workbench/example and propagated via symlink; the vitest workbench's local workflow matches its local-file convention). Changesets correctly split per package. CI: 105 pass, 1 fail = the known Benchmark Vercel (nextjs-turbopack) flake.

Two non-blocking notes:

  1. If the claim loop exhausts all 10 attempts via repeated vanish races (claim deleted between the exclusive-create attempt and every read), it falls through with existingClaim = null and still records a hook_conflict with no conflictingRunId — the #2778 symptom, now requiring ~10 consecutive lost races to reproduce. Practically unreachable, but a runtimeLogger.warn on loop exhaustion would make it diagnosable if it ever fires.
  2. The dispose locks in .locks/hooks/ are now load-bearing durable state (consulted by the claim path and the rebuild), not just transient mutexes. Worth a note somewhere discoverable that they must never be garbage-collected — a future ".locks cleanup" change would silently reintroduce the resurrection bug. The comment at hookDisposeLockPath covers the what; the hazard is for whoever writes a cleanup without reading it.

VaguelySerious added a commit that referenced this pull request Jul 7, 2026
…tall
Both in-flight releasers (the hook_disposed handler and the terminal-run
deleteAllHooksForRun cleanup) deleted the claim file unconditionally after
reading the hook entity. A releaser stalled between those operations could
outlive a claimant force-releasing its stale claim, and its deferred delete
would then destroy the new claimant's live claim — transiently breaking
token uniqueness (a third claimant could claim the token too).
Releasers now re-read the claim and delete it only if it still points at
their own (runId, hookId). Still TOCTOU, but the window shrinks from "a
stall of any length" to adjacent file ops; a claim owned by someone else
is left for the claimant-side force-release path to reap.
Addresses post-merge feedback on #2779.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

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

[core] Fix hook token reuse after dispose() (same-run and cross-run) - #2779

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777
Jul 7, 2026
Merged

[core] Fix hook token reuse after dispose() (same-run and cross-run)#2779
VaguelySerious merged 1 commit into
mainfrom
peter/issue-2777

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 6, 2026

Copy link
Copy Markdown
Member

Closes#2777, closes#2778.

Problem

Reusing a hook token after hook.dispose() deterministically or intermittently failed with a spurious HookConflictError naming an already-disposed hook:

Fix

  • @workflow/core:handleSuspension now processes hook operations grouped per token, in workflow-code order: a dispose of an earlier hook flushes before a later same-token hook's creation is validated, while a hook created and disposed within one suspension is still created first. Different tokens keep processing in parallel, and hooks still process before steps/waits.
  • @workflow/world-local: the hook_created token claim runs in a short bounded loop — it retries when the observed claim vanished mid-check, and treats a claim as vacant when its hook's disposal is committed (dispose lock exists) or its owning run is terminal/missing, releasing the stale claim if the in-flight releaser never finishes. The event-log rebuild (rebuildLiveHookByTokenFromEventLog / hooks.get*) no longer considers a hook live once its dispose lock exists. Live claims still conflict exactly as before.

- core: process hook operations per token in workflow-code order during
suspension handling, so a dispose() of an earlier hook releases the
token before a later same-token hook's creation is validated. A hook
created and disposed within the same suspension is still created
before it is disposed. Fixes the same-run recreate self-conflict
(#2777).
- world-local: retry the token claim when the observed claim vanished
mid-check or is held by a hook whose disposal is committed / a run
that is terminal or missing; never rebuild a claim from the event log
for a hook whose dispose lock exists. Fixes spurious HookConflictError
against an already-disposed hook during fast run-to-run token handoff
(#2778).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 899a288

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@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

@vercel

vercelBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 6, 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🥇 Nitro0.048s (-16.4% 🟢)1.006s (-1.3%)0.958s101.00x
💻 LocalExpress0.050s (+3.5%)1.006s (~)0.957s101.03x
💻 LocalNext.js (Turbopack)0.055s (-2.0%)1.005s (~)0.951s101.13x
🐘 PostgresNitro0.055s (-17.7% 🟢)1.012s (~)0.956s101.15x
🐘 PostgresNext.js (Turbopack)0.062s (-1.0%)1.012s (~)0.950s101.28x
🐘 PostgresExpress0.070s (+9.4% 🔺)1.012s (~)0.942s101.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.259s (+43.3% 🔺)2.147s (+31.9% 🔺)1.888s101.00x
▲ VercelExpress0.381s (+69.9% 🔺)2.003s (+12.3% 🔺)1.623s101.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.084s (~)2.007s (~)0.923s101.00x
💻 LocalNext.js (Turbopack)1.085s (-0.7%)2.006s (~)0.922s101.00x
💻 LocalExpress1.091s (+1.3%)2.007s (~)0.916s101.01x
🐘 PostgresNext.js (Turbopack)1.096s (-0.9%)2.010s (~)0.914s101.01x
🐘 PostgresExpress1.102s (+0.6%)2.009s (~)0.907s101.02x
🐘 PostgresNitro1.105s (+0.9%)2.011s (~)0.906s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.408s (-4.8%)2.719s (-18.6% 🟢)1.311s101.00x
▲ VercelNitro1.494s (+8.4% 🔺)3.455s (+4.5%)1.961s101.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.464s (~)11.023s (~)0.559s31.00x
🐘 PostgresExpress10.472s (~)11.014s (~)0.542s31.00x
🐘 PostgresNitro10.475s (~)11.019s (~)0.545s31.00x
💻 LocalNext.js (Turbopack)10.490s (~)11.022s (~)0.532s31.00x
🐘 PostgresNext.js (Turbopack)10.499s (~)11.014s (~)0.515s31.00x
💻 LocalExpress10.512s (+0.7%)11.023s (~)0.511s31.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.697s (+1.1%)13.812s (+8.1% 🔺)2.116s31.00x
▲ VercelExpress11.771s (+0.9%)13.221s (~)1.450s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)13.613s (-1.1%)14.028s (~)0.415s51.00x
🐘 PostgresExpress13.622s (~)14.020s (~)0.398s51.00x
🐘 PostgresNitro13.671s (~)14.022s (~)0.351s51.00x
💻 LocalNitro13.691s (~)14.026s (~)0.336s51.01x
💻 LocalExpress13.738s (+0.9%)14.029s (~)0.290s51.01x
🐘 PostgresNext.js (Turbopack)13.783s (+0.7%)14.018s (~)0.235s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.359s (-0.6%)18.421s (+2.4%)2.063s41.00x
▲ VercelExpress16.725s (~)18.362s (-0.7%)1.636s41.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express12.311s (+0.6%)13.019s (~)0.708s71.00x
💻 LocalNext.js (Turbopack)12.354s (-0.5%)13.026s (~)0.673s71.00x
🐘 PostgresNext.js (Turbopack)12.376s (~)13.016s (~)0.641s71.01x
💻 LocalExpress12.377s (+2.3%)13.027s (~)0.650s71.01x
🐘 PostgresNitro12.385s (~)13.019s (~)0.634s71.01x
💻 LocalNitro12.493s (+1.8%)13.025s (~)0.532s71.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro18.213s (-1.7%)20.197s (+0.7%)1.984s51.00x
▲ VercelExpress19.008s (+8.2% 🔺)20.547s (+9.1% 🔺)1.538s51.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.5%)2.009s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.199s (~)2.008s (~)0.810s151.02x
🐘 PostgresExpress1.221s (+1.1%)2.075s (+3.3%)0.853s151.04x
💻 LocalNext.js (Turbopack)1.383s (-3.5%)2.006s (~)0.623s151.18x
💻 LocalNitro1.398s (~)2.006s (~)0.608s151.19x
💻 LocalExpress1.469s (+6.5% 🔺)2.007s (~)0.538s151.25x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.157s (+2.1%)3.617s (+1.9%)1.460s91.00x
▲ VercelNitro2.165s (-3.6%)3.883s (+6.0% 🔺)1.718s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.315s (-4.1%)2.394s (-4.7%)1.079s131.00x
🐘 PostgresNitro1.319s (-2.5%)2.510s (+1.6%)1.191s121.00x
🐘 PostgresNext.js (Turbopack)1.347s (+1.7%)2.918s (-3.0%)1.571s111.02x
💻 LocalNitro2.390s (-1.0%)3.009s (~)0.619s101.82x
💻 LocalNext.js (Turbopack)2.493s (+4.3%)3.110s (+3.3%)0.617s101.90x
💻 LocalExpress2.505s (+1.1%)3.009s (-3.2%)0.504s101.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.122s (+28.6% 🔺)4.500s (+12.9% 🔺)1.378s71.00x
▲ VercelNitro3.207s (+32.4% 🔺)4.900s (+30.5% 🔺)1.694s71.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.563s (-2.7%)4.138s (-3.9%)2.575s81.00x
🐘 PostgresExpress1.616s (-4.4%)4.021s (-9.5% 🟢)2.405s81.03x
💻 LocalExpress2.767s (-21.7% 🟢)4.298s (-9.1% 🟢)1.531s71.77x
🐘 PostgresNext.js (Turbopack)2.988s (-6.2% 🟢)6.014s (+2.8%)3.026s51.91x
💻 LocalNitro4.589s (+19.5% 🔺)5.013s (+9.3% 🔺)0.424s62.94x
💻 LocalNext.js (Turbopack)4.690s (+24.0% 🔺)5.178s (+6.4% 🔺)0.488s63.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.705s (+19.6% 🔺)5.746s (+21.4% 🔺)2.041s61.00x
▲ VercelExpress4.016s (+34.9% 🔺)5.695s (+27.0% 🔺)1.678s61.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.171s (-1.8%)2.008s (~)0.838s151.00x
🐘 PostgresNext.js (Turbopack)1.188s (-5.2% 🟢)2.007s (-3.3%)0.819s151.01x
🐘 PostgresExpress1.189s (-3.4%)2.008s (-3.2%)0.819s151.02x
💻 LocalNitro1.441s (+0.6%)2.006s (~)0.565s151.23x
💻 LocalNext.js (Turbopack)1.463s (~)2.006s (~)0.543s151.25x
💻 LocalExpress1.499s (+4.8%)2.007s (~)0.508s151.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.014s (-18.8% 🟢)3.876s (+0.7%)1.861s81.00x
▲ VercelExpress2.130s (~)3.651s (+1.8%)1.521s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.317s (-3.6%)2.150s (-17.1% 🟢)0.834s141.00x
🐘 PostgresNitro1.329s (+0.9%)2.316s (-3.2%)0.987s131.01x
🐘 PostgresNext.js (Turbopack)1.435s (+7.2% 🔺)3.009s (~)1.574s101.09x
💻 LocalExpress2.563s (+4.4%)3.009s (~)0.446s101.95x
💻 LocalNitro2.579s (+4.0%)3.009s (~)0.430s101.96x
💻 LocalNext.js (Turbopack)2.611s (-1.0%)3.109s (+3.3%)0.498s101.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.413s (+2.6%)4.409s (+16.9% 🔺)1.996s71.00x
▲ VercelExpress2.441s (+8.0% 🔺)3.951s (+10.4% 🔺)1.511s81.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.525s (-5.3% 🟢)4.138s (~)2.613s81.00x
🐘 PostgresExpress1.624s (-2.0%)4.583s (+6.6% 🔺)2.959s71.06x
🐘 PostgresNext.js (Turbopack)2.970s (-9.9% 🟢)6.017s (-3.2%)3.048s61.95x
💻 LocalNitro4.840s (-14.1% 🟢)5.850s (-2.7%)1.010s63.17x
💻 LocalNext.js (Turbopack)5.395s (-3.7%)5.847s (-2.8%)0.452s63.54x
💻 LocalExpress5.662s (+3.0%)6.416s (+6.7% 🔺)0.754s53.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.748s (+2.8%)4.556s (+5.8% 🔺)1.808s71.00x
▲ VercelNitro4.034s (+40.6% 🔺)6.082s (+38.0% 🔺)2.048s61.47x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.525s (-12.6% 🟢)1.006s (-1.7%)0.482s601.00x
🐘 PostgresNitro0.557s (-4.7%)1.041s (~)0.484s581.06x
🐘 PostgresNext.js (Turbopack)0.607s (+7.8% 🔺)1.024s (+1.8%)0.417s591.16x
💻 LocalNitro0.609s (+1.5%)1.005s (~)0.397s601.16x
💻 LocalNext.js (Turbopack)0.619s (-1.9%)1.005s (~)0.386s601.18x
💻 LocalExpress0.645s (+13.6% 🔺)1.005s (~)0.360s601.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-1.6%)4.032s (-1.0%)1.496s151.00x
▲ VercelExpress2.578s (-0.7%)3.948s (+4.3%)1.370s161.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.253s (-11.5% 🟢)2.007s (-2.2%)0.754s451.00x
🐘 PostgresNitro1.296s (-4.3%)2.008s (~)0.712s451.03x
🐘 PostgresNext.js (Turbopack)1.382s (-4.0%)2.008s (-1.1%)0.625s451.10x
💻 LocalNitro1.544s (+1.4%)2.029s (+1.1%)0.485s451.23x
💻 LocalNext.js (Turbopack)1.613s (-2.6%)2.006s (-1.1%)0.393s451.29x
💻 LocalExpress1.632s (+10.2% 🔺)2.007s (-1.0%)0.375s451.30x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.805s (-3.4%)7.644s (+1.0%)1.839s121.00x
▲ VercelExpress6.114s (-1.3%)7.668s (+1.0%)1.554s121.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.595s (-2.5%)3.111s (+2.5%)0.516s391.00x
🐘 PostgresNitro2.618s (-3.2%)3.086s (~)0.468s391.01x
🐘 PostgresNext.js (Turbopack)2.725s (-1.7%)3.033s (+0.8%)0.309s401.05x
💻 LocalNext.js (Turbopack)3.398s (-5.3% 🟢)4.009s (-1.7%)0.611s301.31x
💻 LocalNitro3.416s (+5.2% 🔺)4.009s (~)0.593s301.32x
💻 LocalExpress3.557s (+12.3% 🔺)4.077s (+5.1% 🔺)0.520s301.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.756s (-6.3% 🟢)13.617s (-3.3%)1.861s91.00x
▲ VercelNitro12.119s (+1.1%)14.257s (+6.8% 🔺)2.138s91.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.189s (-9.8% 🟢)1.006s (-1.7%)0.817s601.00x
🐘 PostgresExpress0.208s (-2.5%)1.006s (~)0.798s601.10x
🐘 PostgresNitro0.210s (-5.1% 🟢)1.007s (~)0.796s601.11x
💻 LocalNitro0.532s (+6.7% 🔺)1.005s (~)0.473s602.81x
💻 LocalExpress0.562s (+15.0% 🔺)1.005s (~)0.443s602.97x
💻 LocalNext.js (Turbopack)0.605s (-7.2% 🟢)1.005s (-1.7%)0.399s603.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.091s (+3.3%)2.425s (+6.4% 🔺)1.334s261.00x
▲ VercelNitro1.108s (+12.7% 🔺)2.752s (+24.2% 🔺)1.643s221.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.326s (-2.7%)1.018s (+1.2%)0.693s891.00x
🐘 PostgresExpress0.340s (+4.9%)1.029s (+2.2%)0.689s881.04x
🐘 PostgresNext.js (Turbopack)0.352s (+18.6% 🔺)1.041s (+3.4%)0.689s871.08x
💻 LocalNitro2.465s (-3.3%)3.009s (~)0.545s307.56x
💻 LocalExpress2.503s (+1.2%)3.010s (~)0.507s307.68x
💻 LocalNext.js (Turbopack)2.675s (~)3.075s (+2.2%)0.400s308.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.430s (+7.8% 🔺)2.916s (+17.9% 🔺)1.486s311.00x
▲ VercelNitro1.491s (+7.7% 🔺)3.145s (+14.4% 🔺)1.654s291.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.516s (-3.2%)3.034s (+0.8%)2.518s401.00x
🐘 PostgresNitro0.540s (+6.8% 🔺)1.150s (+11.5% 🔺)0.610s1051.05x
🐘 PostgresExpress0.582s (+11.5% 🔺)1.160s (+7.7% 🔺)0.578s1041.13x
💻 LocalNitro5.602s (+4.4%)7.436s (-11.0% 🟢)1.834s1710.85x
💻 LocalExpress5.988s (+2.9%)8.956s (+0.8%)2.968s1411.60x
💻 LocalNext.js (Turbopack)6.274s (+3.8%)9.172s (+2.4%)2.898s1412.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.941s (+7.2% 🔺)3.965s (+16.7% 🔺)2.024s311.00x
▲ VercelExpress2.171s (+18.6% 🔺)3.898s (+6.5% 🔺)1.727s311.12x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.141s (-1.4%)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.868s101.00x
💻 LocalNext.js (Turbopack)1.151s (~)1.964s (~)0.011s (-19.2% 🟢)2.018s (~)0.867s101.01x
💻 LocalNitro1.161s (~)2.004s (~)0.012s (+3.3%)2.019s (~)0.858s101.02x
🐘 PostgresNext.js (Turbopack)1.165s (~)2.001s (~)0.001s (+8.3% 🔺)2.012s (~)0.847s101.02x
🐘 PostgresExpress1.169s (+0.6%)1.999s (~)0.001s (~)2.010s (~)0.841s101.02x
💻 LocalExpress1.173s (+1.8%)2.005s (~)0.013s (+23.1% 🔺)2.021s (~)0.848s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.962s (-7.4% 🟢)3.736s (+17.4% 🔺)2.143s (+15.8% 🔺)6.372s (+16.3% 🔺)4.410s101.00x
▲ VercelExpress2.103s (-3.2%)3.274s (-4.3%)1.979s (+5.8% 🔺)5.705s (~)3.602s101.07x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.554s (-2.4%)2.002s (~)0.005s (-2.5%)2.028s (~)0.474s301.00x
🐘 PostgresNitro1.585s (+1.1%)2.007s (~)0.005s (-5.4% 🟢)2.025s (~)0.440s301.02x
💻 LocalExpress1.592s (+0.6%)2.010s (~)0.012s (-7.6% 🟢)2.025s (~)0.433s301.02x
💻 LocalNext.js (Turbopack)1.605s (~)1.972s (~)0.014s (+0.7%)2.027s (~)0.422s301.03x
🐘 PostgresNext.js (Turbopack)1.657s (-2.0%)2.010s (-1.6%)0.005s (~)2.027s (-1.6%)0.370s301.07x
💻 LocalNitro1.907s (+21.0% 🔺)2.354s (+17.1% 🔺)0.012s (-4.3%)2.372s (+17.1% 🔺)0.464s261.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.530s (+1.6%)7.432s (+13.4% 🔺)0.328s (+71.3% 🔺)8.232s (+14.6% 🔺)2.702s81.00x
▲ VercelExpress5.684s (+5.8% 🔺)6.911s (+3.9%)0.345s (+53.1% 🔺)7.764s (+5.6% 🔺)2.080s81.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.784s (~)1.081s (+3.0%)0.000s (NaN%)1.099s (+1.7%)0.315s561.00x
🐘 PostgresNitro0.788s (+1.0%)1.032s (-3.1%)0.000s (-67.8% 🟢)1.042s (-4.2%)0.254s581.00x
🐘 PostgresNext.js (Turbopack)0.918s (-5.2% 🟢)1.333s (-7.4% 🟢)0.000s (-8.9% 🟢)1.340s (-8.5% 🟢)0.423s451.17x
💻 LocalNext.js (Turbopack)1.327s (-0.8%)1.978s (~)0.000s (-55.6% 🟢)2.017s (~)0.690s301.69x
💻 LocalNitro1.372s (~)1.919s (-3.1%)0.000s (+23.3% 🔺)1.922s (-3.1%)0.550s321.75x
💻 LocalExpress1.453s (+5.2% 🔺)1.982s (+1.6%)0.000s (+62.5% 🔺)1.985s (+1.6%)0.532s311.85x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.761s (+23.7% 🔺)5.227s (+25.2% 🔺)0.003s (+Infinity% 🔺)5.792s (+25.6% 🔺)2.032s111.00x
▲ VercelExpress3.837s (+32.6% 🔺)5.303s (+30.4% 🔺)0.000s (+154.5% 🔺)5.759s (+29.4% 🔺)1.923s111.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.640s (-10.8% 🟢)2.306s (-3.1%)0.000s (NaN%)2.317s (-3.4%)0.677s261.00x
🐘 PostgresExpress1.725s (-2.8%)2.301s (-4.0%)0.000s (-100.0% 🟢)2.318s (-3.8%)0.593s261.05x
🐘 PostgresNext.js (Turbopack)2.275s (-17.3% 🟢)2.859s (-14.3% 🟢)0.000s (-100.0% 🟢)2.868s (-14.3% 🟢)0.593s211.39x
💻 LocalNitro3.218s (+1.3%)3.775s (+2.7%)0.001s (+112.5% 🔺)3.779s (+2.7%)0.561s161.96x
💻 LocalExpress3.363s (-2.3%)3.902s (-3.1%)0.000s (-29.7% 🟢)3.905s (-3.1%)0.543s162.05x
💻 LocalNext.js (Turbopack)3.580s (+3.0%)3.992s (~)0.000s (-28.6% 🟢)4.032s (~)0.452s152.18x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.808s (+15.8% 🔺)5.847s (+10.0% 🔺)0.000s (+10.0% 🔺)6.351s (+10.9% 🔺)1.543s101.00x
▲ VercelExpress5.284s (+24.3% 🔺)6.342s (+20.6% 🔺)0.000s (NaN%)6.789s (+20.3% 🔺)1.505s101.10x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro12/21
🐘 PostgresExpress10/21
▲ VercelNitro14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres19/21
Next.js (Turbopack)🐘 Postgres14/21
Nitro🐘 Postgres18/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
Total7351010648415

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027

📋 View full workflow run

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 6, 2026 17:04
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 6, 2026 17:04

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

ai review: Approving — no blockers or regressions found.

What I checked:

  • @workflow/core suspension handler: verified the per-token grouping preserves queue-insertion (workflow-code) order, so dispose-before-recreate and create-before-dispose orderings both hold; cross-token parallelism and the hooks-before-steps/waits ordering are unchanged. hasHookEvents (hooksNeedingCreation.length > 0) is equivalent to the old hookEvents.length > 0. Skipping disposal for a conflicted creation is a behavior improvement (previously it attempted the dispose and swallowed HookNotFoundError). Abort processing (hooksNeedingAbort) is unaffected.
  • @workflow/world-local claim loop: the loop is bounded (10 attempts, ≤2×10ms sleeps), a genuinely live claim breaks out on the first iteration into the unchanged conflict path, and the own-claim dedup/recovery path is preserved (existingClaim carried out of the loop matches the old post-failure re-read). On loop exhaustion, behavior degrades to the pre-PR single-attempt semantics rather than something worse.
  • Rebuild/resurrection interaction: findLiveHookCreatedEvent already excludes terminal-run hooks (isTerminalRunCache) and now dispose-committed hooks, so a force-released claim is not resurrected by the event-log rebuild in the disposed-hook and terminal-run cases (matching the two new storage tests).
  • Dispose-lock keying: the lock path/naming is byte-identical to the old inline construction (backward compatible with locks already on disk), and hook correlation IDs are hook_${ulid} so keying by hookId alone is safe — same keying the dispose path already relied on.
  • Tests: the new @workflow/vitest workbench tests are race-free because waitForHook excludes hooks that already have a hook_received event, so each round can only observe the next round's hook; the e2e helper's new excludeHookId handles the same race for the token-lookup-based poll. CI is green including the required E2E check.

Left two non-blocking inline notes on narrow crash/race windows in the world-local force-release path.

// The releaser is not coming. Release the stale claim and
// the hook entity it points at, mirroring the hook_disposed
// cleanup.
await deleteJSON(constraintPath);

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.

ai review (non-blocking): there's a narrow cross-process window where this force-release can lead to a transient token-uniqueness violation. Both in-flight releasers (hook_disposed handler and deleteAllHooksForRun) delete the claim file unconditionally after reading the hook entity. Sequence: releaser writes the dispose lock (or run goes terminal) and reads the hook entity, then stalls; this claimant observes the stale claim releasable 3×, deletes it, and writes its own fresh claim; the stalled releaser then executes its deleteJSON(constraintPath) — deleting the new claimant's live claim, so a third claimant can also claim the token. The 2×10ms grace period narrows this but doesn't close it. A cheap hardening would be for the releasers to re-read the claim and only delete it if it still points at their own (runId, hookId) (still TOCTOU, but the window shrinks from "stall of any length" to adjacent file ops), or to record the claim's identity here and have releasers skip deletion when the hook entity is already gone — which hook_disposed already partially does via the existingHook guard.

WorkflowRunSchema,
tag
);
if (!owningRun) {

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.

ai review (non-blocking): the missing-run semantics here disagree with the event-log rebuild. This treats a claim whose run file is absent as releasable, but findLiveHookCreatedEvent's isTerminalRunCache returns false for a missing run file, i.e. the rebuild treats that same hook as live and re-writes the claim. If a claim + surviving hook_created event ever exist without a run file (crash-lost run cache), the claim loop deletes the claim, the next iteration's rebuild resurrects it, and the loop ping-pongs to exhaustion — ending in the conflict branch with existingClaim = null, i.e. a durable hook_conflict with conflictingRunId: undefined, the exact symptom #2778 fixes. Unreachable today AFAICT (world-local never deletes run files), but worth aligning the two predicates — either both treat a missing run as terminal, or drop this branch.

@VaguelySerious
VaguelySerious merged commit 7637196 into mainJul 7, 2026
180 of 184 checks passed
@VaguelySerious
VaguelySerious deleted the peter/issue-2777 branch July 7, 2026 21:13
@VaguelySerious

Copy link
Copy Markdown
MemberAuthor

Addressing review comment in a follow-up PR

@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 7637196 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

7637196cf0f605ce62243bf8c7762a26153dcd36

@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026

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

Reviewed both halves in depth, ran the full affected suites plus the new regression tests against a live dev server. Approving.

Core (same-run, #2777): The per-token grouping is the right shape. I traced the ordering semantics:

  • Queue-insertion order within a token group = workflow code order, so dispose-of-earlier flushes before create-of-later is validated, while a created-and-disposed-in-one-suspension hook still creates first. Both directions are covered by the new unit tests.
  • The new event-log order (hook_disposed before the successor's hook_created) also matches replay consumption order — under the old code the log recorded them inverted relative to code order, so this is strictly better for replay, not just for conflict avoidance.
  • hasHookConflict / hasAwaitedHookCreation accumulation reaches the same set of creations as before, and hasHookEvents is computed from the same population — no semantic drift in the V2 re-invoke signaling.
  • The creationConflicted gate on dispose is correct belt-and-braces: a conflicted creation has nothing to dispose, and skipping avoids even the benign HookNotFoundError round-trip.
  • Cross-token parallelism is preserved, so unrelated hooks don't serialize.

world-local (cross-run, #2778): The claim loop's force-release is the risky part, so I audited its failure modes:

  • readJSON returns null only on ENOENT (anything else throws), so isHookTokenClaimReleasable can't misclassify a live claim due to a transient read error — a "missing" owning run is genuinely missing, and terminal status is monotonic.
  • Racing force-releasers and the in-flight disposer are all safe: deleteJSON is ENOENT-idempotent, and re-claim arbitration still lands on writeExclusive's EEXIST semantics, so exactly one claimant wins and the loser correctly conflicts against the winner's live claim.
  • The dispose lock as earliest-durable-disposal-marker works because the lock write already preceded the destructive deletes pre-PR — the change formalizes existing ordering rather than introducing new ordering requirements. The findLiveHookCreatedEvent lock check closing the rebuild-resurrection window is a nice catch.
  • I also confirmed the asymmetry with world-postgres is justified: there the hook row is the token claim and hook_disposed releases it via atomic DELETE ... RETURNING, so no separate-release window exists — the core fix alone covers it.

Validation: 1354/1354 core unit tests, 432/432 world-local (after full build — the 17 initial failures were unbuilt-SWC-plugin/e2e-env artifacts), 2/2 new workbench/vitest regression tests, and against a live turbopack dev server: the new hookTokenReuseLoopWorkflow e2e plus all 26 existing hook e2e tests pass. Workbench conventions followed (workflow added to workbench/example and propagated via symlink; the vitest workbench's local workflow matches its local-file convention). Changesets correctly split per package. CI: 105 pass, 1 fail = the known Benchmark Vercel (nextjs-turbopack) flake.

Two non-blocking notes:

  1. If the claim loop exhausts all 10 attempts via repeated vanish races (claim deleted between the exclusive-create attempt and every read), it falls through with existingClaim = null and still records a hook_conflict with no conflictingRunId — the #2778 symptom, now requiring ~10 consecutive lost races to reproduce. Practically unreachable, but a runtimeLogger.warn on loop exhaustion would make it diagnosable if it ever fires.
  2. The dispose locks in .locks/hooks/ are now load-bearing durable state (consulted by the claim path and the rebuild), not just transient mutexes. Worth a note somewhere discoverable that they must never be garbage-collected — a future ".locks cleanup" change would silently reintroduce the resurrection bug. The comment at hookDisposeLockPath covers the what; the hazard is for whoever writes a cleanup without reading it.

VaguelySerious added a commit that referenced this pull request Jul 7, 2026
…tall
Both in-flight releasers (the hook_disposed handler and the terminal-run
deleteAllHooksForRun cleanup) deleted the claim file unconditionally after
reading the hook entity. A releaser stalled between those operations could
outlive a claimant force-releasing its stale claim, and its deferred delete
would then destroy the new claimant's live claim — transiently breaking
token uniqueness (a third claimant could claim the token too).
Releasers now re-read the claim and delete it only if it still points at
their own (runId, hookId). Still TOCTOU, but the window shrinks from "a
stall of any length" to adjacent file ops; a claim owned by someone else
is left for the claimant-side force-release path to reap.
Addresses post-merge feedback on #2779.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants

@VaguelySerious@TooTallNate@karthikscale3