Skip to content

docs: document run idempotency - #2011

Merged
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency
Jun 14, 2026
Merged

docs: document run idempotency#2011
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expand the foundations and cookbook idempotency docs to cover run idempotency in addition to step side-effect idempotency
  • document that start() currently creates a new run for each call and does not accept an idempotency key
  • document the deterministic hook-token claim pattern using hook.getConflict() (Add hook.hasConflict for early hook conflict detection #2015, revised by Replace hook.hasConflict with hook.getConflict() returning the conflicting Run #2373): the workflow creates a hook with a domain-derived token and checks const conflict = await hook.getConflict() before duplicate-sensitive work — awaiting getConflict() suspends the workflow to commit the hook registration and resolves with the conflicting Run (v5) / { runId } (v4) when another active run owns the token, or null once registered
  • add a conflict-handling strategies guide to the run-idempotency docs: instead of static ID-reuse policies (cf. Temporal's Workflow ID conflict policy), the duplicate run handles the conflict in code — reject the duplicate, adopt the owner's result via conflict.returnValue, inspect conflict.status before deciding, signal the owner via resumeHook() from a step, or supersede it via conflict.cancel() and re-claim the token
  • add cross-links/callouts from relevant API reference and cookbook pages: start(), getRun(), hook APIs, workflow fetch(), child workflows, session integrations, scheduling, timeouts, and publishing-library examples

Note

Stacked on #2373 (hook.getConflict(), the follow-up to the merged #2015): this PR's base is that branch, so the diff here is the idempotency docs only. Merge #2373 first — GitHub will retarget this PR to main automatically.

Notes

  • Current guidance is to reserve an application/business key atomically before calling start() when duplicate run creation is unsafe.
  • Hook-token lookup (getHookByToken()) is documented as an advisory pre-check, not atomic enough to replace run idempotency; the race is resolved inside the workflow via hook.getConflict().
  • The route example detects the concurrent-start race by comparing resumeHook()'s runId against the started run's runId — race-correct without blocking on workflow completion.

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
Foundations: Idempotency (new Run idempotency section + conflict-handling strategies)v4v5
Cookbook: Idempotencyv4v5
start() API referencev4v5
getHookByToken() API referencev4v5
getRun() API referencev4v5
resumeHook() API referencev4v5
createHook() API referencev4v5
fetch() API referencev4v5
World storage referencev4v5

Smaller idempotency cross-link callouts were also added to the scheduling, timeouts, workflow composition, child workflows, publishing libraries, AI SDK, Chat SDK, and Sandbox cookbook pages (same paths under /v5/docs for v5).

Validation

  • pnpm test:docs (921 samples typechecked, 2 files) — also fixed a pre-existing failure in the run-idempotency route sample with a @skip-typecheck cross-block marker
  • git diff --check

@vercel

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd22586

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production123302171450
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7634010668700

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+2.1%)1.006s (~)0.962s101.00x
💻 LocalExpress0.052s (+22.6% 🔺)1.007s (~)0.955s101.20x
🐘 PostgresExpress0.062s (+0.7%)1.014s (~)0.952s101.43x
💻 LocalNext.js (Turbopack)0.063s (~)1.006s (~)0.943s101.46x
🐘 PostgresNitro0.079s (+19.8% 🔺)1.016s (~)0.938s101.82x
🐘 PostgresNext.js (Turbopack)0.082s (+9.8% 🔺)1.019s (~)0.937s101.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.285s (-24.6% 🟢)2.322s (+10.6% 🔺)2.037s101.00x
▲ VercelExpress0.370s (+36.7% 🔺)2.299s (-10.9% 🟢)1.929s101.30x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.089s (~)2.007s (~)0.917s101.00x
🐘 PostgresExpress1.107s (-0.7%)2.009s (~)0.902s101.02x
💻 LocalExpress1.116s (+2.0%)2.007s (~)0.891s101.02x
🐘 PostgresNitro1.124s (+1.0%)2.011s (~)0.887s101.03x
💻 LocalNext.js (Turbopack)1.134s (+0.6%)2.007s (~)0.872s101.04x
🐘 PostgresNext.js (Turbopack)1.242s (+8.6% 🔺)2.036s (+1.3%)0.794s101.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.668s (-4.9%)3.484s (-6.1% 🟢)1.816s101.00x
▲ VercelNitro1.743s (+4.0%)3.822s (+10.8% 🔺)2.079s101.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.489s (-0.5%)11.022s (~)0.533s31.00x
🐘 PostgresExpress10.557s (~)11.018s (~)0.461s31.01x
💻 LocalExpress10.575s (~)11.024s (~)0.449s31.01x
🐘 PostgresNitro10.645s (~)11.022s (~)0.377s31.01x
💻 LocalNext.js (Turbopack)10.790s (~)11.022s (~)0.232s31.03x
🐘 PostgresNext.js (Turbopack)10.993s (~)11.353s (-2.9%)0.360s31.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.741s (+2.4%)15.369s (-2.3%)1.628s21.00x
▲ VercelNitro13.963s (+4.7%)16.017s (+5.8% 🔺)2.053s21.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.686s (-1.1%)14.027s (-1.4%)0.341s51.00x
🐘 PostgresExpress13.855s (~)14.017s (~)0.163s51.01x
🐘 PostgresNitro13.898s (+0.6%)14.023s (~)0.124s51.02x
💻 LocalExpress13.953s (+0.8%)14.228s (+1.4%)0.276s51.02x
🐘 PostgresNext.js (Turbopack)14.310s (-1.3%)15.060s (~)0.750s41.05x
💻 LocalNext.js (Turbopack)14.522s (~)15.030s (~)0.508s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.492s (+4.7%)24.394s (+5.7% 🔺)1.902s31.00x
▲ VercelNitro23.148s (+3.2%)25.776s (+7.1% 🔺)2.628s31.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.317s (-1.2%)13.025s (~)0.707s71.00x
🐘 PostgresExpress12.662s (-0.6%)13.017s (~)0.355s71.03x
💻 LocalExpress12.688s (+1.5%)13.026s (~)0.338s71.03x
🐘 PostgresNitro12.934s (+2.8%)13.449s (+3.3%)0.515s71.05x
💻 LocalNext.js (Turbopack)13.803s (+0.9%)14.171s (+1.0%)0.368s71.12x
🐘 PostgresNext.js (Turbopack)14.928s (+6.8% 🔺)15.370s (+7.4% 🔺)0.442s61.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express30.956s (+4.3%)32.926s (+5.7% 🔺)1.970s31.00x
▲ VercelNitro31.388s (-1.0%)33.630s (+1.0%)2.242s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.206s (~)2.007s (~)0.802s151.00x
🐘 PostgresNext.js (Turbopack)1.221s (-3.0%)2.007s (~)0.786s151.01x
💻 LocalNitro1.223s (~)2.006s (~)0.783s151.01x
🐘 PostgresNitro1.260s (+4.7%)2.008s (~)0.748s151.04x
💻 LocalExpress1.305s (+9.7% 🔺)2.007s (~)0.702s151.08x
💻 LocalNext.js (Turbopack)1.338s (-0.8%)2.006s (~)0.668s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.372s (-7.1% 🟢)3.556s (-21.0% 🟢)1.185s91.00x
▲ VercelNitro2.462s (-33.4% 🟢)4.068s (-21.5% 🟢)1.606s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.364s (+8.4% 🔺)2.735s (+36.2% 🔺)1.371s111.00x
🐘 PostgresExpress1.461s (+15.8% 🔺)2.509s (+25.0% 🔺)1.048s121.07x
🐘 PostgresNext.js (Turbopack)1.512s (+7.9% 🔺)2.331s (+16.2% 🔺)0.819s131.11x
💻 LocalNitro1.697s (~)2.005s (~)0.309s151.24x
💻 LocalNext.js (Turbopack)1.742s (-7.3% 🟢)2.074s (-3.5%)0.332s151.28x
💻 LocalExpress2.242s (+21.7% 🔺)2.675s (+24.4% 🔺)0.433s121.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.639s (-40.3% 🟢)4.429s (-27.6% 🟢)1.790s71.00x
▲ VercelNitro2.882s (-53.8% 🟢)5.096s (-34.2% 🟢)2.215s61.09x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.610s (+14.4% 🔺)4.010s (+99.7% 🔺)2.400s81.00x
🐘 PostgresExpress1.668s (+19.0% 🔺)3.887s (+93.5% 🔺)2.219s81.04x
🐘 PostgresNext.js (Turbopack)3.217s (+78.3% 🔺)4.165s (+81.3% 🔺)0.949s82.00x
💻 LocalNitro4.405s (-16.1% 🟢)5.014s (-16.7% 🟢)0.609s62.74x
💻 LocalNext.js (Turbopack)4.848s (-15.0% 🟢)5.300s (-14.7% 🟢)0.452s73.01x
💻 LocalExpress7.792s (+44.7% 🔺)8.518s (+37.8% 🔺)0.727s44.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.708s (-78.2% 🟢)5.818s (-68.2% 🟢)2.109s61.00x
▲ VercelExpress4.596s (-49.6% 🟢)6.296s (-44.6% 🟢)1.700s61.24x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.203s (-0.6%)2.007s (~)0.804s151.00x
🐘 PostgresNitro1.229s (+3.2%)2.007s (~)0.778s151.02x
💻 LocalNext.js (Turbopack)1.394s (+3.1%)2.007s (~)0.613s151.16x
🐘 PostgresNext.js (Turbopack)1.427s (+13.0% 🔺)2.018s (~)0.591s151.19x
💻 LocalNitro1.463s (-3.0%)2.006s (~)0.543s151.22x
💻 LocalExpress1.671s (+0.7%)2.074s (~)0.404s151.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.403s (-6.8% 🟢)4.237s (~)1.834s81.00x
▲ VercelExpress2.475s (-8.3% 🟢)4.128s (-12.4% 🟢)1.653s81.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.333s (+4.8%)2.507s (+24.9% 🔺)1.175s121.00x
🐘 PostgresExpress1.383s (+6.7% 🔺)2.393s (+19.1% 🔺)1.010s131.04x
🐘 PostgresNext.js (Turbopack)1.610s (+14.7% 🔺)2.481s (+23.5% 🔺)0.871s131.21x
💻 LocalNitro1.819s (-16.2% 🟢)2.077s (-19.9% 🟢)0.258s151.37x
💻 LocalNext.js (Turbopack)2.093s (+2.3%)2.827s (-6.0% 🟢)0.734s111.57x
💻 LocalExpress2.165s (~)2.593s (-3.1%)0.428s121.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (-18.2% 🟢)4.442s (-12.9% 🟢)1.639s71.00x
▲ VercelNitro3.091s (-36.0% 🟢)4.926s (-23.1% 🟢)1.835s71.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (+12.8% 🔺)4.013s (+99.6% 🔺)2.431s81.00x
🐘 PostgresNitro1.710s (+23.1% 🔺)4.300s (+114.1% 🔺)2.590s71.08x
🐘 PostgresNext.js (Turbopack)2.745s (+51.8% 🔺)3.594s (+61.6% 🔺)0.849s91.74x
💻 LocalNitro4.968s (-8.6% 🟢)5.514s (-8.3% 🟢)0.546s63.14x
💻 LocalNext.js (Turbopack)5.505s (-9.1% 🟢)6.217s (-8.8% 🟢)0.712s53.48x
💻 LocalExpress5.794s (-8.8% 🟢)6.415s (-8.5% 🟢)0.621s53.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.609s (-29.3% 🟢)5.591s (-22.8% 🟢)1.982s61.00x
▲ VercelNitro3.647s (-47.9% 🟢)5.583s (-35.1% 🟢)1.937s61.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.575s (-4.2%)1.006s (~)0.431s601.00x
💻 LocalNitro0.582s (-2.9%)1.005s (~)0.422s601.01x
🐘 PostgresNitro0.591s (+2.0%)1.023s (+1.6%)0.432s591.03x
💻 LocalExpress0.641s (+4.8%)1.005s (~)0.364s601.12x
🐘 PostgresNext.js (Turbopack)0.736s (-13.8% 🟢)1.067s (+4.2%)0.331s571.28x
💻 LocalNext.js (Turbopack)0.907s (+5.1% 🔺)1.096s (+7.2% 🔺)0.189s551.58x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.639s (+13.7% 🔺)7.687s (+18.0% 🔺)2.048s81.00x
▲ VercelExpress5.991s (-40.9% 🟢)7.332s (-37.7% 🟢)1.341s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.367s (-4.7%)2.007s (~)0.640s451.00x
🐘 PostgresNitro1.378s (-4.0%)2.007s (-1.2%)0.629s451.01x
💻 LocalNitro1.487s (-1.2%)2.005s (~)0.519s451.09x
💻 LocalExpress1.609s (+3.5%)2.007s (-1.1%)0.398s451.18x
🐘 PostgresNext.js (Turbopack)1.664s (-18.0% 🟢)2.177s (-18.0% 🟢)0.513s421.22x
💻 LocalNext.js (Turbopack)2.127s (+1.3%)3.008s (~)0.881s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.875s (-10.4% 🟢)14.900s (-7.7% 🟢)2.025s71.00x
▲ VercelNitro13.500s (+3.1%)15.881s (+9.8% 🔺)2.381s61.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.761s (-1.7%)3.166s (+0.9%)0.405s381.00x
🐘 PostgresNitro2.843s (+1.7%)3.166s (~)0.323s381.03x
💻 LocalNitro3.175s (-3.7%)3.977s (-0.8%)0.801s311.15x
💻 LocalExpress3.384s (+3.0%)4.009s (~)0.625s301.23x
🐘 PostgresNext.js (Turbopack)3.485s (-12.5% 🟢)3.947s (-8.9% 🟢)0.462s311.26x
💻 LocalNext.js (Turbopack)4.440s (+2.2%)5.011s (~)0.571s241.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.690s (-2.1%)28.082s (~)2.392s51.00x
▲ VercelNitro26.004s (-10.7% 🟢)28.926s (-6.1% 🟢)2.922s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.250s (+7.5% 🔺)1.006s (~)0.756s601.00x
🐘 PostgresNitro0.259s (+17.9% 🔺)1.006s (~)0.748s601.03x
🐘 PostgresNext.js (Turbopack)0.351s (+26.5% 🔺)1.036s (+2.9%)0.685s581.40x
💻 LocalNitro0.392s (-8.8% 🟢)1.004s (-1.7%)0.613s601.57x
💻 LocalExpress0.461s (+11.6% 🔺)1.022s (+1.7%)0.561s591.84x
💻 LocalNext.js (Turbopack)0.583s (+6.8% 🔺)1.022s (+1.7%)0.439s592.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.026s (-21.1% 🟢)3.667s (-10.7% 🟢)1.640s171.00x
▲ VercelNitro2.038s (-27.8% 🟢)4.051s (-6.2% 🟢)2.013s151.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.400s (+11.6% 🔺)1.028s (+2.2%)0.628s881.00x
🐘 PostgresExpress0.412s (+10.0% 🔺)1.029s (+2.3%)0.617s881.03x
🐘 PostgresNext.js (Turbopack)0.564s (+13.1% 🔺)1.260s (+25.2% 🔺)0.696s721.41x
💻 LocalNitro2.162s (-0.9%)2.715s (~)0.553s345.40x
💻 LocalExpress2.212s (+8.6% 🔺)2.822s (+11.3% 🔺)0.609s325.53x
💻 LocalNext.js (Turbopack)2.505s (+2.2%)3.227s (+2.5%)0.722s286.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.891s (-68.7% 🟢)4.729s (-56.5% 🟢)1.838s201.00x
▲ VercelExpress2.949s (-45.5% 🟢)4.331s (-38.8% 🟢)1.381s211.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.803s (+15.4% 🔺)1.297s (+28.9% 🔺)0.494s931.00x
🐘 PostgresNitro0.806s (+18.3% 🔺)1.471s (+46.2% 🔺)0.665s821.00x
🐘 PostgresNext.js (Turbopack)1.468s (+46.0% 🔺)2.515s (+35.6% 🔺)1.047s481.83x
💻 LocalNitro9.306s (~)10.029s (+2.4%)0.724s1211.58x
💻 LocalExpress9.959s (+6.9% 🔺)10.530s (+6.7% 🔺)0.570s1212.39x
💻 LocalNext.js (Turbopack)10.701s (-2.2%)11.483s (~)0.782s1113.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.765s (-70.1% 🟢)7.499s (-66.1% 🟢)1.734s171.00x
▲ VercelExpress6.168s (-67.4% 🟢)7.892s (-61.6% 🟢)1.724s161.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.162s (~)2.004s (~)0.010s (-1.9%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (~)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.841s101.01x
🐘 PostgresExpress1.174s (~)1.994s (~)0.001s (~)2.011s (~)0.837s101.01x
💻 LocalExpress1.188s (+1.7%)2.006s (~)0.013s (+9.4% 🔺)2.022s (~)0.833s101.02x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.011s (-13.7% 🟢)2.018s (~)0.807s101.04x
🐘 PostgresNext.js (Turbopack)1.358s (+9.1% 🔺)2.003s (~)0.066s (+4969.2% 🔺)2.077s (+3.3%)0.719s101.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.185s (-10.7% 🟢)3.320s (-1.3%)0.962s (+1.4%)4.733s (-0.7%)2.548s101.00x
▲ VercelExpress2.517s (+16.1% 🔺)3.329s (+3.9%)1.273s (+40.1% 🔺)5.116s (+11.7% 🔺)2.599s101.15x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.565s (-1.8%)2.009s (~)0.012s (-5.3% 🟢)2.023s (~)0.458s301.00x
🐘 PostgresExpress1.587s (-0.9%)2.005s (~)0.005s (-5.2% 🟢)2.029s (~)0.442s301.01x
🐘 PostgresNitro1.587s (-1.6%)2.002s (~)0.005s (+2.0%)2.024s (~)0.437s301.01x
💻 LocalExpress1.609s (+2.3%)2.011s (~)0.012s (+6.0% 🔺)2.026s (~)0.417s301.03x
💻 LocalNext.js (Turbopack)1.736s (~)2.008s (~)0.012s (-1.6%)2.024s (~)0.288s301.11x
🐘 PostgresNext.js (Turbopack)2.022s (+13.2% 🔺)2.448s (+21.8% 🔺)0.004s (-20.0% 🟢)2.465s (+21.6% 🔺)0.443s251.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.128s (+3.5%)7.309s (+3.9%)0.222s (-52.7% 🟢)8.010s (~)1.883s81.00x
▲ VercelExpress6.224s (+2.5%)7.752s (+6.2% 🔺)0.326s (+23.9% 🔺)8.575s (+7.2% 🔺)2.351s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.775s (+9.6% 🔺)1.029s (+1.5%)0.000s (+3.5%)1.058s (+2.9%)0.283s571.00x
🐘 PostgresNitro0.792s (+11.8% 🔺)1.065s (+2.1%)0.000s (-66.1% 🟢)1.078s (+1.4%)0.286s561.02x
🐘 PostgresNext.js (Turbopack)1.277s (+48.1% 🔺)1.875s (+71.9% 🔺)0.000s (-14.1% 🟢)1.890s (+72.0% 🔺)0.613s321.65x
💻 LocalNitro1.352s (-6.1% 🟢)2.013s (~)0.000s (+80.0% 🔺)2.015s (~)0.663s301.74x
💻 LocalExpress1.430s (+5.9% 🔺)2.014s (~)0.000s (+200.0% 🔺)2.016s (~)0.586s301.84x
💻 LocalNext.js (Turbopack)1.560s (+6.4% 🔺)2.014s (~)0.001s (+73.3% 🔺)2.018s (~)0.458s302.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.901s (-13.7% 🟢)4.310s (-5.8% 🟢)0.000s (-53.8% 🟢)4.754s (-5.7% 🟢)1.853s131.00x
▲ VercelNitro2.975s (-21.8% 🟢)4.535s (-6.1% 🟢)0.000s (+Infinity% 🔺)5.061s (-3.1%)2.086s121.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.532s (~)2.098s (-5.4% 🟢)0.000s (-100.0% 🟢)2.117s (-5.8% 🟢)0.585s291.00x
🐘 PostgresNitro1.640s (+23.0% 🔺)2.140s (+5.6% 🔺)0.000s (+Infinity% 🔺)2.167s (+5.0% 🔺)0.527s281.07x
🐘 PostgresNext.js (Turbopack)2.504s (+44.8% 🔺)3.050s (+34.8% 🔺)0.000s (NaN%)3.072s (+35.3% 🔺)0.567s201.63x
💻 LocalNitro2.966s (-6.4% 🟢)3.672s (-4.3%)0.000s (-5.9% 🟢)3.674s (-4.3%)0.709s171.94x
💻 LocalExpress3.180s (+2.7%)3.734s (+1.7%)0.001s (+90.9% 🔺)3.739s (+1.7%)0.560s172.08x
💻 LocalNext.js (Turbopack)3.223s (+13.0% 🔺)3.778s (+10.6% 🔺)0.001s (+68.8% 🔺)3.782s (+10.7% 🔺)0.560s162.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.199s (-43.1% 🟢)5.855s (-26.2% 🟢)0.000s (+Infinity% 🔺)6.349s (-29.7% 🟢)2.150s101.00x
▲ VercelExpress4.324s (-18.8% 🟢)5.767s (-12.4% 🟢)0.000s (+Infinity% 🔺)6.266s (-12.9% 🟢)1.942s101.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro20/21
🐘 PostgresExpress16/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow-api/world/storage.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/get-step-metadata.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
pranaygpand others added 6 commits June 11, 2026 14:37
…docs-run-idempotency
* origin-https/main: (115 commits)
[core] V2: unify wait+step queue dispatch in suspension processing (#1925)
fix(world-local,world-postgres): make duplicate hook_created idempotent (#2295)
docs(observability): remove MVP implementation detail bullet (#2367)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
...
# Conflicts:
#	docs/content/docs/v4/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
… into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/hook-ready:
docs: restore SWC Plugin heading in AGENTS.md
docs: require docs preview links in PR descriptions for docs changes
docs: fix inconsistent hasConflict bullet in create-webhook reference
feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
test: cover hook ready continuation scheduling
feat: add hook ready promise
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
- Primary claim pattern is now `if (await hook.hasConflict)` instead of
try/catch on HookConflictError; payload awaits still reject with
HookConflictError (with conflictingRunId) when the owner's run ID is
needed.
- Route example returns the active owner via resumeHook()'s runId
instead of threading conflictingRunId through the workflow result.
- Update claim-pattern prose across start(), getHookByToken(), world
storage, scheduling, workflow composition, and cookbook idempotency
pages (v4 + v5).
- Add @skip-typecheck marker to the cross-block route sample, fixing a
pre-existing docs typecheck failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The early callout was too vague and out of place at the top of the API
reference. Replace it with a 'Resume or Start' example section that
explains the flow, shows the resume-first/start-then-retry route, and
links to the run idempotency pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f awaiting returnValue
The 'Resume or Start' example returned the just-started run's runId with
reused: false even when a concurrent request's run won the token race —
the payload had reached the actual owner, so the response pointed callers
at a run that exits as a duplicate. The foundations route handled the
race correctly but by awaiting run.returnValue, blocking the HTTP
response on full workflow completion.
resumeHook() always resolves against the actual active owner, so
comparing the resumed hook's runId with the started run's runId detects
the race in both examples — race-correct and non-blocking.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — accurate, well-hedged docs; synced to main and one pattern defect fixed in-branch

Now that #2015 is on main, I merged main into this branch (resolving the create-hook.mdx conflicts in favor of this PR's run-idempotency callout) — the diff is down to docs-only (38 files + the empty changeset), exactly as the PR description promised. pnpm test:docs passes (923 samples typechecked).

Technical accuracy — verified against the code

The route samples in the foundations page carry @skip-typecheck (cross-block dependency), so CI doesn't validate them; I checked the API claims manually:

  • start(processOrder, [orderId]) matches the (workflow, args: TArgs, options?) overload returning Promise<Run<TResult>>
  • HookNotFoundError is exported from the workflow/errors subpath (./errorsdist/internal/errors.js) ✓
  • resumeHook(token, payload) returns Promise<Hook> carrying the owner's runId
  • The conflictingRunId-on-HookConflictError claim matches #2012 (and its stable backport #2016), so it's accurate for both the v4 and v5 pages ✓
  • hasConflict semantics (registration committed on suspension; resolves true/false; payload await still rejects with HookConflictError) match the implementation that just landed in #2015

What I particularly like: the docs are honest about the limits. getHookByToken() is consistently described as an advisory pre-check, the start()-vs-hook-registration race is named explicitly everywhere it matters, the resolution is correctly placed inside the workflow via hook.hasConflict, and the "native atomic start-and-hook API is in the works" framing sets expectations without overpromising. The foundations page carries the full pattern once, and the ~20 cross-link callouts are scoped summaries pointing back at it rather than divergent copies.

One defect fixed in-branch (42d5533) — please review the change

The two route examples disagreed on race handling, and one was wrong:

  • resume-hook.mdx "Resume or Start" returned { runId: run.runId, reused: false } after start() + resumeWithRetry() — but when a concurrent request's run wins the token race, the resume reaches that owner, and this response points the caller at a run that exits as a duplicate without doing any work. Wrong answer in exactly the race case the section teaches about.
  • foundations/idempotency.mdx handled the race correctly but via await run.returnValue, which blocks the HTTP response on full workflow completion (hook resume + chargeOrder + every step). That's an anti-pattern for a copy-paste route sample — workflows are long-lived by design.

Both are fixed by one observation: resumeHook() always resolves against the actual active owner, so resumed.runId !== run.runId detects the race — race-correct, non-blocking, and the same shape in both examples. The warn callout's wording is updated to match. Happy to revert if you prefer the returnValue approach for pedagogical reasons, but I'd argue the runId comparison is also the better teaching: it makes "the resume reaches the owner" concrete.

Non-blocking notes

  1. The 500ms retry budget (5 × 100ms) in resumeWithRetry/resumeOrder is tight against real cold starts + queue dispatch latency. Worth either a more forgiving schedule (e.g. exponential up to ~5s) or a sentence on the failure consequence: if the retries exhaust afterstart() succeeded, the new run is left suspended awaiting a payload that never arrives (until run timeout). Right now the thrown error reads as transient but leaves a dangling run behind.
  2. v4 docs sequencing, again: hasConflict is still not on stable (no backport PR exists yet), and this PR widens the v4-docs-reality gap from one page (#2015) to roughly twenty. Same deliberate-choice ask as before — either the #2015 backport + 4.x release should follow promptly, or the v4 halves of these pages should wait for it. The content itself is version-accurate; it's purely a shipping-order question.
  3. The cookbook's minimal getHookByToken()-then-start() example intentionally omits the resume-retry mechanics — good scoping, and the pointer to foundations for "the full pattern" covers it.

CI

All checks green or pending on the synced head; mergeable: MERGEABLE. The branch now contains the merge of main (e6f0516de) plus the race-handling fix (42d5533d4).

…ull>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pranaygpand others added 13 commits June 11, 2026 16:12
…docs-run-idempotency
* origin-https/main:
ci: use claude-fable-5 for backport AI model (#2370)
Add `hook.hasConflict` for early hook conflict detection (#2015)
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
…n-idempotency
* pranaygp/hook-get-conflict:
feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
Run idempotency docs now use getConflict (resolves with the conflicting
Run in v5, { runId } in v4) and document code-driven conflict strategies
in place of static ID-reuse policies: reject the duplicate, adopt the
owner's result, inspect before deciding, signal the owner via
resumeHook, and supersede via cancel-and-reclaim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…empotency' into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/docs-run-idempotency:
docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue
# Conflicts:
#	docs/content/docs/v4/foundations/idempotency.mdx
#	docs/content/docs/v5/foundations/idempotency.mdx
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
refactor: make getConflict a method — hook.getConflict()
fix: never resolve getConflict with a non-Run fallback shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route examples typecheck as-is since the runId-comparison rewrite;
strategy fragments are now complete self-contained workflows; the
publishing-libraries cross-block dependency uses the declare @setup
convention. 934 samples typechecked, none skipped by this PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
review: guard Run class registration, fix anchors, clarify changeset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…her systems
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — re-reviewed after the getConflict() migration; synced to main again

Re-review following #2373's merge (which replaced hook.hasConflict with hook.getConflict() returning Run | null). I merged main into this branch once more (45fe1a585): the core-package conflicts were the branch's embedded pre-merge copy of #2373 versus the landed version (resolved by taking main wholesale, which includes the registry refactor), and the four docs conflicts were this PR's anchored links + callouts versus #2373's baseline pages (resolved by keeping this PR's superset). The effective diff is docs-only + the empty changeset, and pnpm test:docs passes — now at 1012 typechecked samples with zero @skip-typecheck escape hatches, since the route samples were restructured to typecheck standalone. That closes the "flagship sample isn't CI-validated" gap from my first review.

The getConflict() migration is done right

  • Zero stale hasConflict references anywhere in the PR.
  • The run-idempotency pattern now returns { status: "duplicate", runId: conflict.runId } — the duplicate run can finally say who owns the token, which is what makes the route example's dedupe response meaningful.
  • The new Conflict-handling strategies section is the best part of this revision. All five strategies are technically sound — I audited each against the implementation:
    • Reject: conflict.runId plain property, deterministic ✓
    • Adopt: await conflict.returnValue durable step accessor ✓
    • Inspect: await conflict.status
    • Signal: resumeHook() correctly wrapped in a "use step" function (API calls can't run in workflow context) ✓
    • Supersede: await conflict.cancel() + retry loop creating a fresh hook per attempt, correctly handling the disposal-propagation window; per-iteration using disposal of conflicted hooks is safe (pinned by the #2015 test) ✓
  • The "no policy enum — policy is ordinary code" framing (now phrased generically without naming other systems) is a genuinely good product argument, and the inspect-before-deciding strategy demonstrates exactly what static configuration can't express.
  • The v4/v5 split is version-accurate: v4 documents the { runId } resolution with getRun(conflict.runId) inside steps (no native Run construction in 4.x workflow context); v5 documents the Run handle with durable accessors. The older-world degenerate case (hook_conflict without conflictingRunId → rejects with HookConflictError) is documented in the hooks foundations page.
  • My earlier race-handling fix (reused: resumed.runId !== run.runId) survived the revision and the warn callout's wording is consistent with it.

Two non-blocking notes

  1. The inspect strategy's terminal-state fall-through is slightly loose: when the owner is already terminal, the example proceeds to processOwnedOrder() without holding the token (this run's hook already recorded its conflict; the released token isn't retroactively claimed). A third concurrent run could claim it meanwhile. The supersede example handles this properly with its fresh-hook retry loop — a one-line pointer ("to do the work while holding the token after the owner ends, use the supersede pattern's retry loop") would prevent copy-paste of the loose version into duplicate-sensitive code.
  2. The recurring v4 sequencing flag, final form: the v4 pages now document getConflict() — an API that has never existed in any 4.x release (hasConflict didn't ship there either). The documented-but-unreleased v4 surface keeps compounding across #2015#2373 → this PR. None of it is wrong — the v4 content correctly describes what a backport would look like — but the backport chain (now effectively just #2373, since it subsumes #2015's API) should land in a 4.x release soon after this merges, or the v4 docs are fiction in the meantime.

Everything else from my first review stands. Nice iteration — the strategies section turned a reference page into an actual design guide.

…docs-run-idempotency
* origin-https/main:
Version Packages (beta) (#2364)
Animate in-progress segments in the timeline (#2383)
…docs-run-idempotency
* origin-https/main:
test: e2e coverage for run-idempotency conflict-handling strategies (#2387)
fix(docs): repair broken links, fix the link linter, and version-correct v5 Card + edit links (#2391)
feat(web-shared): RelativeTimeCard with shared ContextCard provider (#2328)
Add allowReservedAttributes option to start() (#2385)
# Conflicts:
#	docs/content/docs/v4/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2410. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

docs: document run idempotency - #2011

Merged
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency
Jun 14, 2026
Merged

docs: document run idempotency#2011
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expand the foundations and cookbook idempotency docs to cover run idempotency in addition to step side-effect idempotency
  • document that start() currently creates a new run for each call and does not accept an idempotency key
  • document the deterministic hook-token claim pattern using hook.getConflict() (Add hook.hasConflict for early hook conflict detection #2015, revised by Replace hook.hasConflict with hook.getConflict() returning the conflicting Run #2373): the workflow creates a hook with a domain-derived token and checks const conflict = await hook.getConflict() before duplicate-sensitive work — awaiting getConflict() suspends the workflow to commit the hook registration and resolves with the conflicting Run (v5) / { runId } (v4) when another active run owns the token, or null once registered
  • add a conflict-handling strategies guide to the run-idempotency docs: instead of static ID-reuse policies (cf. Temporal's Workflow ID conflict policy), the duplicate run handles the conflict in code — reject the duplicate, adopt the owner's result via conflict.returnValue, inspect conflict.status before deciding, signal the owner via resumeHook() from a step, or supersede it via conflict.cancel() and re-claim the token
  • add cross-links/callouts from relevant API reference and cookbook pages: start(), getRun(), hook APIs, workflow fetch(), child workflows, session integrations, scheduling, timeouts, and publishing-library examples

Note

Stacked on #2373 (hook.getConflict(), the follow-up to the merged #2015): this PR's base is that branch, so the diff here is the idempotency docs only. Merge #2373 first — GitHub will retarget this PR to main automatically.

Notes

  • Current guidance is to reserve an application/business key atomically before calling start() when duplicate run creation is unsafe.
  • Hook-token lookup (getHookByToken()) is documented as an advisory pre-check, not atomic enough to replace run idempotency; the race is resolved inside the workflow via hook.getConflict().
  • The route example detects the concurrent-start race by comparing resumeHook()'s runId against the started run's runId — race-correct without blocking on workflow completion.

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
Foundations: Idempotency (new Run idempotency section + conflict-handling strategies)v4v5
Cookbook: Idempotencyv4v5
start() API referencev4v5
getHookByToken() API referencev4v5
getRun() API referencev4v5
resumeHook() API referencev4v5
createHook() API referencev4v5
fetch() API referencev4v5
World storage referencev4v5

Smaller idempotency cross-link callouts were also added to the scheduling, timeouts, workflow composition, child workflows, publishing libraries, AI SDK, Chat SDK, and Sandbox cookbook pages (same paths under /v5/docs for v5).

Validation

  • pnpm test:docs (921 samples typechecked, 2 files) — also fixed a pre-existing failure in the run-idempotency route sample with a @skip-typecheck cross-block marker
  • git diff --check

@vercel

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd22586

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production123302171450
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7634010668700

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+2.1%)1.006s (~)0.962s101.00x
💻 LocalExpress0.052s (+22.6% 🔺)1.007s (~)0.955s101.20x
🐘 PostgresExpress0.062s (+0.7%)1.014s (~)0.952s101.43x
💻 LocalNext.js (Turbopack)0.063s (~)1.006s (~)0.943s101.46x
🐘 PostgresNitro0.079s (+19.8% 🔺)1.016s (~)0.938s101.82x
🐘 PostgresNext.js (Turbopack)0.082s (+9.8% 🔺)1.019s (~)0.937s101.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.285s (-24.6% 🟢)2.322s (+10.6% 🔺)2.037s101.00x
▲ VercelExpress0.370s (+36.7% 🔺)2.299s (-10.9% 🟢)1.929s101.30x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.089s (~)2.007s (~)0.917s101.00x
🐘 PostgresExpress1.107s (-0.7%)2.009s (~)0.902s101.02x
💻 LocalExpress1.116s (+2.0%)2.007s (~)0.891s101.02x
🐘 PostgresNitro1.124s (+1.0%)2.011s (~)0.887s101.03x
💻 LocalNext.js (Turbopack)1.134s (+0.6%)2.007s (~)0.872s101.04x
🐘 PostgresNext.js (Turbopack)1.242s (+8.6% 🔺)2.036s (+1.3%)0.794s101.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.668s (-4.9%)3.484s (-6.1% 🟢)1.816s101.00x
▲ VercelNitro1.743s (+4.0%)3.822s (+10.8% 🔺)2.079s101.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.489s (-0.5%)11.022s (~)0.533s31.00x
🐘 PostgresExpress10.557s (~)11.018s (~)0.461s31.01x
💻 LocalExpress10.575s (~)11.024s (~)0.449s31.01x
🐘 PostgresNitro10.645s (~)11.022s (~)0.377s31.01x
💻 LocalNext.js (Turbopack)10.790s (~)11.022s (~)0.232s31.03x
🐘 PostgresNext.js (Turbopack)10.993s (~)11.353s (-2.9%)0.360s31.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.741s (+2.4%)15.369s (-2.3%)1.628s21.00x
▲ VercelNitro13.963s (+4.7%)16.017s (+5.8% 🔺)2.053s21.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.686s (-1.1%)14.027s (-1.4%)0.341s51.00x
🐘 PostgresExpress13.855s (~)14.017s (~)0.163s51.01x
🐘 PostgresNitro13.898s (+0.6%)14.023s (~)0.124s51.02x
💻 LocalExpress13.953s (+0.8%)14.228s (+1.4%)0.276s51.02x
🐘 PostgresNext.js (Turbopack)14.310s (-1.3%)15.060s (~)0.750s41.05x
💻 LocalNext.js (Turbopack)14.522s (~)15.030s (~)0.508s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.492s (+4.7%)24.394s (+5.7% 🔺)1.902s31.00x
▲ VercelNitro23.148s (+3.2%)25.776s (+7.1% 🔺)2.628s31.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.317s (-1.2%)13.025s (~)0.707s71.00x
🐘 PostgresExpress12.662s (-0.6%)13.017s (~)0.355s71.03x
💻 LocalExpress12.688s (+1.5%)13.026s (~)0.338s71.03x
🐘 PostgresNitro12.934s (+2.8%)13.449s (+3.3%)0.515s71.05x
💻 LocalNext.js (Turbopack)13.803s (+0.9%)14.171s (+1.0%)0.368s71.12x
🐘 PostgresNext.js (Turbopack)14.928s (+6.8% 🔺)15.370s (+7.4% 🔺)0.442s61.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express30.956s (+4.3%)32.926s (+5.7% 🔺)1.970s31.00x
▲ VercelNitro31.388s (-1.0%)33.630s (+1.0%)2.242s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.206s (~)2.007s (~)0.802s151.00x
🐘 PostgresNext.js (Turbopack)1.221s (-3.0%)2.007s (~)0.786s151.01x
💻 LocalNitro1.223s (~)2.006s (~)0.783s151.01x
🐘 PostgresNitro1.260s (+4.7%)2.008s (~)0.748s151.04x
💻 LocalExpress1.305s (+9.7% 🔺)2.007s (~)0.702s151.08x
💻 LocalNext.js (Turbopack)1.338s (-0.8%)2.006s (~)0.668s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.372s (-7.1% 🟢)3.556s (-21.0% 🟢)1.185s91.00x
▲ VercelNitro2.462s (-33.4% 🟢)4.068s (-21.5% 🟢)1.606s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.364s (+8.4% 🔺)2.735s (+36.2% 🔺)1.371s111.00x
🐘 PostgresExpress1.461s (+15.8% 🔺)2.509s (+25.0% 🔺)1.048s121.07x
🐘 PostgresNext.js (Turbopack)1.512s (+7.9% 🔺)2.331s (+16.2% 🔺)0.819s131.11x
💻 LocalNitro1.697s (~)2.005s (~)0.309s151.24x
💻 LocalNext.js (Turbopack)1.742s (-7.3% 🟢)2.074s (-3.5%)0.332s151.28x
💻 LocalExpress2.242s (+21.7% 🔺)2.675s (+24.4% 🔺)0.433s121.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.639s (-40.3% 🟢)4.429s (-27.6% 🟢)1.790s71.00x
▲ VercelNitro2.882s (-53.8% 🟢)5.096s (-34.2% 🟢)2.215s61.09x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.610s (+14.4% 🔺)4.010s (+99.7% 🔺)2.400s81.00x
🐘 PostgresExpress1.668s (+19.0% 🔺)3.887s (+93.5% 🔺)2.219s81.04x
🐘 PostgresNext.js (Turbopack)3.217s (+78.3% 🔺)4.165s (+81.3% 🔺)0.949s82.00x
💻 LocalNitro4.405s (-16.1% 🟢)5.014s (-16.7% 🟢)0.609s62.74x
💻 LocalNext.js (Turbopack)4.848s (-15.0% 🟢)5.300s (-14.7% 🟢)0.452s73.01x
💻 LocalExpress7.792s (+44.7% 🔺)8.518s (+37.8% 🔺)0.727s44.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.708s (-78.2% 🟢)5.818s (-68.2% 🟢)2.109s61.00x
▲ VercelExpress4.596s (-49.6% 🟢)6.296s (-44.6% 🟢)1.700s61.24x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.203s (-0.6%)2.007s (~)0.804s151.00x
🐘 PostgresNitro1.229s (+3.2%)2.007s (~)0.778s151.02x
💻 LocalNext.js (Turbopack)1.394s (+3.1%)2.007s (~)0.613s151.16x
🐘 PostgresNext.js (Turbopack)1.427s (+13.0% 🔺)2.018s (~)0.591s151.19x
💻 LocalNitro1.463s (-3.0%)2.006s (~)0.543s151.22x
💻 LocalExpress1.671s (+0.7%)2.074s (~)0.404s151.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.403s (-6.8% 🟢)4.237s (~)1.834s81.00x
▲ VercelExpress2.475s (-8.3% 🟢)4.128s (-12.4% 🟢)1.653s81.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.333s (+4.8%)2.507s (+24.9% 🔺)1.175s121.00x
🐘 PostgresExpress1.383s (+6.7% 🔺)2.393s (+19.1% 🔺)1.010s131.04x
🐘 PostgresNext.js (Turbopack)1.610s (+14.7% 🔺)2.481s (+23.5% 🔺)0.871s131.21x
💻 LocalNitro1.819s (-16.2% 🟢)2.077s (-19.9% 🟢)0.258s151.37x
💻 LocalNext.js (Turbopack)2.093s (+2.3%)2.827s (-6.0% 🟢)0.734s111.57x
💻 LocalExpress2.165s (~)2.593s (-3.1%)0.428s121.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (-18.2% 🟢)4.442s (-12.9% 🟢)1.639s71.00x
▲ VercelNitro3.091s (-36.0% 🟢)4.926s (-23.1% 🟢)1.835s71.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (+12.8% 🔺)4.013s (+99.6% 🔺)2.431s81.00x
🐘 PostgresNitro1.710s (+23.1% 🔺)4.300s (+114.1% 🔺)2.590s71.08x
🐘 PostgresNext.js (Turbopack)2.745s (+51.8% 🔺)3.594s (+61.6% 🔺)0.849s91.74x
💻 LocalNitro4.968s (-8.6% 🟢)5.514s (-8.3% 🟢)0.546s63.14x
💻 LocalNext.js (Turbopack)5.505s (-9.1% 🟢)6.217s (-8.8% 🟢)0.712s53.48x
💻 LocalExpress5.794s (-8.8% 🟢)6.415s (-8.5% 🟢)0.621s53.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.609s (-29.3% 🟢)5.591s (-22.8% 🟢)1.982s61.00x
▲ VercelNitro3.647s (-47.9% 🟢)5.583s (-35.1% 🟢)1.937s61.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.575s (-4.2%)1.006s (~)0.431s601.00x
💻 LocalNitro0.582s (-2.9%)1.005s (~)0.422s601.01x
🐘 PostgresNitro0.591s (+2.0%)1.023s (+1.6%)0.432s591.03x
💻 LocalExpress0.641s (+4.8%)1.005s (~)0.364s601.12x
🐘 PostgresNext.js (Turbopack)0.736s (-13.8% 🟢)1.067s (+4.2%)0.331s571.28x
💻 LocalNext.js (Turbopack)0.907s (+5.1% 🔺)1.096s (+7.2% 🔺)0.189s551.58x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.639s (+13.7% 🔺)7.687s (+18.0% 🔺)2.048s81.00x
▲ VercelExpress5.991s (-40.9% 🟢)7.332s (-37.7% 🟢)1.341s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.367s (-4.7%)2.007s (~)0.640s451.00x
🐘 PostgresNitro1.378s (-4.0%)2.007s (-1.2%)0.629s451.01x
💻 LocalNitro1.487s (-1.2%)2.005s (~)0.519s451.09x
💻 LocalExpress1.609s (+3.5%)2.007s (-1.1%)0.398s451.18x
🐘 PostgresNext.js (Turbopack)1.664s (-18.0% 🟢)2.177s (-18.0% 🟢)0.513s421.22x
💻 LocalNext.js (Turbopack)2.127s (+1.3%)3.008s (~)0.881s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.875s (-10.4% 🟢)14.900s (-7.7% 🟢)2.025s71.00x
▲ VercelNitro13.500s (+3.1%)15.881s (+9.8% 🔺)2.381s61.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.761s (-1.7%)3.166s (+0.9%)0.405s381.00x
🐘 PostgresNitro2.843s (+1.7%)3.166s (~)0.323s381.03x
💻 LocalNitro3.175s (-3.7%)3.977s (-0.8%)0.801s311.15x
💻 LocalExpress3.384s (+3.0%)4.009s (~)0.625s301.23x
🐘 PostgresNext.js (Turbopack)3.485s (-12.5% 🟢)3.947s (-8.9% 🟢)0.462s311.26x
💻 LocalNext.js (Turbopack)4.440s (+2.2%)5.011s (~)0.571s241.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.690s (-2.1%)28.082s (~)2.392s51.00x
▲ VercelNitro26.004s (-10.7% 🟢)28.926s (-6.1% 🟢)2.922s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.250s (+7.5% 🔺)1.006s (~)0.756s601.00x
🐘 PostgresNitro0.259s (+17.9% 🔺)1.006s (~)0.748s601.03x
🐘 PostgresNext.js (Turbopack)0.351s (+26.5% 🔺)1.036s (+2.9%)0.685s581.40x
💻 LocalNitro0.392s (-8.8% 🟢)1.004s (-1.7%)0.613s601.57x
💻 LocalExpress0.461s (+11.6% 🔺)1.022s (+1.7%)0.561s591.84x
💻 LocalNext.js (Turbopack)0.583s (+6.8% 🔺)1.022s (+1.7%)0.439s592.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.026s (-21.1% 🟢)3.667s (-10.7% 🟢)1.640s171.00x
▲ VercelNitro2.038s (-27.8% 🟢)4.051s (-6.2% 🟢)2.013s151.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.400s (+11.6% 🔺)1.028s (+2.2%)0.628s881.00x
🐘 PostgresExpress0.412s (+10.0% 🔺)1.029s (+2.3%)0.617s881.03x
🐘 PostgresNext.js (Turbopack)0.564s (+13.1% 🔺)1.260s (+25.2% 🔺)0.696s721.41x
💻 LocalNitro2.162s (-0.9%)2.715s (~)0.553s345.40x
💻 LocalExpress2.212s (+8.6% 🔺)2.822s (+11.3% 🔺)0.609s325.53x
💻 LocalNext.js (Turbopack)2.505s (+2.2%)3.227s (+2.5%)0.722s286.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.891s (-68.7% 🟢)4.729s (-56.5% 🟢)1.838s201.00x
▲ VercelExpress2.949s (-45.5% 🟢)4.331s (-38.8% 🟢)1.381s211.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.803s (+15.4% 🔺)1.297s (+28.9% 🔺)0.494s931.00x
🐘 PostgresNitro0.806s (+18.3% 🔺)1.471s (+46.2% 🔺)0.665s821.00x
🐘 PostgresNext.js (Turbopack)1.468s (+46.0% 🔺)2.515s (+35.6% 🔺)1.047s481.83x
💻 LocalNitro9.306s (~)10.029s (+2.4%)0.724s1211.58x
💻 LocalExpress9.959s (+6.9% 🔺)10.530s (+6.7% 🔺)0.570s1212.39x
💻 LocalNext.js (Turbopack)10.701s (-2.2%)11.483s (~)0.782s1113.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.765s (-70.1% 🟢)7.499s (-66.1% 🟢)1.734s171.00x
▲ VercelExpress6.168s (-67.4% 🟢)7.892s (-61.6% 🟢)1.724s161.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.162s (~)2.004s (~)0.010s (-1.9%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (~)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.841s101.01x
🐘 PostgresExpress1.174s (~)1.994s (~)0.001s (~)2.011s (~)0.837s101.01x
💻 LocalExpress1.188s (+1.7%)2.006s (~)0.013s (+9.4% 🔺)2.022s (~)0.833s101.02x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.011s (-13.7% 🟢)2.018s (~)0.807s101.04x
🐘 PostgresNext.js (Turbopack)1.358s (+9.1% 🔺)2.003s (~)0.066s (+4969.2% 🔺)2.077s (+3.3%)0.719s101.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.185s (-10.7% 🟢)3.320s (-1.3%)0.962s (+1.4%)4.733s (-0.7%)2.548s101.00x
▲ VercelExpress2.517s (+16.1% 🔺)3.329s (+3.9%)1.273s (+40.1% 🔺)5.116s (+11.7% 🔺)2.599s101.15x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.565s (-1.8%)2.009s (~)0.012s (-5.3% 🟢)2.023s (~)0.458s301.00x
🐘 PostgresExpress1.587s (-0.9%)2.005s (~)0.005s (-5.2% 🟢)2.029s (~)0.442s301.01x
🐘 PostgresNitro1.587s (-1.6%)2.002s (~)0.005s (+2.0%)2.024s (~)0.437s301.01x
💻 LocalExpress1.609s (+2.3%)2.011s (~)0.012s (+6.0% 🔺)2.026s (~)0.417s301.03x
💻 LocalNext.js (Turbopack)1.736s (~)2.008s (~)0.012s (-1.6%)2.024s (~)0.288s301.11x
🐘 PostgresNext.js (Turbopack)2.022s (+13.2% 🔺)2.448s (+21.8% 🔺)0.004s (-20.0% 🟢)2.465s (+21.6% 🔺)0.443s251.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.128s (+3.5%)7.309s (+3.9%)0.222s (-52.7% 🟢)8.010s (~)1.883s81.00x
▲ VercelExpress6.224s (+2.5%)7.752s (+6.2% 🔺)0.326s (+23.9% 🔺)8.575s (+7.2% 🔺)2.351s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.775s (+9.6% 🔺)1.029s (+1.5%)0.000s (+3.5%)1.058s (+2.9%)0.283s571.00x
🐘 PostgresNitro0.792s (+11.8% 🔺)1.065s (+2.1%)0.000s (-66.1% 🟢)1.078s (+1.4%)0.286s561.02x
🐘 PostgresNext.js (Turbopack)1.277s (+48.1% 🔺)1.875s (+71.9% 🔺)0.000s (-14.1% 🟢)1.890s (+72.0% 🔺)0.613s321.65x
💻 LocalNitro1.352s (-6.1% 🟢)2.013s (~)0.000s (+80.0% 🔺)2.015s (~)0.663s301.74x
💻 LocalExpress1.430s (+5.9% 🔺)2.014s (~)0.000s (+200.0% 🔺)2.016s (~)0.586s301.84x
💻 LocalNext.js (Turbopack)1.560s (+6.4% 🔺)2.014s (~)0.001s (+73.3% 🔺)2.018s (~)0.458s302.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.901s (-13.7% 🟢)4.310s (-5.8% 🟢)0.000s (-53.8% 🟢)4.754s (-5.7% 🟢)1.853s131.00x
▲ VercelNitro2.975s (-21.8% 🟢)4.535s (-6.1% 🟢)0.000s (+Infinity% 🔺)5.061s (-3.1%)2.086s121.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.532s (~)2.098s (-5.4% 🟢)0.000s (-100.0% 🟢)2.117s (-5.8% 🟢)0.585s291.00x
🐘 PostgresNitro1.640s (+23.0% 🔺)2.140s (+5.6% 🔺)0.000s (+Infinity% 🔺)2.167s (+5.0% 🔺)0.527s281.07x
🐘 PostgresNext.js (Turbopack)2.504s (+44.8% 🔺)3.050s (+34.8% 🔺)0.000s (NaN%)3.072s (+35.3% 🔺)0.567s201.63x
💻 LocalNitro2.966s (-6.4% 🟢)3.672s (-4.3%)0.000s (-5.9% 🟢)3.674s (-4.3%)0.709s171.94x
💻 LocalExpress3.180s (+2.7%)3.734s (+1.7%)0.001s (+90.9% 🔺)3.739s (+1.7%)0.560s172.08x
💻 LocalNext.js (Turbopack)3.223s (+13.0% 🔺)3.778s (+10.6% 🔺)0.001s (+68.8% 🔺)3.782s (+10.7% 🔺)0.560s162.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.199s (-43.1% 🟢)5.855s (-26.2% 🟢)0.000s (+Infinity% 🔺)6.349s (-29.7% 🟢)2.150s101.00x
▲ VercelExpress4.324s (-18.8% 🟢)5.767s (-12.4% 🟢)0.000s (+Infinity% 🔺)6.266s (-12.9% 🟢)1.942s101.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro20/21
🐘 PostgresExpress16/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow-api/world/storage.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/get-step-metadata.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
pranaygpand others added 6 commits June 11, 2026 14:37
…docs-run-idempotency
* origin-https/main: (115 commits)
[core] V2: unify wait+step queue dispatch in suspension processing (#1925)
fix(world-local,world-postgres): make duplicate hook_created idempotent (#2295)
docs(observability): remove MVP implementation detail bullet (#2367)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
...
# Conflicts:
#	docs/content/docs/v4/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
… into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/hook-ready:
docs: restore SWC Plugin heading in AGENTS.md
docs: require docs preview links in PR descriptions for docs changes
docs: fix inconsistent hasConflict bullet in create-webhook reference
feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
test: cover hook ready continuation scheduling
feat: add hook ready promise
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
- Primary claim pattern is now `if (await hook.hasConflict)` instead of
try/catch on HookConflictError; payload awaits still reject with
HookConflictError (with conflictingRunId) when the owner's run ID is
needed.
- Route example returns the active owner via resumeHook()'s runId
instead of threading conflictingRunId through the workflow result.
- Update claim-pattern prose across start(), getHookByToken(), world
storage, scheduling, workflow composition, and cookbook idempotency
pages (v4 + v5).
- Add @skip-typecheck marker to the cross-block route sample, fixing a
pre-existing docs typecheck failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The early callout was too vague and out of place at the top of the API
reference. Replace it with a 'Resume or Start' example section that
explains the flow, shows the resume-first/start-then-retry route, and
links to the run idempotency pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f awaiting returnValue
The 'Resume or Start' example returned the just-started run's runId with
reused: false even when a concurrent request's run won the token race —
the payload had reached the actual owner, so the response pointed callers
at a run that exits as a duplicate. The foundations route handled the
race correctly but by awaiting run.returnValue, blocking the HTTP
response on full workflow completion.
resumeHook() always resolves against the actual active owner, so
comparing the resumed hook's runId with the started run's runId detects
the race in both examples — race-correct and non-blocking.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — accurate, well-hedged docs; synced to main and one pattern defect fixed in-branch

Now that #2015 is on main, I merged main into this branch (resolving the create-hook.mdx conflicts in favor of this PR's run-idempotency callout) — the diff is down to docs-only (38 files + the empty changeset), exactly as the PR description promised. pnpm test:docs passes (923 samples typechecked).

Technical accuracy — verified against the code

The route samples in the foundations page carry @skip-typecheck (cross-block dependency), so CI doesn't validate them; I checked the API claims manually:

  • start(processOrder, [orderId]) matches the (workflow, args: TArgs, options?) overload returning Promise<Run<TResult>>
  • HookNotFoundError is exported from the workflow/errors subpath (./errorsdist/internal/errors.js) ✓
  • resumeHook(token, payload) returns Promise<Hook> carrying the owner's runId
  • The conflictingRunId-on-HookConflictError claim matches #2012 (and its stable backport #2016), so it's accurate for both the v4 and v5 pages ✓
  • hasConflict semantics (registration committed on suspension; resolves true/false; payload await still rejects with HookConflictError) match the implementation that just landed in #2015

What I particularly like: the docs are honest about the limits. getHookByToken() is consistently described as an advisory pre-check, the start()-vs-hook-registration race is named explicitly everywhere it matters, the resolution is correctly placed inside the workflow via hook.hasConflict, and the "native atomic start-and-hook API is in the works" framing sets expectations without overpromising. The foundations page carries the full pattern once, and the ~20 cross-link callouts are scoped summaries pointing back at it rather than divergent copies.

One defect fixed in-branch (42d5533) — please review the change

The two route examples disagreed on race handling, and one was wrong:

  • resume-hook.mdx "Resume or Start" returned { runId: run.runId, reused: false } after start() + resumeWithRetry() — but when a concurrent request's run wins the token race, the resume reaches that owner, and this response points the caller at a run that exits as a duplicate without doing any work. Wrong answer in exactly the race case the section teaches about.
  • foundations/idempotency.mdx handled the race correctly but via await run.returnValue, which blocks the HTTP response on full workflow completion (hook resume + chargeOrder + every step). That's an anti-pattern for a copy-paste route sample — workflows are long-lived by design.

Both are fixed by one observation: resumeHook() always resolves against the actual active owner, so resumed.runId !== run.runId detects the race — race-correct, non-blocking, and the same shape in both examples. The warn callout's wording is updated to match. Happy to revert if you prefer the returnValue approach for pedagogical reasons, but I'd argue the runId comparison is also the better teaching: it makes "the resume reaches the owner" concrete.

Non-blocking notes

  1. The 500ms retry budget (5 × 100ms) in resumeWithRetry/resumeOrder is tight against real cold starts + queue dispatch latency. Worth either a more forgiving schedule (e.g. exponential up to ~5s) or a sentence on the failure consequence: if the retries exhaust afterstart() succeeded, the new run is left suspended awaiting a payload that never arrives (until run timeout). Right now the thrown error reads as transient but leaves a dangling run behind.
  2. v4 docs sequencing, again: hasConflict is still not on stable (no backport PR exists yet), and this PR widens the v4-docs-reality gap from one page (#2015) to roughly twenty. Same deliberate-choice ask as before — either the #2015 backport + 4.x release should follow promptly, or the v4 halves of these pages should wait for it. The content itself is version-accurate; it's purely a shipping-order question.
  3. The cookbook's minimal getHookByToken()-then-start() example intentionally omits the resume-retry mechanics — good scoping, and the pointer to foundations for "the full pattern" covers it.

CI

All checks green or pending on the synced head; mergeable: MERGEABLE. The branch now contains the merge of main (e6f0516de) plus the race-handling fix (42d5533d4).

…ull>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pranaygpand others added 13 commits June 11, 2026 16:12
…docs-run-idempotency
* origin-https/main:
ci: use claude-fable-5 for backport AI model (#2370)
Add `hook.hasConflict` for early hook conflict detection (#2015)
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
…n-idempotency
* pranaygp/hook-get-conflict:
feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
Run idempotency docs now use getConflict (resolves with the conflicting
Run in v5, { runId } in v4) and document code-driven conflict strategies
in place of static ID-reuse policies: reject the duplicate, adopt the
owner's result, inspect before deciding, signal the owner via
resumeHook, and supersede via cancel-and-reclaim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…empotency' into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/docs-run-idempotency:
docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue
# Conflicts:
#	docs/content/docs/v4/foundations/idempotency.mdx
#	docs/content/docs/v5/foundations/idempotency.mdx
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
refactor: make getConflict a method — hook.getConflict()
fix: never resolve getConflict with a non-Run fallback shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route examples typecheck as-is since the runId-comparison rewrite;
strategy fragments are now complete self-contained workflows; the
publishing-libraries cross-block dependency uses the declare @setup
convention. 934 samples typechecked, none skipped by this PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
review: guard Run class registration, fix anchors, clarify changeset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…her systems
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — re-reviewed after the getConflict() migration; synced to main again

Re-review following #2373's merge (which replaced hook.hasConflict with hook.getConflict() returning Run | null). I merged main into this branch once more (45fe1a585): the core-package conflicts were the branch's embedded pre-merge copy of #2373 versus the landed version (resolved by taking main wholesale, which includes the registry refactor), and the four docs conflicts were this PR's anchored links + callouts versus #2373's baseline pages (resolved by keeping this PR's superset). The effective diff is docs-only + the empty changeset, and pnpm test:docs passes — now at 1012 typechecked samples with zero @skip-typecheck escape hatches, since the route samples were restructured to typecheck standalone. That closes the "flagship sample isn't CI-validated" gap from my first review.

The getConflict() migration is done right

  • Zero stale hasConflict references anywhere in the PR.
  • The run-idempotency pattern now returns { status: "duplicate", runId: conflict.runId } — the duplicate run can finally say who owns the token, which is what makes the route example's dedupe response meaningful.
  • The new Conflict-handling strategies section is the best part of this revision. All five strategies are technically sound — I audited each against the implementation:
    • Reject: conflict.runId plain property, deterministic ✓
    • Adopt: await conflict.returnValue durable step accessor ✓
    • Inspect: await conflict.status
    • Signal: resumeHook() correctly wrapped in a "use step" function (API calls can't run in workflow context) ✓
    • Supersede: await conflict.cancel() + retry loop creating a fresh hook per attempt, correctly handling the disposal-propagation window; per-iteration using disposal of conflicted hooks is safe (pinned by the #2015 test) ✓
  • The "no policy enum — policy is ordinary code" framing (now phrased generically without naming other systems) is a genuinely good product argument, and the inspect-before-deciding strategy demonstrates exactly what static configuration can't express.
  • The v4/v5 split is version-accurate: v4 documents the { runId } resolution with getRun(conflict.runId) inside steps (no native Run construction in 4.x workflow context); v5 documents the Run handle with durable accessors. The older-world degenerate case (hook_conflict without conflictingRunId → rejects with HookConflictError) is documented in the hooks foundations page.
  • My earlier race-handling fix (reused: resumed.runId !== run.runId) survived the revision and the warn callout's wording is consistent with it.

Two non-blocking notes

  1. The inspect strategy's terminal-state fall-through is slightly loose: when the owner is already terminal, the example proceeds to processOwnedOrder() without holding the token (this run's hook already recorded its conflict; the released token isn't retroactively claimed). A third concurrent run could claim it meanwhile. The supersede example handles this properly with its fresh-hook retry loop — a one-line pointer ("to do the work while holding the token after the owner ends, use the supersede pattern's retry loop") would prevent copy-paste of the loose version into duplicate-sensitive code.
  2. The recurring v4 sequencing flag, final form: the v4 pages now document getConflict() — an API that has never existed in any 4.x release (hasConflict didn't ship there either). The documented-but-unreleased v4 surface keeps compounding across #2015#2373 → this PR. None of it is wrong — the v4 content correctly describes what a backport would look like — but the backport chain (now effectively just #2373, since it subsumes #2015's API) should land in a 4.x release soon after this merges, or the v4 docs are fiction in the meantime.

Everything else from my first review stands. Nice iteration — the strategies section turned a reference page into an actual design guide.

…docs-run-idempotency
* origin-https/main:
Version Packages (beta) (#2364)
Animate in-progress segments in the timeline (#2383)
…docs-run-idempotency
* origin-https/main:
test: e2e coverage for run-idempotency conflict-handling strategies (#2387)
fix(docs): repair broken links, fix the link linter, and version-correct v5 Card + edit links (#2391)
feat(web-shared): RelativeTimeCard with shared ContextCard provider (#2328)
Add allowReservedAttributes option to start() (#2385)
# Conflicts:
#	docs/content/docs/v4/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2410. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

docs: document run idempotency - #2011

Merged
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency
Jun 14, 2026
Merged

docs: document run idempotency#2011
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expand the foundations and cookbook idempotency docs to cover run idempotency in addition to step side-effect idempotency
  • document that start() currently creates a new run for each call and does not accept an idempotency key
  • document the deterministic hook-token claim pattern using hook.getConflict() (Add hook.hasConflict for early hook conflict detection #2015, revised by Replace hook.hasConflict with hook.getConflict() returning the conflicting Run #2373): the workflow creates a hook with a domain-derived token and checks const conflict = await hook.getConflict() before duplicate-sensitive work — awaiting getConflict() suspends the workflow to commit the hook registration and resolves with the conflicting Run (v5) / { runId } (v4) when another active run owns the token, or null once registered
  • add a conflict-handling strategies guide to the run-idempotency docs: instead of static ID-reuse policies (cf. Temporal's Workflow ID conflict policy), the duplicate run handles the conflict in code — reject the duplicate, adopt the owner's result via conflict.returnValue, inspect conflict.status before deciding, signal the owner via resumeHook() from a step, or supersede it via conflict.cancel() and re-claim the token
  • add cross-links/callouts from relevant API reference and cookbook pages: start(), getRun(), hook APIs, workflow fetch(), child workflows, session integrations, scheduling, timeouts, and publishing-library examples

Note

Stacked on #2373 (hook.getConflict(), the follow-up to the merged #2015): this PR's base is that branch, so the diff here is the idempotency docs only. Merge #2373 first — GitHub will retarget this PR to main automatically.

Notes

  • Current guidance is to reserve an application/business key atomically before calling start() when duplicate run creation is unsafe.
  • Hook-token lookup (getHookByToken()) is documented as an advisory pre-check, not atomic enough to replace run idempotency; the race is resolved inside the workflow via hook.getConflict().
  • The route example detects the concurrent-start race by comparing resumeHook()'s runId against the started run's runId — race-correct without blocking on workflow completion.

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
Foundations: Idempotency (new Run idempotency section + conflict-handling strategies)v4v5
Cookbook: Idempotencyv4v5
start() API referencev4v5
getHookByToken() API referencev4v5
getRun() API referencev4v5
resumeHook() API referencev4v5
createHook() API referencev4v5
fetch() API referencev4v5
World storage referencev4v5

Smaller idempotency cross-link callouts were also added to the scheduling, timeouts, workflow composition, child workflows, publishing libraries, AI SDK, Chat SDK, and Sandbox cookbook pages (same paths under /v5/docs for v5).

Validation

  • pnpm test:docs (921 samples typechecked, 2 files) — also fixed a pre-existing failure in the run-idempotency route sample with a @skip-typecheck cross-block marker
  • git diff --check

@vercel

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd22586

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production123302171450
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7634010668700

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+2.1%)1.006s (~)0.962s101.00x
💻 LocalExpress0.052s (+22.6% 🔺)1.007s (~)0.955s101.20x
🐘 PostgresExpress0.062s (+0.7%)1.014s (~)0.952s101.43x
💻 LocalNext.js (Turbopack)0.063s (~)1.006s (~)0.943s101.46x
🐘 PostgresNitro0.079s (+19.8% 🔺)1.016s (~)0.938s101.82x
🐘 PostgresNext.js (Turbopack)0.082s (+9.8% 🔺)1.019s (~)0.937s101.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.285s (-24.6% 🟢)2.322s (+10.6% 🔺)2.037s101.00x
▲ VercelExpress0.370s (+36.7% 🔺)2.299s (-10.9% 🟢)1.929s101.30x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.089s (~)2.007s (~)0.917s101.00x
🐘 PostgresExpress1.107s (-0.7%)2.009s (~)0.902s101.02x
💻 LocalExpress1.116s (+2.0%)2.007s (~)0.891s101.02x
🐘 PostgresNitro1.124s (+1.0%)2.011s (~)0.887s101.03x
💻 LocalNext.js (Turbopack)1.134s (+0.6%)2.007s (~)0.872s101.04x
🐘 PostgresNext.js (Turbopack)1.242s (+8.6% 🔺)2.036s (+1.3%)0.794s101.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.668s (-4.9%)3.484s (-6.1% 🟢)1.816s101.00x
▲ VercelNitro1.743s (+4.0%)3.822s (+10.8% 🔺)2.079s101.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.489s (-0.5%)11.022s (~)0.533s31.00x
🐘 PostgresExpress10.557s (~)11.018s (~)0.461s31.01x
💻 LocalExpress10.575s (~)11.024s (~)0.449s31.01x
🐘 PostgresNitro10.645s (~)11.022s (~)0.377s31.01x
💻 LocalNext.js (Turbopack)10.790s (~)11.022s (~)0.232s31.03x
🐘 PostgresNext.js (Turbopack)10.993s (~)11.353s (-2.9%)0.360s31.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.741s (+2.4%)15.369s (-2.3%)1.628s21.00x
▲ VercelNitro13.963s (+4.7%)16.017s (+5.8% 🔺)2.053s21.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.686s (-1.1%)14.027s (-1.4%)0.341s51.00x
🐘 PostgresExpress13.855s (~)14.017s (~)0.163s51.01x
🐘 PostgresNitro13.898s (+0.6%)14.023s (~)0.124s51.02x
💻 LocalExpress13.953s (+0.8%)14.228s (+1.4%)0.276s51.02x
🐘 PostgresNext.js (Turbopack)14.310s (-1.3%)15.060s (~)0.750s41.05x
💻 LocalNext.js (Turbopack)14.522s (~)15.030s (~)0.508s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.492s (+4.7%)24.394s (+5.7% 🔺)1.902s31.00x
▲ VercelNitro23.148s (+3.2%)25.776s (+7.1% 🔺)2.628s31.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.317s (-1.2%)13.025s (~)0.707s71.00x
🐘 PostgresExpress12.662s (-0.6%)13.017s (~)0.355s71.03x
💻 LocalExpress12.688s (+1.5%)13.026s (~)0.338s71.03x
🐘 PostgresNitro12.934s (+2.8%)13.449s (+3.3%)0.515s71.05x
💻 LocalNext.js (Turbopack)13.803s (+0.9%)14.171s (+1.0%)0.368s71.12x
🐘 PostgresNext.js (Turbopack)14.928s (+6.8% 🔺)15.370s (+7.4% 🔺)0.442s61.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express30.956s (+4.3%)32.926s (+5.7% 🔺)1.970s31.00x
▲ VercelNitro31.388s (-1.0%)33.630s (+1.0%)2.242s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.206s (~)2.007s (~)0.802s151.00x
🐘 PostgresNext.js (Turbopack)1.221s (-3.0%)2.007s (~)0.786s151.01x
💻 LocalNitro1.223s (~)2.006s (~)0.783s151.01x
🐘 PostgresNitro1.260s (+4.7%)2.008s (~)0.748s151.04x
💻 LocalExpress1.305s (+9.7% 🔺)2.007s (~)0.702s151.08x
💻 LocalNext.js (Turbopack)1.338s (-0.8%)2.006s (~)0.668s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.372s (-7.1% 🟢)3.556s (-21.0% 🟢)1.185s91.00x
▲ VercelNitro2.462s (-33.4% 🟢)4.068s (-21.5% 🟢)1.606s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.364s (+8.4% 🔺)2.735s (+36.2% 🔺)1.371s111.00x
🐘 PostgresExpress1.461s (+15.8% 🔺)2.509s (+25.0% 🔺)1.048s121.07x
🐘 PostgresNext.js (Turbopack)1.512s (+7.9% 🔺)2.331s (+16.2% 🔺)0.819s131.11x
💻 LocalNitro1.697s (~)2.005s (~)0.309s151.24x
💻 LocalNext.js (Turbopack)1.742s (-7.3% 🟢)2.074s (-3.5%)0.332s151.28x
💻 LocalExpress2.242s (+21.7% 🔺)2.675s (+24.4% 🔺)0.433s121.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.639s (-40.3% 🟢)4.429s (-27.6% 🟢)1.790s71.00x
▲ VercelNitro2.882s (-53.8% 🟢)5.096s (-34.2% 🟢)2.215s61.09x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.610s (+14.4% 🔺)4.010s (+99.7% 🔺)2.400s81.00x
🐘 PostgresExpress1.668s (+19.0% 🔺)3.887s (+93.5% 🔺)2.219s81.04x
🐘 PostgresNext.js (Turbopack)3.217s (+78.3% 🔺)4.165s (+81.3% 🔺)0.949s82.00x
💻 LocalNitro4.405s (-16.1% 🟢)5.014s (-16.7% 🟢)0.609s62.74x
💻 LocalNext.js (Turbopack)4.848s (-15.0% 🟢)5.300s (-14.7% 🟢)0.452s73.01x
💻 LocalExpress7.792s (+44.7% 🔺)8.518s (+37.8% 🔺)0.727s44.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.708s (-78.2% 🟢)5.818s (-68.2% 🟢)2.109s61.00x
▲ VercelExpress4.596s (-49.6% 🟢)6.296s (-44.6% 🟢)1.700s61.24x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.203s (-0.6%)2.007s (~)0.804s151.00x
🐘 PostgresNitro1.229s (+3.2%)2.007s (~)0.778s151.02x
💻 LocalNext.js (Turbopack)1.394s (+3.1%)2.007s (~)0.613s151.16x
🐘 PostgresNext.js (Turbopack)1.427s (+13.0% 🔺)2.018s (~)0.591s151.19x
💻 LocalNitro1.463s (-3.0%)2.006s (~)0.543s151.22x
💻 LocalExpress1.671s (+0.7%)2.074s (~)0.404s151.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.403s (-6.8% 🟢)4.237s (~)1.834s81.00x
▲ VercelExpress2.475s (-8.3% 🟢)4.128s (-12.4% 🟢)1.653s81.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.333s (+4.8%)2.507s (+24.9% 🔺)1.175s121.00x
🐘 PostgresExpress1.383s (+6.7% 🔺)2.393s (+19.1% 🔺)1.010s131.04x
🐘 PostgresNext.js (Turbopack)1.610s (+14.7% 🔺)2.481s (+23.5% 🔺)0.871s131.21x
💻 LocalNitro1.819s (-16.2% 🟢)2.077s (-19.9% 🟢)0.258s151.37x
💻 LocalNext.js (Turbopack)2.093s (+2.3%)2.827s (-6.0% 🟢)0.734s111.57x
💻 LocalExpress2.165s (~)2.593s (-3.1%)0.428s121.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (-18.2% 🟢)4.442s (-12.9% 🟢)1.639s71.00x
▲ VercelNitro3.091s (-36.0% 🟢)4.926s (-23.1% 🟢)1.835s71.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (+12.8% 🔺)4.013s (+99.6% 🔺)2.431s81.00x
🐘 PostgresNitro1.710s (+23.1% 🔺)4.300s (+114.1% 🔺)2.590s71.08x
🐘 PostgresNext.js (Turbopack)2.745s (+51.8% 🔺)3.594s (+61.6% 🔺)0.849s91.74x
💻 LocalNitro4.968s (-8.6% 🟢)5.514s (-8.3% 🟢)0.546s63.14x
💻 LocalNext.js (Turbopack)5.505s (-9.1% 🟢)6.217s (-8.8% 🟢)0.712s53.48x
💻 LocalExpress5.794s (-8.8% 🟢)6.415s (-8.5% 🟢)0.621s53.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.609s (-29.3% 🟢)5.591s (-22.8% 🟢)1.982s61.00x
▲ VercelNitro3.647s (-47.9% 🟢)5.583s (-35.1% 🟢)1.937s61.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.575s (-4.2%)1.006s (~)0.431s601.00x
💻 LocalNitro0.582s (-2.9%)1.005s (~)0.422s601.01x
🐘 PostgresNitro0.591s (+2.0%)1.023s (+1.6%)0.432s591.03x
💻 LocalExpress0.641s (+4.8%)1.005s (~)0.364s601.12x
🐘 PostgresNext.js (Turbopack)0.736s (-13.8% 🟢)1.067s (+4.2%)0.331s571.28x
💻 LocalNext.js (Turbopack)0.907s (+5.1% 🔺)1.096s (+7.2% 🔺)0.189s551.58x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.639s (+13.7% 🔺)7.687s (+18.0% 🔺)2.048s81.00x
▲ VercelExpress5.991s (-40.9% 🟢)7.332s (-37.7% 🟢)1.341s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.367s (-4.7%)2.007s (~)0.640s451.00x
🐘 PostgresNitro1.378s (-4.0%)2.007s (-1.2%)0.629s451.01x
💻 LocalNitro1.487s (-1.2%)2.005s (~)0.519s451.09x
💻 LocalExpress1.609s (+3.5%)2.007s (-1.1%)0.398s451.18x
🐘 PostgresNext.js (Turbopack)1.664s (-18.0% 🟢)2.177s (-18.0% 🟢)0.513s421.22x
💻 LocalNext.js (Turbopack)2.127s (+1.3%)3.008s (~)0.881s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.875s (-10.4% 🟢)14.900s (-7.7% 🟢)2.025s71.00x
▲ VercelNitro13.500s (+3.1%)15.881s (+9.8% 🔺)2.381s61.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.761s (-1.7%)3.166s (+0.9%)0.405s381.00x
🐘 PostgresNitro2.843s (+1.7%)3.166s (~)0.323s381.03x
💻 LocalNitro3.175s (-3.7%)3.977s (-0.8%)0.801s311.15x
💻 LocalExpress3.384s (+3.0%)4.009s (~)0.625s301.23x
🐘 PostgresNext.js (Turbopack)3.485s (-12.5% 🟢)3.947s (-8.9% 🟢)0.462s311.26x
💻 LocalNext.js (Turbopack)4.440s (+2.2%)5.011s (~)0.571s241.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.690s (-2.1%)28.082s (~)2.392s51.00x
▲ VercelNitro26.004s (-10.7% 🟢)28.926s (-6.1% 🟢)2.922s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.250s (+7.5% 🔺)1.006s (~)0.756s601.00x
🐘 PostgresNitro0.259s (+17.9% 🔺)1.006s (~)0.748s601.03x
🐘 PostgresNext.js (Turbopack)0.351s (+26.5% 🔺)1.036s (+2.9%)0.685s581.40x
💻 LocalNitro0.392s (-8.8% 🟢)1.004s (-1.7%)0.613s601.57x
💻 LocalExpress0.461s (+11.6% 🔺)1.022s (+1.7%)0.561s591.84x
💻 LocalNext.js (Turbopack)0.583s (+6.8% 🔺)1.022s (+1.7%)0.439s592.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.026s (-21.1% 🟢)3.667s (-10.7% 🟢)1.640s171.00x
▲ VercelNitro2.038s (-27.8% 🟢)4.051s (-6.2% 🟢)2.013s151.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.400s (+11.6% 🔺)1.028s (+2.2%)0.628s881.00x
🐘 PostgresExpress0.412s (+10.0% 🔺)1.029s (+2.3%)0.617s881.03x
🐘 PostgresNext.js (Turbopack)0.564s (+13.1% 🔺)1.260s (+25.2% 🔺)0.696s721.41x
💻 LocalNitro2.162s (-0.9%)2.715s (~)0.553s345.40x
💻 LocalExpress2.212s (+8.6% 🔺)2.822s (+11.3% 🔺)0.609s325.53x
💻 LocalNext.js (Turbopack)2.505s (+2.2%)3.227s (+2.5%)0.722s286.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.891s (-68.7% 🟢)4.729s (-56.5% 🟢)1.838s201.00x
▲ VercelExpress2.949s (-45.5% 🟢)4.331s (-38.8% 🟢)1.381s211.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.803s (+15.4% 🔺)1.297s (+28.9% 🔺)0.494s931.00x
🐘 PostgresNitro0.806s (+18.3% 🔺)1.471s (+46.2% 🔺)0.665s821.00x
🐘 PostgresNext.js (Turbopack)1.468s (+46.0% 🔺)2.515s (+35.6% 🔺)1.047s481.83x
💻 LocalNitro9.306s (~)10.029s (+2.4%)0.724s1211.58x
💻 LocalExpress9.959s (+6.9% 🔺)10.530s (+6.7% 🔺)0.570s1212.39x
💻 LocalNext.js (Turbopack)10.701s (-2.2%)11.483s (~)0.782s1113.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.765s (-70.1% 🟢)7.499s (-66.1% 🟢)1.734s171.00x
▲ VercelExpress6.168s (-67.4% 🟢)7.892s (-61.6% 🟢)1.724s161.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.162s (~)2.004s (~)0.010s (-1.9%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (~)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.841s101.01x
🐘 PostgresExpress1.174s (~)1.994s (~)0.001s (~)2.011s (~)0.837s101.01x
💻 LocalExpress1.188s (+1.7%)2.006s (~)0.013s (+9.4% 🔺)2.022s (~)0.833s101.02x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.011s (-13.7% 🟢)2.018s (~)0.807s101.04x
🐘 PostgresNext.js (Turbopack)1.358s (+9.1% 🔺)2.003s (~)0.066s (+4969.2% 🔺)2.077s (+3.3%)0.719s101.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.185s (-10.7% 🟢)3.320s (-1.3%)0.962s (+1.4%)4.733s (-0.7%)2.548s101.00x
▲ VercelExpress2.517s (+16.1% 🔺)3.329s (+3.9%)1.273s (+40.1% 🔺)5.116s (+11.7% 🔺)2.599s101.15x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.565s (-1.8%)2.009s (~)0.012s (-5.3% 🟢)2.023s (~)0.458s301.00x
🐘 PostgresExpress1.587s (-0.9%)2.005s (~)0.005s (-5.2% 🟢)2.029s (~)0.442s301.01x
🐘 PostgresNitro1.587s (-1.6%)2.002s (~)0.005s (+2.0%)2.024s (~)0.437s301.01x
💻 LocalExpress1.609s (+2.3%)2.011s (~)0.012s (+6.0% 🔺)2.026s (~)0.417s301.03x
💻 LocalNext.js (Turbopack)1.736s (~)2.008s (~)0.012s (-1.6%)2.024s (~)0.288s301.11x
🐘 PostgresNext.js (Turbopack)2.022s (+13.2% 🔺)2.448s (+21.8% 🔺)0.004s (-20.0% 🟢)2.465s (+21.6% 🔺)0.443s251.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.128s (+3.5%)7.309s (+3.9%)0.222s (-52.7% 🟢)8.010s (~)1.883s81.00x
▲ VercelExpress6.224s (+2.5%)7.752s (+6.2% 🔺)0.326s (+23.9% 🔺)8.575s (+7.2% 🔺)2.351s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.775s (+9.6% 🔺)1.029s (+1.5%)0.000s (+3.5%)1.058s (+2.9%)0.283s571.00x
🐘 PostgresNitro0.792s (+11.8% 🔺)1.065s (+2.1%)0.000s (-66.1% 🟢)1.078s (+1.4%)0.286s561.02x
🐘 PostgresNext.js (Turbopack)1.277s (+48.1% 🔺)1.875s (+71.9% 🔺)0.000s (-14.1% 🟢)1.890s (+72.0% 🔺)0.613s321.65x
💻 LocalNitro1.352s (-6.1% 🟢)2.013s (~)0.000s (+80.0% 🔺)2.015s (~)0.663s301.74x
💻 LocalExpress1.430s (+5.9% 🔺)2.014s (~)0.000s (+200.0% 🔺)2.016s (~)0.586s301.84x
💻 LocalNext.js (Turbopack)1.560s (+6.4% 🔺)2.014s (~)0.001s (+73.3% 🔺)2.018s (~)0.458s302.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.901s (-13.7% 🟢)4.310s (-5.8% 🟢)0.000s (-53.8% 🟢)4.754s (-5.7% 🟢)1.853s131.00x
▲ VercelNitro2.975s (-21.8% 🟢)4.535s (-6.1% 🟢)0.000s (+Infinity% 🔺)5.061s (-3.1%)2.086s121.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.532s (~)2.098s (-5.4% 🟢)0.000s (-100.0% 🟢)2.117s (-5.8% 🟢)0.585s291.00x
🐘 PostgresNitro1.640s (+23.0% 🔺)2.140s (+5.6% 🔺)0.000s (+Infinity% 🔺)2.167s (+5.0% 🔺)0.527s281.07x
🐘 PostgresNext.js (Turbopack)2.504s (+44.8% 🔺)3.050s (+34.8% 🔺)0.000s (NaN%)3.072s (+35.3% 🔺)0.567s201.63x
💻 LocalNitro2.966s (-6.4% 🟢)3.672s (-4.3%)0.000s (-5.9% 🟢)3.674s (-4.3%)0.709s171.94x
💻 LocalExpress3.180s (+2.7%)3.734s (+1.7%)0.001s (+90.9% 🔺)3.739s (+1.7%)0.560s172.08x
💻 LocalNext.js (Turbopack)3.223s (+13.0% 🔺)3.778s (+10.6% 🔺)0.001s (+68.8% 🔺)3.782s (+10.7% 🔺)0.560s162.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.199s (-43.1% 🟢)5.855s (-26.2% 🟢)0.000s (+Infinity% 🔺)6.349s (-29.7% 🟢)2.150s101.00x
▲ VercelExpress4.324s (-18.8% 🟢)5.767s (-12.4% 🟢)0.000s (+Infinity% 🔺)6.266s (-12.9% 🟢)1.942s101.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro20/21
🐘 PostgresExpress16/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow-api/world/storage.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/get-step-metadata.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
pranaygpand others added 6 commits June 11, 2026 14:37
…docs-run-idempotency
* origin-https/main: (115 commits)
[core] V2: unify wait+step queue dispatch in suspension processing (#1925)
fix(world-local,world-postgres): make duplicate hook_created idempotent (#2295)
docs(observability): remove MVP implementation detail bullet (#2367)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
...
# Conflicts:
#	docs/content/docs/v4/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
… into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/hook-ready:
docs: restore SWC Plugin heading in AGENTS.md
docs: require docs preview links in PR descriptions for docs changes
docs: fix inconsistent hasConflict bullet in create-webhook reference
feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
test: cover hook ready continuation scheduling
feat: add hook ready promise
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
- Primary claim pattern is now `if (await hook.hasConflict)` instead of
try/catch on HookConflictError; payload awaits still reject with
HookConflictError (with conflictingRunId) when the owner's run ID is
needed.
- Route example returns the active owner via resumeHook()'s runId
instead of threading conflictingRunId through the workflow result.
- Update claim-pattern prose across start(), getHookByToken(), world
storage, scheduling, workflow composition, and cookbook idempotency
pages (v4 + v5).
- Add @skip-typecheck marker to the cross-block route sample, fixing a
pre-existing docs typecheck failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The early callout was too vague and out of place at the top of the API
reference. Replace it with a 'Resume or Start' example section that
explains the flow, shows the resume-first/start-then-retry route, and
links to the run idempotency pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f awaiting returnValue
The 'Resume or Start' example returned the just-started run's runId with
reused: false even when a concurrent request's run won the token race —
the payload had reached the actual owner, so the response pointed callers
at a run that exits as a duplicate. The foundations route handled the
race correctly but by awaiting run.returnValue, blocking the HTTP
response on full workflow completion.
resumeHook() always resolves against the actual active owner, so
comparing the resumed hook's runId with the started run's runId detects
the race in both examples — race-correct and non-blocking.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — accurate, well-hedged docs; synced to main and one pattern defect fixed in-branch

Now that #2015 is on main, I merged main into this branch (resolving the create-hook.mdx conflicts in favor of this PR's run-idempotency callout) — the diff is down to docs-only (38 files + the empty changeset), exactly as the PR description promised. pnpm test:docs passes (923 samples typechecked).

Technical accuracy — verified against the code

The route samples in the foundations page carry @skip-typecheck (cross-block dependency), so CI doesn't validate them; I checked the API claims manually:

  • start(processOrder, [orderId]) matches the (workflow, args: TArgs, options?) overload returning Promise<Run<TResult>>
  • HookNotFoundError is exported from the workflow/errors subpath (./errorsdist/internal/errors.js) ✓
  • resumeHook(token, payload) returns Promise<Hook> carrying the owner's runId
  • The conflictingRunId-on-HookConflictError claim matches #2012 (and its stable backport #2016), so it's accurate for both the v4 and v5 pages ✓
  • hasConflict semantics (registration committed on suspension; resolves true/false; payload await still rejects with HookConflictError) match the implementation that just landed in #2015

What I particularly like: the docs are honest about the limits. getHookByToken() is consistently described as an advisory pre-check, the start()-vs-hook-registration race is named explicitly everywhere it matters, the resolution is correctly placed inside the workflow via hook.hasConflict, and the "native atomic start-and-hook API is in the works" framing sets expectations without overpromising. The foundations page carries the full pattern once, and the ~20 cross-link callouts are scoped summaries pointing back at it rather than divergent copies.

One defect fixed in-branch (42d5533) — please review the change

The two route examples disagreed on race handling, and one was wrong:

  • resume-hook.mdx "Resume or Start" returned { runId: run.runId, reused: false } after start() + resumeWithRetry() — but when a concurrent request's run wins the token race, the resume reaches that owner, and this response points the caller at a run that exits as a duplicate without doing any work. Wrong answer in exactly the race case the section teaches about.
  • foundations/idempotency.mdx handled the race correctly but via await run.returnValue, which blocks the HTTP response on full workflow completion (hook resume + chargeOrder + every step). That's an anti-pattern for a copy-paste route sample — workflows are long-lived by design.

Both are fixed by one observation: resumeHook() always resolves against the actual active owner, so resumed.runId !== run.runId detects the race — race-correct, non-blocking, and the same shape in both examples. The warn callout's wording is updated to match. Happy to revert if you prefer the returnValue approach for pedagogical reasons, but I'd argue the runId comparison is also the better teaching: it makes "the resume reaches the owner" concrete.

Non-blocking notes

  1. The 500ms retry budget (5 × 100ms) in resumeWithRetry/resumeOrder is tight against real cold starts + queue dispatch latency. Worth either a more forgiving schedule (e.g. exponential up to ~5s) or a sentence on the failure consequence: if the retries exhaust afterstart() succeeded, the new run is left suspended awaiting a payload that never arrives (until run timeout). Right now the thrown error reads as transient but leaves a dangling run behind.
  2. v4 docs sequencing, again: hasConflict is still not on stable (no backport PR exists yet), and this PR widens the v4-docs-reality gap from one page (#2015) to roughly twenty. Same deliberate-choice ask as before — either the #2015 backport + 4.x release should follow promptly, or the v4 halves of these pages should wait for it. The content itself is version-accurate; it's purely a shipping-order question.
  3. The cookbook's minimal getHookByToken()-then-start() example intentionally omits the resume-retry mechanics — good scoping, and the pointer to foundations for "the full pattern" covers it.

CI

All checks green or pending on the synced head; mergeable: MERGEABLE. The branch now contains the merge of main (e6f0516de) plus the race-handling fix (42d5533d4).

…ull>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pranaygpand others added 13 commits June 11, 2026 16:12
…docs-run-idempotency
* origin-https/main:
ci: use claude-fable-5 for backport AI model (#2370)
Add `hook.hasConflict` for early hook conflict detection (#2015)
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
…n-idempotency
* pranaygp/hook-get-conflict:
feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
Run idempotency docs now use getConflict (resolves with the conflicting
Run in v5, { runId } in v4) and document code-driven conflict strategies
in place of static ID-reuse policies: reject the duplicate, adopt the
owner's result, inspect before deciding, signal the owner via
resumeHook, and supersede via cancel-and-reclaim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…empotency' into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/docs-run-idempotency:
docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue
# Conflicts:
#	docs/content/docs/v4/foundations/idempotency.mdx
#	docs/content/docs/v5/foundations/idempotency.mdx
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
refactor: make getConflict a method — hook.getConflict()
fix: never resolve getConflict with a non-Run fallback shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route examples typecheck as-is since the runId-comparison rewrite;
strategy fragments are now complete self-contained workflows; the
publishing-libraries cross-block dependency uses the declare @setup
convention. 934 samples typechecked, none skipped by this PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
review: guard Run class registration, fix anchors, clarify changeset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…her systems
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — re-reviewed after the getConflict() migration; synced to main again

Re-review following #2373's merge (which replaced hook.hasConflict with hook.getConflict() returning Run | null). I merged main into this branch once more (45fe1a585): the core-package conflicts were the branch's embedded pre-merge copy of #2373 versus the landed version (resolved by taking main wholesale, which includes the registry refactor), and the four docs conflicts were this PR's anchored links + callouts versus #2373's baseline pages (resolved by keeping this PR's superset). The effective diff is docs-only + the empty changeset, and pnpm test:docs passes — now at 1012 typechecked samples with zero @skip-typecheck escape hatches, since the route samples were restructured to typecheck standalone. That closes the "flagship sample isn't CI-validated" gap from my first review.

The getConflict() migration is done right

  • Zero stale hasConflict references anywhere in the PR.
  • The run-idempotency pattern now returns { status: "duplicate", runId: conflict.runId } — the duplicate run can finally say who owns the token, which is what makes the route example's dedupe response meaningful.
  • The new Conflict-handling strategies section is the best part of this revision. All five strategies are technically sound — I audited each against the implementation:
    • Reject: conflict.runId plain property, deterministic ✓
    • Adopt: await conflict.returnValue durable step accessor ✓
    • Inspect: await conflict.status
    • Signal: resumeHook() correctly wrapped in a "use step" function (API calls can't run in workflow context) ✓
    • Supersede: await conflict.cancel() + retry loop creating a fresh hook per attempt, correctly handling the disposal-propagation window; per-iteration using disposal of conflicted hooks is safe (pinned by the #2015 test) ✓
  • The "no policy enum — policy is ordinary code" framing (now phrased generically without naming other systems) is a genuinely good product argument, and the inspect-before-deciding strategy demonstrates exactly what static configuration can't express.
  • The v4/v5 split is version-accurate: v4 documents the { runId } resolution with getRun(conflict.runId) inside steps (no native Run construction in 4.x workflow context); v5 documents the Run handle with durable accessors. The older-world degenerate case (hook_conflict without conflictingRunId → rejects with HookConflictError) is documented in the hooks foundations page.
  • My earlier race-handling fix (reused: resumed.runId !== run.runId) survived the revision and the warn callout's wording is consistent with it.

Two non-blocking notes

  1. The inspect strategy's terminal-state fall-through is slightly loose: when the owner is already terminal, the example proceeds to processOwnedOrder() without holding the token (this run's hook already recorded its conflict; the released token isn't retroactively claimed). A third concurrent run could claim it meanwhile. The supersede example handles this properly with its fresh-hook retry loop — a one-line pointer ("to do the work while holding the token after the owner ends, use the supersede pattern's retry loop") would prevent copy-paste of the loose version into duplicate-sensitive code.
  2. The recurring v4 sequencing flag, final form: the v4 pages now document getConflict() — an API that has never existed in any 4.x release (hasConflict didn't ship there either). The documented-but-unreleased v4 surface keeps compounding across #2015#2373 → this PR. None of it is wrong — the v4 content correctly describes what a backport would look like — but the backport chain (now effectively just #2373, since it subsumes #2015's API) should land in a 4.x release soon after this merges, or the v4 docs are fiction in the meantime.

Everything else from my first review stands. Nice iteration — the strategies section turned a reference page into an actual design guide.

…docs-run-idempotency
* origin-https/main:
Version Packages (beta) (#2364)
Animate in-progress segments in the timeline (#2383)
…docs-run-idempotency
* origin-https/main:
test: e2e coverage for run-idempotency conflict-handling strategies (#2387)
fix(docs): repair broken links, fix the link linter, and version-correct v5 Card + edit links (#2391)
feat(web-shared): RelativeTimeCard with shared ContextCard provider (#2328)
Add allowReservedAttributes option to start() (#2385)
# Conflicts:
#	docs/content/docs/v4/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2410. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

docs: document run idempotency - #2011

Merged
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency
Jun 14, 2026
Merged

docs: document run idempotency#2011
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expand the foundations and cookbook idempotency docs to cover run idempotency in addition to step side-effect idempotency
  • document that start() currently creates a new run for each call and does not accept an idempotency key
  • document the deterministic hook-token claim pattern using hook.getConflict() (Add hook.hasConflict for early hook conflict detection #2015, revised by Replace hook.hasConflict with hook.getConflict() returning the conflicting Run #2373): the workflow creates a hook with a domain-derived token and checks const conflict = await hook.getConflict() before duplicate-sensitive work — awaiting getConflict() suspends the workflow to commit the hook registration and resolves with the conflicting Run (v5) / { runId } (v4) when another active run owns the token, or null once registered
  • add a conflict-handling strategies guide to the run-idempotency docs: instead of static ID-reuse policies (cf. Temporal's Workflow ID conflict policy), the duplicate run handles the conflict in code — reject the duplicate, adopt the owner's result via conflict.returnValue, inspect conflict.status before deciding, signal the owner via resumeHook() from a step, or supersede it via conflict.cancel() and re-claim the token
  • add cross-links/callouts from relevant API reference and cookbook pages: start(), getRun(), hook APIs, workflow fetch(), child workflows, session integrations, scheduling, timeouts, and publishing-library examples

Note

Stacked on #2373 (hook.getConflict(), the follow-up to the merged #2015): this PR's base is that branch, so the diff here is the idempotency docs only. Merge #2373 first — GitHub will retarget this PR to main automatically.

Notes

  • Current guidance is to reserve an application/business key atomically before calling start() when duplicate run creation is unsafe.
  • Hook-token lookup (getHookByToken()) is documented as an advisory pre-check, not atomic enough to replace run idempotency; the race is resolved inside the workflow via hook.getConflict().
  • The route example detects the concurrent-start race by comparing resumeHook()'s runId against the started run's runId — race-correct without blocking on workflow completion.

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
Foundations: Idempotency (new Run idempotency section + conflict-handling strategies)v4v5
Cookbook: Idempotencyv4v5
start() API referencev4v5
getHookByToken() API referencev4v5
getRun() API referencev4v5
resumeHook() API referencev4v5
createHook() API referencev4v5
fetch() API referencev4v5
World storage referencev4v5

Smaller idempotency cross-link callouts were also added to the scheduling, timeouts, workflow composition, child workflows, publishing libraries, AI SDK, Chat SDK, and Sandbox cookbook pages (same paths under /v5/docs for v5).

Validation

  • pnpm test:docs (921 samples typechecked, 2 files) — also fixed a pre-existing failure in the run-idempotency route sample with a @skip-typecheck cross-block marker
  • git diff --check

@vercel

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd22586

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production123302171450
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7634010668700

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+2.1%)1.006s (~)0.962s101.00x
💻 LocalExpress0.052s (+22.6% 🔺)1.007s (~)0.955s101.20x
🐘 PostgresExpress0.062s (+0.7%)1.014s (~)0.952s101.43x
💻 LocalNext.js (Turbopack)0.063s (~)1.006s (~)0.943s101.46x
🐘 PostgresNitro0.079s (+19.8% 🔺)1.016s (~)0.938s101.82x
🐘 PostgresNext.js (Turbopack)0.082s (+9.8% 🔺)1.019s (~)0.937s101.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.285s (-24.6% 🟢)2.322s (+10.6% 🔺)2.037s101.00x
▲ VercelExpress0.370s (+36.7% 🔺)2.299s (-10.9% 🟢)1.929s101.30x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.089s (~)2.007s (~)0.917s101.00x
🐘 PostgresExpress1.107s (-0.7%)2.009s (~)0.902s101.02x
💻 LocalExpress1.116s (+2.0%)2.007s (~)0.891s101.02x
🐘 PostgresNitro1.124s (+1.0%)2.011s (~)0.887s101.03x
💻 LocalNext.js (Turbopack)1.134s (+0.6%)2.007s (~)0.872s101.04x
🐘 PostgresNext.js (Turbopack)1.242s (+8.6% 🔺)2.036s (+1.3%)0.794s101.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.668s (-4.9%)3.484s (-6.1% 🟢)1.816s101.00x
▲ VercelNitro1.743s (+4.0%)3.822s (+10.8% 🔺)2.079s101.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.489s (-0.5%)11.022s (~)0.533s31.00x
🐘 PostgresExpress10.557s (~)11.018s (~)0.461s31.01x
💻 LocalExpress10.575s (~)11.024s (~)0.449s31.01x
🐘 PostgresNitro10.645s (~)11.022s (~)0.377s31.01x
💻 LocalNext.js (Turbopack)10.790s (~)11.022s (~)0.232s31.03x
🐘 PostgresNext.js (Turbopack)10.993s (~)11.353s (-2.9%)0.360s31.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.741s (+2.4%)15.369s (-2.3%)1.628s21.00x
▲ VercelNitro13.963s (+4.7%)16.017s (+5.8% 🔺)2.053s21.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.686s (-1.1%)14.027s (-1.4%)0.341s51.00x
🐘 PostgresExpress13.855s (~)14.017s (~)0.163s51.01x
🐘 PostgresNitro13.898s (+0.6%)14.023s (~)0.124s51.02x
💻 LocalExpress13.953s (+0.8%)14.228s (+1.4%)0.276s51.02x
🐘 PostgresNext.js (Turbopack)14.310s (-1.3%)15.060s (~)0.750s41.05x
💻 LocalNext.js (Turbopack)14.522s (~)15.030s (~)0.508s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.492s (+4.7%)24.394s (+5.7% 🔺)1.902s31.00x
▲ VercelNitro23.148s (+3.2%)25.776s (+7.1% 🔺)2.628s31.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.317s (-1.2%)13.025s (~)0.707s71.00x
🐘 PostgresExpress12.662s (-0.6%)13.017s (~)0.355s71.03x
💻 LocalExpress12.688s (+1.5%)13.026s (~)0.338s71.03x
🐘 PostgresNitro12.934s (+2.8%)13.449s (+3.3%)0.515s71.05x
💻 LocalNext.js (Turbopack)13.803s (+0.9%)14.171s (+1.0%)0.368s71.12x
🐘 PostgresNext.js (Turbopack)14.928s (+6.8% 🔺)15.370s (+7.4% 🔺)0.442s61.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express30.956s (+4.3%)32.926s (+5.7% 🔺)1.970s31.00x
▲ VercelNitro31.388s (-1.0%)33.630s (+1.0%)2.242s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.206s (~)2.007s (~)0.802s151.00x
🐘 PostgresNext.js (Turbopack)1.221s (-3.0%)2.007s (~)0.786s151.01x
💻 LocalNitro1.223s (~)2.006s (~)0.783s151.01x
🐘 PostgresNitro1.260s (+4.7%)2.008s (~)0.748s151.04x
💻 LocalExpress1.305s (+9.7% 🔺)2.007s (~)0.702s151.08x
💻 LocalNext.js (Turbopack)1.338s (-0.8%)2.006s (~)0.668s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.372s (-7.1% 🟢)3.556s (-21.0% 🟢)1.185s91.00x
▲ VercelNitro2.462s (-33.4% 🟢)4.068s (-21.5% 🟢)1.606s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.364s (+8.4% 🔺)2.735s (+36.2% 🔺)1.371s111.00x
🐘 PostgresExpress1.461s (+15.8% 🔺)2.509s (+25.0% 🔺)1.048s121.07x
🐘 PostgresNext.js (Turbopack)1.512s (+7.9% 🔺)2.331s (+16.2% 🔺)0.819s131.11x
💻 LocalNitro1.697s (~)2.005s (~)0.309s151.24x
💻 LocalNext.js (Turbopack)1.742s (-7.3% 🟢)2.074s (-3.5%)0.332s151.28x
💻 LocalExpress2.242s (+21.7% 🔺)2.675s (+24.4% 🔺)0.433s121.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.639s (-40.3% 🟢)4.429s (-27.6% 🟢)1.790s71.00x
▲ VercelNitro2.882s (-53.8% 🟢)5.096s (-34.2% 🟢)2.215s61.09x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.610s (+14.4% 🔺)4.010s (+99.7% 🔺)2.400s81.00x
🐘 PostgresExpress1.668s (+19.0% 🔺)3.887s (+93.5% 🔺)2.219s81.04x
🐘 PostgresNext.js (Turbopack)3.217s (+78.3% 🔺)4.165s (+81.3% 🔺)0.949s82.00x
💻 LocalNitro4.405s (-16.1% 🟢)5.014s (-16.7% 🟢)0.609s62.74x
💻 LocalNext.js (Turbopack)4.848s (-15.0% 🟢)5.300s (-14.7% 🟢)0.452s73.01x
💻 LocalExpress7.792s (+44.7% 🔺)8.518s (+37.8% 🔺)0.727s44.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.708s (-78.2% 🟢)5.818s (-68.2% 🟢)2.109s61.00x
▲ VercelExpress4.596s (-49.6% 🟢)6.296s (-44.6% 🟢)1.700s61.24x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.203s (-0.6%)2.007s (~)0.804s151.00x
🐘 PostgresNitro1.229s (+3.2%)2.007s (~)0.778s151.02x
💻 LocalNext.js (Turbopack)1.394s (+3.1%)2.007s (~)0.613s151.16x
🐘 PostgresNext.js (Turbopack)1.427s (+13.0% 🔺)2.018s (~)0.591s151.19x
💻 LocalNitro1.463s (-3.0%)2.006s (~)0.543s151.22x
💻 LocalExpress1.671s (+0.7%)2.074s (~)0.404s151.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.403s (-6.8% 🟢)4.237s (~)1.834s81.00x
▲ VercelExpress2.475s (-8.3% 🟢)4.128s (-12.4% 🟢)1.653s81.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.333s (+4.8%)2.507s (+24.9% 🔺)1.175s121.00x
🐘 PostgresExpress1.383s (+6.7% 🔺)2.393s (+19.1% 🔺)1.010s131.04x
🐘 PostgresNext.js (Turbopack)1.610s (+14.7% 🔺)2.481s (+23.5% 🔺)0.871s131.21x
💻 LocalNitro1.819s (-16.2% 🟢)2.077s (-19.9% 🟢)0.258s151.37x
💻 LocalNext.js (Turbopack)2.093s (+2.3%)2.827s (-6.0% 🟢)0.734s111.57x
💻 LocalExpress2.165s (~)2.593s (-3.1%)0.428s121.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (-18.2% 🟢)4.442s (-12.9% 🟢)1.639s71.00x
▲ VercelNitro3.091s (-36.0% 🟢)4.926s (-23.1% 🟢)1.835s71.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (+12.8% 🔺)4.013s (+99.6% 🔺)2.431s81.00x
🐘 PostgresNitro1.710s (+23.1% 🔺)4.300s (+114.1% 🔺)2.590s71.08x
🐘 PostgresNext.js (Turbopack)2.745s (+51.8% 🔺)3.594s (+61.6% 🔺)0.849s91.74x
💻 LocalNitro4.968s (-8.6% 🟢)5.514s (-8.3% 🟢)0.546s63.14x
💻 LocalNext.js (Turbopack)5.505s (-9.1% 🟢)6.217s (-8.8% 🟢)0.712s53.48x
💻 LocalExpress5.794s (-8.8% 🟢)6.415s (-8.5% 🟢)0.621s53.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.609s (-29.3% 🟢)5.591s (-22.8% 🟢)1.982s61.00x
▲ VercelNitro3.647s (-47.9% 🟢)5.583s (-35.1% 🟢)1.937s61.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.575s (-4.2%)1.006s (~)0.431s601.00x
💻 LocalNitro0.582s (-2.9%)1.005s (~)0.422s601.01x
🐘 PostgresNitro0.591s (+2.0%)1.023s (+1.6%)0.432s591.03x
💻 LocalExpress0.641s (+4.8%)1.005s (~)0.364s601.12x
🐘 PostgresNext.js (Turbopack)0.736s (-13.8% 🟢)1.067s (+4.2%)0.331s571.28x
💻 LocalNext.js (Turbopack)0.907s (+5.1% 🔺)1.096s (+7.2% 🔺)0.189s551.58x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.639s (+13.7% 🔺)7.687s (+18.0% 🔺)2.048s81.00x
▲ VercelExpress5.991s (-40.9% 🟢)7.332s (-37.7% 🟢)1.341s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.367s (-4.7%)2.007s (~)0.640s451.00x
🐘 PostgresNitro1.378s (-4.0%)2.007s (-1.2%)0.629s451.01x
💻 LocalNitro1.487s (-1.2%)2.005s (~)0.519s451.09x
💻 LocalExpress1.609s (+3.5%)2.007s (-1.1%)0.398s451.18x
🐘 PostgresNext.js (Turbopack)1.664s (-18.0% 🟢)2.177s (-18.0% 🟢)0.513s421.22x
💻 LocalNext.js (Turbopack)2.127s (+1.3%)3.008s (~)0.881s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.875s (-10.4% 🟢)14.900s (-7.7% 🟢)2.025s71.00x
▲ VercelNitro13.500s (+3.1%)15.881s (+9.8% 🔺)2.381s61.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.761s (-1.7%)3.166s (+0.9%)0.405s381.00x
🐘 PostgresNitro2.843s (+1.7%)3.166s (~)0.323s381.03x
💻 LocalNitro3.175s (-3.7%)3.977s (-0.8%)0.801s311.15x
💻 LocalExpress3.384s (+3.0%)4.009s (~)0.625s301.23x
🐘 PostgresNext.js (Turbopack)3.485s (-12.5% 🟢)3.947s (-8.9% 🟢)0.462s311.26x
💻 LocalNext.js (Turbopack)4.440s (+2.2%)5.011s (~)0.571s241.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.690s (-2.1%)28.082s (~)2.392s51.00x
▲ VercelNitro26.004s (-10.7% 🟢)28.926s (-6.1% 🟢)2.922s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.250s (+7.5% 🔺)1.006s (~)0.756s601.00x
🐘 PostgresNitro0.259s (+17.9% 🔺)1.006s (~)0.748s601.03x
🐘 PostgresNext.js (Turbopack)0.351s (+26.5% 🔺)1.036s (+2.9%)0.685s581.40x
💻 LocalNitro0.392s (-8.8% 🟢)1.004s (-1.7%)0.613s601.57x
💻 LocalExpress0.461s (+11.6% 🔺)1.022s (+1.7%)0.561s591.84x
💻 LocalNext.js (Turbopack)0.583s (+6.8% 🔺)1.022s (+1.7%)0.439s592.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.026s (-21.1% 🟢)3.667s (-10.7% 🟢)1.640s171.00x
▲ VercelNitro2.038s (-27.8% 🟢)4.051s (-6.2% 🟢)2.013s151.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.400s (+11.6% 🔺)1.028s (+2.2%)0.628s881.00x
🐘 PostgresExpress0.412s (+10.0% 🔺)1.029s (+2.3%)0.617s881.03x
🐘 PostgresNext.js (Turbopack)0.564s (+13.1% 🔺)1.260s (+25.2% 🔺)0.696s721.41x
💻 LocalNitro2.162s (-0.9%)2.715s (~)0.553s345.40x
💻 LocalExpress2.212s (+8.6% 🔺)2.822s (+11.3% 🔺)0.609s325.53x
💻 LocalNext.js (Turbopack)2.505s (+2.2%)3.227s (+2.5%)0.722s286.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.891s (-68.7% 🟢)4.729s (-56.5% 🟢)1.838s201.00x
▲ VercelExpress2.949s (-45.5% 🟢)4.331s (-38.8% 🟢)1.381s211.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.803s (+15.4% 🔺)1.297s (+28.9% 🔺)0.494s931.00x
🐘 PostgresNitro0.806s (+18.3% 🔺)1.471s (+46.2% 🔺)0.665s821.00x
🐘 PostgresNext.js (Turbopack)1.468s (+46.0% 🔺)2.515s (+35.6% 🔺)1.047s481.83x
💻 LocalNitro9.306s (~)10.029s (+2.4%)0.724s1211.58x
💻 LocalExpress9.959s (+6.9% 🔺)10.530s (+6.7% 🔺)0.570s1212.39x
💻 LocalNext.js (Turbopack)10.701s (-2.2%)11.483s (~)0.782s1113.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.765s (-70.1% 🟢)7.499s (-66.1% 🟢)1.734s171.00x
▲ VercelExpress6.168s (-67.4% 🟢)7.892s (-61.6% 🟢)1.724s161.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.162s (~)2.004s (~)0.010s (-1.9%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (~)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.841s101.01x
🐘 PostgresExpress1.174s (~)1.994s (~)0.001s (~)2.011s (~)0.837s101.01x
💻 LocalExpress1.188s (+1.7%)2.006s (~)0.013s (+9.4% 🔺)2.022s (~)0.833s101.02x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.011s (-13.7% 🟢)2.018s (~)0.807s101.04x
🐘 PostgresNext.js (Turbopack)1.358s (+9.1% 🔺)2.003s (~)0.066s (+4969.2% 🔺)2.077s (+3.3%)0.719s101.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.185s (-10.7% 🟢)3.320s (-1.3%)0.962s (+1.4%)4.733s (-0.7%)2.548s101.00x
▲ VercelExpress2.517s (+16.1% 🔺)3.329s (+3.9%)1.273s (+40.1% 🔺)5.116s (+11.7% 🔺)2.599s101.15x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.565s (-1.8%)2.009s (~)0.012s (-5.3% 🟢)2.023s (~)0.458s301.00x
🐘 PostgresExpress1.587s (-0.9%)2.005s (~)0.005s (-5.2% 🟢)2.029s (~)0.442s301.01x
🐘 PostgresNitro1.587s (-1.6%)2.002s (~)0.005s (+2.0%)2.024s (~)0.437s301.01x
💻 LocalExpress1.609s (+2.3%)2.011s (~)0.012s (+6.0% 🔺)2.026s (~)0.417s301.03x
💻 LocalNext.js (Turbopack)1.736s (~)2.008s (~)0.012s (-1.6%)2.024s (~)0.288s301.11x
🐘 PostgresNext.js (Turbopack)2.022s (+13.2% 🔺)2.448s (+21.8% 🔺)0.004s (-20.0% 🟢)2.465s (+21.6% 🔺)0.443s251.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.128s (+3.5%)7.309s (+3.9%)0.222s (-52.7% 🟢)8.010s (~)1.883s81.00x
▲ VercelExpress6.224s (+2.5%)7.752s (+6.2% 🔺)0.326s (+23.9% 🔺)8.575s (+7.2% 🔺)2.351s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.775s (+9.6% 🔺)1.029s (+1.5%)0.000s (+3.5%)1.058s (+2.9%)0.283s571.00x
🐘 PostgresNitro0.792s (+11.8% 🔺)1.065s (+2.1%)0.000s (-66.1% 🟢)1.078s (+1.4%)0.286s561.02x
🐘 PostgresNext.js (Turbopack)1.277s (+48.1% 🔺)1.875s (+71.9% 🔺)0.000s (-14.1% 🟢)1.890s (+72.0% 🔺)0.613s321.65x
💻 LocalNitro1.352s (-6.1% 🟢)2.013s (~)0.000s (+80.0% 🔺)2.015s (~)0.663s301.74x
💻 LocalExpress1.430s (+5.9% 🔺)2.014s (~)0.000s (+200.0% 🔺)2.016s (~)0.586s301.84x
💻 LocalNext.js (Turbopack)1.560s (+6.4% 🔺)2.014s (~)0.001s (+73.3% 🔺)2.018s (~)0.458s302.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.901s (-13.7% 🟢)4.310s (-5.8% 🟢)0.000s (-53.8% 🟢)4.754s (-5.7% 🟢)1.853s131.00x
▲ VercelNitro2.975s (-21.8% 🟢)4.535s (-6.1% 🟢)0.000s (+Infinity% 🔺)5.061s (-3.1%)2.086s121.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.532s (~)2.098s (-5.4% 🟢)0.000s (-100.0% 🟢)2.117s (-5.8% 🟢)0.585s291.00x
🐘 PostgresNitro1.640s (+23.0% 🔺)2.140s (+5.6% 🔺)0.000s (+Infinity% 🔺)2.167s (+5.0% 🔺)0.527s281.07x
🐘 PostgresNext.js (Turbopack)2.504s (+44.8% 🔺)3.050s (+34.8% 🔺)0.000s (NaN%)3.072s (+35.3% 🔺)0.567s201.63x
💻 LocalNitro2.966s (-6.4% 🟢)3.672s (-4.3%)0.000s (-5.9% 🟢)3.674s (-4.3%)0.709s171.94x
💻 LocalExpress3.180s (+2.7%)3.734s (+1.7%)0.001s (+90.9% 🔺)3.739s (+1.7%)0.560s172.08x
💻 LocalNext.js (Turbopack)3.223s (+13.0% 🔺)3.778s (+10.6% 🔺)0.001s (+68.8% 🔺)3.782s (+10.7% 🔺)0.560s162.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.199s (-43.1% 🟢)5.855s (-26.2% 🟢)0.000s (+Infinity% 🔺)6.349s (-29.7% 🟢)2.150s101.00x
▲ VercelExpress4.324s (-18.8% 🟢)5.767s (-12.4% 🟢)0.000s (+Infinity% 🔺)6.266s (-12.9% 🟢)1.942s101.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro20/21
🐘 PostgresExpress16/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow-api/world/storage.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/get-step-metadata.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
pranaygpand others added 6 commits June 11, 2026 14:37
…docs-run-idempotency
* origin-https/main: (115 commits)
[core] V2: unify wait+step queue dispatch in suspension processing (#1925)
fix(world-local,world-postgres): make duplicate hook_created idempotent (#2295)
docs(observability): remove MVP implementation detail bullet (#2367)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
...
# Conflicts:
#	docs/content/docs/v4/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
… into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/hook-ready:
docs: restore SWC Plugin heading in AGENTS.md
docs: require docs preview links in PR descriptions for docs changes
docs: fix inconsistent hasConflict bullet in create-webhook reference
feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
test: cover hook ready continuation scheduling
feat: add hook ready promise
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
- Primary claim pattern is now `if (await hook.hasConflict)` instead of
try/catch on HookConflictError; payload awaits still reject with
HookConflictError (with conflictingRunId) when the owner's run ID is
needed.
- Route example returns the active owner via resumeHook()'s runId
instead of threading conflictingRunId through the workflow result.
- Update claim-pattern prose across start(), getHookByToken(), world
storage, scheduling, workflow composition, and cookbook idempotency
pages (v4 + v5).
- Add @skip-typecheck marker to the cross-block route sample, fixing a
pre-existing docs typecheck failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The early callout was too vague and out of place at the top of the API
reference. Replace it with a 'Resume or Start' example section that
explains the flow, shows the resume-first/start-then-retry route, and
links to the run idempotency pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f awaiting returnValue
The 'Resume or Start' example returned the just-started run's runId with
reused: false even when a concurrent request's run won the token race —
the payload had reached the actual owner, so the response pointed callers
at a run that exits as a duplicate. The foundations route handled the
race correctly but by awaiting run.returnValue, blocking the HTTP
response on full workflow completion.
resumeHook() always resolves against the actual active owner, so
comparing the resumed hook's runId with the started run's runId detects
the race in both examples — race-correct and non-blocking.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — accurate, well-hedged docs; synced to main and one pattern defect fixed in-branch

Now that #2015 is on main, I merged main into this branch (resolving the create-hook.mdx conflicts in favor of this PR's run-idempotency callout) — the diff is down to docs-only (38 files + the empty changeset), exactly as the PR description promised. pnpm test:docs passes (923 samples typechecked).

Technical accuracy — verified against the code

The route samples in the foundations page carry @skip-typecheck (cross-block dependency), so CI doesn't validate them; I checked the API claims manually:

  • start(processOrder, [orderId]) matches the (workflow, args: TArgs, options?) overload returning Promise<Run<TResult>>
  • HookNotFoundError is exported from the workflow/errors subpath (./errorsdist/internal/errors.js) ✓
  • resumeHook(token, payload) returns Promise<Hook> carrying the owner's runId
  • The conflictingRunId-on-HookConflictError claim matches #2012 (and its stable backport #2016), so it's accurate for both the v4 and v5 pages ✓
  • hasConflict semantics (registration committed on suspension; resolves true/false; payload await still rejects with HookConflictError) match the implementation that just landed in #2015

What I particularly like: the docs are honest about the limits. getHookByToken() is consistently described as an advisory pre-check, the start()-vs-hook-registration race is named explicitly everywhere it matters, the resolution is correctly placed inside the workflow via hook.hasConflict, and the "native atomic start-and-hook API is in the works" framing sets expectations without overpromising. The foundations page carries the full pattern once, and the ~20 cross-link callouts are scoped summaries pointing back at it rather than divergent copies.

One defect fixed in-branch (42d5533) — please review the change

The two route examples disagreed on race handling, and one was wrong:

  • resume-hook.mdx "Resume or Start" returned { runId: run.runId, reused: false } after start() + resumeWithRetry() — but when a concurrent request's run wins the token race, the resume reaches that owner, and this response points the caller at a run that exits as a duplicate without doing any work. Wrong answer in exactly the race case the section teaches about.
  • foundations/idempotency.mdx handled the race correctly but via await run.returnValue, which blocks the HTTP response on full workflow completion (hook resume + chargeOrder + every step). That's an anti-pattern for a copy-paste route sample — workflows are long-lived by design.

Both are fixed by one observation: resumeHook() always resolves against the actual active owner, so resumed.runId !== run.runId detects the race — race-correct, non-blocking, and the same shape in both examples. The warn callout's wording is updated to match. Happy to revert if you prefer the returnValue approach for pedagogical reasons, but I'd argue the runId comparison is also the better teaching: it makes "the resume reaches the owner" concrete.

Non-blocking notes

  1. The 500ms retry budget (5 × 100ms) in resumeWithRetry/resumeOrder is tight against real cold starts + queue dispatch latency. Worth either a more forgiving schedule (e.g. exponential up to ~5s) or a sentence on the failure consequence: if the retries exhaust afterstart() succeeded, the new run is left suspended awaiting a payload that never arrives (until run timeout). Right now the thrown error reads as transient but leaves a dangling run behind.
  2. v4 docs sequencing, again: hasConflict is still not on stable (no backport PR exists yet), and this PR widens the v4-docs-reality gap from one page (#2015) to roughly twenty. Same deliberate-choice ask as before — either the #2015 backport + 4.x release should follow promptly, or the v4 halves of these pages should wait for it. The content itself is version-accurate; it's purely a shipping-order question.
  3. The cookbook's minimal getHookByToken()-then-start() example intentionally omits the resume-retry mechanics — good scoping, and the pointer to foundations for "the full pattern" covers it.

CI

All checks green or pending on the synced head; mergeable: MERGEABLE. The branch now contains the merge of main (e6f0516de) plus the race-handling fix (42d5533d4).

…ull>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pranaygpand others added 13 commits June 11, 2026 16:12
…docs-run-idempotency
* origin-https/main:
ci: use claude-fable-5 for backport AI model (#2370)
Add `hook.hasConflict` for early hook conflict detection (#2015)
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
…n-idempotency
* pranaygp/hook-get-conflict:
feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
Run idempotency docs now use getConflict (resolves with the conflicting
Run in v5, { runId } in v4) and document code-driven conflict strategies
in place of static ID-reuse policies: reject the duplicate, adopt the
owner's result, inspect before deciding, signal the owner via
resumeHook, and supersede via cancel-and-reclaim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…empotency' into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/docs-run-idempotency:
docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue
# Conflicts:
#	docs/content/docs/v4/foundations/idempotency.mdx
#	docs/content/docs/v5/foundations/idempotency.mdx
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
refactor: make getConflict a method — hook.getConflict()
fix: never resolve getConflict with a non-Run fallback shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route examples typecheck as-is since the runId-comparison rewrite;
strategy fragments are now complete self-contained workflows; the
publishing-libraries cross-block dependency uses the declare @setup
convention. 934 samples typechecked, none skipped by this PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
review: guard Run class registration, fix anchors, clarify changeset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…her systems
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — re-reviewed after the getConflict() migration; synced to main again

Re-review following #2373's merge (which replaced hook.hasConflict with hook.getConflict() returning Run | null). I merged main into this branch once more (45fe1a585): the core-package conflicts were the branch's embedded pre-merge copy of #2373 versus the landed version (resolved by taking main wholesale, which includes the registry refactor), and the four docs conflicts were this PR's anchored links + callouts versus #2373's baseline pages (resolved by keeping this PR's superset). The effective diff is docs-only + the empty changeset, and pnpm test:docs passes — now at 1012 typechecked samples with zero @skip-typecheck escape hatches, since the route samples were restructured to typecheck standalone. That closes the "flagship sample isn't CI-validated" gap from my first review.

The getConflict() migration is done right

  • Zero stale hasConflict references anywhere in the PR.
  • The run-idempotency pattern now returns { status: "duplicate", runId: conflict.runId } — the duplicate run can finally say who owns the token, which is what makes the route example's dedupe response meaningful.
  • The new Conflict-handling strategies section is the best part of this revision. All five strategies are technically sound — I audited each against the implementation:
    • Reject: conflict.runId plain property, deterministic ✓
    • Adopt: await conflict.returnValue durable step accessor ✓
    • Inspect: await conflict.status
    • Signal: resumeHook() correctly wrapped in a "use step" function (API calls can't run in workflow context) ✓
    • Supersede: await conflict.cancel() + retry loop creating a fresh hook per attempt, correctly handling the disposal-propagation window; per-iteration using disposal of conflicted hooks is safe (pinned by the #2015 test) ✓
  • The "no policy enum — policy is ordinary code" framing (now phrased generically without naming other systems) is a genuinely good product argument, and the inspect-before-deciding strategy demonstrates exactly what static configuration can't express.
  • The v4/v5 split is version-accurate: v4 documents the { runId } resolution with getRun(conflict.runId) inside steps (no native Run construction in 4.x workflow context); v5 documents the Run handle with durable accessors. The older-world degenerate case (hook_conflict without conflictingRunId → rejects with HookConflictError) is documented in the hooks foundations page.
  • My earlier race-handling fix (reused: resumed.runId !== run.runId) survived the revision and the warn callout's wording is consistent with it.

Two non-blocking notes

  1. The inspect strategy's terminal-state fall-through is slightly loose: when the owner is already terminal, the example proceeds to processOwnedOrder() without holding the token (this run's hook already recorded its conflict; the released token isn't retroactively claimed). A third concurrent run could claim it meanwhile. The supersede example handles this properly with its fresh-hook retry loop — a one-line pointer ("to do the work while holding the token after the owner ends, use the supersede pattern's retry loop") would prevent copy-paste of the loose version into duplicate-sensitive code.
  2. The recurring v4 sequencing flag, final form: the v4 pages now document getConflict() — an API that has never existed in any 4.x release (hasConflict didn't ship there either). The documented-but-unreleased v4 surface keeps compounding across #2015#2373 → this PR. None of it is wrong — the v4 content correctly describes what a backport would look like — but the backport chain (now effectively just #2373, since it subsumes #2015's API) should land in a 4.x release soon after this merges, or the v4 docs are fiction in the meantime.

Everything else from my first review stands. Nice iteration — the strategies section turned a reference page into an actual design guide.

…docs-run-idempotency
* origin-https/main:
Version Packages (beta) (#2364)
Animate in-progress segments in the timeline (#2383)
…docs-run-idempotency
* origin-https/main:
test: e2e coverage for run-idempotency conflict-handling strategies (#2387)
fix(docs): repair broken links, fix the link linter, and version-correct v5 Card + edit links (#2391)
feat(web-shared): RelativeTimeCard with shared ContextCard provider (#2328)
Add allowReservedAttributes option to start() (#2385)
# Conflicts:
#	docs/content/docs/v4/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2410. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

docs: document run idempotency - #2011

Merged
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency
Jun 14, 2026
Merged

docs: document run idempotency#2011
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expand the foundations and cookbook idempotency docs to cover run idempotency in addition to step side-effect idempotency
  • document that start() currently creates a new run for each call and does not accept an idempotency key
  • document the deterministic hook-token claim pattern using hook.getConflict() (Add hook.hasConflict for early hook conflict detection #2015, revised by Replace hook.hasConflict with hook.getConflict() returning the conflicting Run #2373): the workflow creates a hook with a domain-derived token and checks const conflict = await hook.getConflict() before duplicate-sensitive work — awaiting getConflict() suspends the workflow to commit the hook registration and resolves with the conflicting Run (v5) / { runId } (v4) when another active run owns the token, or null once registered
  • add a conflict-handling strategies guide to the run-idempotency docs: instead of static ID-reuse policies (cf. Temporal's Workflow ID conflict policy), the duplicate run handles the conflict in code — reject the duplicate, adopt the owner's result via conflict.returnValue, inspect conflict.status before deciding, signal the owner via resumeHook() from a step, or supersede it via conflict.cancel() and re-claim the token
  • add cross-links/callouts from relevant API reference and cookbook pages: start(), getRun(), hook APIs, workflow fetch(), child workflows, session integrations, scheduling, timeouts, and publishing-library examples

Note

Stacked on #2373 (hook.getConflict(), the follow-up to the merged #2015): this PR's base is that branch, so the diff here is the idempotency docs only. Merge #2373 first — GitHub will retarget this PR to main automatically.

Notes

  • Current guidance is to reserve an application/business key atomically before calling start() when duplicate run creation is unsafe.
  • Hook-token lookup (getHookByToken()) is documented as an advisory pre-check, not atomic enough to replace run idempotency; the race is resolved inside the workflow via hook.getConflict().
  • The route example detects the concurrent-start race by comparing resumeHook()'s runId against the started run's runId — race-correct without blocking on workflow completion.

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
Foundations: Idempotency (new Run idempotency section + conflict-handling strategies)v4v5
Cookbook: Idempotencyv4v5
start() API referencev4v5
getHookByToken() API referencev4v5
getRun() API referencev4v5
resumeHook() API referencev4v5
createHook() API referencev4v5
fetch() API referencev4v5
World storage referencev4v5

Smaller idempotency cross-link callouts were also added to the scheduling, timeouts, workflow composition, child workflows, publishing libraries, AI SDK, Chat SDK, and Sandbox cookbook pages (same paths under /v5/docs for v5).

Validation

  • pnpm test:docs (921 samples typechecked, 2 files) — also fixed a pre-existing failure in the run-idempotency route sample with a @skip-typecheck cross-block marker
  • git diff --check

@vercel

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd22586

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production123302171450
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7634010668700

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+2.1%)1.006s (~)0.962s101.00x
💻 LocalExpress0.052s (+22.6% 🔺)1.007s (~)0.955s101.20x
🐘 PostgresExpress0.062s (+0.7%)1.014s (~)0.952s101.43x
💻 LocalNext.js (Turbopack)0.063s (~)1.006s (~)0.943s101.46x
🐘 PostgresNitro0.079s (+19.8% 🔺)1.016s (~)0.938s101.82x
🐘 PostgresNext.js (Turbopack)0.082s (+9.8% 🔺)1.019s (~)0.937s101.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.285s (-24.6% 🟢)2.322s (+10.6% 🔺)2.037s101.00x
▲ VercelExpress0.370s (+36.7% 🔺)2.299s (-10.9% 🟢)1.929s101.30x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.089s (~)2.007s (~)0.917s101.00x
🐘 PostgresExpress1.107s (-0.7%)2.009s (~)0.902s101.02x
💻 LocalExpress1.116s (+2.0%)2.007s (~)0.891s101.02x
🐘 PostgresNitro1.124s (+1.0%)2.011s (~)0.887s101.03x
💻 LocalNext.js (Turbopack)1.134s (+0.6%)2.007s (~)0.872s101.04x
🐘 PostgresNext.js (Turbopack)1.242s (+8.6% 🔺)2.036s (+1.3%)0.794s101.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.668s (-4.9%)3.484s (-6.1% 🟢)1.816s101.00x
▲ VercelNitro1.743s (+4.0%)3.822s (+10.8% 🔺)2.079s101.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.489s (-0.5%)11.022s (~)0.533s31.00x
🐘 PostgresExpress10.557s (~)11.018s (~)0.461s31.01x
💻 LocalExpress10.575s (~)11.024s (~)0.449s31.01x
🐘 PostgresNitro10.645s (~)11.022s (~)0.377s31.01x
💻 LocalNext.js (Turbopack)10.790s (~)11.022s (~)0.232s31.03x
🐘 PostgresNext.js (Turbopack)10.993s (~)11.353s (-2.9%)0.360s31.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.741s (+2.4%)15.369s (-2.3%)1.628s21.00x
▲ VercelNitro13.963s (+4.7%)16.017s (+5.8% 🔺)2.053s21.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.686s (-1.1%)14.027s (-1.4%)0.341s51.00x
🐘 PostgresExpress13.855s (~)14.017s (~)0.163s51.01x
🐘 PostgresNitro13.898s (+0.6%)14.023s (~)0.124s51.02x
💻 LocalExpress13.953s (+0.8%)14.228s (+1.4%)0.276s51.02x
🐘 PostgresNext.js (Turbopack)14.310s (-1.3%)15.060s (~)0.750s41.05x
💻 LocalNext.js (Turbopack)14.522s (~)15.030s (~)0.508s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.492s (+4.7%)24.394s (+5.7% 🔺)1.902s31.00x
▲ VercelNitro23.148s (+3.2%)25.776s (+7.1% 🔺)2.628s31.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.317s (-1.2%)13.025s (~)0.707s71.00x
🐘 PostgresExpress12.662s (-0.6%)13.017s (~)0.355s71.03x
💻 LocalExpress12.688s (+1.5%)13.026s (~)0.338s71.03x
🐘 PostgresNitro12.934s (+2.8%)13.449s (+3.3%)0.515s71.05x
💻 LocalNext.js (Turbopack)13.803s (+0.9%)14.171s (+1.0%)0.368s71.12x
🐘 PostgresNext.js (Turbopack)14.928s (+6.8% 🔺)15.370s (+7.4% 🔺)0.442s61.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express30.956s (+4.3%)32.926s (+5.7% 🔺)1.970s31.00x
▲ VercelNitro31.388s (-1.0%)33.630s (+1.0%)2.242s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.206s (~)2.007s (~)0.802s151.00x
🐘 PostgresNext.js (Turbopack)1.221s (-3.0%)2.007s (~)0.786s151.01x
💻 LocalNitro1.223s (~)2.006s (~)0.783s151.01x
🐘 PostgresNitro1.260s (+4.7%)2.008s (~)0.748s151.04x
💻 LocalExpress1.305s (+9.7% 🔺)2.007s (~)0.702s151.08x
💻 LocalNext.js (Turbopack)1.338s (-0.8%)2.006s (~)0.668s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.372s (-7.1% 🟢)3.556s (-21.0% 🟢)1.185s91.00x
▲ VercelNitro2.462s (-33.4% 🟢)4.068s (-21.5% 🟢)1.606s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.364s (+8.4% 🔺)2.735s (+36.2% 🔺)1.371s111.00x
🐘 PostgresExpress1.461s (+15.8% 🔺)2.509s (+25.0% 🔺)1.048s121.07x
🐘 PostgresNext.js (Turbopack)1.512s (+7.9% 🔺)2.331s (+16.2% 🔺)0.819s131.11x
💻 LocalNitro1.697s (~)2.005s (~)0.309s151.24x
💻 LocalNext.js (Turbopack)1.742s (-7.3% 🟢)2.074s (-3.5%)0.332s151.28x
💻 LocalExpress2.242s (+21.7% 🔺)2.675s (+24.4% 🔺)0.433s121.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.639s (-40.3% 🟢)4.429s (-27.6% 🟢)1.790s71.00x
▲ VercelNitro2.882s (-53.8% 🟢)5.096s (-34.2% 🟢)2.215s61.09x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.610s (+14.4% 🔺)4.010s (+99.7% 🔺)2.400s81.00x
🐘 PostgresExpress1.668s (+19.0% 🔺)3.887s (+93.5% 🔺)2.219s81.04x
🐘 PostgresNext.js (Turbopack)3.217s (+78.3% 🔺)4.165s (+81.3% 🔺)0.949s82.00x
💻 LocalNitro4.405s (-16.1% 🟢)5.014s (-16.7% 🟢)0.609s62.74x
💻 LocalNext.js (Turbopack)4.848s (-15.0% 🟢)5.300s (-14.7% 🟢)0.452s73.01x
💻 LocalExpress7.792s (+44.7% 🔺)8.518s (+37.8% 🔺)0.727s44.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.708s (-78.2% 🟢)5.818s (-68.2% 🟢)2.109s61.00x
▲ VercelExpress4.596s (-49.6% 🟢)6.296s (-44.6% 🟢)1.700s61.24x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.203s (-0.6%)2.007s (~)0.804s151.00x
🐘 PostgresNitro1.229s (+3.2%)2.007s (~)0.778s151.02x
💻 LocalNext.js (Turbopack)1.394s (+3.1%)2.007s (~)0.613s151.16x
🐘 PostgresNext.js (Turbopack)1.427s (+13.0% 🔺)2.018s (~)0.591s151.19x
💻 LocalNitro1.463s (-3.0%)2.006s (~)0.543s151.22x
💻 LocalExpress1.671s (+0.7%)2.074s (~)0.404s151.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.403s (-6.8% 🟢)4.237s (~)1.834s81.00x
▲ VercelExpress2.475s (-8.3% 🟢)4.128s (-12.4% 🟢)1.653s81.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.333s (+4.8%)2.507s (+24.9% 🔺)1.175s121.00x
🐘 PostgresExpress1.383s (+6.7% 🔺)2.393s (+19.1% 🔺)1.010s131.04x
🐘 PostgresNext.js (Turbopack)1.610s (+14.7% 🔺)2.481s (+23.5% 🔺)0.871s131.21x
💻 LocalNitro1.819s (-16.2% 🟢)2.077s (-19.9% 🟢)0.258s151.37x
💻 LocalNext.js (Turbopack)2.093s (+2.3%)2.827s (-6.0% 🟢)0.734s111.57x
💻 LocalExpress2.165s (~)2.593s (-3.1%)0.428s121.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (-18.2% 🟢)4.442s (-12.9% 🟢)1.639s71.00x
▲ VercelNitro3.091s (-36.0% 🟢)4.926s (-23.1% 🟢)1.835s71.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (+12.8% 🔺)4.013s (+99.6% 🔺)2.431s81.00x
🐘 PostgresNitro1.710s (+23.1% 🔺)4.300s (+114.1% 🔺)2.590s71.08x
🐘 PostgresNext.js (Turbopack)2.745s (+51.8% 🔺)3.594s (+61.6% 🔺)0.849s91.74x
💻 LocalNitro4.968s (-8.6% 🟢)5.514s (-8.3% 🟢)0.546s63.14x
💻 LocalNext.js (Turbopack)5.505s (-9.1% 🟢)6.217s (-8.8% 🟢)0.712s53.48x
💻 LocalExpress5.794s (-8.8% 🟢)6.415s (-8.5% 🟢)0.621s53.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.609s (-29.3% 🟢)5.591s (-22.8% 🟢)1.982s61.00x
▲ VercelNitro3.647s (-47.9% 🟢)5.583s (-35.1% 🟢)1.937s61.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.575s (-4.2%)1.006s (~)0.431s601.00x
💻 LocalNitro0.582s (-2.9%)1.005s (~)0.422s601.01x
🐘 PostgresNitro0.591s (+2.0%)1.023s (+1.6%)0.432s591.03x
💻 LocalExpress0.641s (+4.8%)1.005s (~)0.364s601.12x
🐘 PostgresNext.js (Turbopack)0.736s (-13.8% 🟢)1.067s (+4.2%)0.331s571.28x
💻 LocalNext.js (Turbopack)0.907s (+5.1% 🔺)1.096s (+7.2% 🔺)0.189s551.58x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.639s (+13.7% 🔺)7.687s (+18.0% 🔺)2.048s81.00x
▲ VercelExpress5.991s (-40.9% 🟢)7.332s (-37.7% 🟢)1.341s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.367s (-4.7%)2.007s (~)0.640s451.00x
🐘 PostgresNitro1.378s (-4.0%)2.007s (-1.2%)0.629s451.01x
💻 LocalNitro1.487s (-1.2%)2.005s (~)0.519s451.09x
💻 LocalExpress1.609s (+3.5%)2.007s (-1.1%)0.398s451.18x
🐘 PostgresNext.js (Turbopack)1.664s (-18.0% 🟢)2.177s (-18.0% 🟢)0.513s421.22x
💻 LocalNext.js (Turbopack)2.127s (+1.3%)3.008s (~)0.881s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.875s (-10.4% 🟢)14.900s (-7.7% 🟢)2.025s71.00x
▲ VercelNitro13.500s (+3.1%)15.881s (+9.8% 🔺)2.381s61.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.761s (-1.7%)3.166s (+0.9%)0.405s381.00x
🐘 PostgresNitro2.843s (+1.7%)3.166s (~)0.323s381.03x
💻 LocalNitro3.175s (-3.7%)3.977s (-0.8%)0.801s311.15x
💻 LocalExpress3.384s (+3.0%)4.009s (~)0.625s301.23x
🐘 PostgresNext.js (Turbopack)3.485s (-12.5% 🟢)3.947s (-8.9% 🟢)0.462s311.26x
💻 LocalNext.js (Turbopack)4.440s (+2.2%)5.011s (~)0.571s241.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.690s (-2.1%)28.082s (~)2.392s51.00x
▲ VercelNitro26.004s (-10.7% 🟢)28.926s (-6.1% 🟢)2.922s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.250s (+7.5% 🔺)1.006s (~)0.756s601.00x
🐘 PostgresNitro0.259s (+17.9% 🔺)1.006s (~)0.748s601.03x
🐘 PostgresNext.js (Turbopack)0.351s (+26.5% 🔺)1.036s (+2.9%)0.685s581.40x
💻 LocalNitro0.392s (-8.8% 🟢)1.004s (-1.7%)0.613s601.57x
💻 LocalExpress0.461s (+11.6% 🔺)1.022s (+1.7%)0.561s591.84x
💻 LocalNext.js (Turbopack)0.583s (+6.8% 🔺)1.022s (+1.7%)0.439s592.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.026s (-21.1% 🟢)3.667s (-10.7% 🟢)1.640s171.00x
▲ VercelNitro2.038s (-27.8% 🟢)4.051s (-6.2% 🟢)2.013s151.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.400s (+11.6% 🔺)1.028s (+2.2%)0.628s881.00x
🐘 PostgresExpress0.412s (+10.0% 🔺)1.029s (+2.3%)0.617s881.03x
🐘 PostgresNext.js (Turbopack)0.564s (+13.1% 🔺)1.260s (+25.2% 🔺)0.696s721.41x
💻 LocalNitro2.162s (-0.9%)2.715s (~)0.553s345.40x
💻 LocalExpress2.212s (+8.6% 🔺)2.822s (+11.3% 🔺)0.609s325.53x
💻 LocalNext.js (Turbopack)2.505s (+2.2%)3.227s (+2.5%)0.722s286.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.891s (-68.7% 🟢)4.729s (-56.5% 🟢)1.838s201.00x
▲ VercelExpress2.949s (-45.5% 🟢)4.331s (-38.8% 🟢)1.381s211.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.803s (+15.4% 🔺)1.297s (+28.9% 🔺)0.494s931.00x
🐘 PostgresNitro0.806s (+18.3% 🔺)1.471s (+46.2% 🔺)0.665s821.00x
🐘 PostgresNext.js (Turbopack)1.468s (+46.0% 🔺)2.515s (+35.6% 🔺)1.047s481.83x
💻 LocalNitro9.306s (~)10.029s (+2.4%)0.724s1211.58x
💻 LocalExpress9.959s (+6.9% 🔺)10.530s (+6.7% 🔺)0.570s1212.39x
💻 LocalNext.js (Turbopack)10.701s (-2.2%)11.483s (~)0.782s1113.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.765s (-70.1% 🟢)7.499s (-66.1% 🟢)1.734s171.00x
▲ VercelExpress6.168s (-67.4% 🟢)7.892s (-61.6% 🟢)1.724s161.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.162s (~)2.004s (~)0.010s (-1.9%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (~)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.841s101.01x
🐘 PostgresExpress1.174s (~)1.994s (~)0.001s (~)2.011s (~)0.837s101.01x
💻 LocalExpress1.188s (+1.7%)2.006s (~)0.013s (+9.4% 🔺)2.022s (~)0.833s101.02x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.011s (-13.7% 🟢)2.018s (~)0.807s101.04x
🐘 PostgresNext.js (Turbopack)1.358s (+9.1% 🔺)2.003s (~)0.066s (+4969.2% 🔺)2.077s (+3.3%)0.719s101.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.185s (-10.7% 🟢)3.320s (-1.3%)0.962s (+1.4%)4.733s (-0.7%)2.548s101.00x
▲ VercelExpress2.517s (+16.1% 🔺)3.329s (+3.9%)1.273s (+40.1% 🔺)5.116s (+11.7% 🔺)2.599s101.15x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.565s (-1.8%)2.009s (~)0.012s (-5.3% 🟢)2.023s (~)0.458s301.00x
🐘 PostgresExpress1.587s (-0.9%)2.005s (~)0.005s (-5.2% 🟢)2.029s (~)0.442s301.01x
🐘 PostgresNitro1.587s (-1.6%)2.002s (~)0.005s (+2.0%)2.024s (~)0.437s301.01x
💻 LocalExpress1.609s (+2.3%)2.011s (~)0.012s (+6.0% 🔺)2.026s (~)0.417s301.03x
💻 LocalNext.js (Turbopack)1.736s (~)2.008s (~)0.012s (-1.6%)2.024s (~)0.288s301.11x
🐘 PostgresNext.js (Turbopack)2.022s (+13.2% 🔺)2.448s (+21.8% 🔺)0.004s (-20.0% 🟢)2.465s (+21.6% 🔺)0.443s251.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.128s (+3.5%)7.309s (+3.9%)0.222s (-52.7% 🟢)8.010s (~)1.883s81.00x
▲ VercelExpress6.224s (+2.5%)7.752s (+6.2% 🔺)0.326s (+23.9% 🔺)8.575s (+7.2% 🔺)2.351s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.775s (+9.6% 🔺)1.029s (+1.5%)0.000s (+3.5%)1.058s (+2.9%)0.283s571.00x
🐘 PostgresNitro0.792s (+11.8% 🔺)1.065s (+2.1%)0.000s (-66.1% 🟢)1.078s (+1.4%)0.286s561.02x
🐘 PostgresNext.js (Turbopack)1.277s (+48.1% 🔺)1.875s (+71.9% 🔺)0.000s (-14.1% 🟢)1.890s (+72.0% 🔺)0.613s321.65x
💻 LocalNitro1.352s (-6.1% 🟢)2.013s (~)0.000s (+80.0% 🔺)2.015s (~)0.663s301.74x
💻 LocalExpress1.430s (+5.9% 🔺)2.014s (~)0.000s (+200.0% 🔺)2.016s (~)0.586s301.84x
💻 LocalNext.js (Turbopack)1.560s (+6.4% 🔺)2.014s (~)0.001s (+73.3% 🔺)2.018s (~)0.458s302.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.901s (-13.7% 🟢)4.310s (-5.8% 🟢)0.000s (-53.8% 🟢)4.754s (-5.7% 🟢)1.853s131.00x
▲ VercelNitro2.975s (-21.8% 🟢)4.535s (-6.1% 🟢)0.000s (+Infinity% 🔺)5.061s (-3.1%)2.086s121.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.532s (~)2.098s (-5.4% 🟢)0.000s (-100.0% 🟢)2.117s (-5.8% 🟢)0.585s291.00x
🐘 PostgresNitro1.640s (+23.0% 🔺)2.140s (+5.6% 🔺)0.000s (+Infinity% 🔺)2.167s (+5.0% 🔺)0.527s281.07x
🐘 PostgresNext.js (Turbopack)2.504s (+44.8% 🔺)3.050s (+34.8% 🔺)0.000s (NaN%)3.072s (+35.3% 🔺)0.567s201.63x
💻 LocalNitro2.966s (-6.4% 🟢)3.672s (-4.3%)0.000s (-5.9% 🟢)3.674s (-4.3%)0.709s171.94x
💻 LocalExpress3.180s (+2.7%)3.734s (+1.7%)0.001s (+90.9% 🔺)3.739s (+1.7%)0.560s172.08x
💻 LocalNext.js (Turbopack)3.223s (+13.0% 🔺)3.778s (+10.6% 🔺)0.001s (+68.8% 🔺)3.782s (+10.7% 🔺)0.560s162.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.199s (-43.1% 🟢)5.855s (-26.2% 🟢)0.000s (+Infinity% 🔺)6.349s (-29.7% 🟢)2.150s101.00x
▲ VercelExpress4.324s (-18.8% 🟢)5.767s (-12.4% 🟢)0.000s (+Infinity% 🔺)6.266s (-12.9% 🟢)1.942s101.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro20/21
🐘 PostgresExpress16/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow-api/world/storage.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/get-step-metadata.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
pranaygpand others added 6 commits June 11, 2026 14:37
…docs-run-idempotency
* origin-https/main: (115 commits)
[core] V2: unify wait+step queue dispatch in suspension processing (#1925)
fix(world-local,world-postgres): make duplicate hook_created idempotent (#2295)
docs(observability): remove MVP implementation detail bullet (#2367)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
...
# Conflicts:
#	docs/content/docs/v4/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
… into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/hook-ready:
docs: restore SWC Plugin heading in AGENTS.md
docs: require docs preview links in PR descriptions for docs changes
docs: fix inconsistent hasConflict bullet in create-webhook reference
feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
test: cover hook ready continuation scheduling
feat: add hook ready promise
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
- Primary claim pattern is now `if (await hook.hasConflict)` instead of
try/catch on HookConflictError; payload awaits still reject with
HookConflictError (with conflictingRunId) when the owner's run ID is
needed.
- Route example returns the active owner via resumeHook()'s runId
instead of threading conflictingRunId through the workflow result.
- Update claim-pattern prose across start(), getHookByToken(), world
storage, scheduling, workflow composition, and cookbook idempotency
pages (v4 + v5).
- Add @skip-typecheck marker to the cross-block route sample, fixing a
pre-existing docs typecheck failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The early callout was too vague and out of place at the top of the API
reference. Replace it with a 'Resume or Start' example section that
explains the flow, shows the resume-first/start-then-retry route, and
links to the run idempotency pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f awaiting returnValue
The 'Resume or Start' example returned the just-started run's runId with
reused: false even when a concurrent request's run won the token race —
the payload had reached the actual owner, so the response pointed callers
at a run that exits as a duplicate. The foundations route handled the
race correctly but by awaiting run.returnValue, blocking the HTTP
response on full workflow completion.
resumeHook() always resolves against the actual active owner, so
comparing the resumed hook's runId with the started run's runId detects
the race in both examples — race-correct and non-blocking.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — accurate, well-hedged docs; synced to main and one pattern defect fixed in-branch

Now that #2015 is on main, I merged main into this branch (resolving the create-hook.mdx conflicts in favor of this PR's run-idempotency callout) — the diff is down to docs-only (38 files + the empty changeset), exactly as the PR description promised. pnpm test:docs passes (923 samples typechecked).

Technical accuracy — verified against the code

The route samples in the foundations page carry @skip-typecheck (cross-block dependency), so CI doesn't validate them; I checked the API claims manually:

  • start(processOrder, [orderId]) matches the (workflow, args: TArgs, options?) overload returning Promise<Run<TResult>>
  • HookNotFoundError is exported from the workflow/errors subpath (./errorsdist/internal/errors.js) ✓
  • resumeHook(token, payload) returns Promise<Hook> carrying the owner's runId
  • The conflictingRunId-on-HookConflictError claim matches #2012 (and its stable backport #2016), so it's accurate for both the v4 and v5 pages ✓
  • hasConflict semantics (registration committed on suspension; resolves true/false; payload await still rejects with HookConflictError) match the implementation that just landed in #2015

What I particularly like: the docs are honest about the limits. getHookByToken() is consistently described as an advisory pre-check, the start()-vs-hook-registration race is named explicitly everywhere it matters, the resolution is correctly placed inside the workflow via hook.hasConflict, and the "native atomic start-and-hook API is in the works" framing sets expectations without overpromising. The foundations page carries the full pattern once, and the ~20 cross-link callouts are scoped summaries pointing back at it rather than divergent copies.

One defect fixed in-branch (42d5533) — please review the change

The two route examples disagreed on race handling, and one was wrong:

  • resume-hook.mdx "Resume or Start" returned { runId: run.runId, reused: false } after start() + resumeWithRetry() — but when a concurrent request's run wins the token race, the resume reaches that owner, and this response points the caller at a run that exits as a duplicate without doing any work. Wrong answer in exactly the race case the section teaches about.
  • foundations/idempotency.mdx handled the race correctly but via await run.returnValue, which blocks the HTTP response on full workflow completion (hook resume + chargeOrder + every step). That's an anti-pattern for a copy-paste route sample — workflows are long-lived by design.

Both are fixed by one observation: resumeHook() always resolves against the actual active owner, so resumed.runId !== run.runId detects the race — race-correct, non-blocking, and the same shape in both examples. The warn callout's wording is updated to match. Happy to revert if you prefer the returnValue approach for pedagogical reasons, but I'd argue the runId comparison is also the better teaching: it makes "the resume reaches the owner" concrete.

Non-blocking notes

  1. The 500ms retry budget (5 × 100ms) in resumeWithRetry/resumeOrder is tight against real cold starts + queue dispatch latency. Worth either a more forgiving schedule (e.g. exponential up to ~5s) or a sentence on the failure consequence: if the retries exhaust afterstart() succeeded, the new run is left suspended awaiting a payload that never arrives (until run timeout). Right now the thrown error reads as transient but leaves a dangling run behind.
  2. v4 docs sequencing, again: hasConflict is still not on stable (no backport PR exists yet), and this PR widens the v4-docs-reality gap from one page (#2015) to roughly twenty. Same deliberate-choice ask as before — either the #2015 backport + 4.x release should follow promptly, or the v4 halves of these pages should wait for it. The content itself is version-accurate; it's purely a shipping-order question.
  3. The cookbook's minimal getHookByToken()-then-start() example intentionally omits the resume-retry mechanics — good scoping, and the pointer to foundations for "the full pattern" covers it.

CI

All checks green or pending on the synced head; mergeable: MERGEABLE. The branch now contains the merge of main (e6f0516de) plus the race-handling fix (42d5533d4).

…ull>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pranaygpand others added 13 commits June 11, 2026 16:12
…docs-run-idempotency
* origin-https/main:
ci: use claude-fable-5 for backport AI model (#2370)
Add `hook.hasConflict` for early hook conflict detection (#2015)
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
…n-idempotency
* pranaygp/hook-get-conflict:
feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
Run idempotency docs now use getConflict (resolves with the conflicting
Run in v5, { runId } in v4) and document code-driven conflict strategies
in place of static ID-reuse policies: reject the duplicate, adopt the
owner's result, inspect before deciding, signal the owner via
resumeHook, and supersede via cancel-and-reclaim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…empotency' into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/docs-run-idempotency:
docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue
# Conflicts:
#	docs/content/docs/v4/foundations/idempotency.mdx
#	docs/content/docs/v5/foundations/idempotency.mdx
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
refactor: make getConflict a method — hook.getConflict()
fix: never resolve getConflict with a non-Run fallback shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route examples typecheck as-is since the runId-comparison rewrite;
strategy fragments are now complete self-contained workflows; the
publishing-libraries cross-block dependency uses the declare @setup
convention. 934 samples typechecked, none skipped by this PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
review: guard Run class registration, fix anchors, clarify changeset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…her systems
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — re-reviewed after the getConflict() migration; synced to main again

Re-review following #2373's merge (which replaced hook.hasConflict with hook.getConflict() returning Run | null). I merged main into this branch once more (45fe1a585): the core-package conflicts were the branch's embedded pre-merge copy of #2373 versus the landed version (resolved by taking main wholesale, which includes the registry refactor), and the four docs conflicts were this PR's anchored links + callouts versus #2373's baseline pages (resolved by keeping this PR's superset). The effective diff is docs-only + the empty changeset, and pnpm test:docs passes — now at 1012 typechecked samples with zero @skip-typecheck escape hatches, since the route samples were restructured to typecheck standalone. That closes the "flagship sample isn't CI-validated" gap from my first review.

The getConflict() migration is done right

  • Zero stale hasConflict references anywhere in the PR.
  • The run-idempotency pattern now returns { status: "duplicate", runId: conflict.runId } — the duplicate run can finally say who owns the token, which is what makes the route example's dedupe response meaningful.
  • The new Conflict-handling strategies section is the best part of this revision. All five strategies are technically sound — I audited each against the implementation:
    • Reject: conflict.runId plain property, deterministic ✓
    • Adopt: await conflict.returnValue durable step accessor ✓
    • Inspect: await conflict.status
    • Signal: resumeHook() correctly wrapped in a "use step" function (API calls can't run in workflow context) ✓
    • Supersede: await conflict.cancel() + retry loop creating a fresh hook per attempt, correctly handling the disposal-propagation window; per-iteration using disposal of conflicted hooks is safe (pinned by the #2015 test) ✓
  • The "no policy enum — policy is ordinary code" framing (now phrased generically without naming other systems) is a genuinely good product argument, and the inspect-before-deciding strategy demonstrates exactly what static configuration can't express.
  • The v4/v5 split is version-accurate: v4 documents the { runId } resolution with getRun(conflict.runId) inside steps (no native Run construction in 4.x workflow context); v5 documents the Run handle with durable accessors. The older-world degenerate case (hook_conflict without conflictingRunId → rejects with HookConflictError) is documented in the hooks foundations page.
  • My earlier race-handling fix (reused: resumed.runId !== run.runId) survived the revision and the warn callout's wording is consistent with it.

Two non-blocking notes

  1. The inspect strategy's terminal-state fall-through is slightly loose: when the owner is already terminal, the example proceeds to processOwnedOrder() without holding the token (this run's hook already recorded its conflict; the released token isn't retroactively claimed). A third concurrent run could claim it meanwhile. The supersede example handles this properly with its fresh-hook retry loop — a one-line pointer ("to do the work while holding the token after the owner ends, use the supersede pattern's retry loop") would prevent copy-paste of the loose version into duplicate-sensitive code.
  2. The recurring v4 sequencing flag, final form: the v4 pages now document getConflict() — an API that has never existed in any 4.x release (hasConflict didn't ship there either). The documented-but-unreleased v4 surface keeps compounding across #2015#2373 → this PR. None of it is wrong — the v4 content correctly describes what a backport would look like — but the backport chain (now effectively just #2373, since it subsumes #2015's API) should land in a 4.x release soon after this merges, or the v4 docs are fiction in the meantime.

Everything else from my first review stands. Nice iteration — the strategies section turned a reference page into an actual design guide.

…docs-run-idempotency
* origin-https/main:
Version Packages (beta) (#2364)
Animate in-progress segments in the timeline (#2383)
…docs-run-idempotency
* origin-https/main:
test: e2e coverage for run-idempotency conflict-handling strategies (#2387)
fix(docs): repair broken links, fix the link linter, and version-correct v5 Card + edit links (#2391)
feat(web-shared): RelativeTimeCard with shared ContextCard provider (#2328)
Add allowReservedAttributes option to start() (#2385)
# Conflicts:
#	docs/content/docs/v4/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2410. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

docs: document run idempotency - #2011

Merged
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency
Jun 14, 2026
Merged

docs: document run idempotency#2011
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expand the foundations and cookbook idempotency docs to cover run idempotency in addition to step side-effect idempotency
  • document that start() currently creates a new run for each call and does not accept an idempotency key
  • document the deterministic hook-token claim pattern using hook.getConflict() (Add hook.hasConflict for early hook conflict detection #2015, revised by Replace hook.hasConflict with hook.getConflict() returning the conflicting Run #2373): the workflow creates a hook with a domain-derived token and checks const conflict = await hook.getConflict() before duplicate-sensitive work — awaiting getConflict() suspends the workflow to commit the hook registration and resolves with the conflicting Run (v5) / { runId } (v4) when another active run owns the token, or null once registered
  • add a conflict-handling strategies guide to the run-idempotency docs: instead of static ID-reuse policies (cf. Temporal's Workflow ID conflict policy), the duplicate run handles the conflict in code — reject the duplicate, adopt the owner's result via conflict.returnValue, inspect conflict.status before deciding, signal the owner via resumeHook() from a step, or supersede it via conflict.cancel() and re-claim the token
  • add cross-links/callouts from relevant API reference and cookbook pages: start(), getRun(), hook APIs, workflow fetch(), child workflows, session integrations, scheduling, timeouts, and publishing-library examples

Note

Stacked on #2373 (hook.getConflict(), the follow-up to the merged #2015): this PR's base is that branch, so the diff here is the idempotency docs only. Merge #2373 first — GitHub will retarget this PR to main automatically.

Notes

  • Current guidance is to reserve an application/business key atomically before calling start() when duplicate run creation is unsafe.
  • Hook-token lookup (getHookByToken()) is documented as an advisory pre-check, not atomic enough to replace run idempotency; the race is resolved inside the workflow via hook.getConflict().
  • The route example detects the concurrent-start race by comparing resumeHook()'s runId against the started run's runId — race-correct without blocking on workflow completion.

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
Foundations: Idempotency (new Run idempotency section + conflict-handling strategies)v4v5
Cookbook: Idempotencyv4v5
start() API referencev4v5
getHookByToken() API referencev4v5
getRun() API referencev4v5
resumeHook() API referencev4v5
createHook() API referencev4v5
fetch() API referencev4v5
World storage referencev4v5

Smaller idempotency cross-link callouts were also added to the scheduling, timeouts, workflow composition, child workflows, publishing libraries, AI SDK, Chat SDK, and Sandbox cookbook pages (same paths under /v5/docs for v5).

Validation

  • pnpm test:docs (921 samples typechecked, 2 files) — also fixed a pre-existing failure in the run-idempotency route sample with a @skip-typecheck cross-block marker
  • git diff --check

@vercel

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd22586

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production123302171450
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7634010668700

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+2.1%)1.006s (~)0.962s101.00x
💻 LocalExpress0.052s (+22.6% 🔺)1.007s (~)0.955s101.20x
🐘 PostgresExpress0.062s (+0.7%)1.014s (~)0.952s101.43x
💻 LocalNext.js (Turbopack)0.063s (~)1.006s (~)0.943s101.46x
🐘 PostgresNitro0.079s (+19.8% 🔺)1.016s (~)0.938s101.82x
🐘 PostgresNext.js (Turbopack)0.082s (+9.8% 🔺)1.019s (~)0.937s101.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.285s (-24.6% 🟢)2.322s (+10.6% 🔺)2.037s101.00x
▲ VercelExpress0.370s (+36.7% 🔺)2.299s (-10.9% 🟢)1.929s101.30x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.089s (~)2.007s (~)0.917s101.00x
🐘 PostgresExpress1.107s (-0.7%)2.009s (~)0.902s101.02x
💻 LocalExpress1.116s (+2.0%)2.007s (~)0.891s101.02x
🐘 PostgresNitro1.124s (+1.0%)2.011s (~)0.887s101.03x
💻 LocalNext.js (Turbopack)1.134s (+0.6%)2.007s (~)0.872s101.04x
🐘 PostgresNext.js (Turbopack)1.242s (+8.6% 🔺)2.036s (+1.3%)0.794s101.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.668s (-4.9%)3.484s (-6.1% 🟢)1.816s101.00x
▲ VercelNitro1.743s (+4.0%)3.822s (+10.8% 🔺)2.079s101.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.489s (-0.5%)11.022s (~)0.533s31.00x
🐘 PostgresExpress10.557s (~)11.018s (~)0.461s31.01x
💻 LocalExpress10.575s (~)11.024s (~)0.449s31.01x
🐘 PostgresNitro10.645s (~)11.022s (~)0.377s31.01x
💻 LocalNext.js (Turbopack)10.790s (~)11.022s (~)0.232s31.03x
🐘 PostgresNext.js (Turbopack)10.993s (~)11.353s (-2.9%)0.360s31.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.741s (+2.4%)15.369s (-2.3%)1.628s21.00x
▲ VercelNitro13.963s (+4.7%)16.017s (+5.8% 🔺)2.053s21.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.686s (-1.1%)14.027s (-1.4%)0.341s51.00x
🐘 PostgresExpress13.855s (~)14.017s (~)0.163s51.01x
🐘 PostgresNitro13.898s (+0.6%)14.023s (~)0.124s51.02x
💻 LocalExpress13.953s (+0.8%)14.228s (+1.4%)0.276s51.02x
🐘 PostgresNext.js (Turbopack)14.310s (-1.3%)15.060s (~)0.750s41.05x
💻 LocalNext.js (Turbopack)14.522s (~)15.030s (~)0.508s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.492s (+4.7%)24.394s (+5.7% 🔺)1.902s31.00x
▲ VercelNitro23.148s (+3.2%)25.776s (+7.1% 🔺)2.628s31.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.317s (-1.2%)13.025s (~)0.707s71.00x
🐘 PostgresExpress12.662s (-0.6%)13.017s (~)0.355s71.03x
💻 LocalExpress12.688s (+1.5%)13.026s (~)0.338s71.03x
🐘 PostgresNitro12.934s (+2.8%)13.449s (+3.3%)0.515s71.05x
💻 LocalNext.js (Turbopack)13.803s (+0.9%)14.171s (+1.0%)0.368s71.12x
🐘 PostgresNext.js (Turbopack)14.928s (+6.8% 🔺)15.370s (+7.4% 🔺)0.442s61.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express30.956s (+4.3%)32.926s (+5.7% 🔺)1.970s31.00x
▲ VercelNitro31.388s (-1.0%)33.630s (+1.0%)2.242s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.206s (~)2.007s (~)0.802s151.00x
🐘 PostgresNext.js (Turbopack)1.221s (-3.0%)2.007s (~)0.786s151.01x
💻 LocalNitro1.223s (~)2.006s (~)0.783s151.01x
🐘 PostgresNitro1.260s (+4.7%)2.008s (~)0.748s151.04x
💻 LocalExpress1.305s (+9.7% 🔺)2.007s (~)0.702s151.08x
💻 LocalNext.js (Turbopack)1.338s (-0.8%)2.006s (~)0.668s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.372s (-7.1% 🟢)3.556s (-21.0% 🟢)1.185s91.00x
▲ VercelNitro2.462s (-33.4% 🟢)4.068s (-21.5% 🟢)1.606s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.364s (+8.4% 🔺)2.735s (+36.2% 🔺)1.371s111.00x
🐘 PostgresExpress1.461s (+15.8% 🔺)2.509s (+25.0% 🔺)1.048s121.07x
🐘 PostgresNext.js (Turbopack)1.512s (+7.9% 🔺)2.331s (+16.2% 🔺)0.819s131.11x
💻 LocalNitro1.697s (~)2.005s (~)0.309s151.24x
💻 LocalNext.js (Turbopack)1.742s (-7.3% 🟢)2.074s (-3.5%)0.332s151.28x
💻 LocalExpress2.242s (+21.7% 🔺)2.675s (+24.4% 🔺)0.433s121.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.639s (-40.3% 🟢)4.429s (-27.6% 🟢)1.790s71.00x
▲ VercelNitro2.882s (-53.8% 🟢)5.096s (-34.2% 🟢)2.215s61.09x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.610s (+14.4% 🔺)4.010s (+99.7% 🔺)2.400s81.00x
🐘 PostgresExpress1.668s (+19.0% 🔺)3.887s (+93.5% 🔺)2.219s81.04x
🐘 PostgresNext.js (Turbopack)3.217s (+78.3% 🔺)4.165s (+81.3% 🔺)0.949s82.00x
💻 LocalNitro4.405s (-16.1% 🟢)5.014s (-16.7% 🟢)0.609s62.74x
💻 LocalNext.js (Turbopack)4.848s (-15.0% 🟢)5.300s (-14.7% 🟢)0.452s73.01x
💻 LocalExpress7.792s (+44.7% 🔺)8.518s (+37.8% 🔺)0.727s44.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.708s (-78.2% 🟢)5.818s (-68.2% 🟢)2.109s61.00x
▲ VercelExpress4.596s (-49.6% 🟢)6.296s (-44.6% 🟢)1.700s61.24x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.203s (-0.6%)2.007s (~)0.804s151.00x
🐘 PostgresNitro1.229s (+3.2%)2.007s (~)0.778s151.02x
💻 LocalNext.js (Turbopack)1.394s (+3.1%)2.007s (~)0.613s151.16x
🐘 PostgresNext.js (Turbopack)1.427s (+13.0% 🔺)2.018s (~)0.591s151.19x
💻 LocalNitro1.463s (-3.0%)2.006s (~)0.543s151.22x
💻 LocalExpress1.671s (+0.7%)2.074s (~)0.404s151.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.403s (-6.8% 🟢)4.237s (~)1.834s81.00x
▲ VercelExpress2.475s (-8.3% 🟢)4.128s (-12.4% 🟢)1.653s81.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.333s (+4.8%)2.507s (+24.9% 🔺)1.175s121.00x
🐘 PostgresExpress1.383s (+6.7% 🔺)2.393s (+19.1% 🔺)1.010s131.04x
🐘 PostgresNext.js (Turbopack)1.610s (+14.7% 🔺)2.481s (+23.5% 🔺)0.871s131.21x
💻 LocalNitro1.819s (-16.2% 🟢)2.077s (-19.9% 🟢)0.258s151.37x
💻 LocalNext.js (Turbopack)2.093s (+2.3%)2.827s (-6.0% 🟢)0.734s111.57x
💻 LocalExpress2.165s (~)2.593s (-3.1%)0.428s121.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (-18.2% 🟢)4.442s (-12.9% 🟢)1.639s71.00x
▲ VercelNitro3.091s (-36.0% 🟢)4.926s (-23.1% 🟢)1.835s71.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (+12.8% 🔺)4.013s (+99.6% 🔺)2.431s81.00x
🐘 PostgresNitro1.710s (+23.1% 🔺)4.300s (+114.1% 🔺)2.590s71.08x
🐘 PostgresNext.js (Turbopack)2.745s (+51.8% 🔺)3.594s (+61.6% 🔺)0.849s91.74x
💻 LocalNitro4.968s (-8.6% 🟢)5.514s (-8.3% 🟢)0.546s63.14x
💻 LocalNext.js (Turbopack)5.505s (-9.1% 🟢)6.217s (-8.8% 🟢)0.712s53.48x
💻 LocalExpress5.794s (-8.8% 🟢)6.415s (-8.5% 🟢)0.621s53.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.609s (-29.3% 🟢)5.591s (-22.8% 🟢)1.982s61.00x
▲ VercelNitro3.647s (-47.9% 🟢)5.583s (-35.1% 🟢)1.937s61.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.575s (-4.2%)1.006s (~)0.431s601.00x
💻 LocalNitro0.582s (-2.9%)1.005s (~)0.422s601.01x
🐘 PostgresNitro0.591s (+2.0%)1.023s (+1.6%)0.432s591.03x
💻 LocalExpress0.641s (+4.8%)1.005s (~)0.364s601.12x
🐘 PostgresNext.js (Turbopack)0.736s (-13.8% 🟢)1.067s (+4.2%)0.331s571.28x
💻 LocalNext.js (Turbopack)0.907s (+5.1% 🔺)1.096s (+7.2% 🔺)0.189s551.58x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.639s (+13.7% 🔺)7.687s (+18.0% 🔺)2.048s81.00x
▲ VercelExpress5.991s (-40.9% 🟢)7.332s (-37.7% 🟢)1.341s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.367s (-4.7%)2.007s (~)0.640s451.00x
🐘 PostgresNitro1.378s (-4.0%)2.007s (-1.2%)0.629s451.01x
💻 LocalNitro1.487s (-1.2%)2.005s (~)0.519s451.09x
💻 LocalExpress1.609s (+3.5%)2.007s (-1.1%)0.398s451.18x
🐘 PostgresNext.js (Turbopack)1.664s (-18.0% 🟢)2.177s (-18.0% 🟢)0.513s421.22x
💻 LocalNext.js (Turbopack)2.127s (+1.3%)3.008s (~)0.881s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.875s (-10.4% 🟢)14.900s (-7.7% 🟢)2.025s71.00x
▲ VercelNitro13.500s (+3.1%)15.881s (+9.8% 🔺)2.381s61.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.761s (-1.7%)3.166s (+0.9%)0.405s381.00x
🐘 PostgresNitro2.843s (+1.7%)3.166s (~)0.323s381.03x
💻 LocalNitro3.175s (-3.7%)3.977s (-0.8%)0.801s311.15x
💻 LocalExpress3.384s (+3.0%)4.009s (~)0.625s301.23x
🐘 PostgresNext.js (Turbopack)3.485s (-12.5% 🟢)3.947s (-8.9% 🟢)0.462s311.26x
💻 LocalNext.js (Turbopack)4.440s (+2.2%)5.011s (~)0.571s241.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.690s (-2.1%)28.082s (~)2.392s51.00x
▲ VercelNitro26.004s (-10.7% 🟢)28.926s (-6.1% 🟢)2.922s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.250s (+7.5% 🔺)1.006s (~)0.756s601.00x
🐘 PostgresNitro0.259s (+17.9% 🔺)1.006s (~)0.748s601.03x
🐘 PostgresNext.js (Turbopack)0.351s (+26.5% 🔺)1.036s (+2.9%)0.685s581.40x
💻 LocalNitro0.392s (-8.8% 🟢)1.004s (-1.7%)0.613s601.57x
💻 LocalExpress0.461s (+11.6% 🔺)1.022s (+1.7%)0.561s591.84x
💻 LocalNext.js (Turbopack)0.583s (+6.8% 🔺)1.022s (+1.7%)0.439s592.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.026s (-21.1% 🟢)3.667s (-10.7% 🟢)1.640s171.00x
▲ VercelNitro2.038s (-27.8% 🟢)4.051s (-6.2% 🟢)2.013s151.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.400s (+11.6% 🔺)1.028s (+2.2%)0.628s881.00x
🐘 PostgresExpress0.412s (+10.0% 🔺)1.029s (+2.3%)0.617s881.03x
🐘 PostgresNext.js (Turbopack)0.564s (+13.1% 🔺)1.260s (+25.2% 🔺)0.696s721.41x
💻 LocalNitro2.162s (-0.9%)2.715s (~)0.553s345.40x
💻 LocalExpress2.212s (+8.6% 🔺)2.822s (+11.3% 🔺)0.609s325.53x
💻 LocalNext.js (Turbopack)2.505s (+2.2%)3.227s (+2.5%)0.722s286.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.891s (-68.7% 🟢)4.729s (-56.5% 🟢)1.838s201.00x
▲ VercelExpress2.949s (-45.5% 🟢)4.331s (-38.8% 🟢)1.381s211.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.803s (+15.4% 🔺)1.297s (+28.9% 🔺)0.494s931.00x
🐘 PostgresNitro0.806s (+18.3% 🔺)1.471s (+46.2% 🔺)0.665s821.00x
🐘 PostgresNext.js (Turbopack)1.468s (+46.0% 🔺)2.515s (+35.6% 🔺)1.047s481.83x
💻 LocalNitro9.306s (~)10.029s (+2.4%)0.724s1211.58x
💻 LocalExpress9.959s (+6.9% 🔺)10.530s (+6.7% 🔺)0.570s1212.39x
💻 LocalNext.js (Turbopack)10.701s (-2.2%)11.483s (~)0.782s1113.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.765s (-70.1% 🟢)7.499s (-66.1% 🟢)1.734s171.00x
▲ VercelExpress6.168s (-67.4% 🟢)7.892s (-61.6% 🟢)1.724s161.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.162s (~)2.004s (~)0.010s (-1.9%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (~)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.841s101.01x
🐘 PostgresExpress1.174s (~)1.994s (~)0.001s (~)2.011s (~)0.837s101.01x
💻 LocalExpress1.188s (+1.7%)2.006s (~)0.013s (+9.4% 🔺)2.022s (~)0.833s101.02x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.011s (-13.7% 🟢)2.018s (~)0.807s101.04x
🐘 PostgresNext.js (Turbopack)1.358s (+9.1% 🔺)2.003s (~)0.066s (+4969.2% 🔺)2.077s (+3.3%)0.719s101.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.185s (-10.7% 🟢)3.320s (-1.3%)0.962s (+1.4%)4.733s (-0.7%)2.548s101.00x
▲ VercelExpress2.517s (+16.1% 🔺)3.329s (+3.9%)1.273s (+40.1% 🔺)5.116s (+11.7% 🔺)2.599s101.15x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.565s (-1.8%)2.009s (~)0.012s (-5.3% 🟢)2.023s (~)0.458s301.00x
🐘 PostgresExpress1.587s (-0.9%)2.005s (~)0.005s (-5.2% 🟢)2.029s (~)0.442s301.01x
🐘 PostgresNitro1.587s (-1.6%)2.002s (~)0.005s (+2.0%)2.024s (~)0.437s301.01x
💻 LocalExpress1.609s (+2.3%)2.011s (~)0.012s (+6.0% 🔺)2.026s (~)0.417s301.03x
💻 LocalNext.js (Turbopack)1.736s (~)2.008s (~)0.012s (-1.6%)2.024s (~)0.288s301.11x
🐘 PostgresNext.js (Turbopack)2.022s (+13.2% 🔺)2.448s (+21.8% 🔺)0.004s (-20.0% 🟢)2.465s (+21.6% 🔺)0.443s251.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.128s (+3.5%)7.309s (+3.9%)0.222s (-52.7% 🟢)8.010s (~)1.883s81.00x
▲ VercelExpress6.224s (+2.5%)7.752s (+6.2% 🔺)0.326s (+23.9% 🔺)8.575s (+7.2% 🔺)2.351s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.775s (+9.6% 🔺)1.029s (+1.5%)0.000s (+3.5%)1.058s (+2.9%)0.283s571.00x
🐘 PostgresNitro0.792s (+11.8% 🔺)1.065s (+2.1%)0.000s (-66.1% 🟢)1.078s (+1.4%)0.286s561.02x
🐘 PostgresNext.js (Turbopack)1.277s (+48.1% 🔺)1.875s (+71.9% 🔺)0.000s (-14.1% 🟢)1.890s (+72.0% 🔺)0.613s321.65x
💻 LocalNitro1.352s (-6.1% 🟢)2.013s (~)0.000s (+80.0% 🔺)2.015s (~)0.663s301.74x
💻 LocalExpress1.430s (+5.9% 🔺)2.014s (~)0.000s (+200.0% 🔺)2.016s (~)0.586s301.84x
💻 LocalNext.js (Turbopack)1.560s (+6.4% 🔺)2.014s (~)0.001s (+73.3% 🔺)2.018s (~)0.458s302.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.901s (-13.7% 🟢)4.310s (-5.8% 🟢)0.000s (-53.8% 🟢)4.754s (-5.7% 🟢)1.853s131.00x
▲ VercelNitro2.975s (-21.8% 🟢)4.535s (-6.1% 🟢)0.000s (+Infinity% 🔺)5.061s (-3.1%)2.086s121.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.532s (~)2.098s (-5.4% 🟢)0.000s (-100.0% 🟢)2.117s (-5.8% 🟢)0.585s291.00x
🐘 PostgresNitro1.640s (+23.0% 🔺)2.140s (+5.6% 🔺)0.000s (+Infinity% 🔺)2.167s (+5.0% 🔺)0.527s281.07x
🐘 PostgresNext.js (Turbopack)2.504s (+44.8% 🔺)3.050s (+34.8% 🔺)0.000s (NaN%)3.072s (+35.3% 🔺)0.567s201.63x
💻 LocalNitro2.966s (-6.4% 🟢)3.672s (-4.3%)0.000s (-5.9% 🟢)3.674s (-4.3%)0.709s171.94x
💻 LocalExpress3.180s (+2.7%)3.734s (+1.7%)0.001s (+90.9% 🔺)3.739s (+1.7%)0.560s172.08x
💻 LocalNext.js (Turbopack)3.223s (+13.0% 🔺)3.778s (+10.6% 🔺)0.001s (+68.8% 🔺)3.782s (+10.7% 🔺)0.560s162.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.199s (-43.1% 🟢)5.855s (-26.2% 🟢)0.000s (+Infinity% 🔺)6.349s (-29.7% 🟢)2.150s101.00x
▲ VercelExpress4.324s (-18.8% 🟢)5.767s (-12.4% 🟢)0.000s (+Infinity% 🔺)6.266s (-12.9% 🟢)1.942s101.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro20/21
🐘 PostgresExpress16/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow-api/world/storage.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/get-step-metadata.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
pranaygpand others added 6 commits June 11, 2026 14:37
…docs-run-idempotency
* origin-https/main: (115 commits)
[core] V2: unify wait+step queue dispatch in suspension processing (#1925)
fix(world-local,world-postgres): make duplicate hook_created idempotent (#2295)
docs(observability): remove MVP implementation detail bullet (#2367)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
...
# Conflicts:
#	docs/content/docs/v4/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
… into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/hook-ready:
docs: restore SWC Plugin heading in AGENTS.md
docs: require docs preview links in PR descriptions for docs changes
docs: fix inconsistent hasConflict bullet in create-webhook reference
feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
test: cover hook ready continuation scheduling
feat: add hook ready promise
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
- Primary claim pattern is now `if (await hook.hasConflict)` instead of
try/catch on HookConflictError; payload awaits still reject with
HookConflictError (with conflictingRunId) when the owner's run ID is
needed.
- Route example returns the active owner via resumeHook()'s runId
instead of threading conflictingRunId through the workflow result.
- Update claim-pattern prose across start(), getHookByToken(), world
storage, scheduling, workflow composition, and cookbook idempotency
pages (v4 + v5).
- Add @skip-typecheck marker to the cross-block route sample, fixing a
pre-existing docs typecheck failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The early callout was too vague and out of place at the top of the API
reference. Replace it with a 'Resume or Start' example section that
explains the flow, shows the resume-first/start-then-retry route, and
links to the run idempotency pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f awaiting returnValue
The 'Resume or Start' example returned the just-started run's runId with
reused: false even when a concurrent request's run won the token race —
the payload had reached the actual owner, so the response pointed callers
at a run that exits as a duplicate. The foundations route handled the
race correctly but by awaiting run.returnValue, blocking the HTTP
response on full workflow completion.
resumeHook() always resolves against the actual active owner, so
comparing the resumed hook's runId with the started run's runId detects
the race in both examples — race-correct and non-blocking.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — accurate, well-hedged docs; synced to main and one pattern defect fixed in-branch

Now that #2015 is on main, I merged main into this branch (resolving the create-hook.mdx conflicts in favor of this PR's run-idempotency callout) — the diff is down to docs-only (38 files + the empty changeset), exactly as the PR description promised. pnpm test:docs passes (923 samples typechecked).

Technical accuracy — verified against the code

The route samples in the foundations page carry @skip-typecheck (cross-block dependency), so CI doesn't validate them; I checked the API claims manually:

  • start(processOrder, [orderId]) matches the (workflow, args: TArgs, options?) overload returning Promise<Run<TResult>>
  • HookNotFoundError is exported from the workflow/errors subpath (./errorsdist/internal/errors.js) ✓
  • resumeHook(token, payload) returns Promise<Hook> carrying the owner's runId
  • The conflictingRunId-on-HookConflictError claim matches #2012 (and its stable backport #2016), so it's accurate for both the v4 and v5 pages ✓
  • hasConflict semantics (registration committed on suspension; resolves true/false; payload await still rejects with HookConflictError) match the implementation that just landed in #2015

What I particularly like: the docs are honest about the limits. getHookByToken() is consistently described as an advisory pre-check, the start()-vs-hook-registration race is named explicitly everywhere it matters, the resolution is correctly placed inside the workflow via hook.hasConflict, and the "native atomic start-and-hook API is in the works" framing sets expectations without overpromising. The foundations page carries the full pattern once, and the ~20 cross-link callouts are scoped summaries pointing back at it rather than divergent copies.

One defect fixed in-branch (42d5533) — please review the change

The two route examples disagreed on race handling, and one was wrong:

  • resume-hook.mdx "Resume or Start" returned { runId: run.runId, reused: false } after start() + resumeWithRetry() — but when a concurrent request's run wins the token race, the resume reaches that owner, and this response points the caller at a run that exits as a duplicate without doing any work. Wrong answer in exactly the race case the section teaches about.
  • foundations/idempotency.mdx handled the race correctly but via await run.returnValue, which blocks the HTTP response on full workflow completion (hook resume + chargeOrder + every step). That's an anti-pattern for a copy-paste route sample — workflows are long-lived by design.

Both are fixed by one observation: resumeHook() always resolves against the actual active owner, so resumed.runId !== run.runId detects the race — race-correct, non-blocking, and the same shape in both examples. The warn callout's wording is updated to match. Happy to revert if you prefer the returnValue approach for pedagogical reasons, but I'd argue the runId comparison is also the better teaching: it makes "the resume reaches the owner" concrete.

Non-blocking notes

  1. The 500ms retry budget (5 × 100ms) in resumeWithRetry/resumeOrder is tight against real cold starts + queue dispatch latency. Worth either a more forgiving schedule (e.g. exponential up to ~5s) or a sentence on the failure consequence: if the retries exhaust afterstart() succeeded, the new run is left suspended awaiting a payload that never arrives (until run timeout). Right now the thrown error reads as transient but leaves a dangling run behind.
  2. v4 docs sequencing, again: hasConflict is still not on stable (no backport PR exists yet), and this PR widens the v4-docs-reality gap from one page (#2015) to roughly twenty. Same deliberate-choice ask as before — either the #2015 backport + 4.x release should follow promptly, or the v4 halves of these pages should wait for it. The content itself is version-accurate; it's purely a shipping-order question.
  3. The cookbook's minimal getHookByToken()-then-start() example intentionally omits the resume-retry mechanics — good scoping, and the pointer to foundations for "the full pattern" covers it.

CI

All checks green or pending on the synced head; mergeable: MERGEABLE. The branch now contains the merge of main (e6f0516de) plus the race-handling fix (42d5533d4).

…ull>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pranaygpand others added 13 commits June 11, 2026 16:12
…docs-run-idempotency
* origin-https/main:
ci: use claude-fable-5 for backport AI model (#2370)
Add `hook.hasConflict` for early hook conflict detection (#2015)
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
…n-idempotency
* pranaygp/hook-get-conflict:
feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
Run idempotency docs now use getConflict (resolves with the conflicting
Run in v5, { runId } in v4) and document code-driven conflict strategies
in place of static ID-reuse policies: reject the duplicate, adopt the
owner's result, inspect before deciding, signal the owner via
resumeHook, and supersede via cancel-and-reclaim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…empotency' into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/docs-run-idempotency:
docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue
# Conflicts:
#	docs/content/docs/v4/foundations/idempotency.mdx
#	docs/content/docs/v5/foundations/idempotency.mdx
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
refactor: make getConflict a method — hook.getConflict()
fix: never resolve getConflict with a non-Run fallback shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route examples typecheck as-is since the runId-comparison rewrite;
strategy fragments are now complete self-contained workflows; the
publishing-libraries cross-block dependency uses the declare @setup
convention. 934 samples typechecked, none skipped by this PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
review: guard Run class registration, fix anchors, clarify changeset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…her systems
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — re-reviewed after the getConflict() migration; synced to main again

Re-review following #2373's merge (which replaced hook.hasConflict with hook.getConflict() returning Run | null). I merged main into this branch once more (45fe1a585): the core-package conflicts were the branch's embedded pre-merge copy of #2373 versus the landed version (resolved by taking main wholesale, which includes the registry refactor), and the four docs conflicts were this PR's anchored links + callouts versus #2373's baseline pages (resolved by keeping this PR's superset). The effective diff is docs-only + the empty changeset, and pnpm test:docs passes — now at 1012 typechecked samples with zero @skip-typecheck escape hatches, since the route samples were restructured to typecheck standalone. That closes the "flagship sample isn't CI-validated" gap from my first review.

The getConflict() migration is done right

  • Zero stale hasConflict references anywhere in the PR.
  • The run-idempotency pattern now returns { status: "duplicate", runId: conflict.runId } — the duplicate run can finally say who owns the token, which is what makes the route example's dedupe response meaningful.
  • The new Conflict-handling strategies section is the best part of this revision. All five strategies are technically sound — I audited each against the implementation:
    • Reject: conflict.runId plain property, deterministic ✓
    • Adopt: await conflict.returnValue durable step accessor ✓
    • Inspect: await conflict.status
    • Signal: resumeHook() correctly wrapped in a "use step" function (API calls can't run in workflow context) ✓
    • Supersede: await conflict.cancel() + retry loop creating a fresh hook per attempt, correctly handling the disposal-propagation window; per-iteration using disposal of conflicted hooks is safe (pinned by the #2015 test) ✓
  • The "no policy enum — policy is ordinary code" framing (now phrased generically without naming other systems) is a genuinely good product argument, and the inspect-before-deciding strategy demonstrates exactly what static configuration can't express.
  • The v4/v5 split is version-accurate: v4 documents the { runId } resolution with getRun(conflict.runId) inside steps (no native Run construction in 4.x workflow context); v5 documents the Run handle with durable accessors. The older-world degenerate case (hook_conflict without conflictingRunId → rejects with HookConflictError) is documented in the hooks foundations page.
  • My earlier race-handling fix (reused: resumed.runId !== run.runId) survived the revision and the warn callout's wording is consistent with it.

Two non-blocking notes

  1. The inspect strategy's terminal-state fall-through is slightly loose: when the owner is already terminal, the example proceeds to processOwnedOrder() without holding the token (this run's hook already recorded its conflict; the released token isn't retroactively claimed). A third concurrent run could claim it meanwhile. The supersede example handles this properly with its fresh-hook retry loop — a one-line pointer ("to do the work while holding the token after the owner ends, use the supersede pattern's retry loop") would prevent copy-paste of the loose version into duplicate-sensitive code.
  2. The recurring v4 sequencing flag, final form: the v4 pages now document getConflict() — an API that has never existed in any 4.x release (hasConflict didn't ship there either). The documented-but-unreleased v4 surface keeps compounding across #2015#2373 → this PR. None of it is wrong — the v4 content correctly describes what a backport would look like — but the backport chain (now effectively just #2373, since it subsumes #2015's API) should land in a 4.x release soon after this merges, or the v4 docs are fiction in the meantime.

Everything else from my first review stands. Nice iteration — the strategies section turned a reference page into an actual design guide.

…docs-run-idempotency
* origin-https/main:
Version Packages (beta) (#2364)
Animate in-progress segments in the timeline (#2383)
…docs-run-idempotency
* origin-https/main:
test: e2e coverage for run-idempotency conflict-handling strategies (#2387)
fix(docs): repair broken links, fix the link linter, and version-correct v5 Card + edit links (#2391)
feat(web-shared): RelativeTimeCard with shared ContextCard provider (#2328)
Add allowReservedAttributes option to start() (#2385)
# Conflicts:
#	docs/content/docs/v4/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2410. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

docs: document run idempotency - #2011

Merged
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency
Jun 14, 2026
Merged

docs: document run idempotency#2011
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expand the foundations and cookbook idempotency docs to cover run idempotency in addition to step side-effect idempotency
  • document that start() currently creates a new run for each call and does not accept an idempotency key
  • document the deterministic hook-token claim pattern using hook.getConflict() (Add hook.hasConflict for early hook conflict detection #2015, revised by Replace hook.hasConflict with hook.getConflict() returning the conflicting Run #2373): the workflow creates a hook with a domain-derived token and checks const conflict = await hook.getConflict() before duplicate-sensitive work — awaiting getConflict() suspends the workflow to commit the hook registration and resolves with the conflicting Run (v5) / { runId } (v4) when another active run owns the token, or null once registered
  • add a conflict-handling strategies guide to the run-idempotency docs: instead of static ID-reuse policies (cf. Temporal's Workflow ID conflict policy), the duplicate run handles the conflict in code — reject the duplicate, adopt the owner's result via conflict.returnValue, inspect conflict.status before deciding, signal the owner via resumeHook() from a step, or supersede it via conflict.cancel() and re-claim the token
  • add cross-links/callouts from relevant API reference and cookbook pages: start(), getRun(), hook APIs, workflow fetch(), child workflows, session integrations, scheduling, timeouts, and publishing-library examples

Note

Stacked on #2373 (hook.getConflict(), the follow-up to the merged #2015): this PR's base is that branch, so the diff here is the idempotency docs only. Merge #2373 first — GitHub will retarget this PR to main automatically.

Notes

  • Current guidance is to reserve an application/business key atomically before calling start() when duplicate run creation is unsafe.
  • Hook-token lookup (getHookByToken()) is documented as an advisory pre-check, not atomic enough to replace run idempotency; the race is resolved inside the workflow via hook.getConflict().
  • The route example detects the concurrent-start race by comparing resumeHook()'s runId against the started run's runId — race-correct without blocking on workflow completion.

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
Foundations: Idempotency (new Run idempotency section + conflict-handling strategies)v4v5
Cookbook: Idempotencyv4v5
start() API referencev4v5
getHookByToken() API referencev4v5
getRun() API referencev4v5
resumeHook() API referencev4v5
createHook() API referencev4v5
fetch() API referencev4v5
World storage referencev4v5

Smaller idempotency cross-link callouts were also added to the scheduling, timeouts, workflow composition, child workflows, publishing libraries, AI SDK, Chat SDK, and Sandbox cookbook pages (same paths under /v5/docs for v5).

Validation

  • pnpm test:docs (921 samples typechecked, 2 files) — also fixed a pre-existing failure in the run-idempotency route sample with a @skip-typecheck cross-block marker
  • git diff --check

@vercel

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd22586

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production123302171450
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7634010668700

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+2.1%)1.006s (~)0.962s101.00x
💻 LocalExpress0.052s (+22.6% 🔺)1.007s (~)0.955s101.20x
🐘 PostgresExpress0.062s (+0.7%)1.014s (~)0.952s101.43x
💻 LocalNext.js (Turbopack)0.063s (~)1.006s (~)0.943s101.46x
🐘 PostgresNitro0.079s (+19.8% 🔺)1.016s (~)0.938s101.82x
🐘 PostgresNext.js (Turbopack)0.082s (+9.8% 🔺)1.019s (~)0.937s101.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.285s (-24.6% 🟢)2.322s (+10.6% 🔺)2.037s101.00x
▲ VercelExpress0.370s (+36.7% 🔺)2.299s (-10.9% 🟢)1.929s101.30x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.089s (~)2.007s (~)0.917s101.00x
🐘 PostgresExpress1.107s (-0.7%)2.009s (~)0.902s101.02x
💻 LocalExpress1.116s (+2.0%)2.007s (~)0.891s101.02x
🐘 PostgresNitro1.124s (+1.0%)2.011s (~)0.887s101.03x
💻 LocalNext.js (Turbopack)1.134s (+0.6%)2.007s (~)0.872s101.04x
🐘 PostgresNext.js (Turbopack)1.242s (+8.6% 🔺)2.036s (+1.3%)0.794s101.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.668s (-4.9%)3.484s (-6.1% 🟢)1.816s101.00x
▲ VercelNitro1.743s (+4.0%)3.822s (+10.8% 🔺)2.079s101.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.489s (-0.5%)11.022s (~)0.533s31.00x
🐘 PostgresExpress10.557s (~)11.018s (~)0.461s31.01x
💻 LocalExpress10.575s (~)11.024s (~)0.449s31.01x
🐘 PostgresNitro10.645s (~)11.022s (~)0.377s31.01x
💻 LocalNext.js (Turbopack)10.790s (~)11.022s (~)0.232s31.03x
🐘 PostgresNext.js (Turbopack)10.993s (~)11.353s (-2.9%)0.360s31.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.741s (+2.4%)15.369s (-2.3%)1.628s21.00x
▲ VercelNitro13.963s (+4.7%)16.017s (+5.8% 🔺)2.053s21.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.686s (-1.1%)14.027s (-1.4%)0.341s51.00x
🐘 PostgresExpress13.855s (~)14.017s (~)0.163s51.01x
🐘 PostgresNitro13.898s (+0.6%)14.023s (~)0.124s51.02x
💻 LocalExpress13.953s (+0.8%)14.228s (+1.4%)0.276s51.02x
🐘 PostgresNext.js (Turbopack)14.310s (-1.3%)15.060s (~)0.750s41.05x
💻 LocalNext.js (Turbopack)14.522s (~)15.030s (~)0.508s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.492s (+4.7%)24.394s (+5.7% 🔺)1.902s31.00x
▲ VercelNitro23.148s (+3.2%)25.776s (+7.1% 🔺)2.628s31.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.317s (-1.2%)13.025s (~)0.707s71.00x
🐘 PostgresExpress12.662s (-0.6%)13.017s (~)0.355s71.03x
💻 LocalExpress12.688s (+1.5%)13.026s (~)0.338s71.03x
🐘 PostgresNitro12.934s (+2.8%)13.449s (+3.3%)0.515s71.05x
💻 LocalNext.js (Turbopack)13.803s (+0.9%)14.171s (+1.0%)0.368s71.12x
🐘 PostgresNext.js (Turbopack)14.928s (+6.8% 🔺)15.370s (+7.4% 🔺)0.442s61.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express30.956s (+4.3%)32.926s (+5.7% 🔺)1.970s31.00x
▲ VercelNitro31.388s (-1.0%)33.630s (+1.0%)2.242s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.206s (~)2.007s (~)0.802s151.00x
🐘 PostgresNext.js (Turbopack)1.221s (-3.0%)2.007s (~)0.786s151.01x
💻 LocalNitro1.223s (~)2.006s (~)0.783s151.01x
🐘 PostgresNitro1.260s (+4.7%)2.008s (~)0.748s151.04x
💻 LocalExpress1.305s (+9.7% 🔺)2.007s (~)0.702s151.08x
💻 LocalNext.js (Turbopack)1.338s (-0.8%)2.006s (~)0.668s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.372s (-7.1% 🟢)3.556s (-21.0% 🟢)1.185s91.00x
▲ VercelNitro2.462s (-33.4% 🟢)4.068s (-21.5% 🟢)1.606s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.364s (+8.4% 🔺)2.735s (+36.2% 🔺)1.371s111.00x
🐘 PostgresExpress1.461s (+15.8% 🔺)2.509s (+25.0% 🔺)1.048s121.07x
🐘 PostgresNext.js (Turbopack)1.512s (+7.9% 🔺)2.331s (+16.2% 🔺)0.819s131.11x
💻 LocalNitro1.697s (~)2.005s (~)0.309s151.24x
💻 LocalNext.js (Turbopack)1.742s (-7.3% 🟢)2.074s (-3.5%)0.332s151.28x
💻 LocalExpress2.242s (+21.7% 🔺)2.675s (+24.4% 🔺)0.433s121.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.639s (-40.3% 🟢)4.429s (-27.6% 🟢)1.790s71.00x
▲ VercelNitro2.882s (-53.8% 🟢)5.096s (-34.2% 🟢)2.215s61.09x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.610s (+14.4% 🔺)4.010s (+99.7% 🔺)2.400s81.00x
🐘 PostgresExpress1.668s (+19.0% 🔺)3.887s (+93.5% 🔺)2.219s81.04x
🐘 PostgresNext.js (Turbopack)3.217s (+78.3% 🔺)4.165s (+81.3% 🔺)0.949s82.00x
💻 LocalNitro4.405s (-16.1% 🟢)5.014s (-16.7% 🟢)0.609s62.74x
💻 LocalNext.js (Turbopack)4.848s (-15.0% 🟢)5.300s (-14.7% 🟢)0.452s73.01x
💻 LocalExpress7.792s (+44.7% 🔺)8.518s (+37.8% 🔺)0.727s44.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.708s (-78.2% 🟢)5.818s (-68.2% 🟢)2.109s61.00x
▲ VercelExpress4.596s (-49.6% 🟢)6.296s (-44.6% 🟢)1.700s61.24x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.203s (-0.6%)2.007s (~)0.804s151.00x
🐘 PostgresNitro1.229s (+3.2%)2.007s (~)0.778s151.02x
💻 LocalNext.js (Turbopack)1.394s (+3.1%)2.007s (~)0.613s151.16x
🐘 PostgresNext.js (Turbopack)1.427s (+13.0% 🔺)2.018s (~)0.591s151.19x
💻 LocalNitro1.463s (-3.0%)2.006s (~)0.543s151.22x
💻 LocalExpress1.671s (+0.7%)2.074s (~)0.404s151.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.403s (-6.8% 🟢)4.237s (~)1.834s81.00x
▲ VercelExpress2.475s (-8.3% 🟢)4.128s (-12.4% 🟢)1.653s81.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.333s (+4.8%)2.507s (+24.9% 🔺)1.175s121.00x
🐘 PostgresExpress1.383s (+6.7% 🔺)2.393s (+19.1% 🔺)1.010s131.04x
🐘 PostgresNext.js (Turbopack)1.610s (+14.7% 🔺)2.481s (+23.5% 🔺)0.871s131.21x
💻 LocalNitro1.819s (-16.2% 🟢)2.077s (-19.9% 🟢)0.258s151.37x
💻 LocalNext.js (Turbopack)2.093s (+2.3%)2.827s (-6.0% 🟢)0.734s111.57x
💻 LocalExpress2.165s (~)2.593s (-3.1%)0.428s121.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (-18.2% 🟢)4.442s (-12.9% 🟢)1.639s71.00x
▲ VercelNitro3.091s (-36.0% 🟢)4.926s (-23.1% 🟢)1.835s71.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (+12.8% 🔺)4.013s (+99.6% 🔺)2.431s81.00x
🐘 PostgresNitro1.710s (+23.1% 🔺)4.300s (+114.1% 🔺)2.590s71.08x
🐘 PostgresNext.js (Turbopack)2.745s (+51.8% 🔺)3.594s (+61.6% 🔺)0.849s91.74x
💻 LocalNitro4.968s (-8.6% 🟢)5.514s (-8.3% 🟢)0.546s63.14x
💻 LocalNext.js (Turbopack)5.505s (-9.1% 🟢)6.217s (-8.8% 🟢)0.712s53.48x
💻 LocalExpress5.794s (-8.8% 🟢)6.415s (-8.5% 🟢)0.621s53.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.609s (-29.3% 🟢)5.591s (-22.8% 🟢)1.982s61.00x
▲ VercelNitro3.647s (-47.9% 🟢)5.583s (-35.1% 🟢)1.937s61.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.575s (-4.2%)1.006s (~)0.431s601.00x
💻 LocalNitro0.582s (-2.9%)1.005s (~)0.422s601.01x
🐘 PostgresNitro0.591s (+2.0%)1.023s (+1.6%)0.432s591.03x
💻 LocalExpress0.641s (+4.8%)1.005s (~)0.364s601.12x
🐘 PostgresNext.js (Turbopack)0.736s (-13.8% 🟢)1.067s (+4.2%)0.331s571.28x
💻 LocalNext.js (Turbopack)0.907s (+5.1% 🔺)1.096s (+7.2% 🔺)0.189s551.58x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.639s (+13.7% 🔺)7.687s (+18.0% 🔺)2.048s81.00x
▲ VercelExpress5.991s (-40.9% 🟢)7.332s (-37.7% 🟢)1.341s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.367s (-4.7%)2.007s (~)0.640s451.00x
🐘 PostgresNitro1.378s (-4.0%)2.007s (-1.2%)0.629s451.01x
💻 LocalNitro1.487s (-1.2%)2.005s (~)0.519s451.09x
💻 LocalExpress1.609s (+3.5%)2.007s (-1.1%)0.398s451.18x
🐘 PostgresNext.js (Turbopack)1.664s (-18.0% 🟢)2.177s (-18.0% 🟢)0.513s421.22x
💻 LocalNext.js (Turbopack)2.127s (+1.3%)3.008s (~)0.881s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.875s (-10.4% 🟢)14.900s (-7.7% 🟢)2.025s71.00x
▲ VercelNitro13.500s (+3.1%)15.881s (+9.8% 🔺)2.381s61.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.761s (-1.7%)3.166s (+0.9%)0.405s381.00x
🐘 PostgresNitro2.843s (+1.7%)3.166s (~)0.323s381.03x
💻 LocalNitro3.175s (-3.7%)3.977s (-0.8%)0.801s311.15x
💻 LocalExpress3.384s (+3.0%)4.009s (~)0.625s301.23x
🐘 PostgresNext.js (Turbopack)3.485s (-12.5% 🟢)3.947s (-8.9% 🟢)0.462s311.26x
💻 LocalNext.js (Turbopack)4.440s (+2.2%)5.011s (~)0.571s241.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.690s (-2.1%)28.082s (~)2.392s51.00x
▲ VercelNitro26.004s (-10.7% 🟢)28.926s (-6.1% 🟢)2.922s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.250s (+7.5% 🔺)1.006s (~)0.756s601.00x
🐘 PostgresNitro0.259s (+17.9% 🔺)1.006s (~)0.748s601.03x
🐘 PostgresNext.js (Turbopack)0.351s (+26.5% 🔺)1.036s (+2.9%)0.685s581.40x
💻 LocalNitro0.392s (-8.8% 🟢)1.004s (-1.7%)0.613s601.57x
💻 LocalExpress0.461s (+11.6% 🔺)1.022s (+1.7%)0.561s591.84x
💻 LocalNext.js (Turbopack)0.583s (+6.8% 🔺)1.022s (+1.7%)0.439s592.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.026s (-21.1% 🟢)3.667s (-10.7% 🟢)1.640s171.00x
▲ VercelNitro2.038s (-27.8% 🟢)4.051s (-6.2% 🟢)2.013s151.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.400s (+11.6% 🔺)1.028s (+2.2%)0.628s881.00x
🐘 PostgresExpress0.412s (+10.0% 🔺)1.029s (+2.3%)0.617s881.03x
🐘 PostgresNext.js (Turbopack)0.564s (+13.1% 🔺)1.260s (+25.2% 🔺)0.696s721.41x
💻 LocalNitro2.162s (-0.9%)2.715s (~)0.553s345.40x
💻 LocalExpress2.212s (+8.6% 🔺)2.822s (+11.3% 🔺)0.609s325.53x
💻 LocalNext.js (Turbopack)2.505s (+2.2%)3.227s (+2.5%)0.722s286.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.891s (-68.7% 🟢)4.729s (-56.5% 🟢)1.838s201.00x
▲ VercelExpress2.949s (-45.5% 🟢)4.331s (-38.8% 🟢)1.381s211.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.803s (+15.4% 🔺)1.297s (+28.9% 🔺)0.494s931.00x
🐘 PostgresNitro0.806s (+18.3% 🔺)1.471s (+46.2% 🔺)0.665s821.00x
🐘 PostgresNext.js (Turbopack)1.468s (+46.0% 🔺)2.515s (+35.6% 🔺)1.047s481.83x
💻 LocalNitro9.306s (~)10.029s (+2.4%)0.724s1211.58x
💻 LocalExpress9.959s (+6.9% 🔺)10.530s (+6.7% 🔺)0.570s1212.39x
💻 LocalNext.js (Turbopack)10.701s (-2.2%)11.483s (~)0.782s1113.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.765s (-70.1% 🟢)7.499s (-66.1% 🟢)1.734s171.00x
▲ VercelExpress6.168s (-67.4% 🟢)7.892s (-61.6% 🟢)1.724s161.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.162s (~)2.004s (~)0.010s (-1.9%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (~)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.841s101.01x
🐘 PostgresExpress1.174s (~)1.994s (~)0.001s (~)2.011s (~)0.837s101.01x
💻 LocalExpress1.188s (+1.7%)2.006s (~)0.013s (+9.4% 🔺)2.022s (~)0.833s101.02x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.011s (-13.7% 🟢)2.018s (~)0.807s101.04x
🐘 PostgresNext.js (Turbopack)1.358s (+9.1% 🔺)2.003s (~)0.066s (+4969.2% 🔺)2.077s (+3.3%)0.719s101.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.185s (-10.7% 🟢)3.320s (-1.3%)0.962s (+1.4%)4.733s (-0.7%)2.548s101.00x
▲ VercelExpress2.517s (+16.1% 🔺)3.329s (+3.9%)1.273s (+40.1% 🔺)5.116s (+11.7% 🔺)2.599s101.15x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.565s (-1.8%)2.009s (~)0.012s (-5.3% 🟢)2.023s (~)0.458s301.00x
🐘 PostgresExpress1.587s (-0.9%)2.005s (~)0.005s (-5.2% 🟢)2.029s (~)0.442s301.01x
🐘 PostgresNitro1.587s (-1.6%)2.002s (~)0.005s (+2.0%)2.024s (~)0.437s301.01x
💻 LocalExpress1.609s (+2.3%)2.011s (~)0.012s (+6.0% 🔺)2.026s (~)0.417s301.03x
💻 LocalNext.js (Turbopack)1.736s (~)2.008s (~)0.012s (-1.6%)2.024s (~)0.288s301.11x
🐘 PostgresNext.js (Turbopack)2.022s (+13.2% 🔺)2.448s (+21.8% 🔺)0.004s (-20.0% 🟢)2.465s (+21.6% 🔺)0.443s251.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.128s (+3.5%)7.309s (+3.9%)0.222s (-52.7% 🟢)8.010s (~)1.883s81.00x
▲ VercelExpress6.224s (+2.5%)7.752s (+6.2% 🔺)0.326s (+23.9% 🔺)8.575s (+7.2% 🔺)2.351s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.775s (+9.6% 🔺)1.029s (+1.5%)0.000s (+3.5%)1.058s (+2.9%)0.283s571.00x
🐘 PostgresNitro0.792s (+11.8% 🔺)1.065s (+2.1%)0.000s (-66.1% 🟢)1.078s (+1.4%)0.286s561.02x
🐘 PostgresNext.js (Turbopack)1.277s (+48.1% 🔺)1.875s (+71.9% 🔺)0.000s (-14.1% 🟢)1.890s (+72.0% 🔺)0.613s321.65x
💻 LocalNitro1.352s (-6.1% 🟢)2.013s (~)0.000s (+80.0% 🔺)2.015s (~)0.663s301.74x
💻 LocalExpress1.430s (+5.9% 🔺)2.014s (~)0.000s (+200.0% 🔺)2.016s (~)0.586s301.84x
💻 LocalNext.js (Turbopack)1.560s (+6.4% 🔺)2.014s (~)0.001s (+73.3% 🔺)2.018s (~)0.458s302.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.901s (-13.7% 🟢)4.310s (-5.8% 🟢)0.000s (-53.8% 🟢)4.754s (-5.7% 🟢)1.853s131.00x
▲ VercelNitro2.975s (-21.8% 🟢)4.535s (-6.1% 🟢)0.000s (+Infinity% 🔺)5.061s (-3.1%)2.086s121.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.532s (~)2.098s (-5.4% 🟢)0.000s (-100.0% 🟢)2.117s (-5.8% 🟢)0.585s291.00x
🐘 PostgresNitro1.640s (+23.0% 🔺)2.140s (+5.6% 🔺)0.000s (+Infinity% 🔺)2.167s (+5.0% 🔺)0.527s281.07x
🐘 PostgresNext.js (Turbopack)2.504s (+44.8% 🔺)3.050s (+34.8% 🔺)0.000s (NaN%)3.072s (+35.3% 🔺)0.567s201.63x
💻 LocalNitro2.966s (-6.4% 🟢)3.672s (-4.3%)0.000s (-5.9% 🟢)3.674s (-4.3%)0.709s171.94x
💻 LocalExpress3.180s (+2.7%)3.734s (+1.7%)0.001s (+90.9% 🔺)3.739s (+1.7%)0.560s172.08x
💻 LocalNext.js (Turbopack)3.223s (+13.0% 🔺)3.778s (+10.6% 🔺)0.001s (+68.8% 🔺)3.782s (+10.7% 🔺)0.560s162.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.199s (-43.1% 🟢)5.855s (-26.2% 🟢)0.000s (+Infinity% 🔺)6.349s (-29.7% 🟢)2.150s101.00x
▲ VercelExpress4.324s (-18.8% 🟢)5.767s (-12.4% 🟢)0.000s (+Infinity% 🔺)6.266s (-12.9% 🟢)1.942s101.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro20/21
🐘 PostgresExpress16/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow-api/world/storage.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/get-step-metadata.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
pranaygpand others added 6 commits June 11, 2026 14:37
…docs-run-idempotency
* origin-https/main: (115 commits)
[core] V2: unify wait+step queue dispatch in suspension processing (#1925)
fix(world-local,world-postgres): make duplicate hook_created idempotent (#2295)
docs(observability): remove MVP implementation detail bullet (#2367)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
...
# Conflicts:
#	docs/content/docs/v4/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
… into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/hook-ready:
docs: restore SWC Plugin heading in AGENTS.md
docs: require docs preview links in PR descriptions for docs changes
docs: fix inconsistent hasConflict bullet in create-webhook reference
feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
test: cover hook ready continuation scheduling
feat: add hook ready promise
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
- Primary claim pattern is now `if (await hook.hasConflict)` instead of
try/catch on HookConflictError; payload awaits still reject with
HookConflictError (with conflictingRunId) when the owner's run ID is
needed.
- Route example returns the active owner via resumeHook()'s runId
instead of threading conflictingRunId through the workflow result.
- Update claim-pattern prose across start(), getHookByToken(), world
storage, scheduling, workflow composition, and cookbook idempotency
pages (v4 + v5).
- Add @skip-typecheck marker to the cross-block route sample, fixing a
pre-existing docs typecheck failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The early callout was too vague and out of place at the top of the API
reference. Replace it with a 'Resume or Start' example section that
explains the flow, shows the resume-first/start-then-retry route, and
links to the run idempotency pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f awaiting returnValue
The 'Resume or Start' example returned the just-started run's runId with
reused: false even when a concurrent request's run won the token race —
the payload had reached the actual owner, so the response pointed callers
at a run that exits as a duplicate. The foundations route handled the
race correctly but by awaiting run.returnValue, blocking the HTTP
response on full workflow completion.
resumeHook() always resolves against the actual active owner, so
comparing the resumed hook's runId with the started run's runId detects
the race in both examples — race-correct and non-blocking.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — accurate, well-hedged docs; synced to main and one pattern defect fixed in-branch

Now that #2015 is on main, I merged main into this branch (resolving the create-hook.mdx conflicts in favor of this PR's run-idempotency callout) — the diff is down to docs-only (38 files + the empty changeset), exactly as the PR description promised. pnpm test:docs passes (923 samples typechecked).

Technical accuracy — verified against the code

The route samples in the foundations page carry @skip-typecheck (cross-block dependency), so CI doesn't validate them; I checked the API claims manually:

  • start(processOrder, [orderId]) matches the (workflow, args: TArgs, options?) overload returning Promise<Run<TResult>>
  • HookNotFoundError is exported from the workflow/errors subpath (./errorsdist/internal/errors.js) ✓
  • resumeHook(token, payload) returns Promise<Hook> carrying the owner's runId
  • The conflictingRunId-on-HookConflictError claim matches #2012 (and its stable backport #2016), so it's accurate for both the v4 and v5 pages ✓
  • hasConflict semantics (registration committed on suspension; resolves true/false; payload await still rejects with HookConflictError) match the implementation that just landed in #2015

What I particularly like: the docs are honest about the limits. getHookByToken() is consistently described as an advisory pre-check, the start()-vs-hook-registration race is named explicitly everywhere it matters, the resolution is correctly placed inside the workflow via hook.hasConflict, and the "native atomic start-and-hook API is in the works" framing sets expectations without overpromising. The foundations page carries the full pattern once, and the ~20 cross-link callouts are scoped summaries pointing back at it rather than divergent copies.

One defect fixed in-branch (42d5533) — please review the change

The two route examples disagreed on race handling, and one was wrong:

  • resume-hook.mdx "Resume or Start" returned { runId: run.runId, reused: false } after start() + resumeWithRetry() — but when a concurrent request's run wins the token race, the resume reaches that owner, and this response points the caller at a run that exits as a duplicate without doing any work. Wrong answer in exactly the race case the section teaches about.
  • foundations/idempotency.mdx handled the race correctly but via await run.returnValue, which blocks the HTTP response on full workflow completion (hook resume + chargeOrder + every step). That's an anti-pattern for a copy-paste route sample — workflows are long-lived by design.

Both are fixed by one observation: resumeHook() always resolves against the actual active owner, so resumed.runId !== run.runId detects the race — race-correct, non-blocking, and the same shape in both examples. The warn callout's wording is updated to match. Happy to revert if you prefer the returnValue approach for pedagogical reasons, but I'd argue the runId comparison is also the better teaching: it makes "the resume reaches the owner" concrete.

Non-blocking notes

  1. The 500ms retry budget (5 × 100ms) in resumeWithRetry/resumeOrder is tight against real cold starts + queue dispatch latency. Worth either a more forgiving schedule (e.g. exponential up to ~5s) or a sentence on the failure consequence: if the retries exhaust afterstart() succeeded, the new run is left suspended awaiting a payload that never arrives (until run timeout). Right now the thrown error reads as transient but leaves a dangling run behind.
  2. v4 docs sequencing, again: hasConflict is still not on stable (no backport PR exists yet), and this PR widens the v4-docs-reality gap from one page (#2015) to roughly twenty. Same deliberate-choice ask as before — either the #2015 backport + 4.x release should follow promptly, or the v4 halves of these pages should wait for it. The content itself is version-accurate; it's purely a shipping-order question.
  3. The cookbook's minimal getHookByToken()-then-start() example intentionally omits the resume-retry mechanics — good scoping, and the pointer to foundations for "the full pattern" covers it.

CI

All checks green or pending on the synced head; mergeable: MERGEABLE. The branch now contains the merge of main (e6f0516de) plus the race-handling fix (42d5533d4).

…ull>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pranaygpand others added 13 commits June 11, 2026 16:12
…docs-run-idempotency
* origin-https/main:
ci: use claude-fable-5 for backport AI model (#2370)
Add `hook.hasConflict` for early hook conflict detection (#2015)
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
…n-idempotency
* pranaygp/hook-get-conflict:
feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
Run idempotency docs now use getConflict (resolves with the conflicting
Run in v5, { runId } in v4) and document code-driven conflict strategies
in place of static ID-reuse policies: reject the duplicate, adopt the
owner's result, inspect before deciding, signal the owner via
resumeHook, and supersede via cancel-and-reclaim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…empotency' into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/docs-run-idempotency:
docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue
# Conflicts:
#	docs/content/docs/v4/foundations/idempotency.mdx
#	docs/content/docs/v5/foundations/idempotency.mdx
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
refactor: make getConflict a method — hook.getConflict()
fix: never resolve getConflict with a non-Run fallback shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route examples typecheck as-is since the runId-comparison rewrite;
strategy fragments are now complete self-contained workflows; the
publishing-libraries cross-block dependency uses the declare @setup
convention. 934 samples typechecked, none skipped by this PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
review: guard Run class registration, fix anchors, clarify changeset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…her systems
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — re-reviewed after the getConflict() migration; synced to main again

Re-review following #2373's merge (which replaced hook.hasConflict with hook.getConflict() returning Run | null). I merged main into this branch once more (45fe1a585): the core-package conflicts were the branch's embedded pre-merge copy of #2373 versus the landed version (resolved by taking main wholesale, which includes the registry refactor), and the four docs conflicts were this PR's anchored links + callouts versus #2373's baseline pages (resolved by keeping this PR's superset). The effective diff is docs-only + the empty changeset, and pnpm test:docs passes — now at 1012 typechecked samples with zero @skip-typecheck escape hatches, since the route samples were restructured to typecheck standalone. That closes the "flagship sample isn't CI-validated" gap from my first review.

The getConflict() migration is done right

  • Zero stale hasConflict references anywhere in the PR.
  • The run-idempotency pattern now returns { status: "duplicate", runId: conflict.runId } — the duplicate run can finally say who owns the token, which is what makes the route example's dedupe response meaningful.
  • The new Conflict-handling strategies section is the best part of this revision. All five strategies are technically sound — I audited each against the implementation:
    • Reject: conflict.runId plain property, deterministic ✓
    • Adopt: await conflict.returnValue durable step accessor ✓
    • Inspect: await conflict.status
    • Signal: resumeHook() correctly wrapped in a "use step" function (API calls can't run in workflow context) ✓
    • Supersede: await conflict.cancel() + retry loop creating a fresh hook per attempt, correctly handling the disposal-propagation window; per-iteration using disposal of conflicted hooks is safe (pinned by the #2015 test) ✓
  • The "no policy enum — policy is ordinary code" framing (now phrased generically without naming other systems) is a genuinely good product argument, and the inspect-before-deciding strategy demonstrates exactly what static configuration can't express.
  • The v4/v5 split is version-accurate: v4 documents the { runId } resolution with getRun(conflict.runId) inside steps (no native Run construction in 4.x workflow context); v5 documents the Run handle with durable accessors. The older-world degenerate case (hook_conflict without conflictingRunId → rejects with HookConflictError) is documented in the hooks foundations page.
  • My earlier race-handling fix (reused: resumed.runId !== run.runId) survived the revision and the warn callout's wording is consistent with it.

Two non-blocking notes

  1. The inspect strategy's terminal-state fall-through is slightly loose: when the owner is already terminal, the example proceeds to processOwnedOrder() without holding the token (this run's hook already recorded its conflict; the released token isn't retroactively claimed). A third concurrent run could claim it meanwhile. The supersede example handles this properly with its fresh-hook retry loop — a one-line pointer ("to do the work while holding the token after the owner ends, use the supersede pattern's retry loop") would prevent copy-paste of the loose version into duplicate-sensitive code.
  2. The recurring v4 sequencing flag, final form: the v4 pages now document getConflict() — an API that has never existed in any 4.x release (hasConflict didn't ship there either). The documented-but-unreleased v4 surface keeps compounding across #2015#2373 → this PR. None of it is wrong — the v4 content correctly describes what a backport would look like — but the backport chain (now effectively just #2373, since it subsumes #2015's API) should land in a 4.x release soon after this merges, or the v4 docs are fiction in the meantime.

Everything else from my first review stands. Nice iteration — the strategies section turned a reference page into an actual design guide.

…docs-run-idempotency
* origin-https/main:
Version Packages (beta) (#2364)
Animate in-progress segments in the timeline (#2383)
…docs-run-idempotency
* origin-https/main:
test: e2e coverage for run-idempotency conflict-handling strategies (#2387)
fix(docs): repair broken links, fix the link linter, and version-correct v5 Card + edit links (#2391)
feat(web-shared): RelativeTimeCard with shared ContextCard provider (#2328)
Add allowReservedAttributes option to start() (#2385)
# Conflicts:
#	docs/content/docs/v4/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2410. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

docs: document run idempotency - #2011

Merged
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency
Jun 14, 2026
Merged

docs: document run idempotency#2011
pranaygp merged 47 commits into
mainfrom
pranaygp/codex/docs-run-idempotency

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expand the foundations and cookbook idempotency docs to cover run idempotency in addition to step side-effect idempotency
  • document that start() currently creates a new run for each call and does not accept an idempotency key
  • document the deterministic hook-token claim pattern using hook.getConflict() (Add hook.hasConflict for early hook conflict detection #2015, revised by Replace hook.hasConflict with hook.getConflict() returning the conflicting Run #2373): the workflow creates a hook with a domain-derived token and checks const conflict = await hook.getConflict() before duplicate-sensitive work — awaiting getConflict() suspends the workflow to commit the hook registration and resolves with the conflicting Run (v5) / { runId } (v4) when another active run owns the token, or null once registered
  • add a conflict-handling strategies guide to the run-idempotency docs: instead of static ID-reuse policies (cf. Temporal's Workflow ID conflict policy), the duplicate run handles the conflict in code — reject the duplicate, adopt the owner's result via conflict.returnValue, inspect conflict.status before deciding, signal the owner via resumeHook() from a step, or supersede it via conflict.cancel() and re-claim the token
  • add cross-links/callouts from relevant API reference and cookbook pages: start(), getRun(), hook APIs, workflow fetch(), child workflows, session integrations, scheduling, timeouts, and publishing-library examples

Note

Stacked on #2373 (hook.getConflict(), the follow-up to the merged #2015): this PR's base is that branch, so the diff here is the idempotency docs only. Merge #2373 first — GitHub will retarget this PR to main automatically.

Notes

  • Current guidance is to reserve an application/business key atomically before calling start() when duplicate run creation is unsafe.
  • Hook-token lookup (getHookByToken()) is documented as an advisory pre-check, not atomic enough to replace run idempotency; the race is resolved inside the workflow via hook.getConflict().
  • The route example detects the concurrent-start race by comparing resumeHook()'s runId against the started run's runId — race-correct without blocking on workflow completion.

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
Foundations: Idempotency (new Run idempotency section + conflict-handling strategies)v4v5
Cookbook: Idempotencyv4v5
start() API referencev4v5
getHookByToken() API referencev4v5
getRun() API referencev4v5
resumeHook() API referencev4v5
createHook() API referencev4v5
fetch() API referencev4v5
World storage referencev4v5

Smaller idempotency cross-link callouts were also added to the scheduling, timeouts, workflow composition, child workflows, publishing libraries, AI SDK, Chat SDK, and Sandbox cookbook pages (same paths under /v5/docs for v5).

Validation

  • pnpm test:docs (921 samples typechecked, 2 files) — also fixed a pre-existing failure in the run-idempotency route sample with a @skip-typecheck cross-block marker
  • git diff --check

@vercel

vercelBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd22586

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production123302171450
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7634010668700

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+2.1%)1.006s (~)0.962s101.00x
💻 LocalExpress0.052s (+22.6% 🔺)1.007s (~)0.955s101.20x
🐘 PostgresExpress0.062s (+0.7%)1.014s (~)0.952s101.43x
💻 LocalNext.js (Turbopack)0.063s (~)1.006s (~)0.943s101.46x
🐘 PostgresNitro0.079s (+19.8% 🔺)1.016s (~)0.938s101.82x
🐘 PostgresNext.js (Turbopack)0.082s (+9.8% 🔺)1.019s (~)0.937s101.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.285s (-24.6% 🟢)2.322s (+10.6% 🔺)2.037s101.00x
▲ VercelExpress0.370s (+36.7% 🔺)2.299s (-10.9% 🟢)1.929s101.30x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.089s (~)2.007s (~)0.917s101.00x
🐘 PostgresExpress1.107s (-0.7%)2.009s (~)0.902s101.02x
💻 LocalExpress1.116s (+2.0%)2.007s (~)0.891s101.02x
🐘 PostgresNitro1.124s (+1.0%)2.011s (~)0.887s101.03x
💻 LocalNext.js (Turbopack)1.134s (+0.6%)2.007s (~)0.872s101.04x
🐘 PostgresNext.js (Turbopack)1.242s (+8.6% 🔺)2.036s (+1.3%)0.794s101.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.668s (-4.9%)3.484s (-6.1% 🟢)1.816s101.00x
▲ VercelNitro1.743s (+4.0%)3.822s (+10.8% 🔺)2.079s101.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.489s (-0.5%)11.022s (~)0.533s31.00x
🐘 PostgresExpress10.557s (~)11.018s (~)0.461s31.01x
💻 LocalExpress10.575s (~)11.024s (~)0.449s31.01x
🐘 PostgresNitro10.645s (~)11.022s (~)0.377s31.01x
💻 LocalNext.js (Turbopack)10.790s (~)11.022s (~)0.232s31.03x
🐘 PostgresNext.js (Turbopack)10.993s (~)11.353s (-2.9%)0.360s31.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.741s (+2.4%)15.369s (-2.3%)1.628s21.00x
▲ VercelNitro13.963s (+4.7%)16.017s (+5.8% 🔺)2.053s21.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.686s (-1.1%)14.027s (-1.4%)0.341s51.00x
🐘 PostgresExpress13.855s (~)14.017s (~)0.163s51.01x
🐘 PostgresNitro13.898s (+0.6%)14.023s (~)0.124s51.02x
💻 LocalExpress13.953s (+0.8%)14.228s (+1.4%)0.276s51.02x
🐘 PostgresNext.js (Turbopack)14.310s (-1.3%)15.060s (~)0.750s41.05x
💻 LocalNext.js (Turbopack)14.522s (~)15.030s (~)0.508s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.492s (+4.7%)24.394s (+5.7% 🔺)1.902s31.00x
▲ VercelNitro23.148s (+3.2%)25.776s (+7.1% 🔺)2.628s31.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.317s (-1.2%)13.025s (~)0.707s71.00x
🐘 PostgresExpress12.662s (-0.6%)13.017s (~)0.355s71.03x
💻 LocalExpress12.688s (+1.5%)13.026s (~)0.338s71.03x
🐘 PostgresNitro12.934s (+2.8%)13.449s (+3.3%)0.515s71.05x
💻 LocalNext.js (Turbopack)13.803s (+0.9%)14.171s (+1.0%)0.368s71.12x
🐘 PostgresNext.js (Turbopack)14.928s (+6.8% 🔺)15.370s (+7.4% 🔺)0.442s61.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express30.956s (+4.3%)32.926s (+5.7% 🔺)1.970s31.00x
▲ VercelNitro31.388s (-1.0%)33.630s (+1.0%)2.242s31.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.206s (~)2.007s (~)0.802s151.00x
🐘 PostgresNext.js (Turbopack)1.221s (-3.0%)2.007s (~)0.786s151.01x
💻 LocalNitro1.223s (~)2.006s (~)0.783s151.01x
🐘 PostgresNitro1.260s (+4.7%)2.008s (~)0.748s151.04x
💻 LocalExpress1.305s (+9.7% 🔺)2.007s (~)0.702s151.08x
💻 LocalNext.js (Turbopack)1.338s (-0.8%)2.006s (~)0.668s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.372s (-7.1% 🟢)3.556s (-21.0% 🟢)1.185s91.00x
▲ VercelNitro2.462s (-33.4% 🟢)4.068s (-21.5% 🟢)1.606s81.04x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.364s (+8.4% 🔺)2.735s (+36.2% 🔺)1.371s111.00x
🐘 PostgresExpress1.461s (+15.8% 🔺)2.509s (+25.0% 🔺)1.048s121.07x
🐘 PostgresNext.js (Turbopack)1.512s (+7.9% 🔺)2.331s (+16.2% 🔺)0.819s131.11x
💻 LocalNitro1.697s (~)2.005s (~)0.309s151.24x
💻 LocalNext.js (Turbopack)1.742s (-7.3% 🟢)2.074s (-3.5%)0.332s151.28x
💻 LocalExpress2.242s (+21.7% 🔺)2.675s (+24.4% 🔺)0.433s121.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.639s (-40.3% 🟢)4.429s (-27.6% 🟢)1.790s71.00x
▲ VercelNitro2.882s (-53.8% 🟢)5.096s (-34.2% 🟢)2.215s61.09x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.610s (+14.4% 🔺)4.010s (+99.7% 🔺)2.400s81.00x
🐘 PostgresExpress1.668s (+19.0% 🔺)3.887s (+93.5% 🔺)2.219s81.04x
🐘 PostgresNext.js (Turbopack)3.217s (+78.3% 🔺)4.165s (+81.3% 🔺)0.949s82.00x
💻 LocalNitro4.405s (-16.1% 🟢)5.014s (-16.7% 🟢)0.609s62.74x
💻 LocalNext.js (Turbopack)4.848s (-15.0% 🟢)5.300s (-14.7% 🟢)0.452s73.01x
💻 LocalExpress7.792s (+44.7% 🔺)8.518s (+37.8% 🔺)0.727s44.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.708s (-78.2% 🟢)5.818s (-68.2% 🟢)2.109s61.00x
▲ VercelExpress4.596s (-49.6% 🟢)6.296s (-44.6% 🟢)1.700s61.24x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.203s (-0.6%)2.007s (~)0.804s151.00x
🐘 PostgresNitro1.229s (+3.2%)2.007s (~)0.778s151.02x
💻 LocalNext.js (Turbopack)1.394s (+3.1%)2.007s (~)0.613s151.16x
🐘 PostgresNext.js (Turbopack)1.427s (+13.0% 🔺)2.018s (~)0.591s151.19x
💻 LocalNitro1.463s (-3.0%)2.006s (~)0.543s151.22x
💻 LocalExpress1.671s (+0.7%)2.074s (~)0.404s151.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.403s (-6.8% 🟢)4.237s (~)1.834s81.00x
▲ VercelExpress2.475s (-8.3% 🟢)4.128s (-12.4% 🟢)1.653s81.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.333s (+4.8%)2.507s (+24.9% 🔺)1.175s121.00x
🐘 PostgresExpress1.383s (+6.7% 🔺)2.393s (+19.1% 🔺)1.010s131.04x
🐘 PostgresNext.js (Turbopack)1.610s (+14.7% 🔺)2.481s (+23.5% 🔺)0.871s131.21x
💻 LocalNitro1.819s (-16.2% 🟢)2.077s (-19.9% 🟢)0.258s151.37x
💻 LocalNext.js (Turbopack)2.093s (+2.3%)2.827s (-6.0% 🟢)0.734s111.57x
💻 LocalExpress2.165s (~)2.593s (-3.1%)0.428s121.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.803s (-18.2% 🟢)4.442s (-12.9% 🟢)1.639s71.00x
▲ VercelNitro3.091s (-36.0% 🟢)4.926s (-23.1% 🟢)1.835s71.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.582s (+12.8% 🔺)4.013s (+99.6% 🔺)2.431s81.00x
🐘 PostgresNitro1.710s (+23.1% 🔺)4.300s (+114.1% 🔺)2.590s71.08x
🐘 PostgresNext.js (Turbopack)2.745s (+51.8% 🔺)3.594s (+61.6% 🔺)0.849s91.74x
💻 LocalNitro4.968s (-8.6% 🟢)5.514s (-8.3% 🟢)0.546s63.14x
💻 LocalNext.js (Turbopack)5.505s (-9.1% 🟢)6.217s (-8.8% 🟢)0.712s53.48x
💻 LocalExpress5.794s (-8.8% 🟢)6.415s (-8.5% 🟢)0.621s53.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.609s (-29.3% 🟢)5.591s (-22.8% 🟢)1.982s61.00x
▲ VercelNitro3.647s (-47.9% 🟢)5.583s (-35.1% 🟢)1.937s61.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.575s (-4.2%)1.006s (~)0.431s601.00x
💻 LocalNitro0.582s (-2.9%)1.005s (~)0.422s601.01x
🐘 PostgresNitro0.591s (+2.0%)1.023s (+1.6%)0.432s591.03x
💻 LocalExpress0.641s (+4.8%)1.005s (~)0.364s601.12x
🐘 PostgresNext.js (Turbopack)0.736s (-13.8% 🟢)1.067s (+4.2%)0.331s571.28x
💻 LocalNext.js (Turbopack)0.907s (+5.1% 🔺)1.096s (+7.2% 🔺)0.189s551.58x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.639s (+13.7% 🔺)7.687s (+18.0% 🔺)2.048s81.00x
▲ VercelExpress5.991s (-40.9% 🟢)7.332s (-37.7% 🟢)1.341s91.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.367s (-4.7%)2.007s (~)0.640s451.00x
🐘 PostgresNitro1.378s (-4.0%)2.007s (-1.2%)0.629s451.01x
💻 LocalNitro1.487s (-1.2%)2.005s (~)0.519s451.09x
💻 LocalExpress1.609s (+3.5%)2.007s (-1.1%)0.398s451.18x
🐘 PostgresNext.js (Turbopack)1.664s (-18.0% 🟢)2.177s (-18.0% 🟢)0.513s421.22x
💻 LocalNext.js (Turbopack)2.127s (+1.3%)3.008s (~)0.881s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.875s (-10.4% 🟢)14.900s (-7.7% 🟢)2.025s71.00x
▲ VercelNitro13.500s (+3.1%)15.881s (+9.8% 🔺)2.381s61.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.761s (-1.7%)3.166s (+0.9%)0.405s381.00x
🐘 PostgresNitro2.843s (+1.7%)3.166s (~)0.323s381.03x
💻 LocalNitro3.175s (-3.7%)3.977s (-0.8%)0.801s311.15x
💻 LocalExpress3.384s (+3.0%)4.009s (~)0.625s301.23x
🐘 PostgresNext.js (Turbopack)3.485s (-12.5% 🟢)3.947s (-8.9% 🟢)0.462s311.26x
💻 LocalNext.js (Turbopack)4.440s (+2.2%)5.011s (~)0.571s241.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.690s (-2.1%)28.082s (~)2.392s51.00x
▲ VercelNitro26.004s (-10.7% 🟢)28.926s (-6.1% 🟢)2.922s51.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.250s (+7.5% 🔺)1.006s (~)0.756s601.00x
🐘 PostgresNitro0.259s (+17.9% 🔺)1.006s (~)0.748s601.03x
🐘 PostgresNext.js (Turbopack)0.351s (+26.5% 🔺)1.036s (+2.9%)0.685s581.40x
💻 LocalNitro0.392s (-8.8% 🟢)1.004s (-1.7%)0.613s601.57x
💻 LocalExpress0.461s (+11.6% 🔺)1.022s (+1.7%)0.561s591.84x
💻 LocalNext.js (Turbopack)0.583s (+6.8% 🔺)1.022s (+1.7%)0.439s592.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.026s (-21.1% 🟢)3.667s (-10.7% 🟢)1.640s171.00x
▲ VercelNitro2.038s (-27.8% 🟢)4.051s (-6.2% 🟢)2.013s151.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.400s (+11.6% 🔺)1.028s (+2.2%)0.628s881.00x
🐘 PostgresExpress0.412s (+10.0% 🔺)1.029s (+2.3%)0.617s881.03x
🐘 PostgresNext.js (Turbopack)0.564s (+13.1% 🔺)1.260s (+25.2% 🔺)0.696s721.41x
💻 LocalNitro2.162s (-0.9%)2.715s (~)0.553s345.40x
💻 LocalExpress2.212s (+8.6% 🔺)2.822s (+11.3% 🔺)0.609s325.53x
💻 LocalNext.js (Turbopack)2.505s (+2.2%)3.227s (+2.5%)0.722s286.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.891s (-68.7% 🟢)4.729s (-56.5% 🟢)1.838s201.00x
▲ VercelExpress2.949s (-45.5% 🟢)4.331s (-38.8% 🟢)1.381s211.02x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.803s (+15.4% 🔺)1.297s (+28.9% 🔺)0.494s931.00x
🐘 PostgresNitro0.806s (+18.3% 🔺)1.471s (+46.2% 🔺)0.665s821.00x
🐘 PostgresNext.js (Turbopack)1.468s (+46.0% 🔺)2.515s (+35.6% 🔺)1.047s481.83x
💻 LocalNitro9.306s (~)10.029s (+2.4%)0.724s1211.58x
💻 LocalExpress9.959s (+6.9% 🔺)10.530s (+6.7% 🔺)0.570s1212.39x
💻 LocalNext.js (Turbopack)10.701s (-2.2%)11.483s (~)0.782s1113.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.765s (-70.1% 🟢)7.499s (-66.1% 🟢)1.734s171.00x
▲ VercelExpress6.168s (-67.4% 🟢)7.892s (-61.6% 🟢)1.724s161.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.162s (~)2.004s (~)0.010s (-1.9%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (~)2.000s (~)0.001s (-7.7% 🟢)2.010s (~)0.841s101.01x
🐘 PostgresExpress1.174s (~)1.994s (~)0.001s (~)2.011s (~)0.837s101.01x
💻 LocalExpress1.188s (+1.7%)2.006s (~)0.013s (+9.4% 🔺)2.022s (~)0.833s101.02x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.011s (-13.7% 🟢)2.018s (~)0.807s101.04x
🐘 PostgresNext.js (Turbopack)1.358s (+9.1% 🔺)2.003s (~)0.066s (+4969.2% 🔺)2.077s (+3.3%)0.719s101.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.185s (-10.7% 🟢)3.320s (-1.3%)0.962s (+1.4%)4.733s (-0.7%)2.548s101.00x
▲ VercelExpress2.517s (+16.1% 🔺)3.329s (+3.9%)1.273s (+40.1% 🔺)5.116s (+11.7% 🔺)2.599s101.15x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.565s (-1.8%)2.009s (~)0.012s (-5.3% 🟢)2.023s (~)0.458s301.00x
🐘 PostgresExpress1.587s (-0.9%)2.005s (~)0.005s (-5.2% 🟢)2.029s (~)0.442s301.01x
🐘 PostgresNitro1.587s (-1.6%)2.002s (~)0.005s (+2.0%)2.024s (~)0.437s301.01x
💻 LocalExpress1.609s (+2.3%)2.011s (~)0.012s (+6.0% 🔺)2.026s (~)0.417s301.03x
💻 LocalNext.js (Turbopack)1.736s (~)2.008s (~)0.012s (-1.6%)2.024s (~)0.288s301.11x
🐘 PostgresNext.js (Turbopack)2.022s (+13.2% 🔺)2.448s (+21.8% 🔺)0.004s (-20.0% 🟢)2.465s (+21.6% 🔺)0.443s251.29x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.128s (+3.5%)7.309s (+3.9%)0.222s (-52.7% 🟢)8.010s (~)1.883s81.00x
▲ VercelExpress6.224s (+2.5%)7.752s (+6.2% 🔺)0.326s (+23.9% 🔺)8.575s (+7.2% 🔺)2.351s81.02x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.775s (+9.6% 🔺)1.029s (+1.5%)0.000s (+3.5%)1.058s (+2.9%)0.283s571.00x
🐘 PostgresNitro0.792s (+11.8% 🔺)1.065s (+2.1%)0.000s (-66.1% 🟢)1.078s (+1.4%)0.286s561.02x
🐘 PostgresNext.js (Turbopack)1.277s (+48.1% 🔺)1.875s (+71.9% 🔺)0.000s (-14.1% 🟢)1.890s (+72.0% 🔺)0.613s321.65x
💻 LocalNitro1.352s (-6.1% 🟢)2.013s (~)0.000s (+80.0% 🔺)2.015s (~)0.663s301.74x
💻 LocalExpress1.430s (+5.9% 🔺)2.014s (~)0.000s (+200.0% 🔺)2.016s (~)0.586s301.84x
💻 LocalNext.js (Turbopack)1.560s (+6.4% 🔺)2.014s (~)0.001s (+73.3% 🔺)2.018s (~)0.458s302.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.901s (-13.7% 🟢)4.310s (-5.8% 🟢)0.000s (-53.8% 🟢)4.754s (-5.7% 🟢)1.853s131.00x
▲ VercelNitro2.975s (-21.8% 🟢)4.535s (-6.1% 🟢)0.000s (+Infinity% 🔺)5.061s (-3.1%)2.086s121.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.532s (~)2.098s (-5.4% 🟢)0.000s (-100.0% 🟢)2.117s (-5.8% 🟢)0.585s291.00x
🐘 PostgresNitro1.640s (+23.0% 🔺)2.140s (+5.6% 🔺)0.000s (+Infinity% 🔺)2.167s (+5.0% 🔺)0.527s281.07x
🐘 PostgresNext.js (Turbopack)2.504s (+44.8% 🔺)3.050s (+34.8% 🔺)0.000s (NaN%)3.072s (+35.3% 🔺)0.567s201.63x
💻 LocalNitro2.966s (-6.4% 🟢)3.672s (-4.3%)0.000s (-5.9% 🟢)3.674s (-4.3%)0.709s171.94x
💻 LocalExpress3.180s (+2.7%)3.734s (+1.7%)0.001s (+90.9% 🔺)3.739s (+1.7%)0.560s172.08x
💻 LocalNext.js (Turbopack)3.223s (+13.0% 🔺)3.778s (+10.6% 🔺)0.001s (+68.8% 🔺)3.782s (+10.7% 🔺)0.560s162.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.199s (-43.1% 🟢)5.855s (-26.2% 🟢)0.000s (+Infinity% 🔺)6.349s (-29.7% 🟢)2.150s101.00x
▲ VercelExpress4.324s (-18.8% 🟢)5.767s (-12.4% 🟢)0.000s (+Infinity% 🔺)6.266s (-12.9% 🟢)1.942s101.03x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro20/21
🐘 PostgresExpress16/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow-api/world/storage.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/get-step-metadata.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
Comment threaddocs/content/docs/v4/cookbook/advanced/child-workflows.mdx Outdated
pranaygpand others added 6 commits June 11, 2026 14:37
…docs-run-idempotency
* origin-https/main: (115 commits)
[core] V2: unify wait+step queue dispatch in suspension processing (#1925)
fix(world-local,world-postgres): make duplicate hook_created idempotent (#2295)
docs(observability): remove MVP implementation detail bullet (#2367)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
...
# Conflicts:
#	docs/content/docs/v4/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
… into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/hook-ready:
docs: restore SWC Plugin heading in AGENTS.md
docs: require docs preview links in PR descriptions for docs changes
docs: fix inconsistent hasConflict bullet in create-webhook reference
feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
test: cover hook ready continuation scheduling
feat: add hook ready promise
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
- Primary claim pattern is now `if (await hook.hasConflict)` instead of
try/catch on HookConflictError; payload awaits still reject with
HookConflictError (with conflictingRunId) when the owner's run ID is
needed.
- Route example returns the active owner via resumeHook()'s runId
instead of threading conflictingRunId through the workflow result.
- Update claim-pattern prose across start(), getHookByToken(), world
storage, scheduling, workflow composition, and cookbook idempotency
pages (v4 + v5).
- Add @skip-typecheck marker to the cross-block route sample, fixing a
pre-existing docs typecheck failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The early callout was too vague and out of place at the top of the API
reference. Replace it with a 'Resume or Start' example section that
explains the flow, shows the resume-first/start-then-retry route, and
links to the run idempotency pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f awaiting returnValue
The 'Resume or Start' example returned the just-started run's runId with
reused: false even when a concurrent request's run won the token race —
the payload had reached the actual owner, so the response pointed callers
at a run that exits as a duplicate. The foundations route handled the
race correctly but by awaiting run.returnValue, blocking the HTTP
response on full workflow completion.
resumeHook() always resolves against the actual active owner, so
comparing the resumed hook's runId with the started run's runId detects
the race in both examples — race-correct and non-blocking.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — accurate, well-hedged docs; synced to main and one pattern defect fixed in-branch

Now that #2015 is on main, I merged main into this branch (resolving the create-hook.mdx conflicts in favor of this PR's run-idempotency callout) — the diff is down to docs-only (38 files + the empty changeset), exactly as the PR description promised. pnpm test:docs passes (923 samples typechecked).

Technical accuracy — verified against the code

The route samples in the foundations page carry @skip-typecheck (cross-block dependency), so CI doesn't validate them; I checked the API claims manually:

  • start(processOrder, [orderId]) matches the (workflow, args: TArgs, options?) overload returning Promise<Run<TResult>>
  • HookNotFoundError is exported from the workflow/errors subpath (./errorsdist/internal/errors.js) ✓
  • resumeHook(token, payload) returns Promise<Hook> carrying the owner's runId
  • The conflictingRunId-on-HookConflictError claim matches #2012 (and its stable backport #2016), so it's accurate for both the v4 and v5 pages ✓
  • hasConflict semantics (registration committed on suspension; resolves true/false; payload await still rejects with HookConflictError) match the implementation that just landed in #2015

What I particularly like: the docs are honest about the limits. getHookByToken() is consistently described as an advisory pre-check, the start()-vs-hook-registration race is named explicitly everywhere it matters, the resolution is correctly placed inside the workflow via hook.hasConflict, and the "native atomic start-and-hook API is in the works" framing sets expectations without overpromising. The foundations page carries the full pattern once, and the ~20 cross-link callouts are scoped summaries pointing back at it rather than divergent copies.

One defect fixed in-branch (42d5533) — please review the change

The two route examples disagreed on race handling, and one was wrong:

  • resume-hook.mdx "Resume or Start" returned { runId: run.runId, reused: false } after start() + resumeWithRetry() — but when a concurrent request's run wins the token race, the resume reaches that owner, and this response points the caller at a run that exits as a duplicate without doing any work. Wrong answer in exactly the race case the section teaches about.
  • foundations/idempotency.mdx handled the race correctly but via await run.returnValue, which blocks the HTTP response on full workflow completion (hook resume + chargeOrder + every step). That's an anti-pattern for a copy-paste route sample — workflows are long-lived by design.

Both are fixed by one observation: resumeHook() always resolves against the actual active owner, so resumed.runId !== run.runId detects the race — race-correct, non-blocking, and the same shape in both examples. The warn callout's wording is updated to match. Happy to revert if you prefer the returnValue approach for pedagogical reasons, but I'd argue the runId comparison is also the better teaching: it makes "the resume reaches the owner" concrete.

Non-blocking notes

  1. The 500ms retry budget (5 × 100ms) in resumeWithRetry/resumeOrder is tight against real cold starts + queue dispatch latency. Worth either a more forgiving schedule (e.g. exponential up to ~5s) or a sentence on the failure consequence: if the retries exhaust afterstart() succeeded, the new run is left suspended awaiting a payload that never arrives (until run timeout). Right now the thrown error reads as transient but leaves a dangling run behind.
  2. v4 docs sequencing, again: hasConflict is still not on stable (no backport PR exists yet), and this PR widens the v4-docs-reality gap from one page (#2015) to roughly twenty. Same deliberate-choice ask as before — either the #2015 backport + 4.x release should follow promptly, or the v4 halves of these pages should wait for it. The content itself is version-accurate; it's purely a shipping-order question.
  3. The cookbook's minimal getHookByToken()-then-start() example intentionally omits the resume-retry mechanics — good scoping, and the pointer to foundations for "the full pattern" covers it.

CI

All checks green or pending on the synced head; mergeable: MERGEABLE. The branch now contains the merge of main (e6f0516de) plus the race-handling fix (42d5533d4).

…ull>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pranaygpand others added 13 commits June 11, 2026 16:12
…docs-run-idempotency
* origin-https/main:
ci: use claude-fable-5 for backport AI model (#2370)
Add `hook.hasConflict` for early hook conflict detection (#2015)
# Conflicts:
#	docs/content/docs/v4/api-reference/workflow/create-hook.mdx
#	docs/content/docs/v5/api-reference/workflow/create-hook.mdx
…n-idempotency
* pranaygp/hook-get-conflict:
feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
Run idempotency docs now use getConflict (resolves with the conflicting
Run in v5, { runId } in v4) and document code-driven conflict strategies
in place of static ID-reuse policies: reject the duplicate, adopt the
owner's result, inspect before deciding, signal the owner via
resumeHook, and supersede via cancel-and-reclaim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…empotency' into pranaygp/codex/docs-run-idempotency
* origin-https/pranaygp/codex/docs-run-idempotency:
docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue
# Conflicts:
#	docs/content/docs/v4/foundations/idempotency.mdx
#	docs/content/docs/v5/foundations/idempotency.mdx
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
refactor: make getConflict a method — hook.getConflict()
fix: never resolve getConflict with a non-Run fallback shape
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route examples typecheck as-is since the runId-comparison rewrite;
strategy fragments are now complete self-contained workflows; the
publishing-libraries cross-block dependency uses the declare @setup
convention. 934 samples typechecked, none skipped by this PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-idempotency
* pranaygp/hook-get-conflict:
review: guard Run class registration, fix anchors, clarify changeset
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…her systems
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — re-reviewed after the getConflict() migration; synced to main again

Re-review following #2373's merge (which replaced hook.hasConflict with hook.getConflict() returning Run | null). I merged main into this branch once more (45fe1a585): the core-package conflicts were the branch's embedded pre-merge copy of #2373 versus the landed version (resolved by taking main wholesale, which includes the registry refactor), and the four docs conflicts were this PR's anchored links + callouts versus #2373's baseline pages (resolved by keeping this PR's superset). The effective diff is docs-only + the empty changeset, and pnpm test:docs passes — now at 1012 typechecked samples with zero @skip-typecheck escape hatches, since the route samples were restructured to typecheck standalone. That closes the "flagship sample isn't CI-validated" gap from my first review.

The getConflict() migration is done right

  • Zero stale hasConflict references anywhere in the PR.
  • The run-idempotency pattern now returns { status: "duplicate", runId: conflict.runId } — the duplicate run can finally say who owns the token, which is what makes the route example's dedupe response meaningful.
  • The new Conflict-handling strategies section is the best part of this revision. All five strategies are technically sound — I audited each against the implementation:
    • Reject: conflict.runId plain property, deterministic ✓
    • Adopt: await conflict.returnValue durable step accessor ✓
    • Inspect: await conflict.status
    • Signal: resumeHook() correctly wrapped in a "use step" function (API calls can't run in workflow context) ✓
    • Supersede: await conflict.cancel() + retry loop creating a fresh hook per attempt, correctly handling the disposal-propagation window; per-iteration using disposal of conflicted hooks is safe (pinned by the #2015 test) ✓
  • The "no policy enum — policy is ordinary code" framing (now phrased generically without naming other systems) is a genuinely good product argument, and the inspect-before-deciding strategy demonstrates exactly what static configuration can't express.
  • The v4/v5 split is version-accurate: v4 documents the { runId } resolution with getRun(conflict.runId) inside steps (no native Run construction in 4.x workflow context); v5 documents the Run handle with durable accessors. The older-world degenerate case (hook_conflict without conflictingRunId → rejects with HookConflictError) is documented in the hooks foundations page.
  • My earlier race-handling fix (reused: resumed.runId !== run.runId) survived the revision and the warn callout's wording is consistent with it.

Two non-blocking notes

  1. The inspect strategy's terminal-state fall-through is slightly loose: when the owner is already terminal, the example proceeds to processOwnedOrder() without holding the token (this run's hook already recorded its conflict; the released token isn't retroactively claimed). A third concurrent run could claim it meanwhile. The supersede example handles this properly with its fresh-hook retry loop — a one-line pointer ("to do the work while holding the token after the owner ends, use the supersede pattern's retry loop") would prevent copy-paste of the loose version into duplicate-sensitive code.
  2. The recurring v4 sequencing flag, final form: the v4 pages now document getConflict() — an API that has never existed in any 4.x release (hasConflict didn't ship there either). The documented-but-unreleased v4 surface keeps compounding across #2015#2373 → this PR. None of it is wrong — the v4 content correctly describes what a backport would look like — but the backport chain (now effectively just #2373, since it subsumes #2015's API) should land in a 4.x release soon after this merges, or the v4 docs are fiction in the meantime.

Everything else from my first review stands. Nice iteration — the strategies section turned a reference page into an actual design guide.

…docs-run-idempotency
* origin-https/main:
Version Packages (beta) (#2364)
Animate in-progress segments in the timeline (#2383)
…docs-run-idempotency
* origin-https/main:
test: e2e coverage for run-idempotency conflict-handling strategies (#2387)
fix(docs): repair broken links, fix the link linter, and version-correct v5 Card + edit links (#2391)
feat(web-shared): RelativeTimeCard with shared ContextCard provider (#2328)
Add allowReservedAttributes option to start() (#2385)
# Conflicts:
#	docs/content/docs/v4/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2410. Merge conflicts were resolved by AI — please review carefully. (backport job run)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@pranaygp@TooTallNate@VaguelySerious