Skip to content

[core] Extend OCC fence to all branch-decision writes - #2132

Merged
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage
May 28, 2026
Merged

[core] Extend OCC fence to all branch-decision writes#2132
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Layered on top of #2113. Extends the OCC fence — which currently covers only wait_completed writes from the elapsed-wait scan — to every other write whose outcome depends on a branch decision the workflow VM made from its loaded event log:

Write siteStatus
wait_completed (elapsed-wait scan)already fenced by #2113
step_creatednow fenced
wait_creatednow fenced
hook_creatednow fenced
hook_disposednow fenced
run_completednow fenced
run_failednow fenced
hook_receiveddeliberately not fenced (preserved from #2113)

hook_received stays unfenced for the same reason it's unfenced in #2113: fencing the user's signal would drop it on contention. Stale-snapshot protection belongs on the writes that consume hooks, not the writes that deliver them.

Why

Under concurrent replay (two invocations of the same run with overlapping but non-identical event-log snapshots), the SDK's deterministic ULID factory can allocate the same correlationId for different step calls depending on which side of a Promise.race each replay sees as the winner. Both invocations then try to write step_created for the same correlationId with different stepNames. The server's entity idempotency rejects the loser at the entity level — but if the winner was the stale-view invocation, its branch decision is the one that lands in the log, and future replays will see a step_created they don't expect.

Fencing the write makes the stale-view invocation lose the CAS, retry against the freshly-loaded tail, observe the events its snapshot was missing, and arrive at the same branch decision the authoritative invocation made.

Implementation

The retry loop from #2113's elapsed-wait scan is extracted into packages/core/src/runtime/fenced-write.ts so the six new fenced sites can share it without copy-paste. Each call site provides its own onConflictRefresh that runs an idempotency check against the reloaded log (e.g. "is this wait_created already in the log? → abort instead of retrying").

handleSuspension receives the load-time tail eventId + cursor from the runtime, and runs all four of its fenced writes (step_created, hook_created, hook_disposed, wait_created) against that fence. Each successful write advances the fence so chained writes in the same suspension don't conflict with each other.

The terminal run_completed / run_failed writes in runtime.ts use the same helper, with the idempotency check verifying no other terminal event has landed since the snapshot.

Test plan

  • All 1014 @workflow/core unit tests pass.
  • TypeScript clean (0 errors).
  • End-to-end stress: re-ran the same 40-attempt hammer that exposed the underlying race against the matching workflow-server preview — 0 / 40 failures (vs ~2 / 40 baseline on stable, and 5 / 40 against an alternative server-side fix that rewrites event IDs but doesn't fence). Verified each successful run has run.lastKnownEventId materialized as expected.

Temporary diagnostic instrumentation for investigating intermittent
CorruptedEventLogError 'step consumer mismatch' failures.
Emits console.log lines tagged 'WF_TRACE' at four points:
- runWorkflow start: dumps the full event array the replay will consume
(eventIds, types, correlationIds, stepNames) plus a sha256 digest
- step/hook/sleep subscribe: per-replay correlationId -> name assignment
- step consumer mismatch: structured record of the failure including the
event index in the SDK's view of the log
- runWorkflow end: completed | failed | suspended
Used to diff successive replays of the same runId and confirm whether
the SDK actually sees the same event array each time.
Peter's PR #2113 fences `wait_completed` writes from the elapsed-wait
scan. This commit extends the fence to every other write whose outcome
depends on a branch decision the workflow VM made from its loaded event
log — per the table @VaguelySerious himself laid out in his PR comment:
suspension-handler.ts:
- step_created (the smoking gun on wrun_01KSPS7XEGHF4A6WYF4DB03D40)
- hook_created
- hook_disposed
- wait_created
runtime.ts terminal writes:
- run_completed
- run_failed
`hook_received` is deliberately NOT fenced (Peter's reasoning preserved
verbatim: fencing the user's signal would drop it on contention; stale-
snapshot protection belongs on the writes that consume hooks, not the
ones that deliver them).
The fence value is the load-time tail of the events array passed into
`runWorkflow`. `suspension-handler` receives the fence + cursor from
the runtime and reloads on conflict; the runtime's terminal writes read
the cursor directly.
The new `__fenced-write.ts` helper encapsulates the retry loop so we
don't have to copy/paste Peter's pattern six times. It's named with the
leading-underscore convention to flag it as throwaway diagnostic code,
matching `__debug-replay-trace.ts`.
@vercel

vercelBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a7efa5a

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

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

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

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

@TooTallNate
TooTallNate changed the base branch from stable to mainMay 28, 2026 08:29
Two changes both needed for the extended-fence test loop to actually
exercise the OCC code path on the server:
1. Hardcode WORKFLOW_SERVER_URL_OVERRIDE to
https://workflow-server-83nn57dvc.vercel.sh (preview deployment of
workflow-server PR 447, branch alias
workflow-server-git-peter-event-write-cas.vercel.sh). The previous
preview at workflow-server-7pxaxn4d4.vercel.sh was Pranay's monotonic-
append PR 456 \u2014 different fix, doesn't implement the CAS the SDK side
now sends.
2. Map HTTP 412 \u2192 EntityConflictError in the world-vercel error mapper.
workflow-server PR 447 returns 412 with a 'fence conflict' message
for EventLogFenceConflictError; the SDK's existing fence-retry loops
(Peter's wait_completed scan + the new ones in suspension-handler
and runtime terminal writes) match on /fence conflict/i against the
message of an EntityConflictError. Without this mapping the 412 falls
through to WorkflowWorldError and the regex match never fires.
@github-actions

github-actionsBot commented May 28, 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.029s (-31.8% 🟢)1.005s (~)0.976s101.00x
💻 LocalExpress0.030s (-31.6% 🟢)1.005s (~)0.975s101.03x
💻 LocalNext.js (Turbopack)0.042s1.004s0.962s101.43x
🐘 PostgresNext.js (Turbopack)0.046s1.009s0.963s101.56x
🐘 PostgresExpress0.046s (-20.5% 🟢)1.012s (~)0.966s101.57x
🐘 PostgresNitro0.050s (-47.3% 🟢)1.012s (-2.9%)0.962s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.233s (-0.8%)1.941s (-9.1% 🟢)1.707s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.072s (-5.2% 🟢)2.006s (~)0.934s101.00x
💻 LocalExpress1.073s (-4.6%)2.006s (~)0.933s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.009s (~)0.925s101.01x
🐘 PostgresNitro1.086s (-4.7%)2.008s (~)0.922s101.01x
💻 LocalNext.js (Turbopack)1.089s2.005s0.916s101.02x
🐘 PostgresNext.js (Turbopack)1.101s2.008s0.907s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.767s (-5.7% 🟢)3.654s (-4.0%)1.887s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.410s (-4.9%)11.023s (~)0.613s31.00x
💻 LocalExpress10.418s (-4.6%)11.023s (~)0.605s31.00x
🐘 PostgresExpress10.424s (-4.9%)11.019s (~)0.595s31.00x
🐘 PostgresNitro10.430s (-4.1%)11.014s (~)0.584s31.00x
💻 LocalNext.js (Turbopack)10.557s11.022s0.465s31.01x
🐘 PostgresNext.js (Turbopack)10.623s11.018s0.394s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.910s (-18.1% 🟢)15.954s (-20.3% 🟢)2.044s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.467s (-10.0% 🟢)14.027s (-6.7% 🟢)0.560s51.00x
💻 LocalNitro13.488s (-10.5% 🟢)14.027s (-12.5% 🟢)0.539s51.00x
🐘 PostgresExpress13.515s (-7.3% 🟢)14.018s (-6.7% 🟢)0.503s51.00x
🐘 PostgresNitro13.573s (-7.0% 🟢)14.023s (-6.7% 🟢)0.450s51.01x
💻 LocalNext.js (Turbopack)13.808s14.026s0.218s51.03x
🐘 PostgresNext.js (Turbopack)13.860s14.017s0.157s51.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.720s (-58.8% 🟢)22.449s (-57.3% 🟢)1.729s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.978s (-28.6% 🟢)12.273s (-27.9% 🟢)0.295s81.00x
💻 LocalExpress11.995s (-27.7% 🟢)12.398s (-27.2% 🟢)0.403s81.00x
🐘 PostgresExpress12.247s (-12.6% 🟢)12.876s (-11.8% 🟢)0.629s71.02x
🐘 PostgresNitro12.256s (-12.3% 🟢)13.019s (-9.0% 🟢)0.763s71.02x
💻 LocalNext.js (Turbopack)12.585s13.023s0.438s71.05x
🐘 PostgresNext.js (Turbopack)12.744s13.013s0.270s71.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.522s (-76.5% 🟢)30.389s (-75.4% 🟢)1.867s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.4% 🟢)2.007s (~)0.865s151.00x
🐘 PostgresNitro1.164s (-8.7% 🟢)2.007s (~)0.844s151.02x
💻 LocalExpress1.170s (-21.4% 🟢)2.006s (~)0.836s151.02x
💻 LocalNitro1.172s (-28.1% 🟢)2.006s (-3.3%)0.834s151.03x
🐘 PostgresNext.js (Turbopack)1.175s2.006s0.832s151.03x
💻 LocalNext.js (Turbopack)1.211s2.005s0.794s151.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express103.949s (+3534.8% 🔺)105.665s (+2185.6% 🔺)1.716s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.215s (-48.6% 🟢)2.007s (-33.3% 🟢)0.793s151.00x
🐘 PostgresNitro1.221s (-48.1% 🟢)2.008s (-33.3% 🟢)0.787s151.01x
🐘 PostgresNext.js (Turbopack)1.273s2.005s0.733s151.05x
💻 LocalNext.js (Turbopack)1.514s2.005s0.492s151.25x
💻 LocalNitro1.682s (-46.5% 🟢)2.006s (-48.4% 🟢)0.323s151.39x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.300s151.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.692s (+2.0%)5.373s (+5.1% 🔺)1.681s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.338s (-61.6% 🟢)2.007s (-50.0% 🟢)0.669s151.00x
🐘 PostgresNitro1.353s (-61.1% 🟢)2.007s (-49.9% 🟢)0.654s151.01x
🐘 PostgresNext.js (Turbopack)1.479s2.006s0.528s151.10x
💻 LocalNext.js (Turbopack)3.944s4.295s0.350s72.95x
💻 LocalNitro4.618s (-44.7% 🟢)5.010s (-44.5% 🟢)0.392s63.45x
💻 LocalExpress4.704s (-43.6% 🟢)5.180s (-42.6% 🟢)0.476s63.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.082s (+19.9% 🔺)7.070s (+15.4% 🔺)1.987s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.151s (-8.4% 🟢)2.007s (~)0.856s151.00x
🐘 PostgresNitro1.162s (-7.5% 🟢)2.010s (~)0.848s151.01x
🐘 PostgresNext.js (Turbopack)1.170s2.007s0.837s151.02x
💻 LocalNext.js (Turbopack)1.265s2.005s0.740s151.10x
💻 LocalExpress1.443s (-23.8% 🟢)2.006s (-15.1% 🟢)0.563s151.25x
💻 LocalNitro1.745s (-6.5% 🟢)2.392s (+2.2%)0.646s131.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.525s (-2.2%)4.081s (-6.2% 🟢)1.556s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.234s (-47.3% 🟢)2.008s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNitro1.235s (-47.2% 🟢)2.009s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNext.js (Turbopack)1.273s2.009s0.736s151.03x
💻 LocalNext.js (Turbopack)1.812s2.072s0.260s151.47x
💻 LocalNitro1.869s (-39.0% 🟢)2.316s (-40.4% 🟢)0.447s131.51x
💻 LocalExpress1.913s (-38.9% 🟢)2.316s (-38.5% 🟢)0.402s131.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.712s (+16.3% 🔺)5.226s (+9.0% 🔺)1.514s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.351s (-61.4% 🟢)2.008s (-49.9% 🟢)0.657s151.00x
🐘 PostgresNitro1.376s (-60.5% 🟢)2.009s (-49.9% 🟢)0.633s151.02x
🐘 PostgresNext.js (Turbopack)1.477s2.006s0.529s151.09x
💻 LocalNitro4.542s (-50.3% 🟢)5.179s (-48.3% 🟢)0.637s63.36x
💻 LocalNext.js (Turbopack)4.602s5.011s0.410s73.41x
💻 LocalExpress6.036s (-31.4% 🟢)6.416s (-30.8% 🟢)0.380s54.47x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.559s (+2.2%)8.517s (+4.1%)1.959s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.427s (-49.1% 🟢)1.007s (-1.6%)0.580s601.00x
🐘 PostgresNitro0.486s (-40.7% 🟢)1.007s (~)0.521s601.14x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.7% 🟢)0.526s591.16x
💻 LocalExpress0.512s (-48.0% 🟢)1.021s (-5.1% 🟢)0.509s591.20x
🐘 PostgresNext.js (Turbopack)0.546s1.005s0.460s601.28x
💻 LocalNext.js (Turbopack)0.639s1.004s0.365s601.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.726s (-69.9% 🟢)7.306s (-65.7% 🟢)1.580s91.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.156s (-41.5% 🟢)1.903s (-15.7% 🟢)0.747s481.00x
💻 LocalNitro1.221s (-59.8% 🟢)2.005s (-46.6% 🟢)0.784s451.06x
🐘 PostgresNitro1.222s (-36.6% 🟢)2.030s (-3.4%)0.808s451.06x
💻 LocalExpress1.272s (-57.8% 🟢)2.006s (-44.1% 🟢)0.734s451.10x
🐘 PostgresNext.js (Turbopack)1.342s2.006s0.664s451.16x
💻 LocalNext.js (Turbopack)1.617s2.027s0.410s451.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.876s (-62.7% 🟢)14.564s (-60.4% 🟢)1.688s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.181s (-45.3% 🟢)2.984s (-31.7% 🟢)0.803s411.00x
🐘 PostgresNitro2.333s (-43.1% 🟢)3.060s (-33.5% 🟢)0.726s401.07x
🐘 PostgresNext.js (Turbopack)2.651s3.007s0.356s401.22x
💻 LocalNitro2.771s (-70.2% 🟢)3.032s (-69.7% 🟢)0.261s401.27x
💻 LocalExpress2.878s (-68.7% 🟢)3.136s (-68.7% 🟢)0.257s391.32x
💻 LocalNext.js (Turbopack)3.407s4.008s0.600s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.597s (-80.3% 🟢)27.381s (-79.3% 🟢)1.784s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.175s (-38.2% 🟢)1.006s (~)0.831s601.00x
🐘 PostgresNext.js (Turbopack)0.184s1.005s0.821s601.05x
🐘 PostgresNitro0.200s (-29.4% 🟢)1.006s (~)0.806s601.14x
💻 LocalExpress0.386s (-31.2% 🟢)1.004s (~)0.618s602.21x
💻 LocalNitro0.386s (-36.2% 🟢)1.004s (-1.7%)0.618s602.21x
💻 LocalNext.js (Turbopack)0.474s1.004s0.529s602.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.161s (+10.6% 🔺)3.810s (+4.8%)1.649s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.298s (-41.5% 🟢)1.006s (~)0.708s901.00x
🐘 PostgresNitro0.317s (-36.1% 🟢)1.006s (~)0.689s901.06x
🐘 PostgresNext.js (Turbopack)0.341s1.005s0.664s901.14x
💻 LocalNext.js (Turbopack)2.026s2.536s0.510s366.80x
💻 LocalNitro2.118s (-16.6% 🟢)2.610s (-13.3% 🟢)0.492s357.10x
💻 LocalExpress2.170s (-13.7% 🟢)2.822s (-6.2% 🟢)0.652s327.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.148s (+36.1% 🔺)5.654s (+17.6% 🔺)1.507s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.652s (-20.3% 🟢)1.006s (-1.1%)0.354s1201.00x
🐘 PostgresNext.js (Turbopack)0.681s1.005s0.324s1201.04x
🐘 PostgresNitro0.688s (-13.0% 🟢)1.006s (~)0.319s1201.05x
💻 LocalNext.js (Turbopack)9.197s9.718s0.521s1314.10x
💻 LocalNitro9.393s (-16.1% 🟢)10.024s (-14.1% 🟢)0.632s1214.40x
💻 LocalExpress10.171s (-9.1% 🟢)10.695s (-10.4% 🟢)0.524s1215.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.240s (+78.4% 🔺)15.181s (+64.2% 🔺)1.942s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+467.9% 🔺)2.005s (+99.6% 🔺)0.010s (-14.9% 🟢)2.017s (+98.2% 🔺)0.887s101.00x
💻 LocalNitro1.136s (+431.6% 🔺)2.004s (+99.5% 🔺)0.010s (-18.4% 🟢)2.017s (+98.0% 🔺)0.881s101.00x
🐘 PostgresExpress1.137s (+454.2% 🔺)2.001s (+100.3% 🔺)0.001s (-25.0% 🟢)2.011s (+98.8% 🔺)0.874s101.01x
💻 LocalNext.js (Turbopack)1.147s2.004s0.008s2.015s0.868s101.01x
🐘 PostgresNext.js (Turbopack)1.155s2.001s0.001s2.008s0.852s101.02x
🐘 PostgresNitro1.164s (+467.6% 🔺)2.000s (+100.0% 🔺)0.002s (+13.3% 🔺)2.013s (+99.0% 🔺)0.849s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.134s (-14.8% 🟢)3.175s (-22.4% 🟢)2.057s (+114.0% 🔺)5.682s (+1.6%)3.549s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.506s (+139.1% 🔺)2.003s (+99.0% 🔺)0.004s (+5.3% 🔺)2.024s (+97.9% 🔺)0.518s301.00x
💻 LocalNitro1.518s (+81.0% 🔺)2.012s (+98.8% 🔺)0.009s (-0.8%)2.023s (+81.3% 🔺)0.506s301.01x
💻 LocalExpress1.520s (+100.8% 🔺)2.012s (+95.5% 🔺)0.009s (-0.9%)2.023s (+94.5% 🔺)0.503s301.01x
💻 LocalNext.js (Turbopack)1.546s2.008s0.009s2.020s0.474s301.03x
🐘 PostgresNitro1.591s (+154.9% 🔺)2.007s (+99.3% 🔺)0.004s (-3.3%)2.026s (+98.2% 🔺)0.436s301.06x
🐘 PostgresNext.js (Turbopack)1.632s2.041s0.003s2.053s0.421s301.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.080s (-6.5% 🟢)7.362s (-8.1% 🟢)0.403s (-1.4%)8.205s (-7.1% 🟢)2.125s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.645s (-32.9% 🟢)1.016s (-20.5% 🟢)0.000s (-61.0% 🟢)1.029s (-21.2% 🟢)0.384s591.00x
🐘 PostgresNitro0.669s (-31.0% 🟢)1.033s (-17.2% 🟢)0.000s (+65.5% 🔺)1.048s (-16.7% 🟢)0.379s581.04x
🐘 PostgresNext.js (Turbopack)0.878s1.088s0.000s1.186s0.308s511.36x
💻 LocalNext.js (Turbopack)1.198s1.860s0.000s1.863s0.665s331.86x
💻 LocalNitro1.353s (+10.7% 🔺)2.016s (~)0.000s (+33.3% 🔺)2.018s (~)0.664s302.10x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.649s302.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.631s (-2.9%)4.743s (-7.0% 🟢)0.000s (-100.0% 🟢)5.186s (-6.2% 🟢)1.555s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.282s (-28.5% 🟢)1.996s (-6.8% 🟢)0.000s (+86.7% 🔺)2.048s (-5.8% 🟢)0.766s301.00x
🐘 PostgresExpress1.312s (-26.0% 🟢)2.031s (-6.7% 🟢)0.000s (+Infinity% 🔺)2.054s (-6.6% 🟢)0.742s301.02x
🐘 PostgresNext.js (Turbopack)1.673s2.401s0.000s2.435s0.762s251.30x
💻 LocalNext.js (Turbopack)2.491s3.122s0.000s3.129s0.637s201.94x
💻 LocalNitro3.140s (-7.3% 🟢)3.965s (-1.7%)0.000s (-18.0% 🟢)3.968s (-1.7%)0.828s162.45x
💻 LocalExpress3.291s (-5.1% 🟢)4.026s (~)0.000s (-41.7% 🟢)4.031s (~)0.740s152.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.296s (+15.5% 🔺)6.741s (+11.9% 🔺)0.001s (+Infinity% 🔺)7.181s (+11.2% 🔺)1.884s91.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress19/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@github-actions

github-actionsBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production122202191441
✅ 💻 Local Development161502191834
✅ 📦 Local Production161502191834
✅ 🐘 Local Postgres161502191834
✅ 🪟 Windows13100131
✅ 📋 Other7410176917
Total6939010527991

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro105026
✅ example105026
✅ express105026
✅ fastify105026
✅ hono105026
✅ nextjs-turbopack12902
✅ nextjs-webpack12902
✅ nitro105026
✅ nuxt105026
✅ sveltekit12407
✅ vite105026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13100
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable106025
✅ e2e-local-dev-tanstack-start-106025
✅ e2e-local-postgres-nest-stable106025
✅ e2e-local-postgres-tanstack-start-106025
✅ e2e-local-prod-nest-stable106025
✅ e2e-local-prod-tanstack-start-106025
✅ e2e-vercel-prod-tanstack-start105026

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's merge into upstream branch? and then review as one

Strip the WF_TRACE replay tracing that was used to diagnose the
CORRUPTED_EVENT_LOG race \u2014 it's served its purpose now that the fix
is in. Specifically:
- Delete packages/core/src/__debug-replay-trace.ts and its 8 call sites
in workflow.ts, step.ts, workflow/hook.ts, workflow/sleep.ts.
- Drop the matching [DEBUG] inline narrative comments at each call site.
- Rename packages/core/src/runtime/__fenced-write.ts \u2192 fenced-write.ts
(the leading-underscore convention marked it as throwaway diagnostic
code; the helper is intended to stay).
- Trim the file header on fenced-write.ts and the related narrative
comment in suspension-handler.ts to drop the failing-runId / PR-number
references that only made sense in the debug context.
No behavioral change. typecheck clean (0 errors); 1014/1014 unit tests
pass (same as parent commit 77f057a).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Extends optimistic concurrency control (OCC) “fencing” to additional workflow event writes whose correctness depends on branch decisions made from a potentially stale event-log snapshot, and adjusts step dispatch behavior to avoid duplicate execution under concurrent replay.

Changes:

  • Introduce a shared fencedEventCreate helper to retry CAS-fenced writes with refresh + idempotency checks.
  • Apply fencing to step_created / wait_created / hook create+dispose paths in handleSuspension, and to terminal run_completed / run_failed writes in runtime.ts.
  • Improve local queue idempotency by retaining completed idempotency keys (plus tests/docs updates).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsMap HTTP 412 OCC fence conflicts into EntityConflictError for downstream handling.
packages/world-testing/src/inline-batches-debug.mtsTighten debug assertions for race/skip counters.
packages/world-local/src/queue.tsAdd a bounded cache of completed idempotency keys to prevent post-completion duplicate dispatch.
packages/world-local/src/queue.test.tsAdd coverage for post-completion idempotency dedupe behavior.
packages/core/src/workflow/hook.tsMinor formatting-only change.
packages/core/src/step.tsMinor formatting-only change.
packages/core/src/runtime/suspension-handler.tsFence branch-decision writes and serialize them to avoid self-conflicts; add refresh/idempotency logic.
packages/core/src/runtime/suspension-handler.test.tsNew test ensuring the fence is chained across writes within a suspension.
packages/core/src/runtime/fenced-write.tsNew shared helper implementing fence-conflict retries with backoff and caller-provided refresh/idempotency checks.
packages/core/src/runtime.tsFence terminal writes; owner-scope step queueing when inline execution is possible; crash-recovery exception for redelivery.
docs/content/docs/v5/changelog/eager-processing.mdxUpdate documentation to reflect owner-scoped queueing semantics.
docs/content/docs/v4/changelog/eager-processing.mdxSame as v5 doc update for v4 docs.
.changeset/quick-local-queues.mdChangeset for local queue idempotency behavior.
.changeset/chilly-fences-chain.mdChangeset for core fencing + dispatch changes.

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

Comment threadpackages/world-vercel/src/utils.ts
Address Copilot review on PR 2132 (#2132 (comment)).
The fence-retry loop in runtime/fenced-write.ts detects OCC conflicts
via /fence conflict/i.test(err.message). The 412 branch was relying on
the server's JSON body to populate that message via errorData.message,
but parseResponseBody().catch(() => ({})) swallows JSON parse failures
silently — so any non-JSON 412 response (CDN HTML, gateway timeout
page, intermediate proxy error) would surface as
EntityConflictError("<METHOD> /endpoint -> HTTP 412: Precondition
Failed"), the regex would miss it, and the retry loop would
mis-classify the conflict as terminal.
Prefix the message with `fence conflict:` whenever the parsed body
didn't already carry the marker, so the retry detection is robust to
response-body parse failures.
Tests: world-vercel 69/69 pass.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@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" + '
[core] Extend OCC fence to all branch-decision writes by TooTallNate · Pull Request #2132 · vercel/workflow · GitHub
Skip to content

[core] Extend OCC fence to all branch-decision writes - #2132

Merged
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage
May 28, 2026
Merged

[core] Extend OCC fence to all branch-decision writes#2132
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Layered on top of #2113. Extends the OCC fence — which currently covers only wait_completed writes from the elapsed-wait scan — to every other write whose outcome depends on a branch decision the workflow VM made from its loaded event log:

Write siteStatus
wait_completed (elapsed-wait scan)already fenced by #2113
step_creatednow fenced
wait_creatednow fenced
hook_creatednow fenced
hook_disposednow fenced
run_completednow fenced
run_failednow fenced
hook_receiveddeliberately not fenced (preserved from #2113)

hook_received stays unfenced for the same reason it's unfenced in #2113: fencing the user's signal would drop it on contention. Stale-snapshot protection belongs on the writes that consume hooks, not the writes that deliver them.

Why

Under concurrent replay (two invocations of the same run with overlapping but non-identical event-log snapshots), the SDK's deterministic ULID factory can allocate the same correlationId for different step calls depending on which side of a Promise.race each replay sees as the winner. Both invocations then try to write step_created for the same correlationId with different stepNames. The server's entity idempotency rejects the loser at the entity level — but if the winner was the stale-view invocation, its branch decision is the one that lands in the log, and future replays will see a step_created they don't expect.

Fencing the write makes the stale-view invocation lose the CAS, retry against the freshly-loaded tail, observe the events its snapshot was missing, and arrive at the same branch decision the authoritative invocation made.

Implementation

The retry loop from #2113's elapsed-wait scan is extracted into packages/core/src/runtime/fenced-write.ts so the six new fenced sites can share it without copy-paste. Each call site provides its own onConflictRefresh that runs an idempotency check against the reloaded log (e.g. "is this wait_created already in the log? → abort instead of retrying").

handleSuspension receives the load-time tail eventId + cursor from the runtime, and runs all four of its fenced writes (step_created, hook_created, hook_disposed, wait_created) against that fence. Each successful write advances the fence so chained writes in the same suspension don't conflict with each other.

The terminal run_completed / run_failed writes in runtime.ts use the same helper, with the idempotency check verifying no other terminal event has landed since the snapshot.

Test plan

  • All 1014 @workflow/core unit tests pass.
  • TypeScript clean (0 errors).
  • End-to-end stress: re-ran the same 40-attempt hammer that exposed the underlying race against the matching workflow-server preview — 0 / 40 failures (vs ~2 / 40 baseline on stable, and 5 / 40 against an alternative server-side fix that rewrites event IDs but doesn't fence). Verified each successful run has run.lastKnownEventId materialized as expected.

Temporary diagnostic instrumentation for investigating intermittent
CorruptedEventLogError 'step consumer mismatch' failures.
Emits console.log lines tagged 'WF_TRACE' at four points:
- runWorkflow start: dumps the full event array the replay will consume
(eventIds, types, correlationIds, stepNames) plus a sha256 digest
- step/hook/sleep subscribe: per-replay correlationId -> name assignment
- step consumer mismatch: structured record of the failure including the
event index in the SDK's view of the log
- runWorkflow end: completed | failed | suspended
Used to diff successive replays of the same runId and confirm whether
the SDK actually sees the same event array each time.
Peter's PR #2113 fences `wait_completed` writes from the elapsed-wait
scan. This commit extends the fence to every other write whose outcome
depends on a branch decision the workflow VM made from its loaded event
log — per the table @VaguelySerious himself laid out in his PR comment:
suspension-handler.ts:
- step_created (the smoking gun on wrun_01KSPS7XEGHF4A6WYF4DB03D40)
- hook_created
- hook_disposed
- wait_created
runtime.ts terminal writes:
- run_completed
- run_failed
`hook_received` is deliberately NOT fenced (Peter's reasoning preserved
verbatim: fencing the user's signal would drop it on contention; stale-
snapshot protection belongs on the writes that consume hooks, not the
ones that deliver them).
The fence value is the load-time tail of the events array passed into
`runWorkflow`. `suspension-handler` receives the fence + cursor from
the runtime and reloads on conflict; the runtime's terminal writes read
the cursor directly.
The new `__fenced-write.ts` helper encapsulates the retry loop so we
don't have to copy/paste Peter's pattern six times. It's named with the
leading-underscore convention to flag it as throwaway diagnostic code,
matching `__debug-replay-trace.ts`.
@vercel

vercelBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a7efa5a

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

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

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

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

@TooTallNate
TooTallNate changed the base branch from stable to mainMay 28, 2026 08:29
Two changes both needed for the extended-fence test loop to actually
exercise the OCC code path on the server:
1. Hardcode WORKFLOW_SERVER_URL_OVERRIDE to
https://workflow-server-83nn57dvc.vercel.sh (preview deployment of
workflow-server PR 447, branch alias
workflow-server-git-peter-event-write-cas.vercel.sh). The previous
preview at workflow-server-7pxaxn4d4.vercel.sh was Pranay's monotonic-
append PR 456 \u2014 different fix, doesn't implement the CAS the SDK side
now sends.
2. Map HTTP 412 \u2192 EntityConflictError in the world-vercel error mapper.
workflow-server PR 447 returns 412 with a 'fence conflict' message
for EventLogFenceConflictError; the SDK's existing fence-retry loops
(Peter's wait_completed scan + the new ones in suspension-handler
and runtime terminal writes) match on /fence conflict/i against the
message of an EntityConflictError. Without this mapping the 412 falls
through to WorkflowWorldError and the regex match never fires.
@github-actions

github-actionsBot commented May 28, 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.029s (-31.8% 🟢)1.005s (~)0.976s101.00x
💻 LocalExpress0.030s (-31.6% 🟢)1.005s (~)0.975s101.03x
💻 LocalNext.js (Turbopack)0.042s1.004s0.962s101.43x
🐘 PostgresNext.js (Turbopack)0.046s1.009s0.963s101.56x
🐘 PostgresExpress0.046s (-20.5% 🟢)1.012s (~)0.966s101.57x
🐘 PostgresNitro0.050s (-47.3% 🟢)1.012s (-2.9%)0.962s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.233s (-0.8%)1.941s (-9.1% 🟢)1.707s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.072s (-5.2% 🟢)2.006s (~)0.934s101.00x
💻 LocalExpress1.073s (-4.6%)2.006s (~)0.933s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.009s (~)0.925s101.01x
🐘 PostgresNitro1.086s (-4.7%)2.008s (~)0.922s101.01x
💻 LocalNext.js (Turbopack)1.089s2.005s0.916s101.02x
🐘 PostgresNext.js (Turbopack)1.101s2.008s0.907s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.767s (-5.7% 🟢)3.654s (-4.0%)1.887s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.410s (-4.9%)11.023s (~)0.613s31.00x
💻 LocalExpress10.418s (-4.6%)11.023s (~)0.605s31.00x
🐘 PostgresExpress10.424s (-4.9%)11.019s (~)0.595s31.00x
🐘 PostgresNitro10.430s (-4.1%)11.014s (~)0.584s31.00x
💻 LocalNext.js (Turbopack)10.557s11.022s0.465s31.01x
🐘 PostgresNext.js (Turbopack)10.623s11.018s0.394s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.910s (-18.1% 🟢)15.954s (-20.3% 🟢)2.044s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.467s (-10.0% 🟢)14.027s (-6.7% 🟢)0.560s51.00x
💻 LocalNitro13.488s (-10.5% 🟢)14.027s (-12.5% 🟢)0.539s51.00x
🐘 PostgresExpress13.515s (-7.3% 🟢)14.018s (-6.7% 🟢)0.503s51.00x
🐘 PostgresNitro13.573s (-7.0% 🟢)14.023s (-6.7% 🟢)0.450s51.01x
💻 LocalNext.js (Turbopack)13.808s14.026s0.218s51.03x
🐘 PostgresNext.js (Turbopack)13.860s14.017s0.157s51.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.720s (-58.8% 🟢)22.449s (-57.3% 🟢)1.729s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.978s (-28.6% 🟢)12.273s (-27.9% 🟢)0.295s81.00x
💻 LocalExpress11.995s (-27.7% 🟢)12.398s (-27.2% 🟢)0.403s81.00x
🐘 PostgresExpress12.247s (-12.6% 🟢)12.876s (-11.8% 🟢)0.629s71.02x
🐘 PostgresNitro12.256s (-12.3% 🟢)13.019s (-9.0% 🟢)0.763s71.02x
💻 LocalNext.js (Turbopack)12.585s13.023s0.438s71.05x
🐘 PostgresNext.js (Turbopack)12.744s13.013s0.270s71.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.522s (-76.5% 🟢)30.389s (-75.4% 🟢)1.867s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.4% 🟢)2.007s (~)0.865s151.00x
🐘 PostgresNitro1.164s (-8.7% 🟢)2.007s (~)0.844s151.02x
💻 LocalExpress1.170s (-21.4% 🟢)2.006s (~)0.836s151.02x
💻 LocalNitro1.172s (-28.1% 🟢)2.006s (-3.3%)0.834s151.03x
🐘 PostgresNext.js (Turbopack)1.175s2.006s0.832s151.03x
💻 LocalNext.js (Turbopack)1.211s2.005s0.794s151.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express103.949s (+3534.8% 🔺)105.665s (+2185.6% 🔺)1.716s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.215s (-48.6% 🟢)2.007s (-33.3% 🟢)0.793s151.00x
🐘 PostgresNitro1.221s (-48.1% 🟢)2.008s (-33.3% 🟢)0.787s151.01x
🐘 PostgresNext.js (Turbopack)1.273s2.005s0.733s151.05x
💻 LocalNext.js (Turbopack)1.514s2.005s0.492s151.25x
💻 LocalNitro1.682s (-46.5% 🟢)2.006s (-48.4% 🟢)0.323s151.39x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.300s151.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.692s (+2.0%)5.373s (+5.1% 🔺)1.681s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.338s (-61.6% 🟢)2.007s (-50.0% 🟢)0.669s151.00x
🐘 PostgresNitro1.353s (-61.1% 🟢)2.007s (-49.9% 🟢)0.654s151.01x
🐘 PostgresNext.js (Turbopack)1.479s2.006s0.528s151.10x
💻 LocalNext.js (Turbopack)3.944s4.295s0.350s72.95x
💻 LocalNitro4.618s (-44.7% 🟢)5.010s (-44.5% 🟢)0.392s63.45x
💻 LocalExpress4.704s (-43.6% 🟢)5.180s (-42.6% 🟢)0.476s63.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.082s (+19.9% 🔺)7.070s (+15.4% 🔺)1.987s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.151s (-8.4% 🟢)2.007s (~)0.856s151.00x
🐘 PostgresNitro1.162s (-7.5% 🟢)2.010s (~)0.848s151.01x
🐘 PostgresNext.js (Turbopack)1.170s2.007s0.837s151.02x
💻 LocalNext.js (Turbopack)1.265s2.005s0.740s151.10x
💻 LocalExpress1.443s (-23.8% 🟢)2.006s (-15.1% 🟢)0.563s151.25x
💻 LocalNitro1.745s (-6.5% 🟢)2.392s (+2.2%)0.646s131.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.525s (-2.2%)4.081s (-6.2% 🟢)1.556s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.234s (-47.3% 🟢)2.008s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNitro1.235s (-47.2% 🟢)2.009s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNext.js (Turbopack)1.273s2.009s0.736s151.03x
💻 LocalNext.js (Turbopack)1.812s2.072s0.260s151.47x
💻 LocalNitro1.869s (-39.0% 🟢)2.316s (-40.4% 🟢)0.447s131.51x
💻 LocalExpress1.913s (-38.9% 🟢)2.316s (-38.5% 🟢)0.402s131.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.712s (+16.3% 🔺)5.226s (+9.0% 🔺)1.514s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.351s (-61.4% 🟢)2.008s (-49.9% 🟢)0.657s151.00x
🐘 PostgresNitro1.376s (-60.5% 🟢)2.009s (-49.9% 🟢)0.633s151.02x
🐘 PostgresNext.js (Turbopack)1.477s2.006s0.529s151.09x
💻 LocalNitro4.542s (-50.3% 🟢)5.179s (-48.3% 🟢)0.637s63.36x
💻 LocalNext.js (Turbopack)4.602s5.011s0.410s73.41x
💻 LocalExpress6.036s (-31.4% 🟢)6.416s (-30.8% 🟢)0.380s54.47x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.559s (+2.2%)8.517s (+4.1%)1.959s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.427s (-49.1% 🟢)1.007s (-1.6%)0.580s601.00x
🐘 PostgresNitro0.486s (-40.7% 🟢)1.007s (~)0.521s601.14x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.7% 🟢)0.526s591.16x
💻 LocalExpress0.512s (-48.0% 🟢)1.021s (-5.1% 🟢)0.509s591.20x
🐘 PostgresNext.js (Turbopack)0.546s1.005s0.460s601.28x
💻 LocalNext.js (Turbopack)0.639s1.004s0.365s601.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.726s (-69.9% 🟢)7.306s (-65.7% 🟢)1.580s91.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.156s (-41.5% 🟢)1.903s (-15.7% 🟢)0.747s481.00x
💻 LocalNitro1.221s (-59.8% 🟢)2.005s (-46.6% 🟢)0.784s451.06x
🐘 PostgresNitro1.222s (-36.6% 🟢)2.030s (-3.4%)0.808s451.06x
💻 LocalExpress1.272s (-57.8% 🟢)2.006s (-44.1% 🟢)0.734s451.10x
🐘 PostgresNext.js (Turbopack)1.342s2.006s0.664s451.16x
💻 LocalNext.js (Turbopack)1.617s2.027s0.410s451.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.876s (-62.7% 🟢)14.564s (-60.4% 🟢)1.688s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.181s (-45.3% 🟢)2.984s (-31.7% 🟢)0.803s411.00x
🐘 PostgresNitro2.333s (-43.1% 🟢)3.060s (-33.5% 🟢)0.726s401.07x
🐘 PostgresNext.js (Turbopack)2.651s3.007s0.356s401.22x
💻 LocalNitro2.771s (-70.2% 🟢)3.032s (-69.7% 🟢)0.261s401.27x
💻 LocalExpress2.878s (-68.7% 🟢)3.136s (-68.7% 🟢)0.257s391.32x
💻 LocalNext.js (Turbopack)3.407s4.008s0.600s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.597s (-80.3% 🟢)27.381s (-79.3% 🟢)1.784s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.175s (-38.2% 🟢)1.006s (~)0.831s601.00x
🐘 PostgresNext.js (Turbopack)0.184s1.005s0.821s601.05x
🐘 PostgresNitro0.200s (-29.4% 🟢)1.006s (~)0.806s601.14x
💻 LocalExpress0.386s (-31.2% 🟢)1.004s (~)0.618s602.21x
💻 LocalNitro0.386s (-36.2% 🟢)1.004s (-1.7%)0.618s602.21x
💻 LocalNext.js (Turbopack)0.474s1.004s0.529s602.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.161s (+10.6% 🔺)3.810s (+4.8%)1.649s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.298s (-41.5% 🟢)1.006s (~)0.708s901.00x
🐘 PostgresNitro0.317s (-36.1% 🟢)1.006s (~)0.689s901.06x
🐘 PostgresNext.js (Turbopack)0.341s1.005s0.664s901.14x
💻 LocalNext.js (Turbopack)2.026s2.536s0.510s366.80x
💻 LocalNitro2.118s (-16.6% 🟢)2.610s (-13.3% 🟢)0.492s357.10x
💻 LocalExpress2.170s (-13.7% 🟢)2.822s (-6.2% 🟢)0.652s327.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.148s (+36.1% 🔺)5.654s (+17.6% 🔺)1.507s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.652s (-20.3% 🟢)1.006s (-1.1%)0.354s1201.00x
🐘 PostgresNext.js (Turbopack)0.681s1.005s0.324s1201.04x
🐘 PostgresNitro0.688s (-13.0% 🟢)1.006s (~)0.319s1201.05x
💻 LocalNext.js (Turbopack)9.197s9.718s0.521s1314.10x
💻 LocalNitro9.393s (-16.1% 🟢)10.024s (-14.1% 🟢)0.632s1214.40x
💻 LocalExpress10.171s (-9.1% 🟢)10.695s (-10.4% 🟢)0.524s1215.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.240s (+78.4% 🔺)15.181s (+64.2% 🔺)1.942s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+467.9% 🔺)2.005s (+99.6% 🔺)0.010s (-14.9% 🟢)2.017s (+98.2% 🔺)0.887s101.00x
💻 LocalNitro1.136s (+431.6% 🔺)2.004s (+99.5% 🔺)0.010s (-18.4% 🟢)2.017s (+98.0% 🔺)0.881s101.00x
🐘 PostgresExpress1.137s (+454.2% 🔺)2.001s (+100.3% 🔺)0.001s (-25.0% 🟢)2.011s (+98.8% 🔺)0.874s101.01x
💻 LocalNext.js (Turbopack)1.147s2.004s0.008s2.015s0.868s101.01x
🐘 PostgresNext.js (Turbopack)1.155s2.001s0.001s2.008s0.852s101.02x
🐘 PostgresNitro1.164s (+467.6% 🔺)2.000s (+100.0% 🔺)0.002s (+13.3% 🔺)2.013s (+99.0% 🔺)0.849s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.134s (-14.8% 🟢)3.175s (-22.4% 🟢)2.057s (+114.0% 🔺)5.682s (+1.6%)3.549s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.506s (+139.1% 🔺)2.003s (+99.0% 🔺)0.004s (+5.3% 🔺)2.024s (+97.9% 🔺)0.518s301.00x
💻 LocalNitro1.518s (+81.0% 🔺)2.012s (+98.8% 🔺)0.009s (-0.8%)2.023s (+81.3% 🔺)0.506s301.01x
💻 LocalExpress1.520s (+100.8% 🔺)2.012s (+95.5% 🔺)0.009s (-0.9%)2.023s (+94.5% 🔺)0.503s301.01x
💻 LocalNext.js (Turbopack)1.546s2.008s0.009s2.020s0.474s301.03x
🐘 PostgresNitro1.591s (+154.9% 🔺)2.007s (+99.3% 🔺)0.004s (-3.3%)2.026s (+98.2% 🔺)0.436s301.06x
🐘 PostgresNext.js (Turbopack)1.632s2.041s0.003s2.053s0.421s301.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.080s (-6.5% 🟢)7.362s (-8.1% 🟢)0.403s (-1.4%)8.205s (-7.1% 🟢)2.125s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.645s (-32.9% 🟢)1.016s (-20.5% 🟢)0.000s (-61.0% 🟢)1.029s (-21.2% 🟢)0.384s591.00x
🐘 PostgresNitro0.669s (-31.0% 🟢)1.033s (-17.2% 🟢)0.000s (+65.5% 🔺)1.048s (-16.7% 🟢)0.379s581.04x
🐘 PostgresNext.js (Turbopack)0.878s1.088s0.000s1.186s0.308s511.36x
💻 LocalNext.js (Turbopack)1.198s1.860s0.000s1.863s0.665s331.86x
💻 LocalNitro1.353s (+10.7% 🔺)2.016s (~)0.000s (+33.3% 🔺)2.018s (~)0.664s302.10x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.649s302.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.631s (-2.9%)4.743s (-7.0% 🟢)0.000s (-100.0% 🟢)5.186s (-6.2% 🟢)1.555s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.282s (-28.5% 🟢)1.996s (-6.8% 🟢)0.000s (+86.7% 🔺)2.048s (-5.8% 🟢)0.766s301.00x
🐘 PostgresExpress1.312s (-26.0% 🟢)2.031s (-6.7% 🟢)0.000s (+Infinity% 🔺)2.054s (-6.6% 🟢)0.742s301.02x
🐘 PostgresNext.js (Turbopack)1.673s2.401s0.000s2.435s0.762s251.30x
💻 LocalNext.js (Turbopack)2.491s3.122s0.000s3.129s0.637s201.94x
💻 LocalNitro3.140s (-7.3% 🟢)3.965s (-1.7%)0.000s (-18.0% 🟢)3.968s (-1.7%)0.828s162.45x
💻 LocalExpress3.291s (-5.1% 🟢)4.026s (~)0.000s (-41.7% 🟢)4.031s (~)0.740s152.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.296s (+15.5% 🔺)6.741s (+11.9% 🔺)0.001s (+Infinity% 🔺)7.181s (+11.2% 🔺)1.884s91.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress19/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@github-actions

github-actionsBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production122202191441
✅ 💻 Local Development161502191834
✅ 📦 Local Production161502191834
✅ 🐘 Local Postgres161502191834
✅ 🪟 Windows13100131
✅ 📋 Other7410176917
Total6939010527991

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro105026
✅ example105026
✅ express105026
✅ fastify105026
✅ hono105026
✅ nextjs-turbopack12902
✅ nextjs-webpack12902
✅ nitro105026
✅ nuxt105026
✅ sveltekit12407
✅ vite105026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13100
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable106025
✅ e2e-local-dev-tanstack-start-106025
✅ e2e-local-postgres-nest-stable106025
✅ e2e-local-postgres-tanstack-start-106025
✅ e2e-local-prod-nest-stable106025
✅ e2e-local-prod-tanstack-start-106025
✅ e2e-vercel-prod-tanstack-start105026

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's merge into upstream branch? and then review as one

Strip the WF_TRACE replay tracing that was used to diagnose the
CORRUPTED_EVENT_LOG race \u2014 it's served its purpose now that the fix
is in. Specifically:
- Delete packages/core/src/__debug-replay-trace.ts and its 8 call sites
in workflow.ts, step.ts, workflow/hook.ts, workflow/sleep.ts.
- Drop the matching [DEBUG] inline narrative comments at each call site.
- Rename packages/core/src/runtime/__fenced-write.ts \u2192 fenced-write.ts
(the leading-underscore convention marked it as throwaway diagnostic
code; the helper is intended to stay).
- Trim the file header on fenced-write.ts and the related narrative
comment in suspension-handler.ts to drop the failing-runId / PR-number
references that only made sense in the debug context.
No behavioral change. typecheck clean (0 errors); 1014/1014 unit tests
pass (same as parent commit 77f057a).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Extends optimistic concurrency control (OCC) “fencing” to additional workflow event writes whose correctness depends on branch decisions made from a potentially stale event-log snapshot, and adjusts step dispatch behavior to avoid duplicate execution under concurrent replay.

Changes:

  • Introduce a shared fencedEventCreate helper to retry CAS-fenced writes with refresh + idempotency checks.
  • Apply fencing to step_created / wait_created / hook create+dispose paths in handleSuspension, and to terminal run_completed / run_failed writes in runtime.ts.
  • Improve local queue idempotency by retaining completed idempotency keys (plus tests/docs updates).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsMap HTTP 412 OCC fence conflicts into EntityConflictError for downstream handling.
packages/world-testing/src/inline-batches-debug.mtsTighten debug assertions for race/skip counters.
packages/world-local/src/queue.tsAdd a bounded cache of completed idempotency keys to prevent post-completion duplicate dispatch.
packages/world-local/src/queue.test.tsAdd coverage for post-completion idempotency dedupe behavior.
packages/core/src/workflow/hook.tsMinor formatting-only change.
packages/core/src/step.tsMinor formatting-only change.
packages/core/src/runtime/suspension-handler.tsFence branch-decision writes and serialize them to avoid self-conflicts; add refresh/idempotency logic.
packages/core/src/runtime/suspension-handler.test.tsNew test ensuring the fence is chained across writes within a suspension.
packages/core/src/runtime/fenced-write.tsNew shared helper implementing fence-conflict retries with backoff and caller-provided refresh/idempotency checks.
packages/core/src/runtime.tsFence terminal writes; owner-scope step queueing when inline execution is possible; crash-recovery exception for redelivery.
docs/content/docs/v5/changelog/eager-processing.mdxUpdate documentation to reflect owner-scoped queueing semantics.
docs/content/docs/v4/changelog/eager-processing.mdxSame as v5 doc update for v4 docs.
.changeset/quick-local-queues.mdChangeset for local queue idempotency behavior.
.changeset/chilly-fences-chain.mdChangeset for core fencing + dispatch changes.

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

Comment threadpackages/world-vercel/src/utils.ts
Address Copilot review on PR 2132 (#2132 (comment)).
The fence-retry loop in runtime/fenced-write.ts detects OCC conflicts
via /fence conflict/i.test(err.message). The 412 branch was relying on
the server's JSON body to populate that message via errorData.message,
but parseResponseBody().catch(() => ({})) swallows JSON parse failures
silently — so any non-JSON 412 response (CDN HTML, gateway timeout
page, intermediate proxy error) would surface as
EntityConflictError("<METHOD> /endpoint -> HTTP 412: Precondition
Failed"), the regex would miss it, and the retry loop would
mis-classify the conflict as terminal.
Prefix the message with `fence conflict:` whenever the parsed body
didn't already carry the marker, so the retry detection is robust to
response-body parse failures.
Tests: world-vercel 69/69 pass.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@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('^' + ".*" + ' [core] Extend OCC fence to all branch-decision writes by TooTallNate · Pull Request #2132 · vercel/workflow · GitHub
Skip to content

[core] Extend OCC fence to all branch-decision writes - #2132

Merged
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage
May 28, 2026
Merged

[core] Extend OCC fence to all branch-decision writes#2132
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Layered on top of #2113. Extends the OCC fence — which currently covers only wait_completed writes from the elapsed-wait scan — to every other write whose outcome depends on a branch decision the workflow VM made from its loaded event log:

Write siteStatus
wait_completed (elapsed-wait scan)already fenced by #2113
step_creatednow fenced
wait_creatednow fenced
hook_creatednow fenced
hook_disposednow fenced
run_completednow fenced
run_failednow fenced
hook_receiveddeliberately not fenced (preserved from #2113)

hook_received stays unfenced for the same reason it's unfenced in #2113: fencing the user's signal would drop it on contention. Stale-snapshot protection belongs on the writes that consume hooks, not the writes that deliver them.

Why

Under concurrent replay (two invocations of the same run with overlapping but non-identical event-log snapshots), the SDK's deterministic ULID factory can allocate the same correlationId for different step calls depending on which side of a Promise.race each replay sees as the winner. Both invocations then try to write step_created for the same correlationId with different stepNames. The server's entity idempotency rejects the loser at the entity level — but if the winner was the stale-view invocation, its branch decision is the one that lands in the log, and future replays will see a step_created they don't expect.

Fencing the write makes the stale-view invocation lose the CAS, retry against the freshly-loaded tail, observe the events its snapshot was missing, and arrive at the same branch decision the authoritative invocation made.

Implementation

The retry loop from #2113's elapsed-wait scan is extracted into packages/core/src/runtime/fenced-write.ts so the six new fenced sites can share it without copy-paste. Each call site provides its own onConflictRefresh that runs an idempotency check against the reloaded log (e.g. "is this wait_created already in the log? → abort instead of retrying").

handleSuspension receives the load-time tail eventId + cursor from the runtime, and runs all four of its fenced writes (step_created, hook_created, hook_disposed, wait_created) against that fence. Each successful write advances the fence so chained writes in the same suspension don't conflict with each other.

The terminal run_completed / run_failed writes in runtime.ts use the same helper, with the idempotency check verifying no other terminal event has landed since the snapshot.

Test plan

  • All 1014 @workflow/core unit tests pass.
  • TypeScript clean (0 errors).
  • End-to-end stress: re-ran the same 40-attempt hammer that exposed the underlying race against the matching workflow-server preview — 0 / 40 failures (vs ~2 / 40 baseline on stable, and 5 / 40 against an alternative server-side fix that rewrites event IDs but doesn't fence). Verified each successful run has run.lastKnownEventId materialized as expected.

Temporary diagnostic instrumentation for investigating intermittent
CorruptedEventLogError 'step consumer mismatch' failures.
Emits console.log lines tagged 'WF_TRACE' at four points:
- runWorkflow start: dumps the full event array the replay will consume
(eventIds, types, correlationIds, stepNames) plus a sha256 digest
- step/hook/sleep subscribe: per-replay correlationId -> name assignment
- step consumer mismatch: structured record of the failure including the
event index in the SDK's view of the log
- runWorkflow end: completed | failed | suspended
Used to diff successive replays of the same runId and confirm whether
the SDK actually sees the same event array each time.
Peter's PR #2113 fences `wait_completed` writes from the elapsed-wait
scan. This commit extends the fence to every other write whose outcome
depends on a branch decision the workflow VM made from its loaded event
log — per the table @VaguelySerious himself laid out in his PR comment:
suspension-handler.ts:
- step_created (the smoking gun on wrun_01KSPS7XEGHF4A6WYF4DB03D40)
- hook_created
- hook_disposed
- wait_created
runtime.ts terminal writes:
- run_completed
- run_failed
`hook_received` is deliberately NOT fenced (Peter's reasoning preserved
verbatim: fencing the user's signal would drop it on contention; stale-
snapshot protection belongs on the writes that consume hooks, not the
ones that deliver them).
The fence value is the load-time tail of the events array passed into
`runWorkflow`. `suspension-handler` receives the fence + cursor from
the runtime and reloads on conflict; the runtime's terminal writes read
the cursor directly.
The new `__fenced-write.ts` helper encapsulates the retry loop so we
don't have to copy/paste Peter's pattern six times. It's named with the
leading-underscore convention to flag it as throwaway diagnostic code,
matching `__debug-replay-trace.ts`.
@vercel

vercelBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a7efa5a

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

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

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

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

@TooTallNate
TooTallNate changed the base branch from stable to mainMay 28, 2026 08:29
Two changes both needed for the extended-fence test loop to actually
exercise the OCC code path on the server:
1. Hardcode WORKFLOW_SERVER_URL_OVERRIDE to
https://workflow-server-83nn57dvc.vercel.sh (preview deployment of
workflow-server PR 447, branch alias
workflow-server-git-peter-event-write-cas.vercel.sh). The previous
preview at workflow-server-7pxaxn4d4.vercel.sh was Pranay's monotonic-
append PR 456 \u2014 different fix, doesn't implement the CAS the SDK side
now sends.
2. Map HTTP 412 \u2192 EntityConflictError in the world-vercel error mapper.
workflow-server PR 447 returns 412 with a 'fence conflict' message
for EventLogFenceConflictError; the SDK's existing fence-retry loops
(Peter's wait_completed scan + the new ones in suspension-handler
and runtime terminal writes) match on /fence conflict/i against the
message of an EntityConflictError. Without this mapping the 412 falls
through to WorkflowWorldError and the regex match never fires.
@github-actions

github-actionsBot commented May 28, 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.029s (-31.8% 🟢)1.005s (~)0.976s101.00x
💻 LocalExpress0.030s (-31.6% 🟢)1.005s (~)0.975s101.03x
💻 LocalNext.js (Turbopack)0.042s1.004s0.962s101.43x
🐘 PostgresNext.js (Turbopack)0.046s1.009s0.963s101.56x
🐘 PostgresExpress0.046s (-20.5% 🟢)1.012s (~)0.966s101.57x
🐘 PostgresNitro0.050s (-47.3% 🟢)1.012s (-2.9%)0.962s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.233s (-0.8%)1.941s (-9.1% 🟢)1.707s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.072s (-5.2% 🟢)2.006s (~)0.934s101.00x
💻 LocalExpress1.073s (-4.6%)2.006s (~)0.933s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.009s (~)0.925s101.01x
🐘 PostgresNitro1.086s (-4.7%)2.008s (~)0.922s101.01x
💻 LocalNext.js (Turbopack)1.089s2.005s0.916s101.02x
🐘 PostgresNext.js (Turbopack)1.101s2.008s0.907s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.767s (-5.7% 🟢)3.654s (-4.0%)1.887s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.410s (-4.9%)11.023s (~)0.613s31.00x
💻 LocalExpress10.418s (-4.6%)11.023s (~)0.605s31.00x
🐘 PostgresExpress10.424s (-4.9%)11.019s (~)0.595s31.00x
🐘 PostgresNitro10.430s (-4.1%)11.014s (~)0.584s31.00x
💻 LocalNext.js (Turbopack)10.557s11.022s0.465s31.01x
🐘 PostgresNext.js (Turbopack)10.623s11.018s0.394s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.910s (-18.1% 🟢)15.954s (-20.3% 🟢)2.044s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.467s (-10.0% 🟢)14.027s (-6.7% 🟢)0.560s51.00x
💻 LocalNitro13.488s (-10.5% 🟢)14.027s (-12.5% 🟢)0.539s51.00x
🐘 PostgresExpress13.515s (-7.3% 🟢)14.018s (-6.7% 🟢)0.503s51.00x
🐘 PostgresNitro13.573s (-7.0% 🟢)14.023s (-6.7% 🟢)0.450s51.01x
💻 LocalNext.js (Turbopack)13.808s14.026s0.218s51.03x
🐘 PostgresNext.js (Turbopack)13.860s14.017s0.157s51.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.720s (-58.8% 🟢)22.449s (-57.3% 🟢)1.729s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.978s (-28.6% 🟢)12.273s (-27.9% 🟢)0.295s81.00x
💻 LocalExpress11.995s (-27.7% 🟢)12.398s (-27.2% 🟢)0.403s81.00x
🐘 PostgresExpress12.247s (-12.6% 🟢)12.876s (-11.8% 🟢)0.629s71.02x
🐘 PostgresNitro12.256s (-12.3% 🟢)13.019s (-9.0% 🟢)0.763s71.02x
💻 LocalNext.js (Turbopack)12.585s13.023s0.438s71.05x
🐘 PostgresNext.js (Turbopack)12.744s13.013s0.270s71.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.522s (-76.5% 🟢)30.389s (-75.4% 🟢)1.867s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.4% 🟢)2.007s (~)0.865s151.00x
🐘 PostgresNitro1.164s (-8.7% 🟢)2.007s (~)0.844s151.02x
💻 LocalExpress1.170s (-21.4% 🟢)2.006s (~)0.836s151.02x
💻 LocalNitro1.172s (-28.1% 🟢)2.006s (-3.3%)0.834s151.03x
🐘 PostgresNext.js (Turbopack)1.175s2.006s0.832s151.03x
💻 LocalNext.js (Turbopack)1.211s2.005s0.794s151.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express103.949s (+3534.8% 🔺)105.665s (+2185.6% 🔺)1.716s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.215s (-48.6% 🟢)2.007s (-33.3% 🟢)0.793s151.00x
🐘 PostgresNitro1.221s (-48.1% 🟢)2.008s (-33.3% 🟢)0.787s151.01x
🐘 PostgresNext.js (Turbopack)1.273s2.005s0.733s151.05x
💻 LocalNext.js (Turbopack)1.514s2.005s0.492s151.25x
💻 LocalNitro1.682s (-46.5% 🟢)2.006s (-48.4% 🟢)0.323s151.39x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.300s151.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.692s (+2.0%)5.373s (+5.1% 🔺)1.681s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.338s (-61.6% 🟢)2.007s (-50.0% 🟢)0.669s151.00x
🐘 PostgresNitro1.353s (-61.1% 🟢)2.007s (-49.9% 🟢)0.654s151.01x
🐘 PostgresNext.js (Turbopack)1.479s2.006s0.528s151.10x
💻 LocalNext.js (Turbopack)3.944s4.295s0.350s72.95x
💻 LocalNitro4.618s (-44.7% 🟢)5.010s (-44.5% 🟢)0.392s63.45x
💻 LocalExpress4.704s (-43.6% 🟢)5.180s (-42.6% 🟢)0.476s63.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.082s (+19.9% 🔺)7.070s (+15.4% 🔺)1.987s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.151s (-8.4% 🟢)2.007s (~)0.856s151.00x
🐘 PostgresNitro1.162s (-7.5% 🟢)2.010s (~)0.848s151.01x
🐘 PostgresNext.js (Turbopack)1.170s2.007s0.837s151.02x
💻 LocalNext.js (Turbopack)1.265s2.005s0.740s151.10x
💻 LocalExpress1.443s (-23.8% 🟢)2.006s (-15.1% 🟢)0.563s151.25x
💻 LocalNitro1.745s (-6.5% 🟢)2.392s (+2.2%)0.646s131.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.525s (-2.2%)4.081s (-6.2% 🟢)1.556s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.234s (-47.3% 🟢)2.008s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNitro1.235s (-47.2% 🟢)2.009s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNext.js (Turbopack)1.273s2.009s0.736s151.03x
💻 LocalNext.js (Turbopack)1.812s2.072s0.260s151.47x
💻 LocalNitro1.869s (-39.0% 🟢)2.316s (-40.4% 🟢)0.447s131.51x
💻 LocalExpress1.913s (-38.9% 🟢)2.316s (-38.5% 🟢)0.402s131.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.712s (+16.3% 🔺)5.226s (+9.0% 🔺)1.514s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.351s (-61.4% 🟢)2.008s (-49.9% 🟢)0.657s151.00x
🐘 PostgresNitro1.376s (-60.5% 🟢)2.009s (-49.9% 🟢)0.633s151.02x
🐘 PostgresNext.js (Turbopack)1.477s2.006s0.529s151.09x
💻 LocalNitro4.542s (-50.3% 🟢)5.179s (-48.3% 🟢)0.637s63.36x
💻 LocalNext.js (Turbopack)4.602s5.011s0.410s73.41x
💻 LocalExpress6.036s (-31.4% 🟢)6.416s (-30.8% 🟢)0.380s54.47x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.559s (+2.2%)8.517s (+4.1%)1.959s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.427s (-49.1% 🟢)1.007s (-1.6%)0.580s601.00x
🐘 PostgresNitro0.486s (-40.7% 🟢)1.007s (~)0.521s601.14x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.7% 🟢)0.526s591.16x
💻 LocalExpress0.512s (-48.0% 🟢)1.021s (-5.1% 🟢)0.509s591.20x
🐘 PostgresNext.js (Turbopack)0.546s1.005s0.460s601.28x
💻 LocalNext.js (Turbopack)0.639s1.004s0.365s601.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.726s (-69.9% 🟢)7.306s (-65.7% 🟢)1.580s91.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.156s (-41.5% 🟢)1.903s (-15.7% 🟢)0.747s481.00x
💻 LocalNitro1.221s (-59.8% 🟢)2.005s (-46.6% 🟢)0.784s451.06x
🐘 PostgresNitro1.222s (-36.6% 🟢)2.030s (-3.4%)0.808s451.06x
💻 LocalExpress1.272s (-57.8% 🟢)2.006s (-44.1% 🟢)0.734s451.10x
🐘 PostgresNext.js (Turbopack)1.342s2.006s0.664s451.16x
💻 LocalNext.js (Turbopack)1.617s2.027s0.410s451.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.876s (-62.7% 🟢)14.564s (-60.4% 🟢)1.688s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.181s (-45.3% 🟢)2.984s (-31.7% 🟢)0.803s411.00x
🐘 PostgresNitro2.333s (-43.1% 🟢)3.060s (-33.5% 🟢)0.726s401.07x
🐘 PostgresNext.js (Turbopack)2.651s3.007s0.356s401.22x
💻 LocalNitro2.771s (-70.2% 🟢)3.032s (-69.7% 🟢)0.261s401.27x
💻 LocalExpress2.878s (-68.7% 🟢)3.136s (-68.7% 🟢)0.257s391.32x
💻 LocalNext.js (Turbopack)3.407s4.008s0.600s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.597s (-80.3% 🟢)27.381s (-79.3% 🟢)1.784s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.175s (-38.2% 🟢)1.006s (~)0.831s601.00x
🐘 PostgresNext.js (Turbopack)0.184s1.005s0.821s601.05x
🐘 PostgresNitro0.200s (-29.4% 🟢)1.006s (~)0.806s601.14x
💻 LocalExpress0.386s (-31.2% 🟢)1.004s (~)0.618s602.21x
💻 LocalNitro0.386s (-36.2% 🟢)1.004s (-1.7%)0.618s602.21x
💻 LocalNext.js (Turbopack)0.474s1.004s0.529s602.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.161s (+10.6% 🔺)3.810s (+4.8%)1.649s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.298s (-41.5% 🟢)1.006s (~)0.708s901.00x
🐘 PostgresNitro0.317s (-36.1% 🟢)1.006s (~)0.689s901.06x
🐘 PostgresNext.js (Turbopack)0.341s1.005s0.664s901.14x
💻 LocalNext.js (Turbopack)2.026s2.536s0.510s366.80x
💻 LocalNitro2.118s (-16.6% 🟢)2.610s (-13.3% 🟢)0.492s357.10x
💻 LocalExpress2.170s (-13.7% 🟢)2.822s (-6.2% 🟢)0.652s327.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.148s (+36.1% 🔺)5.654s (+17.6% 🔺)1.507s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.652s (-20.3% 🟢)1.006s (-1.1%)0.354s1201.00x
🐘 PostgresNext.js (Turbopack)0.681s1.005s0.324s1201.04x
🐘 PostgresNitro0.688s (-13.0% 🟢)1.006s (~)0.319s1201.05x
💻 LocalNext.js (Turbopack)9.197s9.718s0.521s1314.10x
💻 LocalNitro9.393s (-16.1% 🟢)10.024s (-14.1% 🟢)0.632s1214.40x
💻 LocalExpress10.171s (-9.1% 🟢)10.695s (-10.4% 🟢)0.524s1215.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.240s (+78.4% 🔺)15.181s (+64.2% 🔺)1.942s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+467.9% 🔺)2.005s (+99.6% 🔺)0.010s (-14.9% 🟢)2.017s (+98.2% 🔺)0.887s101.00x
💻 LocalNitro1.136s (+431.6% 🔺)2.004s (+99.5% 🔺)0.010s (-18.4% 🟢)2.017s (+98.0% 🔺)0.881s101.00x
🐘 PostgresExpress1.137s (+454.2% 🔺)2.001s (+100.3% 🔺)0.001s (-25.0% 🟢)2.011s (+98.8% 🔺)0.874s101.01x
💻 LocalNext.js (Turbopack)1.147s2.004s0.008s2.015s0.868s101.01x
🐘 PostgresNext.js (Turbopack)1.155s2.001s0.001s2.008s0.852s101.02x
🐘 PostgresNitro1.164s (+467.6% 🔺)2.000s (+100.0% 🔺)0.002s (+13.3% 🔺)2.013s (+99.0% 🔺)0.849s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.134s (-14.8% 🟢)3.175s (-22.4% 🟢)2.057s (+114.0% 🔺)5.682s (+1.6%)3.549s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.506s (+139.1% 🔺)2.003s (+99.0% 🔺)0.004s (+5.3% 🔺)2.024s (+97.9% 🔺)0.518s301.00x
💻 LocalNitro1.518s (+81.0% 🔺)2.012s (+98.8% 🔺)0.009s (-0.8%)2.023s (+81.3% 🔺)0.506s301.01x
💻 LocalExpress1.520s (+100.8% 🔺)2.012s (+95.5% 🔺)0.009s (-0.9%)2.023s (+94.5% 🔺)0.503s301.01x
💻 LocalNext.js (Turbopack)1.546s2.008s0.009s2.020s0.474s301.03x
🐘 PostgresNitro1.591s (+154.9% 🔺)2.007s (+99.3% 🔺)0.004s (-3.3%)2.026s (+98.2% 🔺)0.436s301.06x
🐘 PostgresNext.js (Turbopack)1.632s2.041s0.003s2.053s0.421s301.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.080s (-6.5% 🟢)7.362s (-8.1% 🟢)0.403s (-1.4%)8.205s (-7.1% 🟢)2.125s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.645s (-32.9% 🟢)1.016s (-20.5% 🟢)0.000s (-61.0% 🟢)1.029s (-21.2% 🟢)0.384s591.00x
🐘 PostgresNitro0.669s (-31.0% 🟢)1.033s (-17.2% 🟢)0.000s (+65.5% 🔺)1.048s (-16.7% 🟢)0.379s581.04x
🐘 PostgresNext.js (Turbopack)0.878s1.088s0.000s1.186s0.308s511.36x
💻 LocalNext.js (Turbopack)1.198s1.860s0.000s1.863s0.665s331.86x
💻 LocalNitro1.353s (+10.7% 🔺)2.016s (~)0.000s (+33.3% 🔺)2.018s (~)0.664s302.10x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.649s302.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.631s (-2.9%)4.743s (-7.0% 🟢)0.000s (-100.0% 🟢)5.186s (-6.2% 🟢)1.555s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.282s (-28.5% 🟢)1.996s (-6.8% 🟢)0.000s (+86.7% 🔺)2.048s (-5.8% 🟢)0.766s301.00x
🐘 PostgresExpress1.312s (-26.0% 🟢)2.031s (-6.7% 🟢)0.000s (+Infinity% 🔺)2.054s (-6.6% 🟢)0.742s301.02x
🐘 PostgresNext.js (Turbopack)1.673s2.401s0.000s2.435s0.762s251.30x
💻 LocalNext.js (Turbopack)2.491s3.122s0.000s3.129s0.637s201.94x
💻 LocalNitro3.140s (-7.3% 🟢)3.965s (-1.7%)0.000s (-18.0% 🟢)3.968s (-1.7%)0.828s162.45x
💻 LocalExpress3.291s (-5.1% 🟢)4.026s (~)0.000s (-41.7% 🟢)4.031s (~)0.740s152.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.296s (+15.5% 🔺)6.741s (+11.9% 🔺)0.001s (+Infinity% 🔺)7.181s (+11.2% 🔺)1.884s91.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress19/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@github-actions

github-actionsBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production122202191441
✅ 💻 Local Development161502191834
✅ 📦 Local Production161502191834
✅ 🐘 Local Postgres161502191834
✅ 🪟 Windows13100131
✅ 📋 Other7410176917
Total6939010527991

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro105026
✅ example105026
✅ express105026
✅ fastify105026
✅ hono105026
✅ nextjs-turbopack12902
✅ nextjs-webpack12902
✅ nitro105026
✅ nuxt105026
✅ sveltekit12407
✅ vite105026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13100
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable106025
✅ e2e-local-dev-tanstack-start-106025
✅ e2e-local-postgres-nest-stable106025
✅ e2e-local-postgres-tanstack-start-106025
✅ e2e-local-prod-nest-stable106025
✅ e2e-local-prod-tanstack-start-106025
✅ e2e-vercel-prod-tanstack-start105026

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's merge into upstream branch? and then review as one

Strip the WF_TRACE replay tracing that was used to diagnose the
CORRUPTED_EVENT_LOG race \u2014 it's served its purpose now that the fix
is in. Specifically:
- Delete packages/core/src/__debug-replay-trace.ts and its 8 call sites
in workflow.ts, step.ts, workflow/hook.ts, workflow/sleep.ts.
- Drop the matching [DEBUG] inline narrative comments at each call site.
- Rename packages/core/src/runtime/__fenced-write.ts \u2192 fenced-write.ts
(the leading-underscore convention marked it as throwaway diagnostic
code; the helper is intended to stay).
- Trim the file header on fenced-write.ts and the related narrative
comment in suspension-handler.ts to drop the failing-runId / PR-number
references that only made sense in the debug context.
No behavioral change. typecheck clean (0 errors); 1014/1014 unit tests
pass (same as parent commit 77f057a).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Extends optimistic concurrency control (OCC) “fencing” to additional workflow event writes whose correctness depends on branch decisions made from a potentially stale event-log snapshot, and adjusts step dispatch behavior to avoid duplicate execution under concurrent replay.

Changes:

  • Introduce a shared fencedEventCreate helper to retry CAS-fenced writes with refresh + idempotency checks.
  • Apply fencing to step_created / wait_created / hook create+dispose paths in handleSuspension, and to terminal run_completed / run_failed writes in runtime.ts.
  • Improve local queue idempotency by retaining completed idempotency keys (plus tests/docs updates).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsMap HTTP 412 OCC fence conflicts into EntityConflictError for downstream handling.
packages/world-testing/src/inline-batches-debug.mtsTighten debug assertions for race/skip counters.
packages/world-local/src/queue.tsAdd a bounded cache of completed idempotency keys to prevent post-completion duplicate dispatch.
packages/world-local/src/queue.test.tsAdd coverage for post-completion idempotency dedupe behavior.
packages/core/src/workflow/hook.tsMinor formatting-only change.
packages/core/src/step.tsMinor formatting-only change.
packages/core/src/runtime/suspension-handler.tsFence branch-decision writes and serialize them to avoid self-conflicts; add refresh/idempotency logic.
packages/core/src/runtime/suspension-handler.test.tsNew test ensuring the fence is chained across writes within a suspension.
packages/core/src/runtime/fenced-write.tsNew shared helper implementing fence-conflict retries with backoff and caller-provided refresh/idempotency checks.
packages/core/src/runtime.tsFence terminal writes; owner-scope step queueing when inline execution is possible; crash-recovery exception for redelivery.
docs/content/docs/v5/changelog/eager-processing.mdxUpdate documentation to reflect owner-scoped queueing semantics.
docs/content/docs/v4/changelog/eager-processing.mdxSame as v5 doc update for v4 docs.
.changeset/quick-local-queues.mdChangeset for local queue idempotency behavior.
.changeset/chilly-fences-chain.mdChangeset for core fencing + dispatch changes.

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

Comment threadpackages/world-vercel/src/utils.ts
Address Copilot review on PR 2132 (#2132 (comment)).
The fence-retry loop in runtime/fenced-write.ts detects OCC conflicts
via /fence conflict/i.test(err.message). The 412 branch was relying on
the server's JSON body to populate that message via errorData.message,
but parseResponseBody().catch(() => ({})) swallows JSON parse failures
silently — so any non-JSON 412 response (CDN HTML, gateway timeout
page, intermediate proxy error) would surface as
EntityConflictError("<METHOD> /endpoint -> HTTP 412: Precondition
Failed"), the regex would miss it, and the retry loop would
mis-classify the conflict as terminal.
Prefix the message with `fence conflict:` whenever the parsed body
didn't already carry the marker, so the retry detection is robust to
response-body parse failures.
Tests: world-vercel 69/69 pass.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@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('^' + ".*" + ' [core] Extend OCC fence to all branch-decision writes by TooTallNate · Pull Request #2132 · vercel/workflow · GitHub
Skip to content

[core] Extend OCC fence to all branch-decision writes - #2132

Merged
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage
May 28, 2026
Merged

[core] Extend OCC fence to all branch-decision writes#2132
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Layered on top of #2113. Extends the OCC fence — which currently covers only wait_completed writes from the elapsed-wait scan — to every other write whose outcome depends on a branch decision the workflow VM made from its loaded event log:

Write siteStatus
wait_completed (elapsed-wait scan)already fenced by #2113
step_creatednow fenced
wait_creatednow fenced
hook_creatednow fenced
hook_disposednow fenced
run_completednow fenced
run_failednow fenced
hook_receiveddeliberately not fenced (preserved from #2113)

hook_received stays unfenced for the same reason it's unfenced in #2113: fencing the user's signal would drop it on contention. Stale-snapshot protection belongs on the writes that consume hooks, not the writes that deliver them.

Why

Under concurrent replay (two invocations of the same run with overlapping but non-identical event-log snapshots), the SDK's deterministic ULID factory can allocate the same correlationId for different step calls depending on which side of a Promise.race each replay sees as the winner. Both invocations then try to write step_created for the same correlationId with different stepNames. The server's entity idempotency rejects the loser at the entity level — but if the winner was the stale-view invocation, its branch decision is the one that lands in the log, and future replays will see a step_created they don't expect.

Fencing the write makes the stale-view invocation lose the CAS, retry against the freshly-loaded tail, observe the events its snapshot was missing, and arrive at the same branch decision the authoritative invocation made.

Implementation

The retry loop from #2113's elapsed-wait scan is extracted into packages/core/src/runtime/fenced-write.ts so the six new fenced sites can share it without copy-paste. Each call site provides its own onConflictRefresh that runs an idempotency check against the reloaded log (e.g. "is this wait_created already in the log? → abort instead of retrying").

handleSuspension receives the load-time tail eventId + cursor from the runtime, and runs all four of its fenced writes (step_created, hook_created, hook_disposed, wait_created) against that fence. Each successful write advances the fence so chained writes in the same suspension don't conflict with each other.

The terminal run_completed / run_failed writes in runtime.ts use the same helper, with the idempotency check verifying no other terminal event has landed since the snapshot.

Test plan

  • All 1014 @workflow/core unit tests pass.
  • TypeScript clean (0 errors).
  • End-to-end stress: re-ran the same 40-attempt hammer that exposed the underlying race against the matching workflow-server preview — 0 / 40 failures (vs ~2 / 40 baseline on stable, and 5 / 40 against an alternative server-side fix that rewrites event IDs but doesn't fence). Verified each successful run has run.lastKnownEventId materialized as expected.

Temporary diagnostic instrumentation for investigating intermittent
CorruptedEventLogError 'step consumer mismatch' failures.
Emits console.log lines tagged 'WF_TRACE' at four points:
- runWorkflow start: dumps the full event array the replay will consume
(eventIds, types, correlationIds, stepNames) plus a sha256 digest
- step/hook/sleep subscribe: per-replay correlationId -> name assignment
- step consumer mismatch: structured record of the failure including the
event index in the SDK's view of the log
- runWorkflow end: completed | failed | suspended
Used to diff successive replays of the same runId and confirm whether
the SDK actually sees the same event array each time.
Peter's PR #2113 fences `wait_completed` writes from the elapsed-wait
scan. This commit extends the fence to every other write whose outcome
depends on a branch decision the workflow VM made from its loaded event
log — per the table @VaguelySerious himself laid out in his PR comment:
suspension-handler.ts:
- step_created (the smoking gun on wrun_01KSPS7XEGHF4A6WYF4DB03D40)
- hook_created
- hook_disposed
- wait_created
runtime.ts terminal writes:
- run_completed
- run_failed
`hook_received` is deliberately NOT fenced (Peter's reasoning preserved
verbatim: fencing the user's signal would drop it on contention; stale-
snapshot protection belongs on the writes that consume hooks, not the
ones that deliver them).
The fence value is the load-time tail of the events array passed into
`runWorkflow`. `suspension-handler` receives the fence + cursor from
the runtime and reloads on conflict; the runtime's terminal writes read
the cursor directly.
The new `__fenced-write.ts` helper encapsulates the retry loop so we
don't have to copy/paste Peter's pattern six times. It's named with the
leading-underscore convention to flag it as throwaway diagnostic code,
matching `__debug-replay-trace.ts`.
@vercel

vercelBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a7efa5a

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

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

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

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

@TooTallNate
TooTallNate changed the base branch from stable to mainMay 28, 2026 08:29
Two changes both needed for the extended-fence test loop to actually
exercise the OCC code path on the server:
1. Hardcode WORKFLOW_SERVER_URL_OVERRIDE to
https://workflow-server-83nn57dvc.vercel.sh (preview deployment of
workflow-server PR 447, branch alias
workflow-server-git-peter-event-write-cas.vercel.sh). The previous
preview at workflow-server-7pxaxn4d4.vercel.sh was Pranay's monotonic-
append PR 456 \u2014 different fix, doesn't implement the CAS the SDK side
now sends.
2. Map HTTP 412 \u2192 EntityConflictError in the world-vercel error mapper.
workflow-server PR 447 returns 412 with a 'fence conflict' message
for EventLogFenceConflictError; the SDK's existing fence-retry loops
(Peter's wait_completed scan + the new ones in suspension-handler
and runtime terminal writes) match on /fence conflict/i against the
message of an EntityConflictError. Without this mapping the 412 falls
through to WorkflowWorldError and the regex match never fires.
@github-actions

github-actionsBot commented May 28, 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.029s (-31.8% 🟢)1.005s (~)0.976s101.00x
💻 LocalExpress0.030s (-31.6% 🟢)1.005s (~)0.975s101.03x
💻 LocalNext.js (Turbopack)0.042s1.004s0.962s101.43x
🐘 PostgresNext.js (Turbopack)0.046s1.009s0.963s101.56x
🐘 PostgresExpress0.046s (-20.5% 🟢)1.012s (~)0.966s101.57x
🐘 PostgresNitro0.050s (-47.3% 🟢)1.012s (-2.9%)0.962s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.233s (-0.8%)1.941s (-9.1% 🟢)1.707s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.072s (-5.2% 🟢)2.006s (~)0.934s101.00x
💻 LocalExpress1.073s (-4.6%)2.006s (~)0.933s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.009s (~)0.925s101.01x
🐘 PostgresNitro1.086s (-4.7%)2.008s (~)0.922s101.01x
💻 LocalNext.js (Turbopack)1.089s2.005s0.916s101.02x
🐘 PostgresNext.js (Turbopack)1.101s2.008s0.907s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.767s (-5.7% 🟢)3.654s (-4.0%)1.887s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.410s (-4.9%)11.023s (~)0.613s31.00x
💻 LocalExpress10.418s (-4.6%)11.023s (~)0.605s31.00x
🐘 PostgresExpress10.424s (-4.9%)11.019s (~)0.595s31.00x
🐘 PostgresNitro10.430s (-4.1%)11.014s (~)0.584s31.00x
💻 LocalNext.js (Turbopack)10.557s11.022s0.465s31.01x
🐘 PostgresNext.js (Turbopack)10.623s11.018s0.394s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.910s (-18.1% 🟢)15.954s (-20.3% 🟢)2.044s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.467s (-10.0% 🟢)14.027s (-6.7% 🟢)0.560s51.00x
💻 LocalNitro13.488s (-10.5% 🟢)14.027s (-12.5% 🟢)0.539s51.00x
🐘 PostgresExpress13.515s (-7.3% 🟢)14.018s (-6.7% 🟢)0.503s51.00x
🐘 PostgresNitro13.573s (-7.0% 🟢)14.023s (-6.7% 🟢)0.450s51.01x
💻 LocalNext.js (Turbopack)13.808s14.026s0.218s51.03x
🐘 PostgresNext.js (Turbopack)13.860s14.017s0.157s51.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.720s (-58.8% 🟢)22.449s (-57.3% 🟢)1.729s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.978s (-28.6% 🟢)12.273s (-27.9% 🟢)0.295s81.00x
💻 LocalExpress11.995s (-27.7% 🟢)12.398s (-27.2% 🟢)0.403s81.00x
🐘 PostgresExpress12.247s (-12.6% 🟢)12.876s (-11.8% 🟢)0.629s71.02x
🐘 PostgresNitro12.256s (-12.3% 🟢)13.019s (-9.0% 🟢)0.763s71.02x
💻 LocalNext.js (Turbopack)12.585s13.023s0.438s71.05x
🐘 PostgresNext.js (Turbopack)12.744s13.013s0.270s71.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.522s (-76.5% 🟢)30.389s (-75.4% 🟢)1.867s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.4% 🟢)2.007s (~)0.865s151.00x
🐘 PostgresNitro1.164s (-8.7% 🟢)2.007s (~)0.844s151.02x
💻 LocalExpress1.170s (-21.4% 🟢)2.006s (~)0.836s151.02x
💻 LocalNitro1.172s (-28.1% 🟢)2.006s (-3.3%)0.834s151.03x
🐘 PostgresNext.js (Turbopack)1.175s2.006s0.832s151.03x
💻 LocalNext.js (Turbopack)1.211s2.005s0.794s151.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express103.949s (+3534.8% 🔺)105.665s (+2185.6% 🔺)1.716s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.215s (-48.6% 🟢)2.007s (-33.3% 🟢)0.793s151.00x
🐘 PostgresNitro1.221s (-48.1% 🟢)2.008s (-33.3% 🟢)0.787s151.01x
🐘 PostgresNext.js (Turbopack)1.273s2.005s0.733s151.05x
💻 LocalNext.js (Turbopack)1.514s2.005s0.492s151.25x
💻 LocalNitro1.682s (-46.5% 🟢)2.006s (-48.4% 🟢)0.323s151.39x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.300s151.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.692s (+2.0%)5.373s (+5.1% 🔺)1.681s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.338s (-61.6% 🟢)2.007s (-50.0% 🟢)0.669s151.00x
🐘 PostgresNitro1.353s (-61.1% 🟢)2.007s (-49.9% 🟢)0.654s151.01x
🐘 PostgresNext.js (Turbopack)1.479s2.006s0.528s151.10x
💻 LocalNext.js (Turbopack)3.944s4.295s0.350s72.95x
💻 LocalNitro4.618s (-44.7% 🟢)5.010s (-44.5% 🟢)0.392s63.45x
💻 LocalExpress4.704s (-43.6% 🟢)5.180s (-42.6% 🟢)0.476s63.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.082s (+19.9% 🔺)7.070s (+15.4% 🔺)1.987s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.151s (-8.4% 🟢)2.007s (~)0.856s151.00x
🐘 PostgresNitro1.162s (-7.5% 🟢)2.010s (~)0.848s151.01x
🐘 PostgresNext.js (Turbopack)1.170s2.007s0.837s151.02x
💻 LocalNext.js (Turbopack)1.265s2.005s0.740s151.10x
💻 LocalExpress1.443s (-23.8% 🟢)2.006s (-15.1% 🟢)0.563s151.25x
💻 LocalNitro1.745s (-6.5% 🟢)2.392s (+2.2%)0.646s131.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.525s (-2.2%)4.081s (-6.2% 🟢)1.556s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.234s (-47.3% 🟢)2.008s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNitro1.235s (-47.2% 🟢)2.009s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNext.js (Turbopack)1.273s2.009s0.736s151.03x
💻 LocalNext.js (Turbopack)1.812s2.072s0.260s151.47x
💻 LocalNitro1.869s (-39.0% 🟢)2.316s (-40.4% 🟢)0.447s131.51x
💻 LocalExpress1.913s (-38.9% 🟢)2.316s (-38.5% 🟢)0.402s131.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.712s (+16.3% 🔺)5.226s (+9.0% 🔺)1.514s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.351s (-61.4% 🟢)2.008s (-49.9% 🟢)0.657s151.00x
🐘 PostgresNitro1.376s (-60.5% 🟢)2.009s (-49.9% 🟢)0.633s151.02x
🐘 PostgresNext.js (Turbopack)1.477s2.006s0.529s151.09x
💻 LocalNitro4.542s (-50.3% 🟢)5.179s (-48.3% 🟢)0.637s63.36x
💻 LocalNext.js (Turbopack)4.602s5.011s0.410s73.41x
💻 LocalExpress6.036s (-31.4% 🟢)6.416s (-30.8% 🟢)0.380s54.47x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.559s (+2.2%)8.517s (+4.1%)1.959s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.427s (-49.1% 🟢)1.007s (-1.6%)0.580s601.00x
🐘 PostgresNitro0.486s (-40.7% 🟢)1.007s (~)0.521s601.14x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.7% 🟢)0.526s591.16x
💻 LocalExpress0.512s (-48.0% 🟢)1.021s (-5.1% 🟢)0.509s591.20x
🐘 PostgresNext.js (Turbopack)0.546s1.005s0.460s601.28x
💻 LocalNext.js (Turbopack)0.639s1.004s0.365s601.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.726s (-69.9% 🟢)7.306s (-65.7% 🟢)1.580s91.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.156s (-41.5% 🟢)1.903s (-15.7% 🟢)0.747s481.00x
💻 LocalNitro1.221s (-59.8% 🟢)2.005s (-46.6% 🟢)0.784s451.06x
🐘 PostgresNitro1.222s (-36.6% 🟢)2.030s (-3.4%)0.808s451.06x
💻 LocalExpress1.272s (-57.8% 🟢)2.006s (-44.1% 🟢)0.734s451.10x
🐘 PostgresNext.js (Turbopack)1.342s2.006s0.664s451.16x
💻 LocalNext.js (Turbopack)1.617s2.027s0.410s451.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.876s (-62.7% 🟢)14.564s (-60.4% 🟢)1.688s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.181s (-45.3% 🟢)2.984s (-31.7% 🟢)0.803s411.00x
🐘 PostgresNitro2.333s (-43.1% 🟢)3.060s (-33.5% 🟢)0.726s401.07x
🐘 PostgresNext.js (Turbopack)2.651s3.007s0.356s401.22x
💻 LocalNitro2.771s (-70.2% 🟢)3.032s (-69.7% 🟢)0.261s401.27x
💻 LocalExpress2.878s (-68.7% 🟢)3.136s (-68.7% 🟢)0.257s391.32x
💻 LocalNext.js (Turbopack)3.407s4.008s0.600s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.597s (-80.3% 🟢)27.381s (-79.3% 🟢)1.784s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.175s (-38.2% 🟢)1.006s (~)0.831s601.00x
🐘 PostgresNext.js (Turbopack)0.184s1.005s0.821s601.05x
🐘 PostgresNitro0.200s (-29.4% 🟢)1.006s (~)0.806s601.14x
💻 LocalExpress0.386s (-31.2% 🟢)1.004s (~)0.618s602.21x
💻 LocalNitro0.386s (-36.2% 🟢)1.004s (-1.7%)0.618s602.21x
💻 LocalNext.js (Turbopack)0.474s1.004s0.529s602.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.161s (+10.6% 🔺)3.810s (+4.8%)1.649s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.298s (-41.5% 🟢)1.006s (~)0.708s901.00x
🐘 PostgresNitro0.317s (-36.1% 🟢)1.006s (~)0.689s901.06x
🐘 PostgresNext.js (Turbopack)0.341s1.005s0.664s901.14x
💻 LocalNext.js (Turbopack)2.026s2.536s0.510s366.80x
💻 LocalNitro2.118s (-16.6% 🟢)2.610s (-13.3% 🟢)0.492s357.10x
💻 LocalExpress2.170s (-13.7% 🟢)2.822s (-6.2% 🟢)0.652s327.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.148s (+36.1% 🔺)5.654s (+17.6% 🔺)1.507s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.652s (-20.3% 🟢)1.006s (-1.1%)0.354s1201.00x
🐘 PostgresNext.js (Turbopack)0.681s1.005s0.324s1201.04x
🐘 PostgresNitro0.688s (-13.0% 🟢)1.006s (~)0.319s1201.05x
💻 LocalNext.js (Turbopack)9.197s9.718s0.521s1314.10x
💻 LocalNitro9.393s (-16.1% 🟢)10.024s (-14.1% 🟢)0.632s1214.40x
💻 LocalExpress10.171s (-9.1% 🟢)10.695s (-10.4% 🟢)0.524s1215.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.240s (+78.4% 🔺)15.181s (+64.2% 🔺)1.942s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+467.9% 🔺)2.005s (+99.6% 🔺)0.010s (-14.9% 🟢)2.017s (+98.2% 🔺)0.887s101.00x
💻 LocalNitro1.136s (+431.6% 🔺)2.004s (+99.5% 🔺)0.010s (-18.4% 🟢)2.017s (+98.0% 🔺)0.881s101.00x
🐘 PostgresExpress1.137s (+454.2% 🔺)2.001s (+100.3% 🔺)0.001s (-25.0% 🟢)2.011s (+98.8% 🔺)0.874s101.01x
💻 LocalNext.js (Turbopack)1.147s2.004s0.008s2.015s0.868s101.01x
🐘 PostgresNext.js (Turbopack)1.155s2.001s0.001s2.008s0.852s101.02x
🐘 PostgresNitro1.164s (+467.6% 🔺)2.000s (+100.0% 🔺)0.002s (+13.3% 🔺)2.013s (+99.0% 🔺)0.849s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.134s (-14.8% 🟢)3.175s (-22.4% 🟢)2.057s (+114.0% 🔺)5.682s (+1.6%)3.549s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.506s (+139.1% 🔺)2.003s (+99.0% 🔺)0.004s (+5.3% 🔺)2.024s (+97.9% 🔺)0.518s301.00x
💻 LocalNitro1.518s (+81.0% 🔺)2.012s (+98.8% 🔺)0.009s (-0.8%)2.023s (+81.3% 🔺)0.506s301.01x
💻 LocalExpress1.520s (+100.8% 🔺)2.012s (+95.5% 🔺)0.009s (-0.9%)2.023s (+94.5% 🔺)0.503s301.01x
💻 LocalNext.js (Turbopack)1.546s2.008s0.009s2.020s0.474s301.03x
🐘 PostgresNitro1.591s (+154.9% 🔺)2.007s (+99.3% 🔺)0.004s (-3.3%)2.026s (+98.2% 🔺)0.436s301.06x
🐘 PostgresNext.js (Turbopack)1.632s2.041s0.003s2.053s0.421s301.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.080s (-6.5% 🟢)7.362s (-8.1% 🟢)0.403s (-1.4%)8.205s (-7.1% 🟢)2.125s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.645s (-32.9% 🟢)1.016s (-20.5% 🟢)0.000s (-61.0% 🟢)1.029s (-21.2% 🟢)0.384s591.00x
🐘 PostgresNitro0.669s (-31.0% 🟢)1.033s (-17.2% 🟢)0.000s (+65.5% 🔺)1.048s (-16.7% 🟢)0.379s581.04x
🐘 PostgresNext.js (Turbopack)0.878s1.088s0.000s1.186s0.308s511.36x
💻 LocalNext.js (Turbopack)1.198s1.860s0.000s1.863s0.665s331.86x
💻 LocalNitro1.353s (+10.7% 🔺)2.016s (~)0.000s (+33.3% 🔺)2.018s (~)0.664s302.10x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.649s302.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.631s (-2.9%)4.743s (-7.0% 🟢)0.000s (-100.0% 🟢)5.186s (-6.2% 🟢)1.555s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.282s (-28.5% 🟢)1.996s (-6.8% 🟢)0.000s (+86.7% 🔺)2.048s (-5.8% 🟢)0.766s301.00x
🐘 PostgresExpress1.312s (-26.0% 🟢)2.031s (-6.7% 🟢)0.000s (+Infinity% 🔺)2.054s (-6.6% 🟢)0.742s301.02x
🐘 PostgresNext.js (Turbopack)1.673s2.401s0.000s2.435s0.762s251.30x
💻 LocalNext.js (Turbopack)2.491s3.122s0.000s3.129s0.637s201.94x
💻 LocalNitro3.140s (-7.3% 🟢)3.965s (-1.7%)0.000s (-18.0% 🟢)3.968s (-1.7%)0.828s162.45x
💻 LocalExpress3.291s (-5.1% 🟢)4.026s (~)0.000s (-41.7% 🟢)4.031s (~)0.740s152.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.296s (+15.5% 🔺)6.741s (+11.9% 🔺)0.001s (+Infinity% 🔺)7.181s (+11.2% 🔺)1.884s91.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress19/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@github-actions

github-actionsBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production122202191441
✅ 💻 Local Development161502191834
✅ 📦 Local Production161502191834
✅ 🐘 Local Postgres161502191834
✅ 🪟 Windows13100131
✅ 📋 Other7410176917
Total6939010527991

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro105026
✅ example105026
✅ express105026
✅ fastify105026
✅ hono105026
✅ nextjs-turbopack12902
✅ nextjs-webpack12902
✅ nitro105026
✅ nuxt105026
✅ sveltekit12407
✅ vite105026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13100
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable106025
✅ e2e-local-dev-tanstack-start-106025
✅ e2e-local-postgres-nest-stable106025
✅ e2e-local-postgres-tanstack-start-106025
✅ e2e-local-prod-nest-stable106025
✅ e2e-local-prod-tanstack-start-106025
✅ e2e-vercel-prod-tanstack-start105026

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's merge into upstream branch? and then review as one

Strip the WF_TRACE replay tracing that was used to diagnose the
CORRUPTED_EVENT_LOG race \u2014 it's served its purpose now that the fix
is in. Specifically:
- Delete packages/core/src/__debug-replay-trace.ts and its 8 call sites
in workflow.ts, step.ts, workflow/hook.ts, workflow/sleep.ts.
- Drop the matching [DEBUG] inline narrative comments at each call site.
- Rename packages/core/src/runtime/__fenced-write.ts \u2192 fenced-write.ts
(the leading-underscore convention marked it as throwaway diagnostic
code; the helper is intended to stay).
- Trim the file header on fenced-write.ts and the related narrative
comment in suspension-handler.ts to drop the failing-runId / PR-number
references that only made sense in the debug context.
No behavioral change. typecheck clean (0 errors); 1014/1014 unit tests
pass (same as parent commit 77f057a).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Extends optimistic concurrency control (OCC) “fencing” to additional workflow event writes whose correctness depends on branch decisions made from a potentially stale event-log snapshot, and adjusts step dispatch behavior to avoid duplicate execution under concurrent replay.

Changes:

  • Introduce a shared fencedEventCreate helper to retry CAS-fenced writes with refresh + idempotency checks.
  • Apply fencing to step_created / wait_created / hook create+dispose paths in handleSuspension, and to terminal run_completed / run_failed writes in runtime.ts.
  • Improve local queue idempotency by retaining completed idempotency keys (plus tests/docs updates).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsMap HTTP 412 OCC fence conflicts into EntityConflictError for downstream handling.
packages/world-testing/src/inline-batches-debug.mtsTighten debug assertions for race/skip counters.
packages/world-local/src/queue.tsAdd a bounded cache of completed idempotency keys to prevent post-completion duplicate dispatch.
packages/world-local/src/queue.test.tsAdd coverage for post-completion idempotency dedupe behavior.
packages/core/src/workflow/hook.tsMinor formatting-only change.
packages/core/src/step.tsMinor formatting-only change.
packages/core/src/runtime/suspension-handler.tsFence branch-decision writes and serialize them to avoid self-conflicts; add refresh/idempotency logic.
packages/core/src/runtime/suspension-handler.test.tsNew test ensuring the fence is chained across writes within a suspension.
packages/core/src/runtime/fenced-write.tsNew shared helper implementing fence-conflict retries with backoff and caller-provided refresh/idempotency checks.
packages/core/src/runtime.tsFence terminal writes; owner-scope step queueing when inline execution is possible; crash-recovery exception for redelivery.
docs/content/docs/v5/changelog/eager-processing.mdxUpdate documentation to reflect owner-scoped queueing semantics.
docs/content/docs/v4/changelog/eager-processing.mdxSame as v5 doc update for v4 docs.
.changeset/quick-local-queues.mdChangeset for local queue idempotency behavior.
.changeset/chilly-fences-chain.mdChangeset for core fencing + dispatch changes.

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

Comment threadpackages/world-vercel/src/utils.ts
Address Copilot review on PR 2132 (#2132 (comment)).
The fence-retry loop in runtime/fenced-write.ts detects OCC conflicts
via /fence conflict/i.test(err.message). The 412 branch was relying on
the server's JSON body to populate that message via errorData.message,
but parseResponseBody().catch(() => ({})) swallows JSON parse failures
silently — so any non-JSON 412 response (CDN HTML, gateway timeout
page, intermediate proxy error) would surface as
EntityConflictError("<METHOD> /endpoint -> HTTP 412: Precondition
Failed"), the regex would miss it, and the retry loop would
mis-classify the conflict as terminal.
Prefix the message with `fence conflict:` whenever the parsed body
didn't already carry the marker, so the retry detection is robust to
response-body parse failures.
Tests: world-vercel 69/69 pass.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@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" + ' [core] Extend OCC fence to all branch-decision writes by TooTallNate · Pull Request #2132 · vercel/workflow · GitHub
Skip to content

[core] Extend OCC fence to all branch-decision writes - #2132

Merged
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage
May 28, 2026
Merged

[core] Extend OCC fence to all branch-decision writes#2132
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Layered on top of #2113. Extends the OCC fence — which currently covers only wait_completed writes from the elapsed-wait scan — to every other write whose outcome depends on a branch decision the workflow VM made from its loaded event log:

Write siteStatus
wait_completed (elapsed-wait scan)already fenced by #2113
step_creatednow fenced
wait_creatednow fenced
hook_creatednow fenced
hook_disposednow fenced
run_completednow fenced
run_failednow fenced
hook_receiveddeliberately not fenced (preserved from #2113)

hook_received stays unfenced for the same reason it's unfenced in #2113: fencing the user's signal would drop it on contention. Stale-snapshot protection belongs on the writes that consume hooks, not the writes that deliver them.

Why

Under concurrent replay (two invocations of the same run with overlapping but non-identical event-log snapshots), the SDK's deterministic ULID factory can allocate the same correlationId for different step calls depending on which side of a Promise.race each replay sees as the winner. Both invocations then try to write step_created for the same correlationId with different stepNames. The server's entity idempotency rejects the loser at the entity level — but if the winner was the stale-view invocation, its branch decision is the one that lands in the log, and future replays will see a step_created they don't expect.

Fencing the write makes the stale-view invocation lose the CAS, retry against the freshly-loaded tail, observe the events its snapshot was missing, and arrive at the same branch decision the authoritative invocation made.

Implementation

The retry loop from #2113's elapsed-wait scan is extracted into packages/core/src/runtime/fenced-write.ts so the six new fenced sites can share it without copy-paste. Each call site provides its own onConflictRefresh that runs an idempotency check against the reloaded log (e.g. "is this wait_created already in the log? → abort instead of retrying").

handleSuspension receives the load-time tail eventId + cursor from the runtime, and runs all four of its fenced writes (step_created, hook_created, hook_disposed, wait_created) against that fence. Each successful write advances the fence so chained writes in the same suspension don't conflict with each other.

The terminal run_completed / run_failed writes in runtime.ts use the same helper, with the idempotency check verifying no other terminal event has landed since the snapshot.

Test plan

  • All 1014 @workflow/core unit tests pass.
  • TypeScript clean (0 errors).
  • End-to-end stress: re-ran the same 40-attempt hammer that exposed the underlying race against the matching workflow-server preview — 0 / 40 failures (vs ~2 / 40 baseline on stable, and 5 / 40 against an alternative server-side fix that rewrites event IDs but doesn't fence). Verified each successful run has run.lastKnownEventId materialized as expected.

Temporary diagnostic instrumentation for investigating intermittent
CorruptedEventLogError 'step consumer mismatch' failures.
Emits console.log lines tagged 'WF_TRACE' at four points:
- runWorkflow start: dumps the full event array the replay will consume
(eventIds, types, correlationIds, stepNames) plus a sha256 digest
- step/hook/sleep subscribe: per-replay correlationId -> name assignment
- step consumer mismatch: structured record of the failure including the
event index in the SDK's view of the log
- runWorkflow end: completed | failed | suspended
Used to diff successive replays of the same runId and confirm whether
the SDK actually sees the same event array each time.
Peter's PR #2113 fences `wait_completed` writes from the elapsed-wait
scan. This commit extends the fence to every other write whose outcome
depends on a branch decision the workflow VM made from its loaded event
log — per the table @VaguelySerious himself laid out in his PR comment:
suspension-handler.ts:
- step_created (the smoking gun on wrun_01KSPS7XEGHF4A6WYF4DB03D40)
- hook_created
- hook_disposed
- wait_created
runtime.ts terminal writes:
- run_completed
- run_failed
`hook_received` is deliberately NOT fenced (Peter's reasoning preserved
verbatim: fencing the user's signal would drop it on contention; stale-
snapshot protection belongs on the writes that consume hooks, not the
ones that deliver them).
The fence value is the load-time tail of the events array passed into
`runWorkflow`. `suspension-handler` receives the fence + cursor from
the runtime and reloads on conflict; the runtime's terminal writes read
the cursor directly.
The new `__fenced-write.ts` helper encapsulates the retry loop so we
don't have to copy/paste Peter's pattern six times. It's named with the
leading-underscore convention to flag it as throwaway diagnostic code,
matching `__debug-replay-trace.ts`.
@vercel

vercelBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a7efa5a

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

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

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

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

@TooTallNate
TooTallNate changed the base branch from stable to mainMay 28, 2026 08:29
Two changes both needed for the extended-fence test loop to actually
exercise the OCC code path on the server:
1. Hardcode WORKFLOW_SERVER_URL_OVERRIDE to
https://workflow-server-83nn57dvc.vercel.sh (preview deployment of
workflow-server PR 447, branch alias
workflow-server-git-peter-event-write-cas.vercel.sh). The previous
preview at workflow-server-7pxaxn4d4.vercel.sh was Pranay's monotonic-
append PR 456 \u2014 different fix, doesn't implement the CAS the SDK side
now sends.
2. Map HTTP 412 \u2192 EntityConflictError in the world-vercel error mapper.
workflow-server PR 447 returns 412 with a 'fence conflict' message
for EventLogFenceConflictError; the SDK's existing fence-retry loops
(Peter's wait_completed scan + the new ones in suspension-handler
and runtime terminal writes) match on /fence conflict/i against the
message of an EntityConflictError. Without this mapping the 412 falls
through to WorkflowWorldError and the regex match never fires.
@github-actions

github-actionsBot commented May 28, 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.029s (-31.8% 🟢)1.005s (~)0.976s101.00x
💻 LocalExpress0.030s (-31.6% 🟢)1.005s (~)0.975s101.03x
💻 LocalNext.js (Turbopack)0.042s1.004s0.962s101.43x
🐘 PostgresNext.js (Turbopack)0.046s1.009s0.963s101.56x
🐘 PostgresExpress0.046s (-20.5% 🟢)1.012s (~)0.966s101.57x
🐘 PostgresNitro0.050s (-47.3% 🟢)1.012s (-2.9%)0.962s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.233s (-0.8%)1.941s (-9.1% 🟢)1.707s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.072s (-5.2% 🟢)2.006s (~)0.934s101.00x
💻 LocalExpress1.073s (-4.6%)2.006s (~)0.933s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.009s (~)0.925s101.01x
🐘 PostgresNitro1.086s (-4.7%)2.008s (~)0.922s101.01x
💻 LocalNext.js (Turbopack)1.089s2.005s0.916s101.02x
🐘 PostgresNext.js (Turbopack)1.101s2.008s0.907s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.767s (-5.7% 🟢)3.654s (-4.0%)1.887s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.410s (-4.9%)11.023s (~)0.613s31.00x
💻 LocalExpress10.418s (-4.6%)11.023s (~)0.605s31.00x
🐘 PostgresExpress10.424s (-4.9%)11.019s (~)0.595s31.00x
🐘 PostgresNitro10.430s (-4.1%)11.014s (~)0.584s31.00x
💻 LocalNext.js (Turbopack)10.557s11.022s0.465s31.01x
🐘 PostgresNext.js (Turbopack)10.623s11.018s0.394s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.910s (-18.1% 🟢)15.954s (-20.3% 🟢)2.044s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.467s (-10.0% 🟢)14.027s (-6.7% 🟢)0.560s51.00x
💻 LocalNitro13.488s (-10.5% 🟢)14.027s (-12.5% 🟢)0.539s51.00x
🐘 PostgresExpress13.515s (-7.3% 🟢)14.018s (-6.7% 🟢)0.503s51.00x
🐘 PostgresNitro13.573s (-7.0% 🟢)14.023s (-6.7% 🟢)0.450s51.01x
💻 LocalNext.js (Turbopack)13.808s14.026s0.218s51.03x
🐘 PostgresNext.js (Turbopack)13.860s14.017s0.157s51.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.720s (-58.8% 🟢)22.449s (-57.3% 🟢)1.729s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.978s (-28.6% 🟢)12.273s (-27.9% 🟢)0.295s81.00x
💻 LocalExpress11.995s (-27.7% 🟢)12.398s (-27.2% 🟢)0.403s81.00x
🐘 PostgresExpress12.247s (-12.6% 🟢)12.876s (-11.8% 🟢)0.629s71.02x
🐘 PostgresNitro12.256s (-12.3% 🟢)13.019s (-9.0% 🟢)0.763s71.02x
💻 LocalNext.js (Turbopack)12.585s13.023s0.438s71.05x
🐘 PostgresNext.js (Turbopack)12.744s13.013s0.270s71.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.522s (-76.5% 🟢)30.389s (-75.4% 🟢)1.867s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.4% 🟢)2.007s (~)0.865s151.00x
🐘 PostgresNitro1.164s (-8.7% 🟢)2.007s (~)0.844s151.02x
💻 LocalExpress1.170s (-21.4% 🟢)2.006s (~)0.836s151.02x
💻 LocalNitro1.172s (-28.1% 🟢)2.006s (-3.3%)0.834s151.03x
🐘 PostgresNext.js (Turbopack)1.175s2.006s0.832s151.03x
💻 LocalNext.js (Turbopack)1.211s2.005s0.794s151.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express103.949s (+3534.8% 🔺)105.665s (+2185.6% 🔺)1.716s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.215s (-48.6% 🟢)2.007s (-33.3% 🟢)0.793s151.00x
🐘 PostgresNitro1.221s (-48.1% 🟢)2.008s (-33.3% 🟢)0.787s151.01x
🐘 PostgresNext.js (Turbopack)1.273s2.005s0.733s151.05x
💻 LocalNext.js (Turbopack)1.514s2.005s0.492s151.25x
💻 LocalNitro1.682s (-46.5% 🟢)2.006s (-48.4% 🟢)0.323s151.39x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.300s151.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.692s (+2.0%)5.373s (+5.1% 🔺)1.681s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.338s (-61.6% 🟢)2.007s (-50.0% 🟢)0.669s151.00x
🐘 PostgresNitro1.353s (-61.1% 🟢)2.007s (-49.9% 🟢)0.654s151.01x
🐘 PostgresNext.js (Turbopack)1.479s2.006s0.528s151.10x
💻 LocalNext.js (Turbopack)3.944s4.295s0.350s72.95x
💻 LocalNitro4.618s (-44.7% 🟢)5.010s (-44.5% 🟢)0.392s63.45x
💻 LocalExpress4.704s (-43.6% 🟢)5.180s (-42.6% 🟢)0.476s63.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.082s (+19.9% 🔺)7.070s (+15.4% 🔺)1.987s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.151s (-8.4% 🟢)2.007s (~)0.856s151.00x
🐘 PostgresNitro1.162s (-7.5% 🟢)2.010s (~)0.848s151.01x
🐘 PostgresNext.js (Turbopack)1.170s2.007s0.837s151.02x
💻 LocalNext.js (Turbopack)1.265s2.005s0.740s151.10x
💻 LocalExpress1.443s (-23.8% 🟢)2.006s (-15.1% 🟢)0.563s151.25x
💻 LocalNitro1.745s (-6.5% 🟢)2.392s (+2.2%)0.646s131.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.525s (-2.2%)4.081s (-6.2% 🟢)1.556s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.234s (-47.3% 🟢)2.008s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNitro1.235s (-47.2% 🟢)2.009s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNext.js (Turbopack)1.273s2.009s0.736s151.03x
💻 LocalNext.js (Turbopack)1.812s2.072s0.260s151.47x
💻 LocalNitro1.869s (-39.0% 🟢)2.316s (-40.4% 🟢)0.447s131.51x
💻 LocalExpress1.913s (-38.9% 🟢)2.316s (-38.5% 🟢)0.402s131.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.712s (+16.3% 🔺)5.226s (+9.0% 🔺)1.514s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.351s (-61.4% 🟢)2.008s (-49.9% 🟢)0.657s151.00x
🐘 PostgresNitro1.376s (-60.5% 🟢)2.009s (-49.9% 🟢)0.633s151.02x
🐘 PostgresNext.js (Turbopack)1.477s2.006s0.529s151.09x
💻 LocalNitro4.542s (-50.3% 🟢)5.179s (-48.3% 🟢)0.637s63.36x
💻 LocalNext.js (Turbopack)4.602s5.011s0.410s73.41x
💻 LocalExpress6.036s (-31.4% 🟢)6.416s (-30.8% 🟢)0.380s54.47x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.559s (+2.2%)8.517s (+4.1%)1.959s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.427s (-49.1% 🟢)1.007s (-1.6%)0.580s601.00x
🐘 PostgresNitro0.486s (-40.7% 🟢)1.007s (~)0.521s601.14x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.7% 🟢)0.526s591.16x
💻 LocalExpress0.512s (-48.0% 🟢)1.021s (-5.1% 🟢)0.509s591.20x
🐘 PostgresNext.js (Turbopack)0.546s1.005s0.460s601.28x
💻 LocalNext.js (Turbopack)0.639s1.004s0.365s601.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.726s (-69.9% 🟢)7.306s (-65.7% 🟢)1.580s91.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.156s (-41.5% 🟢)1.903s (-15.7% 🟢)0.747s481.00x
💻 LocalNitro1.221s (-59.8% 🟢)2.005s (-46.6% 🟢)0.784s451.06x
🐘 PostgresNitro1.222s (-36.6% 🟢)2.030s (-3.4%)0.808s451.06x
💻 LocalExpress1.272s (-57.8% 🟢)2.006s (-44.1% 🟢)0.734s451.10x
🐘 PostgresNext.js (Turbopack)1.342s2.006s0.664s451.16x
💻 LocalNext.js (Turbopack)1.617s2.027s0.410s451.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.876s (-62.7% 🟢)14.564s (-60.4% 🟢)1.688s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.181s (-45.3% 🟢)2.984s (-31.7% 🟢)0.803s411.00x
🐘 PostgresNitro2.333s (-43.1% 🟢)3.060s (-33.5% 🟢)0.726s401.07x
🐘 PostgresNext.js (Turbopack)2.651s3.007s0.356s401.22x
💻 LocalNitro2.771s (-70.2% 🟢)3.032s (-69.7% 🟢)0.261s401.27x
💻 LocalExpress2.878s (-68.7% 🟢)3.136s (-68.7% 🟢)0.257s391.32x
💻 LocalNext.js (Turbopack)3.407s4.008s0.600s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.597s (-80.3% 🟢)27.381s (-79.3% 🟢)1.784s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.175s (-38.2% 🟢)1.006s (~)0.831s601.00x
🐘 PostgresNext.js (Turbopack)0.184s1.005s0.821s601.05x
🐘 PostgresNitro0.200s (-29.4% 🟢)1.006s (~)0.806s601.14x
💻 LocalExpress0.386s (-31.2% 🟢)1.004s (~)0.618s602.21x
💻 LocalNitro0.386s (-36.2% 🟢)1.004s (-1.7%)0.618s602.21x
💻 LocalNext.js (Turbopack)0.474s1.004s0.529s602.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.161s (+10.6% 🔺)3.810s (+4.8%)1.649s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.298s (-41.5% 🟢)1.006s (~)0.708s901.00x
🐘 PostgresNitro0.317s (-36.1% 🟢)1.006s (~)0.689s901.06x
🐘 PostgresNext.js (Turbopack)0.341s1.005s0.664s901.14x
💻 LocalNext.js (Turbopack)2.026s2.536s0.510s366.80x
💻 LocalNitro2.118s (-16.6% 🟢)2.610s (-13.3% 🟢)0.492s357.10x
💻 LocalExpress2.170s (-13.7% 🟢)2.822s (-6.2% 🟢)0.652s327.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.148s (+36.1% 🔺)5.654s (+17.6% 🔺)1.507s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.652s (-20.3% 🟢)1.006s (-1.1%)0.354s1201.00x
🐘 PostgresNext.js (Turbopack)0.681s1.005s0.324s1201.04x
🐘 PostgresNitro0.688s (-13.0% 🟢)1.006s (~)0.319s1201.05x
💻 LocalNext.js (Turbopack)9.197s9.718s0.521s1314.10x
💻 LocalNitro9.393s (-16.1% 🟢)10.024s (-14.1% 🟢)0.632s1214.40x
💻 LocalExpress10.171s (-9.1% 🟢)10.695s (-10.4% 🟢)0.524s1215.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.240s (+78.4% 🔺)15.181s (+64.2% 🔺)1.942s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+467.9% 🔺)2.005s (+99.6% 🔺)0.010s (-14.9% 🟢)2.017s (+98.2% 🔺)0.887s101.00x
💻 LocalNitro1.136s (+431.6% 🔺)2.004s (+99.5% 🔺)0.010s (-18.4% 🟢)2.017s (+98.0% 🔺)0.881s101.00x
🐘 PostgresExpress1.137s (+454.2% 🔺)2.001s (+100.3% 🔺)0.001s (-25.0% 🟢)2.011s (+98.8% 🔺)0.874s101.01x
💻 LocalNext.js (Turbopack)1.147s2.004s0.008s2.015s0.868s101.01x
🐘 PostgresNext.js (Turbopack)1.155s2.001s0.001s2.008s0.852s101.02x
🐘 PostgresNitro1.164s (+467.6% 🔺)2.000s (+100.0% 🔺)0.002s (+13.3% 🔺)2.013s (+99.0% 🔺)0.849s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.134s (-14.8% 🟢)3.175s (-22.4% 🟢)2.057s (+114.0% 🔺)5.682s (+1.6%)3.549s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.506s (+139.1% 🔺)2.003s (+99.0% 🔺)0.004s (+5.3% 🔺)2.024s (+97.9% 🔺)0.518s301.00x
💻 LocalNitro1.518s (+81.0% 🔺)2.012s (+98.8% 🔺)0.009s (-0.8%)2.023s (+81.3% 🔺)0.506s301.01x
💻 LocalExpress1.520s (+100.8% 🔺)2.012s (+95.5% 🔺)0.009s (-0.9%)2.023s (+94.5% 🔺)0.503s301.01x
💻 LocalNext.js (Turbopack)1.546s2.008s0.009s2.020s0.474s301.03x
🐘 PostgresNitro1.591s (+154.9% 🔺)2.007s (+99.3% 🔺)0.004s (-3.3%)2.026s (+98.2% 🔺)0.436s301.06x
🐘 PostgresNext.js (Turbopack)1.632s2.041s0.003s2.053s0.421s301.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.080s (-6.5% 🟢)7.362s (-8.1% 🟢)0.403s (-1.4%)8.205s (-7.1% 🟢)2.125s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.645s (-32.9% 🟢)1.016s (-20.5% 🟢)0.000s (-61.0% 🟢)1.029s (-21.2% 🟢)0.384s591.00x
🐘 PostgresNitro0.669s (-31.0% 🟢)1.033s (-17.2% 🟢)0.000s (+65.5% 🔺)1.048s (-16.7% 🟢)0.379s581.04x
🐘 PostgresNext.js (Turbopack)0.878s1.088s0.000s1.186s0.308s511.36x
💻 LocalNext.js (Turbopack)1.198s1.860s0.000s1.863s0.665s331.86x
💻 LocalNitro1.353s (+10.7% 🔺)2.016s (~)0.000s (+33.3% 🔺)2.018s (~)0.664s302.10x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.649s302.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.631s (-2.9%)4.743s (-7.0% 🟢)0.000s (-100.0% 🟢)5.186s (-6.2% 🟢)1.555s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.282s (-28.5% 🟢)1.996s (-6.8% 🟢)0.000s (+86.7% 🔺)2.048s (-5.8% 🟢)0.766s301.00x
🐘 PostgresExpress1.312s (-26.0% 🟢)2.031s (-6.7% 🟢)0.000s (+Infinity% 🔺)2.054s (-6.6% 🟢)0.742s301.02x
🐘 PostgresNext.js (Turbopack)1.673s2.401s0.000s2.435s0.762s251.30x
💻 LocalNext.js (Turbopack)2.491s3.122s0.000s3.129s0.637s201.94x
💻 LocalNitro3.140s (-7.3% 🟢)3.965s (-1.7%)0.000s (-18.0% 🟢)3.968s (-1.7%)0.828s162.45x
💻 LocalExpress3.291s (-5.1% 🟢)4.026s (~)0.000s (-41.7% 🟢)4.031s (~)0.740s152.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.296s (+15.5% 🔺)6.741s (+11.9% 🔺)0.001s (+Infinity% 🔺)7.181s (+11.2% 🔺)1.884s91.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress19/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@github-actions

github-actionsBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production122202191441
✅ 💻 Local Development161502191834
✅ 📦 Local Production161502191834
✅ 🐘 Local Postgres161502191834
✅ 🪟 Windows13100131
✅ 📋 Other7410176917
Total6939010527991

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro105026
✅ example105026
✅ express105026
✅ fastify105026
✅ hono105026
✅ nextjs-turbopack12902
✅ nextjs-webpack12902
✅ nitro105026
✅ nuxt105026
✅ sveltekit12407
✅ vite105026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13100
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable106025
✅ e2e-local-dev-tanstack-start-106025
✅ e2e-local-postgres-nest-stable106025
✅ e2e-local-postgres-tanstack-start-106025
✅ e2e-local-prod-nest-stable106025
✅ e2e-local-prod-tanstack-start-106025
✅ e2e-vercel-prod-tanstack-start105026

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's merge into upstream branch? and then review as one

Strip the WF_TRACE replay tracing that was used to diagnose the
CORRUPTED_EVENT_LOG race \u2014 it's served its purpose now that the fix
is in. Specifically:
- Delete packages/core/src/__debug-replay-trace.ts and its 8 call sites
in workflow.ts, step.ts, workflow/hook.ts, workflow/sleep.ts.
- Drop the matching [DEBUG] inline narrative comments at each call site.
- Rename packages/core/src/runtime/__fenced-write.ts \u2192 fenced-write.ts
(the leading-underscore convention marked it as throwaway diagnostic
code; the helper is intended to stay).
- Trim the file header on fenced-write.ts and the related narrative
comment in suspension-handler.ts to drop the failing-runId / PR-number
references that only made sense in the debug context.
No behavioral change. typecheck clean (0 errors); 1014/1014 unit tests
pass (same as parent commit 77f057a).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Extends optimistic concurrency control (OCC) “fencing” to additional workflow event writes whose correctness depends on branch decisions made from a potentially stale event-log snapshot, and adjusts step dispatch behavior to avoid duplicate execution under concurrent replay.

Changes:

  • Introduce a shared fencedEventCreate helper to retry CAS-fenced writes with refresh + idempotency checks.
  • Apply fencing to step_created / wait_created / hook create+dispose paths in handleSuspension, and to terminal run_completed / run_failed writes in runtime.ts.
  • Improve local queue idempotency by retaining completed idempotency keys (plus tests/docs updates).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsMap HTTP 412 OCC fence conflicts into EntityConflictError for downstream handling.
packages/world-testing/src/inline-batches-debug.mtsTighten debug assertions for race/skip counters.
packages/world-local/src/queue.tsAdd a bounded cache of completed idempotency keys to prevent post-completion duplicate dispatch.
packages/world-local/src/queue.test.tsAdd coverage for post-completion idempotency dedupe behavior.
packages/core/src/workflow/hook.tsMinor formatting-only change.
packages/core/src/step.tsMinor formatting-only change.
packages/core/src/runtime/suspension-handler.tsFence branch-decision writes and serialize them to avoid self-conflicts; add refresh/idempotency logic.
packages/core/src/runtime/suspension-handler.test.tsNew test ensuring the fence is chained across writes within a suspension.
packages/core/src/runtime/fenced-write.tsNew shared helper implementing fence-conflict retries with backoff and caller-provided refresh/idempotency checks.
packages/core/src/runtime.tsFence terminal writes; owner-scope step queueing when inline execution is possible; crash-recovery exception for redelivery.
docs/content/docs/v5/changelog/eager-processing.mdxUpdate documentation to reflect owner-scoped queueing semantics.
docs/content/docs/v4/changelog/eager-processing.mdxSame as v5 doc update for v4 docs.
.changeset/quick-local-queues.mdChangeset for local queue idempotency behavior.
.changeset/chilly-fences-chain.mdChangeset for core fencing + dispatch changes.

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

Comment threadpackages/world-vercel/src/utils.ts
Address Copilot review on PR 2132 (#2132 (comment)).
The fence-retry loop in runtime/fenced-write.ts detects OCC conflicts
via /fence conflict/i.test(err.message). The 412 branch was relying on
the server's JSON body to populate that message via errorData.message,
but parseResponseBody().catch(() => ({})) swallows JSON parse failures
silently — so any non-JSON 412 response (CDN HTML, gateway timeout
page, intermediate proxy error) would surface as
EntityConflictError("<METHOD> /endpoint -> HTTP 412: Precondition
Failed"), the regex would miss it, and the retry loop would
mis-classify the conflict as terminal.
Prefix the message with `fence conflict:` whenever the parsed body
didn't already carry the marker, so the retry detection is robust to
response-body parse failures.
Tests: world-vercel 69/69 pass.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@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('^' + ".*" + ' [core] Extend OCC fence to all branch-decision writes by TooTallNate · Pull Request #2132 · vercel/workflow · GitHub
Skip to content

[core] Extend OCC fence to all branch-decision writes - #2132

Merged
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage
May 28, 2026
Merged

[core] Extend OCC fence to all branch-decision writes#2132
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Layered on top of #2113. Extends the OCC fence — which currently covers only wait_completed writes from the elapsed-wait scan — to every other write whose outcome depends on a branch decision the workflow VM made from its loaded event log:

Write siteStatus
wait_completed (elapsed-wait scan)already fenced by #2113
step_creatednow fenced
wait_creatednow fenced
hook_creatednow fenced
hook_disposednow fenced
run_completednow fenced
run_failednow fenced
hook_receiveddeliberately not fenced (preserved from #2113)

hook_received stays unfenced for the same reason it's unfenced in #2113: fencing the user's signal would drop it on contention. Stale-snapshot protection belongs on the writes that consume hooks, not the writes that deliver them.

Why

Under concurrent replay (two invocations of the same run with overlapping but non-identical event-log snapshots), the SDK's deterministic ULID factory can allocate the same correlationId for different step calls depending on which side of a Promise.race each replay sees as the winner. Both invocations then try to write step_created for the same correlationId with different stepNames. The server's entity idempotency rejects the loser at the entity level — but if the winner was the stale-view invocation, its branch decision is the one that lands in the log, and future replays will see a step_created they don't expect.

Fencing the write makes the stale-view invocation lose the CAS, retry against the freshly-loaded tail, observe the events its snapshot was missing, and arrive at the same branch decision the authoritative invocation made.

Implementation

The retry loop from #2113's elapsed-wait scan is extracted into packages/core/src/runtime/fenced-write.ts so the six new fenced sites can share it without copy-paste. Each call site provides its own onConflictRefresh that runs an idempotency check against the reloaded log (e.g. "is this wait_created already in the log? → abort instead of retrying").

handleSuspension receives the load-time tail eventId + cursor from the runtime, and runs all four of its fenced writes (step_created, hook_created, hook_disposed, wait_created) against that fence. Each successful write advances the fence so chained writes in the same suspension don't conflict with each other.

The terminal run_completed / run_failed writes in runtime.ts use the same helper, with the idempotency check verifying no other terminal event has landed since the snapshot.

Test plan

  • All 1014 @workflow/core unit tests pass.
  • TypeScript clean (0 errors).
  • End-to-end stress: re-ran the same 40-attempt hammer that exposed the underlying race against the matching workflow-server preview — 0 / 40 failures (vs ~2 / 40 baseline on stable, and 5 / 40 against an alternative server-side fix that rewrites event IDs but doesn't fence). Verified each successful run has run.lastKnownEventId materialized as expected.

Temporary diagnostic instrumentation for investigating intermittent
CorruptedEventLogError 'step consumer mismatch' failures.
Emits console.log lines tagged 'WF_TRACE' at four points:
- runWorkflow start: dumps the full event array the replay will consume
(eventIds, types, correlationIds, stepNames) plus a sha256 digest
- step/hook/sleep subscribe: per-replay correlationId -> name assignment
- step consumer mismatch: structured record of the failure including the
event index in the SDK's view of the log
- runWorkflow end: completed | failed | suspended
Used to diff successive replays of the same runId and confirm whether
the SDK actually sees the same event array each time.
Peter's PR #2113 fences `wait_completed` writes from the elapsed-wait
scan. This commit extends the fence to every other write whose outcome
depends on a branch decision the workflow VM made from its loaded event
log — per the table @VaguelySerious himself laid out in his PR comment:
suspension-handler.ts:
- step_created (the smoking gun on wrun_01KSPS7XEGHF4A6WYF4DB03D40)
- hook_created
- hook_disposed
- wait_created
runtime.ts terminal writes:
- run_completed
- run_failed
`hook_received` is deliberately NOT fenced (Peter's reasoning preserved
verbatim: fencing the user's signal would drop it on contention; stale-
snapshot protection belongs on the writes that consume hooks, not the
ones that deliver them).
The fence value is the load-time tail of the events array passed into
`runWorkflow`. `suspension-handler` receives the fence + cursor from
the runtime and reloads on conflict; the runtime's terminal writes read
the cursor directly.
The new `__fenced-write.ts` helper encapsulates the retry loop so we
don't have to copy/paste Peter's pattern six times. It's named with the
leading-underscore convention to flag it as throwaway diagnostic code,
matching `__debug-replay-trace.ts`.
@vercel

vercelBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a7efa5a

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

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

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

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

@TooTallNate
TooTallNate changed the base branch from stable to mainMay 28, 2026 08:29
Two changes both needed for the extended-fence test loop to actually
exercise the OCC code path on the server:
1. Hardcode WORKFLOW_SERVER_URL_OVERRIDE to
https://workflow-server-83nn57dvc.vercel.sh (preview deployment of
workflow-server PR 447, branch alias
workflow-server-git-peter-event-write-cas.vercel.sh). The previous
preview at workflow-server-7pxaxn4d4.vercel.sh was Pranay's monotonic-
append PR 456 \u2014 different fix, doesn't implement the CAS the SDK side
now sends.
2. Map HTTP 412 \u2192 EntityConflictError in the world-vercel error mapper.
workflow-server PR 447 returns 412 with a 'fence conflict' message
for EventLogFenceConflictError; the SDK's existing fence-retry loops
(Peter's wait_completed scan + the new ones in suspension-handler
and runtime terminal writes) match on /fence conflict/i against the
message of an EntityConflictError. Without this mapping the 412 falls
through to WorkflowWorldError and the regex match never fires.
@github-actions

github-actionsBot commented May 28, 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.029s (-31.8% 🟢)1.005s (~)0.976s101.00x
💻 LocalExpress0.030s (-31.6% 🟢)1.005s (~)0.975s101.03x
💻 LocalNext.js (Turbopack)0.042s1.004s0.962s101.43x
🐘 PostgresNext.js (Turbopack)0.046s1.009s0.963s101.56x
🐘 PostgresExpress0.046s (-20.5% 🟢)1.012s (~)0.966s101.57x
🐘 PostgresNitro0.050s (-47.3% 🟢)1.012s (-2.9%)0.962s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.233s (-0.8%)1.941s (-9.1% 🟢)1.707s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.072s (-5.2% 🟢)2.006s (~)0.934s101.00x
💻 LocalExpress1.073s (-4.6%)2.006s (~)0.933s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.009s (~)0.925s101.01x
🐘 PostgresNitro1.086s (-4.7%)2.008s (~)0.922s101.01x
💻 LocalNext.js (Turbopack)1.089s2.005s0.916s101.02x
🐘 PostgresNext.js (Turbopack)1.101s2.008s0.907s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.767s (-5.7% 🟢)3.654s (-4.0%)1.887s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.410s (-4.9%)11.023s (~)0.613s31.00x
💻 LocalExpress10.418s (-4.6%)11.023s (~)0.605s31.00x
🐘 PostgresExpress10.424s (-4.9%)11.019s (~)0.595s31.00x
🐘 PostgresNitro10.430s (-4.1%)11.014s (~)0.584s31.00x
💻 LocalNext.js (Turbopack)10.557s11.022s0.465s31.01x
🐘 PostgresNext.js (Turbopack)10.623s11.018s0.394s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.910s (-18.1% 🟢)15.954s (-20.3% 🟢)2.044s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.467s (-10.0% 🟢)14.027s (-6.7% 🟢)0.560s51.00x
💻 LocalNitro13.488s (-10.5% 🟢)14.027s (-12.5% 🟢)0.539s51.00x
🐘 PostgresExpress13.515s (-7.3% 🟢)14.018s (-6.7% 🟢)0.503s51.00x
🐘 PostgresNitro13.573s (-7.0% 🟢)14.023s (-6.7% 🟢)0.450s51.01x
💻 LocalNext.js (Turbopack)13.808s14.026s0.218s51.03x
🐘 PostgresNext.js (Turbopack)13.860s14.017s0.157s51.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.720s (-58.8% 🟢)22.449s (-57.3% 🟢)1.729s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.978s (-28.6% 🟢)12.273s (-27.9% 🟢)0.295s81.00x
💻 LocalExpress11.995s (-27.7% 🟢)12.398s (-27.2% 🟢)0.403s81.00x
🐘 PostgresExpress12.247s (-12.6% 🟢)12.876s (-11.8% 🟢)0.629s71.02x
🐘 PostgresNitro12.256s (-12.3% 🟢)13.019s (-9.0% 🟢)0.763s71.02x
💻 LocalNext.js (Turbopack)12.585s13.023s0.438s71.05x
🐘 PostgresNext.js (Turbopack)12.744s13.013s0.270s71.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.522s (-76.5% 🟢)30.389s (-75.4% 🟢)1.867s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.4% 🟢)2.007s (~)0.865s151.00x
🐘 PostgresNitro1.164s (-8.7% 🟢)2.007s (~)0.844s151.02x
💻 LocalExpress1.170s (-21.4% 🟢)2.006s (~)0.836s151.02x
💻 LocalNitro1.172s (-28.1% 🟢)2.006s (-3.3%)0.834s151.03x
🐘 PostgresNext.js (Turbopack)1.175s2.006s0.832s151.03x
💻 LocalNext.js (Turbopack)1.211s2.005s0.794s151.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express103.949s (+3534.8% 🔺)105.665s (+2185.6% 🔺)1.716s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.215s (-48.6% 🟢)2.007s (-33.3% 🟢)0.793s151.00x
🐘 PostgresNitro1.221s (-48.1% 🟢)2.008s (-33.3% 🟢)0.787s151.01x
🐘 PostgresNext.js (Turbopack)1.273s2.005s0.733s151.05x
💻 LocalNext.js (Turbopack)1.514s2.005s0.492s151.25x
💻 LocalNitro1.682s (-46.5% 🟢)2.006s (-48.4% 🟢)0.323s151.39x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.300s151.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.692s (+2.0%)5.373s (+5.1% 🔺)1.681s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.338s (-61.6% 🟢)2.007s (-50.0% 🟢)0.669s151.00x
🐘 PostgresNitro1.353s (-61.1% 🟢)2.007s (-49.9% 🟢)0.654s151.01x
🐘 PostgresNext.js (Turbopack)1.479s2.006s0.528s151.10x
💻 LocalNext.js (Turbopack)3.944s4.295s0.350s72.95x
💻 LocalNitro4.618s (-44.7% 🟢)5.010s (-44.5% 🟢)0.392s63.45x
💻 LocalExpress4.704s (-43.6% 🟢)5.180s (-42.6% 🟢)0.476s63.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.082s (+19.9% 🔺)7.070s (+15.4% 🔺)1.987s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.151s (-8.4% 🟢)2.007s (~)0.856s151.00x
🐘 PostgresNitro1.162s (-7.5% 🟢)2.010s (~)0.848s151.01x
🐘 PostgresNext.js (Turbopack)1.170s2.007s0.837s151.02x
💻 LocalNext.js (Turbopack)1.265s2.005s0.740s151.10x
💻 LocalExpress1.443s (-23.8% 🟢)2.006s (-15.1% 🟢)0.563s151.25x
💻 LocalNitro1.745s (-6.5% 🟢)2.392s (+2.2%)0.646s131.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.525s (-2.2%)4.081s (-6.2% 🟢)1.556s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.234s (-47.3% 🟢)2.008s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNitro1.235s (-47.2% 🟢)2.009s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNext.js (Turbopack)1.273s2.009s0.736s151.03x
💻 LocalNext.js (Turbopack)1.812s2.072s0.260s151.47x
💻 LocalNitro1.869s (-39.0% 🟢)2.316s (-40.4% 🟢)0.447s131.51x
💻 LocalExpress1.913s (-38.9% 🟢)2.316s (-38.5% 🟢)0.402s131.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.712s (+16.3% 🔺)5.226s (+9.0% 🔺)1.514s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.351s (-61.4% 🟢)2.008s (-49.9% 🟢)0.657s151.00x
🐘 PostgresNitro1.376s (-60.5% 🟢)2.009s (-49.9% 🟢)0.633s151.02x
🐘 PostgresNext.js (Turbopack)1.477s2.006s0.529s151.09x
💻 LocalNitro4.542s (-50.3% 🟢)5.179s (-48.3% 🟢)0.637s63.36x
💻 LocalNext.js (Turbopack)4.602s5.011s0.410s73.41x
💻 LocalExpress6.036s (-31.4% 🟢)6.416s (-30.8% 🟢)0.380s54.47x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.559s (+2.2%)8.517s (+4.1%)1.959s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.427s (-49.1% 🟢)1.007s (-1.6%)0.580s601.00x
🐘 PostgresNitro0.486s (-40.7% 🟢)1.007s (~)0.521s601.14x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.7% 🟢)0.526s591.16x
💻 LocalExpress0.512s (-48.0% 🟢)1.021s (-5.1% 🟢)0.509s591.20x
🐘 PostgresNext.js (Turbopack)0.546s1.005s0.460s601.28x
💻 LocalNext.js (Turbopack)0.639s1.004s0.365s601.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.726s (-69.9% 🟢)7.306s (-65.7% 🟢)1.580s91.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.156s (-41.5% 🟢)1.903s (-15.7% 🟢)0.747s481.00x
💻 LocalNitro1.221s (-59.8% 🟢)2.005s (-46.6% 🟢)0.784s451.06x
🐘 PostgresNitro1.222s (-36.6% 🟢)2.030s (-3.4%)0.808s451.06x
💻 LocalExpress1.272s (-57.8% 🟢)2.006s (-44.1% 🟢)0.734s451.10x
🐘 PostgresNext.js (Turbopack)1.342s2.006s0.664s451.16x
💻 LocalNext.js (Turbopack)1.617s2.027s0.410s451.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.876s (-62.7% 🟢)14.564s (-60.4% 🟢)1.688s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.181s (-45.3% 🟢)2.984s (-31.7% 🟢)0.803s411.00x
🐘 PostgresNitro2.333s (-43.1% 🟢)3.060s (-33.5% 🟢)0.726s401.07x
🐘 PostgresNext.js (Turbopack)2.651s3.007s0.356s401.22x
💻 LocalNitro2.771s (-70.2% 🟢)3.032s (-69.7% 🟢)0.261s401.27x
💻 LocalExpress2.878s (-68.7% 🟢)3.136s (-68.7% 🟢)0.257s391.32x
💻 LocalNext.js (Turbopack)3.407s4.008s0.600s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.597s (-80.3% 🟢)27.381s (-79.3% 🟢)1.784s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.175s (-38.2% 🟢)1.006s (~)0.831s601.00x
🐘 PostgresNext.js (Turbopack)0.184s1.005s0.821s601.05x
🐘 PostgresNitro0.200s (-29.4% 🟢)1.006s (~)0.806s601.14x
💻 LocalExpress0.386s (-31.2% 🟢)1.004s (~)0.618s602.21x
💻 LocalNitro0.386s (-36.2% 🟢)1.004s (-1.7%)0.618s602.21x
💻 LocalNext.js (Turbopack)0.474s1.004s0.529s602.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.161s (+10.6% 🔺)3.810s (+4.8%)1.649s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.298s (-41.5% 🟢)1.006s (~)0.708s901.00x
🐘 PostgresNitro0.317s (-36.1% 🟢)1.006s (~)0.689s901.06x
🐘 PostgresNext.js (Turbopack)0.341s1.005s0.664s901.14x
💻 LocalNext.js (Turbopack)2.026s2.536s0.510s366.80x
💻 LocalNitro2.118s (-16.6% 🟢)2.610s (-13.3% 🟢)0.492s357.10x
💻 LocalExpress2.170s (-13.7% 🟢)2.822s (-6.2% 🟢)0.652s327.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.148s (+36.1% 🔺)5.654s (+17.6% 🔺)1.507s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.652s (-20.3% 🟢)1.006s (-1.1%)0.354s1201.00x
🐘 PostgresNext.js (Turbopack)0.681s1.005s0.324s1201.04x
🐘 PostgresNitro0.688s (-13.0% 🟢)1.006s (~)0.319s1201.05x
💻 LocalNext.js (Turbopack)9.197s9.718s0.521s1314.10x
💻 LocalNitro9.393s (-16.1% 🟢)10.024s (-14.1% 🟢)0.632s1214.40x
💻 LocalExpress10.171s (-9.1% 🟢)10.695s (-10.4% 🟢)0.524s1215.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.240s (+78.4% 🔺)15.181s (+64.2% 🔺)1.942s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+467.9% 🔺)2.005s (+99.6% 🔺)0.010s (-14.9% 🟢)2.017s (+98.2% 🔺)0.887s101.00x
💻 LocalNitro1.136s (+431.6% 🔺)2.004s (+99.5% 🔺)0.010s (-18.4% 🟢)2.017s (+98.0% 🔺)0.881s101.00x
🐘 PostgresExpress1.137s (+454.2% 🔺)2.001s (+100.3% 🔺)0.001s (-25.0% 🟢)2.011s (+98.8% 🔺)0.874s101.01x
💻 LocalNext.js (Turbopack)1.147s2.004s0.008s2.015s0.868s101.01x
🐘 PostgresNext.js (Turbopack)1.155s2.001s0.001s2.008s0.852s101.02x
🐘 PostgresNitro1.164s (+467.6% 🔺)2.000s (+100.0% 🔺)0.002s (+13.3% 🔺)2.013s (+99.0% 🔺)0.849s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.134s (-14.8% 🟢)3.175s (-22.4% 🟢)2.057s (+114.0% 🔺)5.682s (+1.6%)3.549s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.506s (+139.1% 🔺)2.003s (+99.0% 🔺)0.004s (+5.3% 🔺)2.024s (+97.9% 🔺)0.518s301.00x
💻 LocalNitro1.518s (+81.0% 🔺)2.012s (+98.8% 🔺)0.009s (-0.8%)2.023s (+81.3% 🔺)0.506s301.01x
💻 LocalExpress1.520s (+100.8% 🔺)2.012s (+95.5% 🔺)0.009s (-0.9%)2.023s (+94.5% 🔺)0.503s301.01x
💻 LocalNext.js (Turbopack)1.546s2.008s0.009s2.020s0.474s301.03x
🐘 PostgresNitro1.591s (+154.9% 🔺)2.007s (+99.3% 🔺)0.004s (-3.3%)2.026s (+98.2% 🔺)0.436s301.06x
🐘 PostgresNext.js (Turbopack)1.632s2.041s0.003s2.053s0.421s301.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.080s (-6.5% 🟢)7.362s (-8.1% 🟢)0.403s (-1.4%)8.205s (-7.1% 🟢)2.125s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.645s (-32.9% 🟢)1.016s (-20.5% 🟢)0.000s (-61.0% 🟢)1.029s (-21.2% 🟢)0.384s591.00x
🐘 PostgresNitro0.669s (-31.0% 🟢)1.033s (-17.2% 🟢)0.000s (+65.5% 🔺)1.048s (-16.7% 🟢)0.379s581.04x
🐘 PostgresNext.js (Turbopack)0.878s1.088s0.000s1.186s0.308s511.36x
💻 LocalNext.js (Turbopack)1.198s1.860s0.000s1.863s0.665s331.86x
💻 LocalNitro1.353s (+10.7% 🔺)2.016s (~)0.000s (+33.3% 🔺)2.018s (~)0.664s302.10x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.649s302.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.631s (-2.9%)4.743s (-7.0% 🟢)0.000s (-100.0% 🟢)5.186s (-6.2% 🟢)1.555s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.282s (-28.5% 🟢)1.996s (-6.8% 🟢)0.000s (+86.7% 🔺)2.048s (-5.8% 🟢)0.766s301.00x
🐘 PostgresExpress1.312s (-26.0% 🟢)2.031s (-6.7% 🟢)0.000s (+Infinity% 🔺)2.054s (-6.6% 🟢)0.742s301.02x
🐘 PostgresNext.js (Turbopack)1.673s2.401s0.000s2.435s0.762s251.30x
💻 LocalNext.js (Turbopack)2.491s3.122s0.000s3.129s0.637s201.94x
💻 LocalNitro3.140s (-7.3% 🟢)3.965s (-1.7%)0.000s (-18.0% 🟢)3.968s (-1.7%)0.828s162.45x
💻 LocalExpress3.291s (-5.1% 🟢)4.026s (~)0.000s (-41.7% 🟢)4.031s (~)0.740s152.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.296s (+15.5% 🔺)6.741s (+11.9% 🔺)0.001s (+Infinity% 🔺)7.181s (+11.2% 🔺)1.884s91.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress19/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@github-actions

github-actionsBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production122202191441
✅ 💻 Local Development161502191834
✅ 📦 Local Production161502191834
✅ 🐘 Local Postgres161502191834
✅ 🪟 Windows13100131
✅ 📋 Other7410176917
Total6939010527991

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro105026
✅ example105026
✅ express105026
✅ fastify105026
✅ hono105026
✅ nextjs-turbopack12902
✅ nextjs-webpack12902
✅ nitro105026
✅ nuxt105026
✅ sveltekit12407
✅ vite105026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13100
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable106025
✅ e2e-local-dev-tanstack-start-106025
✅ e2e-local-postgres-nest-stable106025
✅ e2e-local-postgres-tanstack-start-106025
✅ e2e-local-prod-nest-stable106025
✅ e2e-local-prod-tanstack-start-106025
✅ e2e-vercel-prod-tanstack-start105026

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's merge into upstream branch? and then review as one

Strip the WF_TRACE replay tracing that was used to diagnose the
CORRUPTED_EVENT_LOG race \u2014 it's served its purpose now that the fix
is in. Specifically:
- Delete packages/core/src/__debug-replay-trace.ts and its 8 call sites
in workflow.ts, step.ts, workflow/hook.ts, workflow/sleep.ts.
- Drop the matching [DEBUG] inline narrative comments at each call site.
- Rename packages/core/src/runtime/__fenced-write.ts \u2192 fenced-write.ts
(the leading-underscore convention marked it as throwaway diagnostic
code; the helper is intended to stay).
- Trim the file header on fenced-write.ts and the related narrative
comment in suspension-handler.ts to drop the failing-runId / PR-number
references that only made sense in the debug context.
No behavioral change. typecheck clean (0 errors); 1014/1014 unit tests
pass (same as parent commit 77f057a).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Extends optimistic concurrency control (OCC) “fencing” to additional workflow event writes whose correctness depends on branch decisions made from a potentially stale event-log snapshot, and adjusts step dispatch behavior to avoid duplicate execution under concurrent replay.

Changes:

  • Introduce a shared fencedEventCreate helper to retry CAS-fenced writes with refresh + idempotency checks.
  • Apply fencing to step_created / wait_created / hook create+dispose paths in handleSuspension, and to terminal run_completed / run_failed writes in runtime.ts.
  • Improve local queue idempotency by retaining completed idempotency keys (plus tests/docs updates).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsMap HTTP 412 OCC fence conflicts into EntityConflictError for downstream handling.
packages/world-testing/src/inline-batches-debug.mtsTighten debug assertions for race/skip counters.
packages/world-local/src/queue.tsAdd a bounded cache of completed idempotency keys to prevent post-completion duplicate dispatch.
packages/world-local/src/queue.test.tsAdd coverage for post-completion idempotency dedupe behavior.
packages/core/src/workflow/hook.tsMinor formatting-only change.
packages/core/src/step.tsMinor formatting-only change.
packages/core/src/runtime/suspension-handler.tsFence branch-decision writes and serialize them to avoid self-conflicts; add refresh/idempotency logic.
packages/core/src/runtime/suspension-handler.test.tsNew test ensuring the fence is chained across writes within a suspension.
packages/core/src/runtime/fenced-write.tsNew shared helper implementing fence-conflict retries with backoff and caller-provided refresh/idempotency checks.
packages/core/src/runtime.tsFence terminal writes; owner-scope step queueing when inline execution is possible; crash-recovery exception for redelivery.
docs/content/docs/v5/changelog/eager-processing.mdxUpdate documentation to reflect owner-scoped queueing semantics.
docs/content/docs/v4/changelog/eager-processing.mdxSame as v5 doc update for v4 docs.
.changeset/quick-local-queues.mdChangeset for local queue idempotency behavior.
.changeset/chilly-fences-chain.mdChangeset for core fencing + dispatch changes.

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

Comment threadpackages/world-vercel/src/utils.ts
Address Copilot review on PR 2132 (#2132 (comment)).
The fence-retry loop in runtime/fenced-write.ts detects OCC conflicts
via /fence conflict/i.test(err.message). The 412 branch was relying on
the server's JSON body to populate that message via errorData.message,
but parseResponseBody().catch(() => ({})) swallows JSON parse failures
silently — so any non-JSON 412 response (CDN HTML, gateway timeout
page, intermediate proxy error) would surface as
EntityConflictError("<METHOD> /endpoint -> HTTP 412: Precondition
Failed"), the regex would miss it, and the retry loop would
mis-classify the conflict as terminal.
Prefix the message with `fence conflict:` whenever the parsed body
didn't already carry the marker, so the retry detection is robust to
response-body parse failures.
Tests: world-vercel 69/69 pass.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@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); } })(); })(); [core] Extend OCC fence to all branch-decision writes by TooTallNate · Pull Request #2132 · vercel/workflow · GitHub
Skip to content

[core] Extend OCC fence to all branch-decision writes - #2132

Merged
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage
May 28, 2026
Merged

[core] Extend OCC fence to all branch-decision writes#2132
TooTallNate merged 9 commits into
peter/sdk-event-write-casfrom
debug/extend-fence-coverage

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 28, 2026

Copy link
Copy Markdown
Member

Summary

Layered on top of #2113. Extends the OCC fence — which currently covers only wait_completed writes from the elapsed-wait scan — to every other write whose outcome depends on a branch decision the workflow VM made from its loaded event log:

Write siteStatus
wait_completed (elapsed-wait scan)already fenced by #2113
step_creatednow fenced
wait_creatednow fenced
hook_creatednow fenced
hook_disposednow fenced
run_completednow fenced
run_failednow fenced
hook_receiveddeliberately not fenced (preserved from #2113)

hook_received stays unfenced for the same reason it's unfenced in #2113: fencing the user's signal would drop it on contention. Stale-snapshot protection belongs on the writes that consume hooks, not the writes that deliver them.

Why

Under concurrent replay (two invocations of the same run with overlapping but non-identical event-log snapshots), the SDK's deterministic ULID factory can allocate the same correlationId for different step calls depending on which side of a Promise.race each replay sees as the winner. Both invocations then try to write step_created for the same correlationId with different stepNames. The server's entity idempotency rejects the loser at the entity level — but if the winner was the stale-view invocation, its branch decision is the one that lands in the log, and future replays will see a step_created they don't expect.

Fencing the write makes the stale-view invocation lose the CAS, retry against the freshly-loaded tail, observe the events its snapshot was missing, and arrive at the same branch decision the authoritative invocation made.

Implementation

The retry loop from #2113's elapsed-wait scan is extracted into packages/core/src/runtime/fenced-write.ts so the six new fenced sites can share it without copy-paste. Each call site provides its own onConflictRefresh that runs an idempotency check against the reloaded log (e.g. "is this wait_created already in the log? → abort instead of retrying").

handleSuspension receives the load-time tail eventId + cursor from the runtime, and runs all four of its fenced writes (step_created, hook_created, hook_disposed, wait_created) against that fence. Each successful write advances the fence so chained writes in the same suspension don't conflict with each other.

The terminal run_completed / run_failed writes in runtime.ts use the same helper, with the idempotency check verifying no other terminal event has landed since the snapshot.

Test plan

  • All 1014 @workflow/core unit tests pass.
  • TypeScript clean (0 errors).
  • End-to-end stress: re-ran the same 40-attempt hammer that exposed the underlying race against the matching workflow-server preview — 0 / 40 failures (vs ~2 / 40 baseline on stable, and 5 / 40 against an alternative server-side fix that rewrites event IDs but doesn't fence). Verified each successful run has run.lastKnownEventId materialized as expected.

Temporary diagnostic instrumentation for investigating intermittent
CorruptedEventLogError 'step consumer mismatch' failures.
Emits console.log lines tagged 'WF_TRACE' at four points:
- runWorkflow start: dumps the full event array the replay will consume
(eventIds, types, correlationIds, stepNames) plus a sha256 digest
- step/hook/sleep subscribe: per-replay correlationId -> name assignment
- step consumer mismatch: structured record of the failure including the
event index in the SDK's view of the log
- runWorkflow end: completed | failed | suspended
Used to diff successive replays of the same runId and confirm whether
the SDK actually sees the same event array each time.
Peter's PR #2113 fences `wait_completed` writes from the elapsed-wait
scan. This commit extends the fence to every other write whose outcome
depends on a branch decision the workflow VM made from its loaded event
log — per the table @VaguelySerious himself laid out in his PR comment:
suspension-handler.ts:
- step_created (the smoking gun on wrun_01KSPS7XEGHF4A6WYF4DB03D40)
- hook_created
- hook_disposed
- wait_created
runtime.ts terminal writes:
- run_completed
- run_failed
`hook_received` is deliberately NOT fenced (Peter's reasoning preserved
verbatim: fencing the user's signal would drop it on contention; stale-
snapshot protection belongs on the writes that consume hooks, not the
ones that deliver them).
The fence value is the load-time tail of the events array passed into
`runWorkflow`. `suspension-handler` receives the fence + cursor from
the runtime and reloads on conflict; the runtime's terminal writes read
the cursor directly.
The new `__fenced-write.ts` helper encapsulates the retry loop so we
don't have to copy/paste Peter's pattern six times. It's named with the
leading-underscore convention to flag it as throwaway diagnostic code,
matching `__debug-replay-trace.ts`.
@vercel

vercelBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a7efa5a

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

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

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

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

@TooTallNate
TooTallNate changed the base branch from stable to mainMay 28, 2026 08:29
Two changes both needed for the extended-fence test loop to actually
exercise the OCC code path on the server:
1. Hardcode WORKFLOW_SERVER_URL_OVERRIDE to
https://workflow-server-83nn57dvc.vercel.sh (preview deployment of
workflow-server PR 447, branch alias
workflow-server-git-peter-event-write-cas.vercel.sh). The previous
preview at workflow-server-7pxaxn4d4.vercel.sh was Pranay's monotonic-
append PR 456 \u2014 different fix, doesn't implement the CAS the SDK side
now sends.
2. Map HTTP 412 \u2192 EntityConflictError in the world-vercel error mapper.
workflow-server PR 447 returns 412 with a 'fence conflict' message
for EventLogFenceConflictError; the SDK's existing fence-retry loops
(Peter's wait_completed scan + the new ones in suspension-handler
and runtime terminal writes) match on /fence conflict/i against the
message of an EntityConflictError. Without this mapping the 412 falls
through to WorkflowWorldError and the regex match never fires.
@github-actions

github-actionsBot commented May 28, 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.029s (-31.8% 🟢)1.005s (~)0.976s101.00x
💻 LocalExpress0.030s (-31.6% 🟢)1.005s (~)0.975s101.03x
💻 LocalNext.js (Turbopack)0.042s1.004s0.962s101.43x
🐘 PostgresNext.js (Turbopack)0.046s1.009s0.963s101.56x
🐘 PostgresExpress0.046s (-20.5% 🟢)1.012s (~)0.966s101.57x
🐘 PostgresNitro0.050s (-47.3% 🟢)1.012s (-2.9%)0.962s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.233s (-0.8%)1.941s (-9.1% 🟢)1.707s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.072s (-5.2% 🟢)2.006s (~)0.934s101.00x
💻 LocalExpress1.073s (-4.6%)2.006s (~)0.933s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.009s (~)0.925s101.01x
🐘 PostgresNitro1.086s (-4.7%)2.008s (~)0.922s101.01x
💻 LocalNext.js (Turbopack)1.089s2.005s0.916s101.02x
🐘 PostgresNext.js (Turbopack)1.101s2.008s0.907s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.767s (-5.7% 🟢)3.654s (-4.0%)1.887s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.410s (-4.9%)11.023s (~)0.613s31.00x
💻 LocalExpress10.418s (-4.6%)11.023s (~)0.605s31.00x
🐘 PostgresExpress10.424s (-4.9%)11.019s (~)0.595s31.00x
🐘 PostgresNitro10.430s (-4.1%)11.014s (~)0.584s31.00x
💻 LocalNext.js (Turbopack)10.557s11.022s0.465s31.01x
🐘 PostgresNext.js (Turbopack)10.623s11.018s0.394s31.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.910s (-18.1% 🟢)15.954s (-20.3% 🟢)2.044s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.467s (-10.0% 🟢)14.027s (-6.7% 🟢)0.560s51.00x
💻 LocalNitro13.488s (-10.5% 🟢)14.027s (-12.5% 🟢)0.539s51.00x
🐘 PostgresExpress13.515s (-7.3% 🟢)14.018s (-6.7% 🟢)0.503s51.00x
🐘 PostgresNitro13.573s (-7.0% 🟢)14.023s (-6.7% 🟢)0.450s51.01x
💻 LocalNext.js (Turbopack)13.808s14.026s0.218s51.03x
🐘 PostgresNext.js (Turbopack)13.860s14.017s0.157s51.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.720s (-58.8% 🟢)22.449s (-57.3% 🟢)1.729s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.978s (-28.6% 🟢)12.273s (-27.9% 🟢)0.295s81.00x
💻 LocalExpress11.995s (-27.7% 🟢)12.398s (-27.2% 🟢)0.403s81.00x
🐘 PostgresExpress12.247s (-12.6% 🟢)12.876s (-11.8% 🟢)0.629s71.02x
🐘 PostgresNitro12.256s (-12.3% 🟢)13.019s (-9.0% 🟢)0.763s71.02x
💻 LocalNext.js (Turbopack)12.585s13.023s0.438s71.05x
🐘 PostgresNext.js (Turbopack)12.744s13.013s0.270s71.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express28.522s (-76.5% 🟢)30.389s (-75.4% 🟢)1.867s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.142s (-9.4% 🟢)2.007s (~)0.865s151.00x
🐘 PostgresNitro1.164s (-8.7% 🟢)2.007s (~)0.844s151.02x
💻 LocalExpress1.170s (-21.4% 🟢)2.006s (~)0.836s151.02x
💻 LocalNitro1.172s (-28.1% 🟢)2.006s (-3.3%)0.834s151.03x
🐘 PostgresNext.js (Turbopack)1.175s2.006s0.832s151.03x
💻 LocalNext.js (Turbopack)1.211s2.005s0.794s151.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express103.949s (+3534.8% 🔺)105.665s (+2185.6% 🔺)1.716s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.215s (-48.6% 🟢)2.007s (-33.3% 🟢)0.793s151.00x
🐘 PostgresNitro1.221s (-48.1% 🟢)2.008s (-33.3% 🟢)0.787s151.01x
🐘 PostgresNext.js (Turbopack)1.273s2.005s0.733s151.05x
💻 LocalNext.js (Turbopack)1.514s2.005s0.492s151.25x
💻 LocalNitro1.682s (-46.5% 🟢)2.006s (-48.4% 🟢)0.323s151.39x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.300s151.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.692s (+2.0%)5.373s (+5.1% 🔺)1.681s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.338s (-61.6% 🟢)2.007s (-50.0% 🟢)0.669s151.00x
🐘 PostgresNitro1.353s (-61.1% 🟢)2.007s (-49.9% 🟢)0.654s151.01x
🐘 PostgresNext.js (Turbopack)1.479s2.006s0.528s151.10x
💻 LocalNext.js (Turbopack)3.944s4.295s0.350s72.95x
💻 LocalNitro4.618s (-44.7% 🟢)5.010s (-44.5% 🟢)0.392s63.45x
💻 LocalExpress4.704s (-43.6% 🟢)5.180s (-42.6% 🟢)0.476s63.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.082s (+19.9% 🔺)7.070s (+15.4% 🔺)1.987s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.151s (-8.4% 🟢)2.007s (~)0.856s151.00x
🐘 PostgresNitro1.162s (-7.5% 🟢)2.010s (~)0.848s151.01x
🐘 PostgresNext.js (Turbopack)1.170s2.007s0.837s151.02x
💻 LocalNext.js (Turbopack)1.265s2.005s0.740s151.10x
💻 LocalExpress1.443s (-23.8% 🟢)2.006s (-15.1% 🟢)0.563s151.25x
💻 LocalNitro1.745s (-6.5% 🟢)2.392s (+2.2%)0.646s131.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.525s (-2.2%)4.081s (-6.2% 🟢)1.556s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.234s (-47.3% 🟢)2.008s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNitro1.235s (-47.2% 🟢)2.009s (-33.3% 🟢)0.774s151.00x
🐘 PostgresNext.js (Turbopack)1.273s2.009s0.736s151.03x
💻 LocalNext.js (Turbopack)1.812s2.072s0.260s151.47x
💻 LocalNitro1.869s (-39.0% 🟢)2.316s (-40.4% 🟢)0.447s131.51x
💻 LocalExpress1.913s (-38.9% 🟢)2.316s (-38.5% 🟢)0.402s131.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.712s (+16.3% 🔺)5.226s (+9.0% 🔺)1.514s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.351s (-61.4% 🟢)2.008s (-49.9% 🟢)0.657s151.00x
🐘 PostgresNitro1.376s (-60.5% 🟢)2.009s (-49.9% 🟢)0.633s151.02x
🐘 PostgresNext.js (Turbopack)1.477s2.006s0.529s151.09x
💻 LocalNitro4.542s (-50.3% 🟢)5.179s (-48.3% 🟢)0.637s63.36x
💻 LocalNext.js (Turbopack)4.602s5.011s0.410s73.41x
💻 LocalExpress6.036s (-31.4% 🟢)6.416s (-30.8% 🟢)0.380s54.47x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.559s (+2.2%)8.517s (+4.1%)1.959s41.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.427s (-49.1% 🟢)1.007s (-1.6%)0.580s601.00x
🐘 PostgresNitro0.486s (-40.7% 🟢)1.007s (~)0.521s601.14x
💻 LocalNitro0.495s (-49.5% 🟢)1.021s (-6.7% 🟢)0.526s591.16x
💻 LocalExpress0.512s (-48.0% 🟢)1.021s (-5.1% 🟢)0.509s591.20x
🐘 PostgresNext.js (Turbopack)0.546s1.005s0.460s601.28x
💻 LocalNext.js (Turbopack)0.639s1.004s0.365s601.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.726s (-69.9% 🟢)7.306s (-65.7% 🟢)1.580s91.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.156s (-41.5% 🟢)1.903s (-15.7% 🟢)0.747s481.00x
💻 LocalNitro1.221s (-59.8% 🟢)2.005s (-46.6% 🟢)0.784s451.06x
🐘 PostgresNitro1.222s (-36.6% 🟢)2.030s (-3.4%)0.808s451.06x
💻 LocalExpress1.272s (-57.8% 🟢)2.006s (-44.1% 🟢)0.734s451.10x
🐘 PostgresNext.js (Turbopack)1.342s2.006s0.664s451.16x
💻 LocalNext.js (Turbopack)1.617s2.027s0.410s451.40x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.876s (-62.7% 🟢)14.564s (-60.4% 🟢)1.688s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.181s (-45.3% 🟢)2.984s (-31.7% 🟢)0.803s411.00x
🐘 PostgresNitro2.333s (-43.1% 🟢)3.060s (-33.5% 🟢)0.726s401.07x
🐘 PostgresNext.js (Turbopack)2.651s3.007s0.356s401.22x
💻 LocalNitro2.771s (-70.2% 🟢)3.032s (-69.7% 🟢)0.261s401.27x
💻 LocalExpress2.878s (-68.7% 🟢)3.136s (-68.7% 🟢)0.257s391.32x
💻 LocalNext.js (Turbopack)3.407s4.008s0.600s301.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express25.597s (-80.3% 🟢)27.381s (-79.3% 🟢)1.784s51.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.175s (-38.2% 🟢)1.006s (~)0.831s601.00x
🐘 PostgresNext.js (Turbopack)0.184s1.005s0.821s601.05x
🐘 PostgresNitro0.200s (-29.4% 🟢)1.006s (~)0.806s601.14x
💻 LocalExpress0.386s (-31.2% 🟢)1.004s (~)0.618s602.21x
💻 LocalNitro0.386s (-36.2% 🟢)1.004s (-1.7%)0.618s602.21x
💻 LocalNext.js (Turbopack)0.474s1.004s0.529s602.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.161s (+10.6% 🔺)3.810s (+4.8%)1.649s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.298s (-41.5% 🟢)1.006s (~)0.708s901.00x
🐘 PostgresNitro0.317s (-36.1% 🟢)1.006s (~)0.689s901.06x
🐘 PostgresNext.js (Turbopack)0.341s1.005s0.664s901.14x
💻 LocalNext.js (Turbopack)2.026s2.536s0.510s366.80x
💻 LocalNitro2.118s (-16.6% 🟢)2.610s (-13.3% 🟢)0.492s357.10x
💻 LocalExpress2.170s (-13.7% 🟢)2.822s (-6.2% 🟢)0.652s327.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.148s (+36.1% 🔺)5.654s (+17.6% 🔺)1.507s161.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.652s (-20.3% 🟢)1.006s (-1.1%)0.354s1201.00x
🐘 PostgresNext.js (Turbopack)0.681s1.005s0.324s1201.04x
🐘 PostgresNitro0.688s (-13.0% 🟢)1.006s (~)0.319s1201.05x
💻 LocalNext.js (Turbopack)9.197s9.718s0.521s1314.10x
💻 LocalNitro9.393s (-16.1% 🟢)10.024s (-14.1% 🟢)0.632s1214.40x
💻 LocalExpress10.171s (-9.1% 🟢)10.695s (-10.4% 🟢)0.524s1215.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.240s (+78.4% 🔺)15.181s (+64.2% 🔺)1.942s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.131s (+467.9% 🔺)2.005s (+99.6% 🔺)0.010s (-14.9% 🟢)2.017s (+98.2% 🔺)0.887s101.00x
💻 LocalNitro1.136s (+431.6% 🔺)2.004s (+99.5% 🔺)0.010s (-18.4% 🟢)2.017s (+98.0% 🔺)0.881s101.00x
🐘 PostgresExpress1.137s (+454.2% 🔺)2.001s (+100.3% 🔺)0.001s (-25.0% 🟢)2.011s (+98.8% 🔺)0.874s101.01x
💻 LocalNext.js (Turbopack)1.147s2.004s0.008s2.015s0.868s101.01x
🐘 PostgresNext.js (Turbopack)1.155s2.001s0.001s2.008s0.852s101.02x
🐘 PostgresNitro1.164s (+467.6% 🔺)2.000s (+100.0% 🔺)0.002s (+13.3% 🔺)2.013s (+99.0% 🔺)0.849s101.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.134s (-14.8% 🟢)3.175s (-22.4% 🟢)2.057s (+114.0% 🔺)5.682s (+1.6%)3.549s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.506s (+139.1% 🔺)2.003s (+99.0% 🔺)0.004s (+5.3% 🔺)2.024s (+97.9% 🔺)0.518s301.00x
💻 LocalNitro1.518s (+81.0% 🔺)2.012s (+98.8% 🔺)0.009s (-0.8%)2.023s (+81.3% 🔺)0.506s301.01x
💻 LocalExpress1.520s (+100.8% 🔺)2.012s (+95.5% 🔺)0.009s (-0.9%)2.023s (+94.5% 🔺)0.503s301.01x
💻 LocalNext.js (Turbopack)1.546s2.008s0.009s2.020s0.474s301.03x
🐘 PostgresNitro1.591s (+154.9% 🔺)2.007s (+99.3% 🔺)0.004s (-3.3%)2.026s (+98.2% 🔺)0.436s301.06x
🐘 PostgresNext.js (Turbopack)1.632s2.041s0.003s2.053s0.421s301.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.080s (-6.5% 🟢)7.362s (-8.1% 🟢)0.403s (-1.4%)8.205s (-7.1% 🟢)2.125s81.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.645s (-32.9% 🟢)1.016s (-20.5% 🟢)0.000s (-61.0% 🟢)1.029s (-21.2% 🟢)0.384s591.00x
🐘 PostgresNitro0.669s (-31.0% 🟢)1.033s (-17.2% 🟢)0.000s (+65.5% 🔺)1.048s (-16.7% 🟢)0.379s581.04x
🐘 PostgresNext.js (Turbopack)0.878s1.088s0.000s1.186s0.308s511.36x
💻 LocalNext.js (Turbopack)1.198s1.860s0.000s1.863s0.665s331.86x
💻 LocalNitro1.353s (+10.7% 🔺)2.016s (~)0.000s (+33.3% 🔺)2.018s (~)0.664s302.10x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-30.0% 🟢)2.017s (~)0.649s302.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.631s (-2.9%)4.743s (-7.0% 🟢)0.000s (-100.0% 🟢)5.186s (-6.2% 🟢)1.555s121.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.282s (-28.5% 🟢)1.996s (-6.8% 🟢)0.000s (+86.7% 🔺)2.048s (-5.8% 🟢)0.766s301.00x
🐘 PostgresExpress1.312s (-26.0% 🟢)2.031s (-6.7% 🟢)0.000s (+Infinity% 🔺)2.054s (-6.6% 🟢)0.742s301.02x
🐘 PostgresNext.js (Turbopack)1.673s2.401s0.000s2.435s0.762s251.30x
💻 LocalNext.js (Turbopack)2.491s3.122s0.000s3.129s0.637s201.94x
💻 LocalNitro3.140s (-7.3% 🟢)3.965s (-1.7%)0.000s (-18.0% 🟢)3.968s (-1.7%)0.828s162.45x
💻 LocalExpress3.291s (-5.1% 🟢)4.026s (~)0.000s (-41.7% 🟢)4.031s (~)0.740s152.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.296s (+15.5% 🔺)6.741s (+11.9% 🔺)0.001s (+Infinity% 🔺)7.181s (+11.2% 🔺)1.884s91.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresExpress19/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

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

@github-actions

github-actionsBot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production122202191441
✅ 💻 Local Development161502191834
✅ 📦 Local Production161502191834
✅ 🐘 Local Postgres161502191834
✅ 🪟 Windows13100131
✅ 📋 Other7410176917
Total6939010527991

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro105026
✅ example105026
✅ express105026
✅ fastify105026
✅ hono105026
✅ nextjs-turbopack12902
✅ nextjs-webpack12902
✅ nitro105026
✅ nuxt105026
✅ sveltekit12407
✅ vite105026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable106025
✅ express-stable106025
✅ fastify-stable106025
✅ hono-stable106025
✅ nextjs-turbopack-canary112019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13100
✅ nextjs-turbopack-stable-lazy-discovery-enabled13100
✅ nextjs-webpack-canary112019
✅ nextjs-webpack-stable-lazy-discovery-disabled13100
✅ nextjs-webpack-stable-lazy-discovery-enabled13100
✅ nitro-stable106025
✅ nuxt-stable106025
✅ sveltekit-stable12506
✅ vite-stable106025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13100
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable106025
✅ e2e-local-dev-tanstack-start-106025
✅ e2e-local-postgres-nest-stable106025
✅ e2e-local-postgres-tanstack-start-106025
✅ e2e-local-prod-nest-stable106025
✅ e2e-local-prod-tanstack-start-106025
✅ e2e-vercel-prod-tanstack-start105026

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's merge into upstream branch? and then review as one

Strip the WF_TRACE replay tracing that was used to diagnose the
CORRUPTED_EVENT_LOG race \u2014 it's served its purpose now that the fix
is in. Specifically:
- Delete packages/core/src/__debug-replay-trace.ts and its 8 call sites
in workflow.ts, step.ts, workflow/hook.ts, workflow/sleep.ts.
- Drop the matching [DEBUG] inline narrative comments at each call site.
- Rename packages/core/src/runtime/__fenced-write.ts \u2192 fenced-write.ts
(the leading-underscore convention marked it as throwaway diagnostic
code; the helper is intended to stay).
- Trim the file header on fenced-write.ts and the related narrative
comment in suspension-handler.ts to drop the failing-runId / PR-number
references that only made sense in the debug context.
No behavioral change. typecheck clean (0 errors); 1014/1014 unit tests
pass (same as parent commit 77f057a).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Extends optimistic concurrency control (OCC) “fencing” to additional workflow event writes whose correctness depends on branch decisions made from a potentially stale event-log snapshot, and adjusts step dispatch behavior to avoid duplicate execution under concurrent replay.

Changes:

  • Introduce a shared fencedEventCreate helper to retry CAS-fenced writes with refresh + idempotency checks.
  • Apply fencing to step_created / wait_created / hook create+dispose paths in handleSuspension, and to terminal run_completed / run_failed writes in runtime.ts.
  • Improve local queue idempotency by retaining completed idempotency keys (plus tests/docs updates).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/world-vercel/src/utils.tsMap HTTP 412 OCC fence conflicts into EntityConflictError for downstream handling.
packages/world-testing/src/inline-batches-debug.mtsTighten debug assertions for race/skip counters.
packages/world-local/src/queue.tsAdd a bounded cache of completed idempotency keys to prevent post-completion duplicate dispatch.
packages/world-local/src/queue.test.tsAdd coverage for post-completion idempotency dedupe behavior.
packages/core/src/workflow/hook.tsMinor formatting-only change.
packages/core/src/step.tsMinor formatting-only change.
packages/core/src/runtime/suspension-handler.tsFence branch-decision writes and serialize them to avoid self-conflicts; add refresh/idempotency logic.
packages/core/src/runtime/suspension-handler.test.tsNew test ensuring the fence is chained across writes within a suspension.
packages/core/src/runtime/fenced-write.tsNew shared helper implementing fence-conflict retries with backoff and caller-provided refresh/idempotency checks.
packages/core/src/runtime.tsFence terminal writes; owner-scope step queueing when inline execution is possible; crash-recovery exception for redelivery.
docs/content/docs/v5/changelog/eager-processing.mdxUpdate documentation to reflect owner-scoped queueing semantics.
docs/content/docs/v4/changelog/eager-processing.mdxSame as v5 doc update for v4 docs.
.changeset/quick-local-queues.mdChangeset for local queue idempotency behavior.
.changeset/chilly-fences-chain.mdChangeset for core fencing + dispatch changes.

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

Comment threadpackages/world-vercel/src/utils.ts
Address Copilot review on PR 2132 (#2132 (comment)).
The fence-retry loop in runtime/fenced-write.ts detects OCC conflicts
via /fence conflict/i.test(err.message). The 412 branch was relying on
the server's JSON body to populate that message via errorData.message,
but parseResponseBody().catch(() => ({})) swallows JSON parse failures
silently — so any non-JSON 412 response (CDN HTML, gateway timeout
page, intermediate proxy error) would surface as
EntityConflictError("<METHOD> /endpoint -> HTTP 412: Precondition
Failed"), the regex would miss it, and the retry loop would
mis-classify the conflict as terminal.
Prefix the message with `fence conflict:` whenever the parsed body
didn't already carry the marker, so the retry detection is robust to
response-body parse failures.
Tests: world-vercel 69/69 pass.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious