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

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

changeset-bot Bot commented Jul 6, 2026

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
Name Type
@workflow/world-local Patch
@workflow/core Patch
@workflow/cli Patch
@workflow/vitest Patch
@workflow/world-postgres Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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

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

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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

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

@github-actions

github-actions Bot 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

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.048s (-16.4% 🟢) 1.006s (-1.3%) 0.958s 10 1.00x
💻 Local Express 0.050s (+3.5%) 1.006s (~) 0.957s 10 1.03x
💻 Local Next.js (Turbopack) 0.055s (-2.0%) 1.005s (~) 0.951s 10 1.13x
🐘 Postgres Nitro 0.055s (-17.7% 🟢) 1.012s (~) 0.956s 10 1.15x
🐘 Postgres Next.js (Turbopack) 0.062s (-1.0%) 1.012s (~) 0.950s 10 1.28x
🐘 Postgres Express 0.070s (+9.4% 🔺) 1.012s (~) 0.942s 10 1.45x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 0.259s (+43.3% 🔺) 2.147s (+31.9% 🔺) 1.888s 10 1.00x
▲ Vercel Express 0.381s (+69.9% 🔺) 2.003s (+12.3% 🔺) 1.623s 10 1.47x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.084s (~) 2.007s (~) 0.923s 10 1.00x
💻 Local Next.js (Turbopack) 1.085s (-0.7%) 2.006s (~) 0.922s 10 1.00x
💻 Local Express 1.091s (+1.3%) 2.007s (~) 0.916s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.096s (-0.9%) 2.010s (~) 0.914s 10 1.01x
🐘 Postgres Express 1.102s (+0.6%) 2.009s (~) 0.907s 10 1.02x
🐘 Postgres Nitro 1.105s (+0.9%) 2.011s (~) 0.906s 10 1.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.408s (-4.8%) 2.719s (-18.6% 🟢) 1.311s 10 1.00x
▲ Vercel Nitro 1.494s (+8.4% 🔺) 3.455s (+4.5%) 1.961s 10 1.06x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 10.464s (~) 11.023s (~) 0.559s 3 1.00x
🐘 Postgres Express 10.472s (~) 11.014s (~) 0.542s 3 1.00x
🐘 Postgres Nitro 10.475s (~) 11.019s (~) 0.545s 3 1.00x
💻 Local Next.js (Turbopack) 10.490s (~) 11.022s (~) 0.532s 3 1.00x
🐘 Postgres Next.js (Turbopack) 10.499s (~) 11.014s (~) 0.515s 3 1.00x
💻 Local Express 10.512s (+0.7%) 11.023s (~) 0.511s 3 1.00x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 11.697s (+1.1%) 13.812s (+8.1% 🔺) 2.116s 3 1.00x
▲ Vercel Express 11.771s (+0.9%) 13.221s (~) 1.450s 3 1.01x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 13.613s (-1.1%) 14.028s (~) 0.415s 5 1.00x
🐘 Postgres Express 13.622s (~) 14.020s (~) 0.398s 5 1.00x
🐘 Postgres Nitro 13.671s (~) 14.022s (~) 0.351s 5 1.00x
💻 Local Nitro 13.691s (~) 14.026s (~) 0.336s 5 1.01x
💻 Local Express 13.738s (+0.9%) 14.029s (~) 0.290s 5 1.01x
🐘 Postgres Next.js (Turbopack) 13.783s (+0.7%) 14.018s (~) 0.235s 5 1.01x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 16.359s (-0.6%) 18.421s (+2.4%) 2.063s 4 1.00x
▲ Vercel Express 16.725s (~) 18.362s (-0.7%) 1.636s 4 1.02x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 12.311s (+0.6%) 13.019s (~) 0.708s 7 1.00x
💻 Local Next.js (Turbopack) 12.354s (-0.5%) 13.026s (~) 0.673s 7 1.00x
🐘 Postgres Next.js (Turbopack) 12.376s (~) 13.016s (~) 0.641s 7 1.01x
💻 Local Express 12.377s (+2.3%) 13.027s (~) 0.650s 7 1.01x
🐘 Postgres Nitro 12.385s (~) 13.019s (~) 0.634s 7 1.01x
💻 Local Nitro 12.493s (+1.8%) 13.025s (~) 0.532s 7 1.01x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 18.213s (-1.7%) 20.197s (+0.7%) 1.984s 5 1.00x
▲ Vercel Express 19.008s (+8.2% 🔺) 20.547s (+9.1% 🔺) 1.538s 5 1.04x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.171s (-1.5%) 2.009s (~) 0.837s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.199s (~) 2.008s (~) 0.810s 15 1.02x
🐘 Postgres Express 1.221s (+1.1%) 2.075s (+3.3%) 0.853s 15 1.04x
💻 Local Next.js (Turbopack) 1.383s (-3.5%) 2.006s (~) 0.623s 15 1.18x
💻 Local Nitro 1.398s (~) 2.006s (~) 0.608s 15 1.19x
💻 Local Express 1.469s (+6.5% 🔺) 2.007s (~) 0.538s 15 1.25x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.157s (+2.1%) 3.617s (+1.9%) 1.460s 9 1.00x
▲ Vercel Nitro 2.165s (-3.6%) 3.883s (+6.0% 🔺) 1.718s 8 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.315s (-4.1%) 2.394s (-4.7%) 1.079s 13 1.00x
🐘 Postgres Nitro 1.319s (-2.5%) 2.510s (+1.6%) 1.191s 12 1.00x
🐘 Postgres Next.js (Turbopack) 1.347s (+1.7%) 2.918s (-3.0%) 1.571s 11 1.02x
💻 Local Nitro 2.390s (-1.0%) 3.009s (~) 0.619s 10 1.82x
💻 Local Next.js (Turbopack) 2.493s (+4.3%) 3.110s (+3.3%) 0.617s 10 1.90x
💻 Local Express 2.505s (+1.1%) 3.009s (-3.2%) 0.504s 10 1.91x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.122s (+28.6% 🔺) 4.500s (+12.9% 🔺) 1.378s 7 1.00x
▲ Vercel Nitro 3.207s (+32.4% 🔺) 4.900s (+30.5% 🔺) 1.694s 7 1.03x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.563s (-2.7%) 4.138s (-3.9%) 2.575s 8 1.00x
🐘 Postgres Express 1.616s (-4.4%) 4.021s (-9.5% 🟢) 2.405s 8 1.03x
💻 Local Express 2.767s (-21.7% 🟢) 4.298s (-9.1% 🟢) 1.531s 7 1.77x
🐘 Postgres Next.js (Turbopack) 2.988s (-6.2% 🟢) 6.014s (+2.8%) 3.026s 5 1.91x
💻 Local Nitro 4.589s (+19.5% 🔺) 5.013s (+9.3% 🔺) 0.424s 6 2.94x
💻 Local Next.js (Turbopack) 4.690s (+24.0% 🔺) 5.178s (+6.4% 🔺) 0.488s 6 3.00x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.705s (+19.6% 🔺) 5.746s (+21.4% 🔺) 2.041s 6 1.00x
▲ Vercel Express 4.016s (+34.9% 🔺) 5.695s (+27.0% 🔺) 1.678s 6 1.08x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.171s (-1.8%) 2.008s (~) 0.838s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.188s (-5.2% 🟢) 2.007s (-3.3%) 0.819s 15 1.01x
🐘 Postgres Express 1.189s (-3.4%) 2.008s (-3.2%) 0.819s 15 1.02x
💻 Local Nitro 1.441s (+0.6%) 2.006s (~) 0.565s 15 1.23x
💻 Local Next.js (Turbopack) 1.463s (~) 2.006s (~) 0.543s 15 1.25x
💻 Local Express 1.499s (+4.8%) 2.007s (~) 0.508s 15 1.28x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.014s (-18.8% 🟢) 3.876s (+0.7%) 1.861s 8 1.00x
▲ Vercel Express 2.130s (~) 3.651s (+1.8%) 1.521s 9 1.06x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.317s (-3.6%) 2.150s (-17.1% 🟢) 0.834s 14 1.00x
🐘 Postgres Nitro 1.329s (+0.9%) 2.316s (-3.2%) 0.987s 13 1.01x
🐘 Postgres Next.js (Turbopack) 1.435s (+7.2% 🔺) 3.009s (~) 1.574s 10 1.09x
💻 Local Express 2.563s (+4.4%) 3.009s (~) 0.446s 10 1.95x
💻 Local Nitro 2.579s (+4.0%) 3.009s (~) 0.430s 10 1.96x
💻 Local Next.js (Turbopack) 2.611s (-1.0%) 3.109s (+3.3%) 0.498s 10 1.98x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.413s (+2.6%) 4.409s (+16.9% 🔺) 1.996s 7 1.00x
▲ Vercel Express 2.441s (+8.0% 🔺) 3.951s (+10.4% 🔺) 1.511s 8 1.01x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro | Express

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.525s (-5.3% 🟢) 4.138s (~) 2.613s 8 1.00x
🐘 Postgres Express 1.624s (-2.0%) 4.583s (+6.6% 🔺) 2.959s 7 1.06x
🐘 Postgres Next.js (Turbopack) 2.970s (-9.9% 🟢) 6.017s (-3.2%) 3.048s 6 1.95x
💻 Local Nitro 4.840s (-14.1% 🟢) 5.850s (-2.7%) 1.010s 6 3.17x
💻 Local Next.js (Turbopack) 5.395s (-3.7%) 5.847s (-2.8%) 0.452s 6 3.54x
💻 Local Express 5.662s (+3.0%) 6.416s (+6.7% 🔺) 0.754s 5 3.71x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.748s (+2.8%) 4.556s (+5.8% 🔺) 1.808s 7 1.00x
▲ Vercel Nitro 4.034s (+40.6% 🔺) 6.082s (+38.0% 🔺) 2.048s 6 1.47x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.525s (-12.6% 🟢) 1.006s (-1.7%) 0.482s 60 1.00x
🐘 Postgres Nitro 0.557s (-4.7%) 1.041s (~) 0.484s 58 1.06x
🐘 Postgres Next.js (Turbopack) 0.607s (+7.8% 🔺) 1.024s (+1.8%) 0.417s 59 1.16x
💻 Local Nitro 0.609s (+1.5%) 1.005s (~) 0.397s 60 1.16x
💻 Local Next.js (Turbopack) 0.619s (-1.9%) 1.005s (~) 0.386s 60 1.18x
💻 Local Express 0.645s (+13.6% 🔺) 1.005s (~) 0.360s 60 1.23x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.537s (-1.6%) 4.032s (-1.0%) 1.496s 15 1.00x
▲ Vercel Express 2.578s (-0.7%) 3.948s (+4.3%) 1.370s 16 1.02x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.253s (-11.5% 🟢) 2.007s (-2.2%) 0.754s 45 1.00x
🐘 Postgres Nitro 1.296s (-4.3%) 2.008s (~) 0.712s 45 1.03x
🐘 Postgres Next.js (Turbopack) 1.382s (-4.0%) 2.008s (-1.1%) 0.625s 45 1.10x
💻 Local Nitro 1.544s (+1.4%) 2.029s (+1.1%) 0.485s 45 1.23x
💻 Local Next.js (Turbopack) 1.613s (-2.6%) 2.006s (-1.1%) 0.393s 45 1.29x
💻 Local Express 1.632s (+10.2% 🔺) 2.007s (-1.0%) 0.375s 45 1.30x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.805s (-3.4%) 7.644s (+1.0%) 1.839s 12 1.00x
▲ Vercel Express 6.114s (-1.3%) 7.668s (+1.0%) 1.554s 12 1.05x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.595s (-2.5%) 3.111s (+2.5%) 0.516s 39 1.00x
🐘 Postgres Nitro 2.618s (-3.2%) 3.086s (~) 0.468s 39 1.01x
🐘 Postgres Next.js (Turbopack) 2.725s (-1.7%) 3.033s (+0.8%) 0.309s 40 1.05x
💻 Local Next.js (Turbopack) 3.398s (-5.3% 🟢) 4.009s (-1.7%) 0.611s 30 1.31x
💻 Local Nitro 3.416s (+5.2% 🔺) 4.009s (~) 0.593s 30 1.32x
💻 Local Express 3.557s (+12.3% 🔺) 4.077s (+5.1% 🔺) 0.520s 30 1.37x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 11.756s (-6.3% 🟢) 13.617s (-3.3%) 1.861s 9 1.00x
▲ Vercel Nitro 12.119s (+1.1%) 14.257s (+6.8% 🔺) 2.138s 9 1.03x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.189s (-9.8% 🟢) 1.006s (-1.7%) 0.817s 60 1.00x
🐘 Postgres Express 0.208s (-2.5%) 1.006s (~) 0.798s 60 1.10x
🐘 Postgres Nitro 0.210s (-5.1% 🟢) 1.007s (~) 0.796s 60 1.11x
💻 Local Nitro 0.532s (+6.7% 🔺) 1.005s (~) 0.473s 60 2.81x
💻 Local Express 0.562s (+15.0% 🔺) 1.005s (~) 0.443s 60 2.97x
💻 Local Next.js (Turbopack) 0.605s (-7.2% 🟢) 1.005s (-1.7%) 0.399s 60 3.20x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.091s (+3.3%) 2.425s (+6.4% 🔺) 1.334s 26 1.00x
▲ Vercel Nitro 1.108s (+12.7% 🔺) 2.752s (+24.2% 🔺) 1.643s 22 1.02x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.326s (-2.7%) 1.018s (+1.2%) 0.693s 89 1.00x
🐘 Postgres Express 0.340s (+4.9%) 1.029s (+2.2%) 0.689s 88 1.04x
🐘 Postgres Next.js (Turbopack) 0.352s (+18.6% 🔺) 1.041s (+3.4%) 0.689s 87 1.08x
💻 Local Nitro 2.465s (-3.3%) 3.009s (~) 0.545s 30 7.56x
💻 Local Express 2.503s (+1.2%) 3.010s (~) 0.507s 30 7.68x
💻 Local Next.js (Turbopack) 2.675s (~) 3.075s (+2.2%) 0.400s 30 8.21x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.430s (+7.8% 🔺) 2.916s (+17.9% 🔺) 1.486s 31 1.00x
▲ Vercel Nitro 1.491s (+7.7% 🔺) 3.145s (+14.4% 🔺) 1.654s 29 1.04x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Express | Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.516s (-3.2%) 3.034s (+0.8%) 2.518s 40 1.00x
🐘 Postgres Nitro 0.540s (+6.8% 🔺) 1.150s (+11.5% 🔺) 0.610s 105 1.05x
🐘 Postgres Express 0.582s (+11.5% 🔺) 1.160s (+7.7% 🔺) 0.578s 104 1.13x
💻 Local Nitro 5.602s (+4.4%) 7.436s (-11.0% 🟢) 1.834s 17 10.85x
💻 Local Express 5.988s (+2.9%) 8.956s (+0.8%) 2.968s 14 11.60x
💻 Local Next.js (Turbopack) 6.274s (+3.8%) 9.172s (+2.4%) 2.898s 14 12.16x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.941s (+7.2% 🔺) 3.965s (+16.7% 🔺) 2.024s 31 1.00x
▲ Vercel Express 2.171s (+18.6% 🔺) 3.898s (+6.5% 🔺) 1.727s 31 1.12x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -

🔍 Observability: Nitro | Express

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.141s (-1.4%) 2.000s (~) 0.001s (-7.7% 🟢) 2.010s (~) 0.868s 10 1.00x
💻 Local Next.js (Turbopack) 1.151s (~) 1.964s (~) 0.011s (-19.2% 🟢) 2.018s (~) 0.867s 10 1.01x
💻 Local Nitro 1.161s (~) 2.004s (~) 0.012s (+3.3%) 2.019s (~) 0.858s 10 1.02x
🐘 Postgres Next.js (Turbopack) 1.165s (~) 2.001s (~) 0.001s (+8.3% 🔺) 2.012s (~) 0.847s 10 1.02x
🐘 Postgres Express 1.169s (+0.6%) 1.999s (~) 0.001s (~) 2.010s (~) 0.841s 10 1.02x
💻 Local Express 1.173s (+1.8%) 2.005s (~) 0.013s (+23.1% 🔺) 2.021s (~) 0.848s 10 1.03x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.962s (-7.4% 🟢) 3.736s (+17.4% 🔺) 2.143s (+15.8% 🔺) 6.372s (+16.3% 🔺) 4.410s 10 1.00x
▲ Vercel Express 2.103s (-3.2%) 3.274s (-4.3%) 1.979s (+5.8% 🔺) 5.705s (~) 3.602s 10 1.07x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.554s (-2.4%) 2.002s (~) 0.005s (-2.5%) 2.028s (~) 0.474s 30 1.00x
🐘 Postgres Nitro 1.585s (+1.1%) 2.007s (~) 0.005s (-5.4% 🟢) 2.025s (~) 0.440s 30 1.02x
💻 Local Express 1.592s (+0.6%) 2.010s (~) 0.012s (-7.6% 🟢) 2.025s (~) 0.433s 30 1.02x
💻 Local Next.js (Turbopack) 1.605s (~) 1.972s (~) 0.014s (+0.7%) 2.027s (~) 0.422s 30 1.03x
🐘 Postgres Next.js (Turbopack) 1.657s (-2.0%) 2.010s (-1.6%) 0.005s (~) 2.027s (-1.6%) 0.370s 30 1.07x
💻 Local Nitro 1.907s (+21.0% 🔺) 2.354s (+17.1% 🔺) 0.012s (-4.3%) 2.372s (+17.1% 🔺) 0.464s 26 1.23x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.530s (+1.6%) 7.432s (+13.4% 🔺) 0.328s (+71.3% 🔺) 8.232s (+14.6% 🔺) 2.702s 8 1.00x
▲ Vercel Express 5.684s (+5.8% 🔺) 6.911s (+3.9%) 0.345s (+53.1% 🔺) 7.764s (+5.6% 🔺) 2.080s 8 1.03x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.784s (~) 1.081s (+3.0%) 0.000s (NaN%) 1.099s (+1.7%) 0.315s 56 1.00x
🐘 Postgres Nitro 0.788s (+1.0%) 1.032s (-3.1%) 0.000s (-67.8% 🟢) 1.042s (-4.2%) 0.254s 58 1.00x
🐘 Postgres Next.js (Turbopack) 0.918s (-5.2% 🟢) 1.333s (-7.4% 🟢) 0.000s (-8.9% 🟢) 1.340s (-8.5% 🟢) 0.423s 45 1.17x
💻 Local Next.js (Turbopack) 1.327s (-0.8%) 1.978s (~) 0.000s (-55.6% 🟢) 2.017s (~) 0.690s 30 1.69x
💻 Local Nitro 1.372s (~) 1.919s (-3.1%) 0.000s (+23.3% 🔺) 1.922s (-3.1%) 0.550s 32 1.75x
💻 Local Express 1.453s (+5.2% 🔺) 1.982s (+1.6%) 0.000s (+62.5% 🔺) 1.985s (+1.6%) 0.532s 31 1.85x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.761s (+23.7% 🔺) 5.227s (+25.2% 🔺) 0.003s (+Infinity% 🔺) 5.792s (+25.6% 🔺) 2.032s 11 1.00x
▲ Vercel Express 3.837s (+32.6% 🔺) 5.303s (+30.4% 🔺) 0.000s (+154.5% 🔺) 5.759s (+29.4% 🔺) 1.923s 11 1.02x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -

🔍 Observability: Nitro | Express

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.640s (-10.8% 🟢) 2.306s (-3.1%) 0.000s (NaN%) 2.317s (-3.4%) 0.677s 26 1.00x
🐘 Postgres Express 1.725s (-2.8%) 2.301s (-4.0%) 0.000s (-100.0% 🟢) 2.318s (-3.8%) 0.593s 26 1.05x
🐘 Postgres Next.js (Turbopack) 2.275s (-17.3% 🟢) 2.859s (-14.3% 🟢) 0.000s (-100.0% 🟢) 2.868s (-14.3% 🟢) 0.593s 21 1.39x
💻 Local Nitro 3.218s (+1.3%) 3.775s (+2.7%) 0.001s (+112.5% 🔺) 3.779s (+2.7%) 0.561s 16 1.96x
💻 Local Express 3.363s (-2.3%) 3.902s (-3.1%) 0.000s (-29.7% 🟢) 3.905s (-3.1%) 0.543s 16 2.05x
💻 Local Next.js (Turbopack) 3.580s (+3.0%) 3.992s (~) 0.000s (-28.6% 🟢) 4.032s (~) 0.452s 15 2.18x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.808s (+15.8% 🔺) 5.847s (+10.0% 🔺) 0.000s (+10.0% 🔺) 6.351s (+10.9% 🔺) 1.543s 10 1.00x
▲ Vercel Express 5.284s (+24.3% 🔺) 6.342s (+20.6% 🔺) 0.000s (NaN%) 6.789s (+20.3% 🔺) 1.505s 10 1.10x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

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

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 19/21
Next.js (Turbopack) 🐘 Postgres 14/21
Nitro 🐘 Postgres 18/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-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1453 0 230 1683
✅ 💻 Local Development 1617 0 219 1836
✅ 📦 Local Production 1617 0 219 1836
✅ 🐘 Local Postgres 1617 0 219 1836
✅ 🪟 Windows 153 0 0 153
✅ 📋 Other 894 0 177 1071
Total 7351 0 1064 8415

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 126 0 27
✅ example 126 0 27
✅ express 126 0 27
✅ fastify 126 0 27
✅ hono 126 0 27
✅ nextjs-turbopack 150 0 3
✅ nextjs-webpack 150 0 3
✅ nitro 126 0 27
✅ nuxt 126 0 27
✅ sveltekit 145 0 8
✅ vite 126 0 27
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 153 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 25
✅ e2e-local-dev-tanstack-start- 128 0 25
✅ e2e-local-postgres-nest-stable 128 0 25
✅ e2e-local-postgres-tanstack-start- 128 0 25
✅ e2e-local-prod-nest-stable 128 0 25
✅ e2e-local-prod-tanstack-start- 128 0 25
✅ e2e-vercel-prod-tanstack-start 126 0 27

📋 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 owners July 6, 2026 17:04

@karthikscale3 karthikscale3 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 main Jul 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
Member Author

Addressing review comment in a follow-up PR

@github-actions

github-actions Bot commented Jul 7, 2026

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-actions github-actions Bot mentioned this pull request Jul 7, 2026

@TooTallNate TooTallNate 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants