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
Copy link
Copy Markdown
Member

@TooTallNate TooTallNate commented May 28, 2026

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 site Status
wait_completed (elapsed-wait scan) already fenced by #2113
step_created now fenced
wait_created now fenced
hook_created now fenced
hook_disposed now fenced
run_completed now fenced
run_failed now fenced
hook_received deliberately 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
Copy link
Copy Markdown
Contributor

vercel Bot commented May 28, 2026

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

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

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 28, 2026

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

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

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

@TooTallNate TooTallNate changed the base branch from stable to main May 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
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.029s (-31.8% 🟢) 1.005s (~) 0.976s 10 1.00x
💻 Local Express 0.030s (-31.6% 🟢) 1.005s (~) 0.975s 10 1.03x
💻 Local Next.js (Turbopack) 0.042s 1.004s 0.962s 10 1.43x
🐘 Postgres Next.js (Turbopack) 0.046s 1.009s 0.963s 10 1.56x
🐘 Postgres Express 0.046s (-20.5% 🟢) 1.012s (~) 0.966s 10 1.57x
🐘 Postgres Nitro 0.050s (-47.3% 🟢) 1.012s (-2.9%) 0.962s 10 1.71x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.233s (-0.8%) 1.941s (-9.1% 🟢) 1.707s 10 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.072s (-5.2% 🟢) 2.006s (~) 0.934s 10 1.00x
💻 Local Express 1.073s (-4.6%) 2.006s (~) 0.933s 10 1.00x
🐘 Postgres Express 1.084s (-5.5% 🟢) 2.009s (~) 0.925s 10 1.01x
🐘 Postgres Nitro 1.086s (-4.7%) 2.008s (~) 0.922s 10 1.01x
💻 Local Next.js (Turbopack) 1.089s 2.005s 0.916s 10 1.02x
🐘 Postgres Next.js (Turbopack) 1.101s 2.008s 0.907s 10 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.767s (-5.7% 🟢) 3.654s (-4.0%) 1.887s 10 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 10.410s (-4.9%) 11.023s (~) 0.613s 3 1.00x
💻 Local Express 10.418s (-4.6%) 11.023s (~) 0.605s 3 1.00x
🐘 Postgres Express 10.424s (-4.9%) 11.019s (~) 0.595s 3 1.00x
🐘 Postgres Nitro 10.430s (-4.1%) 11.014s (~) 0.584s 3 1.00x
💻 Local Next.js (Turbopack) 10.557s 11.022s 0.465s 3 1.01x
🐘 Postgres Next.js (Turbopack) 10.623s 11.018s 0.394s 3 1.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 13.910s (-18.1% 🟢) 15.954s (-20.3% 🟢) 2.044s 2 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 13.467s (-10.0% 🟢) 14.027s (-6.7% 🟢) 0.560s 5 1.00x
💻 Local Nitro 13.488s (-10.5% 🟢) 14.027s (-12.5% 🟢) 0.539s 5 1.00x
🐘 Postgres Express 13.515s (-7.3% 🟢) 14.018s (-6.7% 🟢) 0.503s 5 1.00x
🐘 Postgres Nitro 13.573s (-7.0% 🟢) 14.023s (-6.7% 🟢) 0.450s 5 1.01x
💻 Local Next.js (Turbopack) 13.808s 14.026s 0.218s 5 1.03x
🐘 Postgres Next.js (Turbopack) 13.860s 14.017s 0.157s 5 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 20.720s (-58.8% 🟢) 22.449s (-57.3% 🟢) 1.729s 3 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 11.978s (-28.6% 🟢) 12.273s (-27.9% 🟢) 0.295s 8 1.00x
💻 Local Express 11.995s (-27.7% 🟢) 12.398s (-27.2% 🟢) 0.403s 8 1.00x
🐘 Postgres Express 12.247s (-12.6% 🟢) 12.876s (-11.8% 🟢) 0.629s 7 1.02x
🐘 Postgres Nitro 12.256s (-12.3% 🟢) 13.019s (-9.0% 🟢) 0.763s 7 1.02x
💻 Local Next.js (Turbopack) 12.585s 13.023s 0.438s 7 1.05x
🐘 Postgres Next.js (Turbopack) 12.744s 13.013s 0.270s 7 1.06x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 28.522s (-76.5% 🟢) 30.389s (-75.4% 🟢) 1.867s 4 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.142s (-9.4% 🟢) 2.007s (~) 0.865s 15 1.00x
🐘 Postgres Nitro 1.164s (-8.7% 🟢) 2.007s (~) 0.844s 15 1.02x
💻 Local Express 1.170s (-21.4% 🟢) 2.006s (~) 0.836s 15 1.02x
💻 Local Nitro 1.172s (-28.1% 🟢) 2.006s (-3.3%) 0.834s 15 1.03x
🐘 Postgres Next.js (Turbopack) 1.175s 2.006s 0.832s 15 1.03x
💻 Local Next.js (Turbopack) 1.211s 2.005s 0.794s 15 1.06x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 103.949s (+3534.8% 🔺) 105.665s (+2185.6% 🔺) 1.716s 3 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.215s (-48.6% 🟢) 2.007s (-33.3% 🟢) 0.793s 15 1.00x
🐘 Postgres Nitro 1.221s (-48.1% 🟢) 2.008s (-33.3% 🟢) 0.787s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.273s 2.005s 0.733s 15 1.05x
💻 Local Next.js (Turbopack) 1.514s 2.005s 0.492s 15 1.25x
💻 Local Nitro 1.682s (-46.5% 🟢) 2.006s (-48.4% 🟢) 0.323s 15 1.39x
💻 Local Express 1.706s (-42.2% 🟢) 2.006s (-41.9% 🟢) 0.300s 15 1.40x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.692s (+2.0%) 5.373s (+5.1% 🔺) 1.681s 6 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.338s (-61.6% 🟢) 2.007s (-50.0% 🟢) 0.669s 15 1.00x
🐘 Postgres Nitro 1.353s (-61.1% 🟢) 2.007s (-49.9% 🟢) 0.654s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.479s 2.006s 0.528s 15 1.10x
💻 Local Next.js (Turbopack) 3.944s 4.295s 0.350s 7 2.95x
💻 Local Nitro 4.618s (-44.7% 🟢) 5.010s (-44.5% 🟢) 0.392s 6 3.45x
💻 Local Express 4.704s (-43.6% 🟢) 5.180s (-42.6% 🟢) 0.476s 6 3.52x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.082s (+19.9% 🔺) 7.070s (+15.4% 🔺) 1.987s 5 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.151s (-8.4% 🟢) 2.007s (~) 0.856s 15 1.00x
🐘 Postgres Nitro 1.162s (-7.5% 🟢) 2.010s (~) 0.848s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.170s 2.007s 0.837s 15 1.02x
💻 Local Next.js (Turbopack) 1.265s 2.005s 0.740s 15 1.10x
💻 Local Express 1.443s (-23.8% 🟢) 2.006s (-15.1% 🟢) 0.563s 15 1.25x
💻 Local Nitro 1.745s (-6.5% 🟢) 2.392s (+2.2%) 0.646s 13 1.52x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.525s (-2.2%) 4.081s (-6.2% 🟢) 1.556s 8 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.234s (-47.3% 🟢) 2.008s (-33.3% 🟢) 0.774s 15 1.00x
🐘 Postgres Nitro 1.235s (-47.2% 🟢) 2.009s (-33.3% 🟢) 0.774s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.273s 2.009s 0.736s 15 1.03x
💻 Local Next.js (Turbopack) 1.812s 2.072s 0.260s 15 1.47x
💻 Local Nitro 1.869s (-39.0% 🟢) 2.316s (-40.4% 🟢) 0.447s 13 1.51x
💻 Local Express 1.913s (-38.9% 🟢) 2.316s (-38.5% 🟢) 0.402s 13 1.55x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.712s (+16.3% 🔺) 5.226s (+9.0% 🔺) 1.514s 6 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.351s (-61.4% 🟢) 2.008s (-49.9% 🟢) 0.657s 15 1.00x
🐘 Postgres Nitro 1.376s (-60.5% 🟢) 2.009s (-49.9% 🟢) 0.633s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.477s 2.006s 0.529s 15 1.09x
💻 Local Nitro 4.542s (-50.3% 🟢) 5.179s (-48.3% 🟢) 0.637s 6 3.36x
💻 Local Next.js (Turbopack) 4.602s 5.011s 0.410s 7 3.41x
💻 Local Express 6.036s (-31.4% 🟢) 6.416s (-30.8% 🟢) 0.380s 5 4.47x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 6.559s (+2.2%) 8.517s (+4.1%) 1.959s 4 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.427s (-49.1% 🟢) 1.007s (-1.6%) 0.580s 60 1.00x
🐘 Postgres Nitro 0.486s (-40.7% 🟢) 1.007s (~) 0.521s 60 1.14x
💻 Local Nitro 0.495s (-49.5% 🟢) 1.021s (-6.7% 🟢) 0.526s 59 1.16x
💻 Local Express 0.512s (-48.0% 🟢) 1.021s (-5.1% 🟢) 0.509s 59 1.20x
🐘 Postgres Next.js (Turbopack) 0.546s 1.005s 0.460s 60 1.28x
💻 Local Next.js (Turbopack) 0.639s 1.004s 0.365s 60 1.50x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.726s (-69.9% 🟢) 7.306s (-65.7% 🟢) 1.580s 9 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.156s (-41.5% 🟢) 1.903s (-15.7% 🟢) 0.747s 48 1.00x
💻 Local Nitro 1.221s (-59.8% 🟢) 2.005s (-46.6% 🟢) 0.784s 45 1.06x
🐘 Postgres Nitro 1.222s (-36.6% 🟢) 2.030s (-3.4%) 0.808s 45 1.06x
💻 Local Express 1.272s (-57.8% 🟢) 2.006s (-44.1% 🟢) 0.734s 45 1.10x
🐘 Postgres Next.js (Turbopack) 1.342s 2.006s 0.664s 45 1.16x
💻 Local Next.js (Turbopack) 1.617s 2.027s 0.410s 45 1.40x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 12.876s (-62.7% 🟢) 14.564s (-60.4% 🟢) 1.688s 7 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.181s (-45.3% 🟢) 2.984s (-31.7% 🟢) 0.803s 41 1.00x
🐘 Postgres Nitro 2.333s (-43.1% 🟢) 3.060s (-33.5% 🟢) 0.726s 40 1.07x
🐘 Postgres Next.js (Turbopack) 2.651s 3.007s 0.356s 40 1.22x
💻 Local Nitro 2.771s (-70.2% 🟢) 3.032s (-69.7% 🟢) 0.261s 40 1.27x
💻 Local Express 2.878s (-68.7% 🟢) 3.136s (-68.7% 🟢) 0.257s 39 1.32x
💻 Local Next.js (Turbopack) 3.407s 4.008s 0.600s 30 1.56x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 25.597s (-80.3% 🟢) 27.381s (-79.3% 🟢) 1.784s 5 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.175s (-38.2% 🟢) 1.006s (~) 0.831s 60 1.00x
🐘 Postgres Next.js (Turbopack) 0.184s 1.005s 0.821s 60 1.05x
🐘 Postgres Nitro 0.200s (-29.4% 🟢) 1.006s (~) 0.806s 60 1.14x
💻 Local Express 0.386s (-31.2% 🟢) 1.004s (~) 0.618s 60 2.21x
💻 Local Nitro 0.386s (-36.2% 🟢) 1.004s (-1.7%) 0.618s 60 2.21x
💻 Local Next.js (Turbopack) 0.474s 1.004s 0.529s 60 2.72x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.161s (+10.6% 🔺) 3.810s (+4.8%) 1.649s 16 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.298s (-41.5% 🟢) 1.006s (~) 0.708s 90 1.00x
🐘 Postgres Nitro 0.317s (-36.1% 🟢) 1.006s (~) 0.689s 90 1.06x
🐘 Postgres Next.js (Turbopack) 0.341s 1.005s 0.664s 90 1.14x
💻 Local Next.js (Turbopack) 2.026s 2.536s 0.510s 36 6.80x
💻 Local Nitro 2.118s (-16.6% 🟢) 2.610s (-13.3% 🟢) 0.492s 35 7.10x
💻 Local Express 2.170s (-13.7% 🟢) 2.822s (-6.2% 🟢) 0.652s 32 7.28x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.148s (+36.1% 🔺) 5.654s (+17.6% 🔺) 1.507s 16 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.652s (-20.3% 🟢) 1.006s (-1.1%) 0.354s 120 1.00x
🐘 Postgres Next.js (Turbopack) 0.681s 1.005s 0.324s 120 1.04x
🐘 Postgres Nitro 0.688s (-13.0% 🟢) 1.006s (~) 0.319s 120 1.05x
💻 Local Next.js (Turbopack) 9.197s 9.718s 0.521s 13 14.10x
💻 Local Nitro 9.393s (-16.1% 🟢) 10.024s (-14.1% 🟢) 0.632s 12 14.40x
💻 Local Express 10.171s (-9.1% 🟢) 10.695s (-10.4% 🟢) 0.524s 12 15.59x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 13.240s (+78.4% 🔺) 15.181s (+64.2% 🔺) 1.942s 8 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.131s (+467.9% 🔺) 2.005s (+99.6% 🔺) 0.010s (-14.9% 🟢) 2.017s (+98.2% 🔺) 0.887s 10 1.00x
💻 Local Nitro 1.136s (+431.6% 🔺) 2.004s (+99.5% 🔺) 0.010s (-18.4% 🟢) 2.017s (+98.0% 🔺) 0.881s 10 1.00x
🐘 Postgres Express 1.137s (+454.2% 🔺) 2.001s (+100.3% 🔺) 0.001s (-25.0% 🟢) 2.011s (+98.8% 🔺) 0.874s 10 1.01x
💻 Local Next.js (Turbopack) 1.147s 2.004s 0.008s 2.015s 0.868s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.155s 2.001s 0.001s 2.008s 0.852s 10 1.02x
🐘 Postgres Nitro 1.164s (+467.6% 🔺) 2.000s (+100.0% 🔺) 0.002s (+13.3% 🔺) 2.013s (+99.0% 🔺) 0.849s 10 1.03x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.134s (-14.8% 🟢) 3.175s (-22.4% 🟢) 2.057s (+114.0% 🔺) 5.682s (+1.6%) 3.549s 10 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.506s (+139.1% 🔺) 2.003s (+99.0% 🔺) 0.004s (+5.3% 🔺) 2.024s (+97.9% 🔺) 0.518s 30 1.00x
💻 Local Nitro 1.518s (+81.0% 🔺) 2.012s (+98.8% 🔺) 0.009s (-0.8%) 2.023s (+81.3% 🔺) 0.506s 30 1.01x
💻 Local Express 1.520s (+100.8% 🔺) 2.012s (+95.5% 🔺) 0.009s (-0.9%) 2.023s (+94.5% 🔺) 0.503s 30 1.01x
💻 Local Next.js (Turbopack) 1.546s 2.008s 0.009s 2.020s 0.474s 30 1.03x
🐘 Postgres Nitro 1.591s (+154.9% 🔺) 2.007s (+99.3% 🔺) 0.004s (-3.3%) 2.026s (+98.2% 🔺) 0.436s 30 1.06x
🐘 Postgres Next.js (Turbopack) 1.632s 2.041s 0.003s 2.053s 0.421s 30 1.08x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 6.080s (-6.5% 🟢) 7.362s (-8.1% 🟢) 0.403s (-1.4%) 8.205s (-7.1% 🟢) 2.125s 8 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.645s (-32.9% 🟢) 1.016s (-20.5% 🟢) 0.000s (-61.0% 🟢) 1.029s (-21.2% 🟢) 0.384s 59 1.00x
🐘 Postgres Nitro 0.669s (-31.0% 🟢) 1.033s (-17.2% 🟢) 0.000s (+65.5% 🔺) 1.048s (-16.7% 🟢) 0.379s 58 1.04x
🐘 Postgres Next.js (Turbopack) 0.878s 1.088s 0.000s 1.186s 0.308s 51 1.36x
💻 Local Next.js (Turbopack) 1.198s 1.860s 0.000s 1.863s 0.665s 33 1.86x
💻 Local Nitro 1.353s (+10.7% 🔺) 2.016s (~) 0.000s (+33.3% 🔺) 2.018s (~) 0.664s 30 2.10x
💻 Local Express 1.368s (+11.7% 🔺) 2.015s (~) 0.000s (-30.0% 🟢) 2.017s (~) 0.649s 30 2.12x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.631s (-2.9%) 4.743s (-7.0% 🟢) 0.000s (-100.0% 🟢) 5.186s (-6.2% 🟢) 1.555s 12 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.282s (-28.5% 🟢) 1.996s (-6.8% 🟢) 0.000s (+86.7% 🔺) 2.048s (-5.8% 🟢) 0.766s 30 1.00x
🐘 Postgres Express 1.312s (-26.0% 🟢) 2.031s (-6.7% 🟢) 0.000s (+Infinity% 🔺) 2.054s (-6.6% 🟢) 0.742s 30 1.02x
🐘 Postgres Next.js (Turbopack) 1.673s 2.401s 0.000s 2.435s 0.762s 25 1.30x
💻 Local Next.js (Turbopack) 2.491s 3.122s 0.000s 3.129s 0.637s 20 1.94x
💻 Local Nitro 3.140s (-7.3% 🟢) 3.965s (-1.7%) 0.000s (-18.0% 🟢) 3.968s (-1.7%) 0.828s 16 2.45x
💻 Local Express 3.291s (-5.1% 🟢) 4.026s (~) 0.000s (-41.7% 🟢) 4.031s (~) 0.740s 15 2.57x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.296s (+15.5% 🔺) 6.741s (+11.9% 🔺) 0.001s (+Infinity% 🔺) 7.181s (+11.2% 🔺) 1.884s 9 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 9/21
🐘 Postgres Express 19/21
▲ Vercel Express 21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 15/21
Next.js (Turbopack) 🐘 Postgres 14/21
Nitro 🐘 Postgres 13/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
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1222 0 219 1441
✅ 💻 Local Development 1615 0 219 1834
✅ 📦 Local Production 1615 0 219 1834
✅ 🐘 Local Postgres 1615 0 219 1834
✅ 🪟 Windows 131 0 0 131
✅ 📋 Other 741 0 176 917
Total 6939 0 1052 7991

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 105 0 26
✅ example 105 0 26
✅ express 105 0 26
✅ fastify 105 0 26
✅ hono 105 0 26
✅ nextjs-turbopack 129 0 2
✅ nextjs-webpack 129 0 2
✅ nitro 105 0 26
✅ nuxt 105 0 26
✅ sveltekit 124 0 7
✅ vite 105 0 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 106 0 25
✅ express-stable 106 0 25
✅ fastify-stable 106 0 25
✅ hono-stable 106 0 25
✅ nextjs-turbopack-canary 112 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 131 0 0
✅ nextjs-webpack-canary 112 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 131 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 131 0 0
✅ nitro-stable 106 0 25
✅ nuxt-stable 106 0 25
✅ sveltekit-stable 125 0 6
✅ vite-stable 106 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 131 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 106 0 25
✅ e2e-local-dev-tanstack-start- 106 0 25
✅ e2e-local-postgres-nest-stable 106 0 25
✅ e2e-local-postgres-tanstack-start- 106 0 25
✅ e2e-local-prod-nest-stable 106 0 25
✅ e2e-local-prod-tanstack-start- 106 0 25
✅ e2e-vercel-prod-tanstack-start 105 0 26

📋 View full workflow run

Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious left a comment

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).
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
File Description
packages/world-vercel/src/utils.ts Map HTTP 412 OCC fence conflicts into EntityConflictError for downstream handling.
packages/world-testing/src/inline-batches-debug.mts Tighten debug assertions for race/skip counters.
packages/world-local/src/queue.ts Add a bounded cache of completed idempotency keys to prevent post-completion duplicate dispatch.
packages/world-local/src/queue.test.ts Add coverage for post-completion idempotency dedupe behavior.
packages/core/src/workflow/hook.ts Minor formatting-only change.
packages/core/src/step.ts Minor formatting-only change.
packages/core/src/runtime/suspension-handler.ts Fence branch-decision writes and serialize them to avoid self-conflicts; add refresh/idempotency logic.
packages/core/src/runtime/suspension-handler.test.ts New test ensuring the fence is chained across writes within a suspension.
packages/core/src/runtime/fenced-write.ts New shared helper implementing fence-conflict retries with backoff and caller-provided refresh/idempotency checks.
packages/core/src/runtime.ts Fence terminal writes; owner-scope step queueing when inline execution is possible; crash-recovery exception for redelivery.
docs/content/docs/v5/changelog/eager-processing.mdx Update documentation to reflect owner-scoped queueing semantics.
docs/content/docs/v4/changelog/eager-processing.mdx Same as v5 doc update for v4 docs.
.changeset/quick-local-queues.md Changeset for local queue idempotency behavior.
.changeset/chilly-fences-chain.md Changeset for core fencing + dispatch changes.

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

Comment thread packages/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 free to 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