Skip to content

[world-vercel] Validate ref resolve responses before use - #2035

Merged
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload
Jun 8, 2026
Merged

[world-vercel] Validate ref resolve responses before use#2035
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Adds defense-in-depth validation in resolveRefDescriptor so a zero-byte or truncated ref response from GET /v2/runs/:runId/refs no longer corrupts the in-memory event log during workflow replay.

The workflow runtime feeds ref payload bytes directly into decodeFormatPrefix, which requires at least the 4-byte format prefix that the SDK always writes (encodeWithFormatPrefix in @workflow/core). A zero-byte response is therefore never a valid stored value.

Before this change, resolveRefDescriptor checked response.ok and then handed the empty arrayBuffer() downstream verbatim. Multiple users have reported the resulting failure:

Data too short to contain format prefix: expected at least 4 bytes, got 0

That error surfaces deep inside deterministic replay. Because the SDK has already populated its in-memory event snapshot with the empty payload at that point, the run is wedged: every subsequent invocation deterministically replays the same failure, any downstream resumeHook() then surfaces as Hook not found, and the run only clears when stale-run cleanup terminates the sandbox.

Changes

  • packages/world-vercel/src/refs.ts:
    • After await response.arrayBuffer(), validate buffer.byteLength > 0. Empty bodies throw a WorkflowWorldError with code: 'empty-ref-body'.
    • When Content-Length is present, validate it matches the actual body length. Truncated responses (proxy abort, upstream stream cut mid-flight) throw WorkflowWorldError with code: 'ref-body-length-mismatch'.
    • Both errors record on the span via recordException and tag ErrorType for o11y.
  • packages/world-vercel/src/refs.test.ts (new):
    • Happy path coverage (CBOR + application/octet-stream).
    • Zero-byte 200 with both content types.
    • Content-Length mismatch (truncated stream).
    • Absent Content-Length (chunked transfer encoding) — happy path only.
    • Non-2xx status passes through as HTTP <code>.
    • Inline dbrf refs decode without making a network request.
  • Changeset: @workflow/world-vercel patch.

Why both ends validate

This is the SDK-side companion to vercel/workflow-server#432, which rejects zero-byte payloads at the storage boundary. Both layers are necessary because they catch failures at different points in the stack:

  • Server-side catches storage anomalies (S3 inconsistency, Redis empty key, corrupted write).
  • Client-side catches transport anomalies between the server and the SDK (proxy drop, edge cache miss returning truncated content, undici quirks, etc.).

WorkflowWorldError is treated as a retryable transport-level error by the runtime, which is the desired behavior here: surface the empty/truncated body up to the retry layer instead of poisoning event-log replay.

Validation

  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm --filter @workflow/world-vercel test (77 tests pass, including 8 new in refs.test.ts)
  • pnpm --filter @workflow/world-vercel build
  • pnpm biome check --files-ignore-unknown=true packages/world-vercel/src/refs.ts packages/world-vercel/src/refs.test.ts
  • pnpm changeset status --since=main (confirms @workflow/world-vercel patch bump)

When workflow-server returns a ref body to the SDK, the bytes are
fed into the workflow runtime's event log and deserialized via
`decodeFormatPrefix`. The SDK always writes ref payloads with at
least a 4-byte format prefix (see `encodeWithFormatPrefix` in
`@workflow/core`), so a zero-byte response — or one whose length
disagrees with `Content-Length` — is never a valid stored value.
Before this change, `resolveRefDescriptor` had no validation: a
200 with an empty body would be passed downstream as a zero-length
Uint8Array, which then failed deep inside replay with:
Data too short to contain format prefix: expected at least 4 bytes, got 0
By that point the workflow's in-memory event snapshot is already
poisoned with the empty payload, so every subsequent replay
deterministically reproduces the same failure, downstream
`resumeHook()` calls surface as `Hook not found`, and the run
only unsticks when stale-run cleanup terminates the sandbox.
This catches the failure at the transport boundary instead, where
it can be retried as a `WorkflowWorldError`. Both an empty body
and a length mismatch (truncated streaming response) are rejected.
This is the SDK-side companion to vercel/workflow-server#432, which
adds the same validation on the server side.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 20, 2026 17:13
CopilotAI review requested due to automatic review settings May 20, 2026 17:13
@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c8f5dce

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

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

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

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

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 20, 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🥇 Express0.049s (+13.7% 🔺)1.007s (~)0.958s101.00x
💻 LocalNext.js (Turbopack)0.059s (+6.4% 🔺)1.005s (~)0.947s101.19x
🐘 PostgresNitro0.061s (-2.7%)1.012s (~)0.951s101.25x
🐘 PostgresNext.js (Turbopack)0.067s (-3.7%)1.012s (~)0.945s101.37x
🐘 PostgresExpress0.079s (+18.9% 🔺)1.031s (+1.6%)0.952s101.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.291s (-11.6% 🟢)2.378s (+1.6%)2.087s101.00x
▲ VercelNitro0.360s (+37.9% 🔺)2.201s (-9.7% 🟢)1.840s101.24x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.101s (~)2.006s (~)0.905s101.00x
🐘 PostgresNitro1.116s (~)2.010s (~)0.894s101.01x
💻 LocalNext.js (Turbopack)1.130s (+2.0%)2.006s (~)0.876s101.03x
🐘 PostgresNext.js (Turbopack)1.151s (+0.8%)2.009s (~)0.859s101.04x
🐘 PostgresExpress1.170s (+7.1% 🔺)2.027s (+0.8%)0.857s101.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.698s (+8.4% 🔺)3.490s (-1.1%)1.792s101.00x
▲ VercelNext.js (Turbopack)1.817s (+9.1% 🔺)3.995s (-2.0%)2.177s101.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.555s (~)11.020s (~)0.465s31.00x
💻 LocalExpress10.631s (+0.6%)11.024s (~)0.393s31.01x
🐘 PostgresExpress10.684s (~)11.023s (-2.9%)0.339s31.01x
💻 LocalNext.js (Turbopack)10.763s (+1.5%)11.021s (~)0.258s31.02x
🐘 PostgresNext.js (Turbopack)10.827s (~)11.020s (~)0.193s31.03x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.033s (+5.3% 🔺)15.218s (-2.4%)1.186s21.00x
▲ VercelNext.js (Turbopack)15.723s (+16.5% 🔺)17.581s (+9.2% 🔺)1.858s21.12x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.725s (-0.9%)14.018s (~)0.293s51.00x
💻 LocalExpress13.845s (~)14.029s (-1.4%)0.184s51.01x
🐘 PostgresExpress14.191s (+2.6%)14.618s (+2.7%)0.427s51.03x
🐘 PostgresNext.js (Turbopack)14.421s (-1.2%)15.018s (~)0.597s41.05x
💻 LocalNext.js (Turbopack)14.569s (+3.9%)15.028s (+2.7%)0.459s41.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro23.236s (-8.9% 🟢)24.542s (-11.1% 🟢)1.306s31.00x
▲ VercelNext.js (Turbopack)24.439s (-4.2%)26.713s (-1.8%)2.273s31.05x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro12.500s (-1.9%)13.021s (~)0.521s71.00x
💻 LocalExpress12.697s (+0.8%)13.026s (~)0.329s71.02x
🐘 PostgresExpress13.420s (+5.4% 🔺)14.021s (+5.3% 🔺)0.601s71.07x
💻 LocalNext.js (Turbopack)13.706s (+5.8% 🔺)14.169s (+6.5% 🔺)0.464s71.10x
🐘 PostgresNext.js (Turbopack)14.007s (+1.8%)14.450s (+3.1%)0.443s71.12x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro30.766s (+5.9% 🔺)32.566s (+3.7%)1.800s31.00x
▲ VercelNext.js (Turbopack)33.943s (+14.6% 🔺)36.480s (+14.5% 🔺)2.537s31.10x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.174s (-2.1%)2.008s (~)0.834s151.00x
🐘 PostgresExpress1.234s (+2.8%)2.007s (-0.7%)0.774s151.05x
💻 LocalExpress1.235s (-2.3%)2.007s (~)0.772s151.05x
🐘 PostgresNext.js (Turbopack)1.252s (~)2.007s (~)0.755s151.07x
💻 LocalNext.js (Turbopack)1.377s (+12.6% 🔺)2.007s (~)0.630s151.17x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.525s (-8.8% 🟢)4.139s (-4.7%)1.613s81.00x
▲ VercelNext.js (Turbopack)2.534s (-5.7% 🟢)4.174s (-1.0%)1.641s81.00x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.252s (-0.5%)2.007s (~)0.755s151.00x
🐘 PostgresNext.js (Turbopack)1.401s (-0.6%)2.008s (~)0.607s151.12x
🐘 PostgresExpress1.546s (+16.5% 🔺)2.099s (+4.1%)0.554s151.23x
💻 LocalExpress1.681s (-10.7% 🟢)2.006s (-9.7% 🟢)0.325s151.34x
💻 LocalNext.js (Turbopack)1.798s (+4.2%)2.074s (+3.4%)0.276s151.44x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.382s (-16.9% 🟢)4.751s (-23.5% 🟢)1.369s71.00x
▲ VercelNext.js (Turbopack)3.982s (-3.0%)5.923s (-3.5%)1.941s61.18x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.409s (~)2.008s (~)0.599s151.00x
🐘 PostgresNext.js (Turbopack)1.727s (-1.9%)2.317s (+1.0%)0.590s131.23x
🐘 PostgresExpress1.734s (+24.4% 🔺)2.416s (+20.2% 🔺)0.682s131.23x
💻 LocalExpress4.668s (-11.9% 🟢)5.013s (-16.7% 🟢)0.345s63.31x
💻 LocalNext.js (Turbopack)4.701s (+6.9% 🔺)5.180s (+3.4%)0.478s63.34x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.417s (-19.6% 🟢)7.151s (-17.9% 🟢)1.734s51.00x
▲ VercelNext.js (Turbopack)5.831s (-0.7%)7.666s (-1.4%)1.835s51.08x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.195s (~)2.009s (~)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (-1.2%)2.009s (~)0.758s151.05x
🐘 PostgresExpress1.253s (+4.1%)2.026s (+0.9%)0.774s151.05x
💻 LocalNext.js (Turbopack)1.363s (+4.4%)2.006s (~)0.644s151.14x
💻 LocalExpress1.614s (-2.5%)2.007s (-3.2%)0.392s151.35x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.618s (-2.0%)4.137s (-4.5%)1.519s81.00x
▲ VercelNext.js (Turbopack)308.185s (+6948.8% 🔺)309.806s (+4822.6% 🔺)1.621s1117.73x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (-1.0%)2.009s (~)0.748s151.00x
🐘 PostgresNext.js (Turbopack)1.390s (~)2.007s (~)0.617s151.10x
🐘 PostgresExpress1.447s (+16.4% 🔺)2.082s (+3.5%)0.635s151.15x
💻 LocalExpress1.896s (-10.8% 🟢)2.294s (-11.4% 🟢)0.398s141.50x
💻 LocalNext.js (Turbopack)1.978s (-2.1%)2.316s (-7.6% 🟢)0.338s131.57x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.577s (-12.6% 🟢)4.853s (-19.2% 🟢)1.276s71.00x
▲ VercelNext.js (Turbopack)3.827s (-15.3% 🟢)5.471s (-11.3% 🟢)1.644s61.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.391s (-4.8%)2.008s (~)0.617s151.00x
🐘 PostgresExpress1.549s (+3.2%)2.331s (-2.8%)0.783s131.11x
🐘 PostgresNext.js (Turbopack)1.691s (+2.1%)2.316s (+11.7% 🔺)0.625s131.22x
💻 LocalNext.js (Turbopack)4.898s (-5.1% 🟢)5.513s (-8.4% 🟢)0.615s63.52x
💻 LocalExpress5.004s (-14.7% 🟢)5.680s (-8.6% 🟢)0.676s63.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.902s (+19.2% 🔺)9.075s (+19.8% 🔺)2.172s41.00x
▲ VercelNitro81.134s (+1545.9% 🔺)82.628s (+1126.6% 🔺)1.494s411.75x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.539s (-19.2% 🟢)1.029s (-1.8%)0.490s591.00x
🐘 PostgresNitro0.566s (-7.0% 🟢)1.024s (+1.6%)0.457s591.05x
💻 LocalExpress0.660s (+0.5%)1.005s (-3.3%)0.346s601.22x
🐘 PostgresNext.js (Turbopack)0.825s (+1.6%)1.023s (+1.7%)0.199s591.53x
💻 LocalNext.js (Turbopack)0.861s (+23.7% 🔺)1.039s (+3.4%)0.178s581.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.557s (+10.1% 🔺)7.193s (+5.0% 🔺)1.636s91.00x
▲ VercelNitro6.289s (-15.7% 🟢)7.902s (-17.0% 🟢)1.614s81.13x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (-3.6%)2.007s (~)0.641s451.00x
🐘 PostgresExpress1.494s (-2.8%)2.108s (-2.0%)0.615s431.09x
💻 LocalExpress1.619s (+1.7%)2.029s (~)0.410s451.18x
🐘 PostgresNext.js (Turbopack)1.968s (+1.3%)2.284s (+5.0% 🔺)0.316s401.44x
💻 LocalNext.js (Turbopack)2.067s (+19.6% 🔺)2.882s (+42.1% 🔺)0.815s321.51x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.292s (+8.1% 🔺)15.666s (+1.2%)1.374s61.00x
▲ VercelNext.js (Turbopack)16.308s (+10.4% 🔺)18.593s (+9.8% 🔺)2.285s51.14x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.649s (-9.1% 🟢)3.085s (-5.9% 🟢)0.436s391.00x
🐘 PostgresExpress3.135s (+7.7% 🔺)3.682s (+4.0%)0.547s331.18x
💻 LocalExpress3.545s (+3.7%)4.010s (~)0.465s301.34x
🐘 PostgresNext.js (Turbopack)3.876s (~)4.042s (-0.8%)0.166s301.46x
💻 LocalNext.js (Turbopack)4.417s (+18.5% 🔺)5.052s (+22.9% 🔺)0.635s241.67x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.414s (+5.5% 🔺)32.492s (+1.6%)1.078s41.00x
▲ VercelNext.js (Turbopack)32.338s (+19.8% 🔺)34.168s (+16.7% 🔺)1.829s41.03x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.212s (-10.0% 🟢)1.006s (~)0.794s601.00x
🐘 PostgresExpress0.248s (-1.5%)1.011s (~)0.763s601.17x
🐘 PostgresNext.js (Turbopack)0.266s (-5.5% 🟢)1.006s (~)0.740s601.26x
💻 LocalNext.js (Turbopack)0.578s (-5.3% 🟢)1.057s (+1.8%)0.479s572.73x
💻 LocalExpress0.622s (+33.9% 🔺)1.160s (+15.5% 🔺)0.538s522.94x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.746s (+24.9% 🔺)4.424s (+4.8%)1.678s141.00x
▲ VercelNitro78.584s (+3251.3% 🔺)79.773s (+1740.7% 🔺)1.188s428.62x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.364s (-1.8%)1.018s (+1.1%)0.654s891.00x
🐘 PostgresExpress0.465s (+37.0% 🔺)1.025s (+1.8%)0.560s881.28x
🐘 PostgresNext.js (Turbopack)0.491s (+1.6%)1.018s (+1.1%)0.527s891.35x
💻 LocalNext.js (Turbopack)2.222s (-9.3% 🟢)3.010s (-3.2%)0.788s306.10x
💻 LocalExpress2.224s (+1.5%)2.854s (+1.1%)0.630s326.11x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.160s (-85.9% 🟢)7.805s (-83.0% 🟢)1.645s121.00x
▲ VercelNext.js (Turbopack)40.420s (+544.6% 🔺)42.078s (+395.2% 🔺)1.658s96.56x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.682s (-3.5%)1.006s (~)0.324s1201.00x
🐘 PostgresExpress0.855s (+36.6% 🔺)1.256s (+19.0% 🔺)0.401s961.25x
🐘 PostgresNext.js (Turbopack)1.000s (+2.1%)1.814s (+12.8% 🔺)0.814s671.47x
💻 LocalNext.js (Turbopack)10.521s (+2.0%)11.483s (+1.6%)0.962s1115.42x
💻 LocalExpress10.523s (+2.0%)11.126s (+2.4%)0.603s1115.42x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.195s (-94.5% 🟢)18.692s (-94.0% 🟢)1.497s71.00x
▲ VercelNext.js (Turbopack)58.866s (-63.2% 🟢)60.845s (-62.5% 🟢)1.979s73.42x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.163s (-1.5%)2.002s (~)0.001s (-14.3% 🟢)2.010s (~)0.847s101.00x
💻 LocalExpress1.164s (~)2.005s (~)0.011s (-16.4% 🟢)2.019s (~)0.855s101.00x
💻 LocalNext.js (Turbopack)1.199s (+2.6%)2.003s (~)0.010s (+2.0%)2.017s (~)0.818s101.03x
🐘 PostgresNext.js (Turbopack)1.231s (~)2.001s (~)0.001s (-33.3% 🟢)2.011s (~)0.779s101.06x
🐘 PostgresExpress1.243s (+4.1%)1.999s (~)0.001s (-97.7% 🟢)2.012s (-1.5%)0.769s101.07x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.331s (+7.7% 🔺)3.071s (-10.1% 🟢)2.051s (+297.6% 🔺)5.565s (+24.5% 🔺)3.234s101.00x
▲ VercelNext.js (Turbopack)2.494s (+11.3% 🔺)3.775s (+6.3% 🔺)2.256s (-81.8% 🟢)6.587s (-60.1% 🟢)4.092s101.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.595s (-4.1%)2.006s (~)0.004s (+1.7%)2.025s (~)0.431s301.00x
💻 LocalExpress1.596s (-2.5%)2.009s (~)0.011s (+1.5%)2.023s (~)0.427s301.00x
💻 LocalNext.js (Turbopack)1.709s (+4.2%)2.007s (~)0.011s (+5.3% 🔺)2.021s (~)0.312s301.07x
🐘 PostgresNext.js (Turbopack)1.770s (~)2.011s (~)0.004s (-1.7%)2.027s (~)0.257s301.11x
🐘 PostgresExpress1.919s (+10.5% 🔺)2.313s (+7.9% 🔺)0.003s (+1.6%)2.341s (+8.4% 🔺)0.422s261.20x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.971s (+9.4% 🔺)8.581s (+8.1% 🔺)0.274s (-24.3% 🟢)9.398s (+6.0% 🔺)2.426s71.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.743s (+2.8%)1.029s (~)0.000s (-83.3% 🟢)1.049s (~)0.306s581.00x
🐘 PostgresNext.js (Turbopack)0.801s (-3.7%)1.053s (-1.7%)0.000s (+96.5% 🔺)1.060s (-1.8%)0.259s571.08x
🐘 PostgresExpress1.081s (+10.6% 🔺)1.498s (+4.9%)0.000s (-30.0% 🟢)1.518s (+4.8%)0.438s401.45x
💻 LocalNext.js (Turbopack)1.447s (~)2.012s (~)0.000s (-75.0% 🟢)2.015s (~)0.569s301.95x
💻 LocalExpress1.470s (-0.9%)2.013s (~)0.001s (+73.3% 🔺)2.016s (~)0.547s301.98x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.750s (+4.1%)4.851s (-4.4%)0.001s (+Infinity% 🔺)5.276s (-6.3% 🟢)1.527s121.00x
▲ VercelNext.js (Turbopack)4.000s (+20.9% 🔺)5.605s (+14.5% 🔺)0.000s (+Infinity% 🔺)6.117s (+12.0% 🔺)2.117s111.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.384s (-1.9%)2.099s (+5.2% 🔺)0.000s (+Infinity% 🔺)2.114s (+5.1% 🔺)0.731s291.00x
🐘 PostgresNext.js (Turbopack)1.784s (+3.2%)2.310s (+2.2%)0.000s (+Infinity% 🔺)2.333s (+2.8%)0.550s261.29x
🐘 PostgresExpress2.429s (+50.4% 🔺)3.004s (+33.3% 🔺)0.000s (+Infinity% 🔺)3.031s (+33.0% 🔺)0.602s211.76x
💻 LocalNext.js (Turbopack)2.896s (-9.9% 🟢)3.471s (-10.9% 🟢)0.001s (+77.8% 🔺)3.476s (-10.9% 🟢)0.580s182.09x
💻 LocalExpress3.239s (-2.8%)3.837s (~)0.001s (+183.3% 🔺)3.841s (~)0.602s162.34x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.053s (-14.1% 🟢)7.287s (-15.0% 🟢)0.000s (+Infinity% 🔺)7.727s (-15.0% 🟢)1.675s81.00x
▲ VercelNext.js (Turbopack)7.030s (+36.1% 🔺)8.492s (+27.2% 🔺)0.000s (-100.0% 🟢)8.993s (+24.9% 🔺)1.963s71.16x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress14/21
🐘 PostgresNitro20/21
▲ VercelNitro16/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres21/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: failure
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@github-actions

github-actionsBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production126602191485
✅ 💻 Local Development167102191890
✅ 📦 Local Production167102191890
✅ 🐘 Local Postgres167102191890
✅ 🪟 Windows13500135
✅ 📋 Other7690176945
Total7183010528235

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro109026
✅ example109026
✅ express109026
✅ fastify109026
✅ hono109026
✅ nextjs-turbopack13302
✅ nextjs-webpack13302
✅ nitro109026
✅ nuxt109026
✅ sveltekit12807
✅ vite109026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable110025
✅ e2e-local-dev-tanstack-start-110025
✅ e2e-local-postgres-nest-stable110025
✅ e2e-local-postgres-tanstack-start-110025
✅ e2e-local-prod-nest-stable110025
✅ e2e-local-prod-tanstack-start-110025
✅ e2e-vercel-prod-tanstack-start109026

📋 View full workflow run

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

Adds defensive validation to @workflow/world-vercel ref resolution so empty/truncated ref payloads don’t propagate into replay/hydration and wedge runs during deterministic replay.

Changes:

  • Validate GET /v2/runs/:runId/refs bodies for zero-length and Content-Length mismatches, throwing WorkflowWorldError with specific error codes.
  • Add a new Vitest suite covering happy paths, zero-byte 200s, Content-Length mismatch, non-2xx passthrough, and inline dbrf behavior.
  • Add a patch changeset for @workflow/world-vercel.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/refs.tsAdds response body validation (empty body and Content-Length mismatch) before decoding/returning ref payloads.
packages/world-vercel/src/refs.test.tsIntroduces unit tests for ref resolution behavior across success/error/edge cases.
.changeset/world-vercel-reject-empty-ref-payload.mdDeclares a patch release for the new validation behavior.

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

Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.test.ts
Three review changes:
1. Reject any body shorter than the 4-byte format-prefix length, not
just zero-byte bodies. The SDK guarantees every stored ref payload
starts with a 4-byte format prefix (FORMAT_PREFIX_LENGTH in
@workflow/core), so a 1-3 byte body would also fail downstream
replay with the same 'Data too short to contain format prefix'
error this PR exists to prevent.
2. Parse Content-Length safely with parseInt + Number.isFinite +
non-negative checks instead of bare Number(). A non-numeric value
like 'abc' would otherwise produce NaN and silently surface as a
'truncated' error, masking the real cause. Malformed values are
treated as absent; the minimum-length check still defends against
actual truncation in that case.
3. Add tests for the truncated-body-without-Content-Length case
(chunked transfer where Content-Length validation can't see the
truncation), and for a malformed Content-Length header that should
be ignored rather than misreported as truncation.
The validation logic also moves into a small assertValidRefBody
helper to keep the inner trace function under the noExcessiveCognitiveComplexity limit.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential blocker in PR-version packages/world-vercel/src/refs.ts:143: the new Content-Length check compares response.arrayBuffer().byteLength to the header without accounting for Content-Encoding. Node fetch transparently decompresses gzip bodies while preserving the encoded Content-Length, so a valid compressed ref response can now throw ref-body-length-mismatch. I’d skip this check when Content-Encoding is present and not identity, or force/request identity encoding.

fetch/undici transparently decompresses gzip/br bodies but leaves
Content-Length describing the encoded (compressed) size, so comparing it
against the decompressed byteLength would reject valid compressed refs as
a phantom 'ref-body-length-mismatch'. Skip the comparison when a
non-identity Content-Encoding is present; an absent or 'identity' encoding
is still validated. Adds regression tests for both cases.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@karthikscale3 great catch — fixed in c8f5dce.

You're right: fetch/undici transparently decompresses the body but leaves Content-Length describing the encoded (compressed) size, so a gzip/br ref response would have Content-Length (compressed) ≠ decompressed byteLength and falsely throw ref-body-length-mismatch.

The length comparison is now skipped whenever a non-identityContent-Encoding is present (gzip, br, etc.). An absent or identity encoding means no transform was applied, so the lengths remain directly comparable and that path is still validated. The zero-byte and binary 4-byte-minimum checks are unaffected (they operate on the decompressed body and apply regardless of encoding).

Added two regression tests:

  • skips the length check for compressed (Content-Encoding) responsesContent-Length: 20 + Content-Encoding: gzip with a larger decompressed body still decodes successfully.
  • still enforces the length check for identity Content-EncodingContent-Encoding: identity with a real mismatch still throws.

pnpm --filter @workflow/world-vercel test / typecheck / biome all pass.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2297. (backport job run)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

[world-vercel] Validate ref resolve responses before use - #2035

Merged
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload
Jun 8, 2026
Merged

[world-vercel] Validate ref resolve responses before use#2035
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Adds defense-in-depth validation in resolveRefDescriptor so a zero-byte or truncated ref response from GET /v2/runs/:runId/refs no longer corrupts the in-memory event log during workflow replay.

The workflow runtime feeds ref payload bytes directly into decodeFormatPrefix, which requires at least the 4-byte format prefix that the SDK always writes (encodeWithFormatPrefix in @workflow/core). A zero-byte response is therefore never a valid stored value.

Before this change, resolveRefDescriptor checked response.ok and then handed the empty arrayBuffer() downstream verbatim. Multiple users have reported the resulting failure:

Data too short to contain format prefix: expected at least 4 bytes, got 0

That error surfaces deep inside deterministic replay. Because the SDK has already populated its in-memory event snapshot with the empty payload at that point, the run is wedged: every subsequent invocation deterministically replays the same failure, any downstream resumeHook() then surfaces as Hook not found, and the run only clears when stale-run cleanup terminates the sandbox.

Changes

  • packages/world-vercel/src/refs.ts:
    • After await response.arrayBuffer(), validate buffer.byteLength > 0. Empty bodies throw a WorkflowWorldError with code: 'empty-ref-body'.
    • When Content-Length is present, validate it matches the actual body length. Truncated responses (proxy abort, upstream stream cut mid-flight) throw WorkflowWorldError with code: 'ref-body-length-mismatch'.
    • Both errors record on the span via recordException and tag ErrorType for o11y.
  • packages/world-vercel/src/refs.test.ts (new):
    • Happy path coverage (CBOR + application/octet-stream).
    • Zero-byte 200 with both content types.
    • Content-Length mismatch (truncated stream).
    • Absent Content-Length (chunked transfer encoding) — happy path only.
    • Non-2xx status passes through as HTTP <code>.
    • Inline dbrf refs decode without making a network request.
  • Changeset: @workflow/world-vercel patch.

Why both ends validate

This is the SDK-side companion to vercel/workflow-server#432, which rejects zero-byte payloads at the storage boundary. Both layers are necessary because they catch failures at different points in the stack:

  • Server-side catches storage anomalies (S3 inconsistency, Redis empty key, corrupted write).
  • Client-side catches transport anomalies between the server and the SDK (proxy drop, edge cache miss returning truncated content, undici quirks, etc.).

WorkflowWorldError is treated as a retryable transport-level error by the runtime, which is the desired behavior here: surface the empty/truncated body up to the retry layer instead of poisoning event-log replay.

Validation

  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm --filter @workflow/world-vercel test (77 tests pass, including 8 new in refs.test.ts)
  • pnpm --filter @workflow/world-vercel build
  • pnpm biome check --files-ignore-unknown=true packages/world-vercel/src/refs.ts packages/world-vercel/src/refs.test.ts
  • pnpm changeset status --since=main (confirms @workflow/world-vercel patch bump)

When workflow-server returns a ref body to the SDK, the bytes are
fed into the workflow runtime's event log and deserialized via
`decodeFormatPrefix`. The SDK always writes ref payloads with at
least a 4-byte format prefix (see `encodeWithFormatPrefix` in
`@workflow/core`), so a zero-byte response — or one whose length
disagrees with `Content-Length` — is never a valid stored value.
Before this change, `resolveRefDescriptor` had no validation: a
200 with an empty body would be passed downstream as a zero-length
Uint8Array, which then failed deep inside replay with:
Data too short to contain format prefix: expected at least 4 bytes, got 0
By that point the workflow's in-memory event snapshot is already
poisoned with the empty payload, so every subsequent replay
deterministically reproduces the same failure, downstream
`resumeHook()` calls surface as `Hook not found`, and the run
only unsticks when stale-run cleanup terminates the sandbox.
This catches the failure at the transport boundary instead, where
it can be retried as a `WorkflowWorldError`. Both an empty body
and a length mismatch (truncated streaming response) are rejected.
This is the SDK-side companion to vercel/workflow-server#432, which
adds the same validation on the server side.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 20, 2026 17:13
CopilotAI review requested due to automatic review settings May 20, 2026 17:13
@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c8f5dce

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

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

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

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

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 20, 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🥇 Express0.049s (+13.7% 🔺)1.007s (~)0.958s101.00x
💻 LocalNext.js (Turbopack)0.059s (+6.4% 🔺)1.005s (~)0.947s101.19x
🐘 PostgresNitro0.061s (-2.7%)1.012s (~)0.951s101.25x
🐘 PostgresNext.js (Turbopack)0.067s (-3.7%)1.012s (~)0.945s101.37x
🐘 PostgresExpress0.079s (+18.9% 🔺)1.031s (+1.6%)0.952s101.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.291s (-11.6% 🟢)2.378s (+1.6%)2.087s101.00x
▲ VercelNitro0.360s (+37.9% 🔺)2.201s (-9.7% 🟢)1.840s101.24x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.101s (~)2.006s (~)0.905s101.00x
🐘 PostgresNitro1.116s (~)2.010s (~)0.894s101.01x
💻 LocalNext.js (Turbopack)1.130s (+2.0%)2.006s (~)0.876s101.03x
🐘 PostgresNext.js (Turbopack)1.151s (+0.8%)2.009s (~)0.859s101.04x
🐘 PostgresExpress1.170s (+7.1% 🔺)2.027s (+0.8%)0.857s101.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.698s (+8.4% 🔺)3.490s (-1.1%)1.792s101.00x
▲ VercelNext.js (Turbopack)1.817s (+9.1% 🔺)3.995s (-2.0%)2.177s101.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.555s (~)11.020s (~)0.465s31.00x
💻 LocalExpress10.631s (+0.6%)11.024s (~)0.393s31.01x
🐘 PostgresExpress10.684s (~)11.023s (-2.9%)0.339s31.01x
💻 LocalNext.js (Turbopack)10.763s (+1.5%)11.021s (~)0.258s31.02x
🐘 PostgresNext.js (Turbopack)10.827s (~)11.020s (~)0.193s31.03x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.033s (+5.3% 🔺)15.218s (-2.4%)1.186s21.00x
▲ VercelNext.js (Turbopack)15.723s (+16.5% 🔺)17.581s (+9.2% 🔺)1.858s21.12x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.725s (-0.9%)14.018s (~)0.293s51.00x
💻 LocalExpress13.845s (~)14.029s (-1.4%)0.184s51.01x
🐘 PostgresExpress14.191s (+2.6%)14.618s (+2.7%)0.427s51.03x
🐘 PostgresNext.js (Turbopack)14.421s (-1.2%)15.018s (~)0.597s41.05x
💻 LocalNext.js (Turbopack)14.569s (+3.9%)15.028s (+2.7%)0.459s41.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro23.236s (-8.9% 🟢)24.542s (-11.1% 🟢)1.306s31.00x
▲ VercelNext.js (Turbopack)24.439s (-4.2%)26.713s (-1.8%)2.273s31.05x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro12.500s (-1.9%)13.021s (~)0.521s71.00x
💻 LocalExpress12.697s (+0.8%)13.026s (~)0.329s71.02x
🐘 PostgresExpress13.420s (+5.4% 🔺)14.021s (+5.3% 🔺)0.601s71.07x
💻 LocalNext.js (Turbopack)13.706s (+5.8% 🔺)14.169s (+6.5% 🔺)0.464s71.10x
🐘 PostgresNext.js (Turbopack)14.007s (+1.8%)14.450s (+3.1%)0.443s71.12x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro30.766s (+5.9% 🔺)32.566s (+3.7%)1.800s31.00x
▲ VercelNext.js (Turbopack)33.943s (+14.6% 🔺)36.480s (+14.5% 🔺)2.537s31.10x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.174s (-2.1%)2.008s (~)0.834s151.00x
🐘 PostgresExpress1.234s (+2.8%)2.007s (-0.7%)0.774s151.05x
💻 LocalExpress1.235s (-2.3%)2.007s (~)0.772s151.05x
🐘 PostgresNext.js (Turbopack)1.252s (~)2.007s (~)0.755s151.07x
💻 LocalNext.js (Turbopack)1.377s (+12.6% 🔺)2.007s (~)0.630s151.17x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.525s (-8.8% 🟢)4.139s (-4.7%)1.613s81.00x
▲ VercelNext.js (Turbopack)2.534s (-5.7% 🟢)4.174s (-1.0%)1.641s81.00x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.252s (-0.5%)2.007s (~)0.755s151.00x
🐘 PostgresNext.js (Turbopack)1.401s (-0.6%)2.008s (~)0.607s151.12x
🐘 PostgresExpress1.546s (+16.5% 🔺)2.099s (+4.1%)0.554s151.23x
💻 LocalExpress1.681s (-10.7% 🟢)2.006s (-9.7% 🟢)0.325s151.34x
💻 LocalNext.js (Turbopack)1.798s (+4.2%)2.074s (+3.4%)0.276s151.44x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.382s (-16.9% 🟢)4.751s (-23.5% 🟢)1.369s71.00x
▲ VercelNext.js (Turbopack)3.982s (-3.0%)5.923s (-3.5%)1.941s61.18x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.409s (~)2.008s (~)0.599s151.00x
🐘 PostgresNext.js (Turbopack)1.727s (-1.9%)2.317s (+1.0%)0.590s131.23x
🐘 PostgresExpress1.734s (+24.4% 🔺)2.416s (+20.2% 🔺)0.682s131.23x
💻 LocalExpress4.668s (-11.9% 🟢)5.013s (-16.7% 🟢)0.345s63.31x
💻 LocalNext.js (Turbopack)4.701s (+6.9% 🔺)5.180s (+3.4%)0.478s63.34x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.417s (-19.6% 🟢)7.151s (-17.9% 🟢)1.734s51.00x
▲ VercelNext.js (Turbopack)5.831s (-0.7%)7.666s (-1.4%)1.835s51.08x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.195s (~)2.009s (~)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (-1.2%)2.009s (~)0.758s151.05x
🐘 PostgresExpress1.253s (+4.1%)2.026s (+0.9%)0.774s151.05x
💻 LocalNext.js (Turbopack)1.363s (+4.4%)2.006s (~)0.644s151.14x
💻 LocalExpress1.614s (-2.5%)2.007s (-3.2%)0.392s151.35x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.618s (-2.0%)4.137s (-4.5%)1.519s81.00x
▲ VercelNext.js (Turbopack)308.185s (+6948.8% 🔺)309.806s (+4822.6% 🔺)1.621s1117.73x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (-1.0%)2.009s (~)0.748s151.00x
🐘 PostgresNext.js (Turbopack)1.390s (~)2.007s (~)0.617s151.10x
🐘 PostgresExpress1.447s (+16.4% 🔺)2.082s (+3.5%)0.635s151.15x
💻 LocalExpress1.896s (-10.8% 🟢)2.294s (-11.4% 🟢)0.398s141.50x
💻 LocalNext.js (Turbopack)1.978s (-2.1%)2.316s (-7.6% 🟢)0.338s131.57x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.577s (-12.6% 🟢)4.853s (-19.2% 🟢)1.276s71.00x
▲ VercelNext.js (Turbopack)3.827s (-15.3% 🟢)5.471s (-11.3% 🟢)1.644s61.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.391s (-4.8%)2.008s (~)0.617s151.00x
🐘 PostgresExpress1.549s (+3.2%)2.331s (-2.8%)0.783s131.11x
🐘 PostgresNext.js (Turbopack)1.691s (+2.1%)2.316s (+11.7% 🔺)0.625s131.22x
💻 LocalNext.js (Turbopack)4.898s (-5.1% 🟢)5.513s (-8.4% 🟢)0.615s63.52x
💻 LocalExpress5.004s (-14.7% 🟢)5.680s (-8.6% 🟢)0.676s63.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.902s (+19.2% 🔺)9.075s (+19.8% 🔺)2.172s41.00x
▲ VercelNitro81.134s (+1545.9% 🔺)82.628s (+1126.6% 🔺)1.494s411.75x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.539s (-19.2% 🟢)1.029s (-1.8%)0.490s591.00x
🐘 PostgresNitro0.566s (-7.0% 🟢)1.024s (+1.6%)0.457s591.05x
💻 LocalExpress0.660s (+0.5%)1.005s (-3.3%)0.346s601.22x
🐘 PostgresNext.js (Turbopack)0.825s (+1.6%)1.023s (+1.7%)0.199s591.53x
💻 LocalNext.js (Turbopack)0.861s (+23.7% 🔺)1.039s (+3.4%)0.178s581.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.557s (+10.1% 🔺)7.193s (+5.0% 🔺)1.636s91.00x
▲ VercelNitro6.289s (-15.7% 🟢)7.902s (-17.0% 🟢)1.614s81.13x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (-3.6%)2.007s (~)0.641s451.00x
🐘 PostgresExpress1.494s (-2.8%)2.108s (-2.0%)0.615s431.09x
💻 LocalExpress1.619s (+1.7%)2.029s (~)0.410s451.18x
🐘 PostgresNext.js (Turbopack)1.968s (+1.3%)2.284s (+5.0% 🔺)0.316s401.44x
💻 LocalNext.js (Turbopack)2.067s (+19.6% 🔺)2.882s (+42.1% 🔺)0.815s321.51x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.292s (+8.1% 🔺)15.666s (+1.2%)1.374s61.00x
▲ VercelNext.js (Turbopack)16.308s (+10.4% 🔺)18.593s (+9.8% 🔺)2.285s51.14x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.649s (-9.1% 🟢)3.085s (-5.9% 🟢)0.436s391.00x
🐘 PostgresExpress3.135s (+7.7% 🔺)3.682s (+4.0%)0.547s331.18x
💻 LocalExpress3.545s (+3.7%)4.010s (~)0.465s301.34x
🐘 PostgresNext.js (Turbopack)3.876s (~)4.042s (-0.8%)0.166s301.46x
💻 LocalNext.js (Turbopack)4.417s (+18.5% 🔺)5.052s (+22.9% 🔺)0.635s241.67x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.414s (+5.5% 🔺)32.492s (+1.6%)1.078s41.00x
▲ VercelNext.js (Turbopack)32.338s (+19.8% 🔺)34.168s (+16.7% 🔺)1.829s41.03x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.212s (-10.0% 🟢)1.006s (~)0.794s601.00x
🐘 PostgresExpress0.248s (-1.5%)1.011s (~)0.763s601.17x
🐘 PostgresNext.js (Turbopack)0.266s (-5.5% 🟢)1.006s (~)0.740s601.26x
💻 LocalNext.js (Turbopack)0.578s (-5.3% 🟢)1.057s (+1.8%)0.479s572.73x
💻 LocalExpress0.622s (+33.9% 🔺)1.160s (+15.5% 🔺)0.538s522.94x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.746s (+24.9% 🔺)4.424s (+4.8%)1.678s141.00x
▲ VercelNitro78.584s (+3251.3% 🔺)79.773s (+1740.7% 🔺)1.188s428.62x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.364s (-1.8%)1.018s (+1.1%)0.654s891.00x
🐘 PostgresExpress0.465s (+37.0% 🔺)1.025s (+1.8%)0.560s881.28x
🐘 PostgresNext.js (Turbopack)0.491s (+1.6%)1.018s (+1.1%)0.527s891.35x
💻 LocalNext.js (Turbopack)2.222s (-9.3% 🟢)3.010s (-3.2%)0.788s306.10x
💻 LocalExpress2.224s (+1.5%)2.854s (+1.1%)0.630s326.11x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.160s (-85.9% 🟢)7.805s (-83.0% 🟢)1.645s121.00x
▲ VercelNext.js (Turbopack)40.420s (+544.6% 🔺)42.078s (+395.2% 🔺)1.658s96.56x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.682s (-3.5%)1.006s (~)0.324s1201.00x
🐘 PostgresExpress0.855s (+36.6% 🔺)1.256s (+19.0% 🔺)0.401s961.25x
🐘 PostgresNext.js (Turbopack)1.000s (+2.1%)1.814s (+12.8% 🔺)0.814s671.47x
💻 LocalNext.js (Turbopack)10.521s (+2.0%)11.483s (+1.6%)0.962s1115.42x
💻 LocalExpress10.523s (+2.0%)11.126s (+2.4%)0.603s1115.42x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.195s (-94.5% 🟢)18.692s (-94.0% 🟢)1.497s71.00x
▲ VercelNext.js (Turbopack)58.866s (-63.2% 🟢)60.845s (-62.5% 🟢)1.979s73.42x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.163s (-1.5%)2.002s (~)0.001s (-14.3% 🟢)2.010s (~)0.847s101.00x
💻 LocalExpress1.164s (~)2.005s (~)0.011s (-16.4% 🟢)2.019s (~)0.855s101.00x
💻 LocalNext.js (Turbopack)1.199s (+2.6%)2.003s (~)0.010s (+2.0%)2.017s (~)0.818s101.03x
🐘 PostgresNext.js (Turbopack)1.231s (~)2.001s (~)0.001s (-33.3% 🟢)2.011s (~)0.779s101.06x
🐘 PostgresExpress1.243s (+4.1%)1.999s (~)0.001s (-97.7% 🟢)2.012s (-1.5%)0.769s101.07x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.331s (+7.7% 🔺)3.071s (-10.1% 🟢)2.051s (+297.6% 🔺)5.565s (+24.5% 🔺)3.234s101.00x
▲ VercelNext.js (Turbopack)2.494s (+11.3% 🔺)3.775s (+6.3% 🔺)2.256s (-81.8% 🟢)6.587s (-60.1% 🟢)4.092s101.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.595s (-4.1%)2.006s (~)0.004s (+1.7%)2.025s (~)0.431s301.00x
💻 LocalExpress1.596s (-2.5%)2.009s (~)0.011s (+1.5%)2.023s (~)0.427s301.00x
💻 LocalNext.js (Turbopack)1.709s (+4.2%)2.007s (~)0.011s (+5.3% 🔺)2.021s (~)0.312s301.07x
🐘 PostgresNext.js (Turbopack)1.770s (~)2.011s (~)0.004s (-1.7%)2.027s (~)0.257s301.11x
🐘 PostgresExpress1.919s (+10.5% 🔺)2.313s (+7.9% 🔺)0.003s (+1.6%)2.341s (+8.4% 🔺)0.422s261.20x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.971s (+9.4% 🔺)8.581s (+8.1% 🔺)0.274s (-24.3% 🟢)9.398s (+6.0% 🔺)2.426s71.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.743s (+2.8%)1.029s (~)0.000s (-83.3% 🟢)1.049s (~)0.306s581.00x
🐘 PostgresNext.js (Turbopack)0.801s (-3.7%)1.053s (-1.7%)0.000s (+96.5% 🔺)1.060s (-1.8%)0.259s571.08x
🐘 PostgresExpress1.081s (+10.6% 🔺)1.498s (+4.9%)0.000s (-30.0% 🟢)1.518s (+4.8%)0.438s401.45x
💻 LocalNext.js (Turbopack)1.447s (~)2.012s (~)0.000s (-75.0% 🟢)2.015s (~)0.569s301.95x
💻 LocalExpress1.470s (-0.9%)2.013s (~)0.001s (+73.3% 🔺)2.016s (~)0.547s301.98x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.750s (+4.1%)4.851s (-4.4%)0.001s (+Infinity% 🔺)5.276s (-6.3% 🟢)1.527s121.00x
▲ VercelNext.js (Turbopack)4.000s (+20.9% 🔺)5.605s (+14.5% 🔺)0.000s (+Infinity% 🔺)6.117s (+12.0% 🔺)2.117s111.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.384s (-1.9%)2.099s (+5.2% 🔺)0.000s (+Infinity% 🔺)2.114s (+5.1% 🔺)0.731s291.00x
🐘 PostgresNext.js (Turbopack)1.784s (+3.2%)2.310s (+2.2%)0.000s (+Infinity% 🔺)2.333s (+2.8%)0.550s261.29x
🐘 PostgresExpress2.429s (+50.4% 🔺)3.004s (+33.3% 🔺)0.000s (+Infinity% 🔺)3.031s (+33.0% 🔺)0.602s211.76x
💻 LocalNext.js (Turbopack)2.896s (-9.9% 🟢)3.471s (-10.9% 🟢)0.001s (+77.8% 🔺)3.476s (-10.9% 🟢)0.580s182.09x
💻 LocalExpress3.239s (-2.8%)3.837s (~)0.001s (+183.3% 🔺)3.841s (~)0.602s162.34x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.053s (-14.1% 🟢)7.287s (-15.0% 🟢)0.000s (+Infinity% 🔺)7.727s (-15.0% 🟢)1.675s81.00x
▲ VercelNext.js (Turbopack)7.030s (+36.1% 🔺)8.492s (+27.2% 🔺)0.000s (-100.0% 🟢)8.993s (+24.9% 🔺)1.963s71.16x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress14/21
🐘 PostgresNitro20/21
▲ VercelNitro16/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres21/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: failure
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@github-actions

github-actionsBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production126602191485
✅ 💻 Local Development167102191890
✅ 📦 Local Production167102191890
✅ 🐘 Local Postgres167102191890
✅ 🪟 Windows13500135
✅ 📋 Other7690176945
Total7183010528235

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro109026
✅ example109026
✅ express109026
✅ fastify109026
✅ hono109026
✅ nextjs-turbopack13302
✅ nextjs-webpack13302
✅ nitro109026
✅ nuxt109026
✅ sveltekit12807
✅ vite109026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable110025
✅ e2e-local-dev-tanstack-start-110025
✅ e2e-local-postgres-nest-stable110025
✅ e2e-local-postgres-tanstack-start-110025
✅ e2e-local-prod-nest-stable110025
✅ e2e-local-prod-tanstack-start-110025
✅ e2e-vercel-prod-tanstack-start109026

📋 View full workflow run

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

Adds defensive validation to @workflow/world-vercel ref resolution so empty/truncated ref payloads don’t propagate into replay/hydration and wedge runs during deterministic replay.

Changes:

  • Validate GET /v2/runs/:runId/refs bodies for zero-length and Content-Length mismatches, throwing WorkflowWorldError with specific error codes.
  • Add a new Vitest suite covering happy paths, zero-byte 200s, Content-Length mismatch, non-2xx passthrough, and inline dbrf behavior.
  • Add a patch changeset for @workflow/world-vercel.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/refs.tsAdds response body validation (empty body and Content-Length mismatch) before decoding/returning ref payloads.
packages/world-vercel/src/refs.test.tsIntroduces unit tests for ref resolution behavior across success/error/edge cases.
.changeset/world-vercel-reject-empty-ref-payload.mdDeclares a patch release for the new validation behavior.

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

Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.test.ts
Three review changes:
1. Reject any body shorter than the 4-byte format-prefix length, not
just zero-byte bodies. The SDK guarantees every stored ref payload
starts with a 4-byte format prefix (FORMAT_PREFIX_LENGTH in
@workflow/core), so a 1-3 byte body would also fail downstream
replay with the same 'Data too short to contain format prefix'
error this PR exists to prevent.
2. Parse Content-Length safely with parseInt + Number.isFinite +
non-negative checks instead of bare Number(). A non-numeric value
like 'abc' would otherwise produce NaN and silently surface as a
'truncated' error, masking the real cause. Malformed values are
treated as absent; the minimum-length check still defends against
actual truncation in that case.
3. Add tests for the truncated-body-without-Content-Length case
(chunked transfer where Content-Length validation can't see the
truncation), and for a malformed Content-Length header that should
be ignored rather than misreported as truncation.
The validation logic also moves into a small assertValidRefBody
helper to keep the inner trace function under the noExcessiveCognitiveComplexity limit.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential blocker in PR-version packages/world-vercel/src/refs.ts:143: the new Content-Length check compares response.arrayBuffer().byteLength to the header without accounting for Content-Encoding. Node fetch transparently decompresses gzip bodies while preserving the encoded Content-Length, so a valid compressed ref response can now throw ref-body-length-mismatch. I’d skip this check when Content-Encoding is present and not identity, or force/request identity encoding.

fetch/undici transparently decompresses gzip/br bodies but leaves
Content-Length describing the encoded (compressed) size, so comparing it
against the decompressed byteLength would reject valid compressed refs as
a phantom 'ref-body-length-mismatch'. Skip the comparison when a
non-identity Content-Encoding is present; an absent or 'identity' encoding
is still validated. Adds regression tests for both cases.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@karthikscale3 great catch — fixed in c8f5dce.

You're right: fetch/undici transparently decompresses the body but leaves Content-Length describing the encoded (compressed) size, so a gzip/br ref response would have Content-Length (compressed) ≠ decompressed byteLength and falsely throw ref-body-length-mismatch.

The length comparison is now skipped whenever a non-identityContent-Encoding is present (gzip, br, etc.). An absent or identity encoding means no transform was applied, so the lengths remain directly comparable and that path is still validated. The zero-byte and binary 4-byte-minimum checks are unaffected (they operate on the decompressed body and apply regardless of encoding).

Added two regression tests:

  • skips the length check for compressed (Content-Encoding) responsesContent-Length: 20 + Content-Encoding: gzip with a larger decompressed body still decodes successfully.
  • still enforces the length check for identity Content-EncodingContent-Encoding: identity with a real mismatch still throws.

pnpm --filter @workflow/world-vercel test / typecheck / biome all pass.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2297. (backport job run)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

[world-vercel] Validate ref resolve responses before use - #2035

Merged
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload
Jun 8, 2026
Merged

[world-vercel] Validate ref resolve responses before use#2035
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Adds defense-in-depth validation in resolveRefDescriptor so a zero-byte or truncated ref response from GET /v2/runs/:runId/refs no longer corrupts the in-memory event log during workflow replay.

The workflow runtime feeds ref payload bytes directly into decodeFormatPrefix, which requires at least the 4-byte format prefix that the SDK always writes (encodeWithFormatPrefix in @workflow/core). A zero-byte response is therefore never a valid stored value.

Before this change, resolveRefDescriptor checked response.ok and then handed the empty arrayBuffer() downstream verbatim. Multiple users have reported the resulting failure:

Data too short to contain format prefix: expected at least 4 bytes, got 0

That error surfaces deep inside deterministic replay. Because the SDK has already populated its in-memory event snapshot with the empty payload at that point, the run is wedged: every subsequent invocation deterministically replays the same failure, any downstream resumeHook() then surfaces as Hook not found, and the run only clears when stale-run cleanup terminates the sandbox.

Changes

  • packages/world-vercel/src/refs.ts:
    • After await response.arrayBuffer(), validate buffer.byteLength > 0. Empty bodies throw a WorkflowWorldError with code: 'empty-ref-body'.
    • When Content-Length is present, validate it matches the actual body length. Truncated responses (proxy abort, upstream stream cut mid-flight) throw WorkflowWorldError with code: 'ref-body-length-mismatch'.
    • Both errors record on the span via recordException and tag ErrorType for o11y.
  • packages/world-vercel/src/refs.test.ts (new):
    • Happy path coverage (CBOR + application/octet-stream).
    • Zero-byte 200 with both content types.
    • Content-Length mismatch (truncated stream).
    • Absent Content-Length (chunked transfer encoding) — happy path only.
    • Non-2xx status passes through as HTTP <code>.
    • Inline dbrf refs decode without making a network request.
  • Changeset: @workflow/world-vercel patch.

Why both ends validate

This is the SDK-side companion to vercel/workflow-server#432, which rejects zero-byte payloads at the storage boundary. Both layers are necessary because they catch failures at different points in the stack:

  • Server-side catches storage anomalies (S3 inconsistency, Redis empty key, corrupted write).
  • Client-side catches transport anomalies between the server and the SDK (proxy drop, edge cache miss returning truncated content, undici quirks, etc.).

WorkflowWorldError is treated as a retryable transport-level error by the runtime, which is the desired behavior here: surface the empty/truncated body up to the retry layer instead of poisoning event-log replay.

Validation

  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm --filter @workflow/world-vercel test (77 tests pass, including 8 new in refs.test.ts)
  • pnpm --filter @workflow/world-vercel build
  • pnpm biome check --files-ignore-unknown=true packages/world-vercel/src/refs.ts packages/world-vercel/src/refs.test.ts
  • pnpm changeset status --since=main (confirms @workflow/world-vercel patch bump)

When workflow-server returns a ref body to the SDK, the bytes are
fed into the workflow runtime's event log and deserialized via
`decodeFormatPrefix`. The SDK always writes ref payloads with at
least a 4-byte format prefix (see `encodeWithFormatPrefix` in
`@workflow/core`), so a zero-byte response — or one whose length
disagrees with `Content-Length` — is never a valid stored value.
Before this change, `resolveRefDescriptor` had no validation: a
200 with an empty body would be passed downstream as a zero-length
Uint8Array, which then failed deep inside replay with:
Data too short to contain format prefix: expected at least 4 bytes, got 0
By that point the workflow's in-memory event snapshot is already
poisoned with the empty payload, so every subsequent replay
deterministically reproduces the same failure, downstream
`resumeHook()` calls surface as `Hook not found`, and the run
only unsticks when stale-run cleanup terminates the sandbox.
This catches the failure at the transport boundary instead, where
it can be retried as a `WorkflowWorldError`. Both an empty body
and a length mismatch (truncated streaming response) are rejected.
This is the SDK-side companion to vercel/workflow-server#432, which
adds the same validation on the server side.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 20, 2026 17:13
CopilotAI review requested due to automatic review settings May 20, 2026 17:13
@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c8f5dce

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

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

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

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

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 20, 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🥇 Express0.049s (+13.7% 🔺)1.007s (~)0.958s101.00x
💻 LocalNext.js (Turbopack)0.059s (+6.4% 🔺)1.005s (~)0.947s101.19x
🐘 PostgresNitro0.061s (-2.7%)1.012s (~)0.951s101.25x
🐘 PostgresNext.js (Turbopack)0.067s (-3.7%)1.012s (~)0.945s101.37x
🐘 PostgresExpress0.079s (+18.9% 🔺)1.031s (+1.6%)0.952s101.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.291s (-11.6% 🟢)2.378s (+1.6%)2.087s101.00x
▲ VercelNitro0.360s (+37.9% 🔺)2.201s (-9.7% 🟢)1.840s101.24x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.101s (~)2.006s (~)0.905s101.00x
🐘 PostgresNitro1.116s (~)2.010s (~)0.894s101.01x
💻 LocalNext.js (Turbopack)1.130s (+2.0%)2.006s (~)0.876s101.03x
🐘 PostgresNext.js (Turbopack)1.151s (+0.8%)2.009s (~)0.859s101.04x
🐘 PostgresExpress1.170s (+7.1% 🔺)2.027s (+0.8%)0.857s101.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.698s (+8.4% 🔺)3.490s (-1.1%)1.792s101.00x
▲ VercelNext.js (Turbopack)1.817s (+9.1% 🔺)3.995s (-2.0%)2.177s101.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.555s (~)11.020s (~)0.465s31.00x
💻 LocalExpress10.631s (+0.6%)11.024s (~)0.393s31.01x
🐘 PostgresExpress10.684s (~)11.023s (-2.9%)0.339s31.01x
💻 LocalNext.js (Turbopack)10.763s (+1.5%)11.021s (~)0.258s31.02x
🐘 PostgresNext.js (Turbopack)10.827s (~)11.020s (~)0.193s31.03x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.033s (+5.3% 🔺)15.218s (-2.4%)1.186s21.00x
▲ VercelNext.js (Turbopack)15.723s (+16.5% 🔺)17.581s (+9.2% 🔺)1.858s21.12x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.725s (-0.9%)14.018s (~)0.293s51.00x
💻 LocalExpress13.845s (~)14.029s (-1.4%)0.184s51.01x
🐘 PostgresExpress14.191s (+2.6%)14.618s (+2.7%)0.427s51.03x
🐘 PostgresNext.js (Turbopack)14.421s (-1.2%)15.018s (~)0.597s41.05x
💻 LocalNext.js (Turbopack)14.569s (+3.9%)15.028s (+2.7%)0.459s41.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro23.236s (-8.9% 🟢)24.542s (-11.1% 🟢)1.306s31.00x
▲ VercelNext.js (Turbopack)24.439s (-4.2%)26.713s (-1.8%)2.273s31.05x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro12.500s (-1.9%)13.021s (~)0.521s71.00x
💻 LocalExpress12.697s (+0.8%)13.026s (~)0.329s71.02x
🐘 PostgresExpress13.420s (+5.4% 🔺)14.021s (+5.3% 🔺)0.601s71.07x
💻 LocalNext.js (Turbopack)13.706s (+5.8% 🔺)14.169s (+6.5% 🔺)0.464s71.10x
🐘 PostgresNext.js (Turbopack)14.007s (+1.8%)14.450s (+3.1%)0.443s71.12x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro30.766s (+5.9% 🔺)32.566s (+3.7%)1.800s31.00x
▲ VercelNext.js (Turbopack)33.943s (+14.6% 🔺)36.480s (+14.5% 🔺)2.537s31.10x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.174s (-2.1%)2.008s (~)0.834s151.00x
🐘 PostgresExpress1.234s (+2.8%)2.007s (-0.7%)0.774s151.05x
💻 LocalExpress1.235s (-2.3%)2.007s (~)0.772s151.05x
🐘 PostgresNext.js (Turbopack)1.252s (~)2.007s (~)0.755s151.07x
💻 LocalNext.js (Turbopack)1.377s (+12.6% 🔺)2.007s (~)0.630s151.17x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.525s (-8.8% 🟢)4.139s (-4.7%)1.613s81.00x
▲ VercelNext.js (Turbopack)2.534s (-5.7% 🟢)4.174s (-1.0%)1.641s81.00x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.252s (-0.5%)2.007s (~)0.755s151.00x
🐘 PostgresNext.js (Turbopack)1.401s (-0.6%)2.008s (~)0.607s151.12x
🐘 PostgresExpress1.546s (+16.5% 🔺)2.099s (+4.1%)0.554s151.23x
💻 LocalExpress1.681s (-10.7% 🟢)2.006s (-9.7% 🟢)0.325s151.34x
💻 LocalNext.js (Turbopack)1.798s (+4.2%)2.074s (+3.4%)0.276s151.44x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.382s (-16.9% 🟢)4.751s (-23.5% 🟢)1.369s71.00x
▲ VercelNext.js (Turbopack)3.982s (-3.0%)5.923s (-3.5%)1.941s61.18x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.409s (~)2.008s (~)0.599s151.00x
🐘 PostgresNext.js (Turbopack)1.727s (-1.9%)2.317s (+1.0%)0.590s131.23x
🐘 PostgresExpress1.734s (+24.4% 🔺)2.416s (+20.2% 🔺)0.682s131.23x
💻 LocalExpress4.668s (-11.9% 🟢)5.013s (-16.7% 🟢)0.345s63.31x
💻 LocalNext.js (Turbopack)4.701s (+6.9% 🔺)5.180s (+3.4%)0.478s63.34x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.417s (-19.6% 🟢)7.151s (-17.9% 🟢)1.734s51.00x
▲ VercelNext.js (Turbopack)5.831s (-0.7%)7.666s (-1.4%)1.835s51.08x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.195s (~)2.009s (~)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (-1.2%)2.009s (~)0.758s151.05x
🐘 PostgresExpress1.253s (+4.1%)2.026s (+0.9%)0.774s151.05x
💻 LocalNext.js (Turbopack)1.363s (+4.4%)2.006s (~)0.644s151.14x
💻 LocalExpress1.614s (-2.5%)2.007s (-3.2%)0.392s151.35x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.618s (-2.0%)4.137s (-4.5%)1.519s81.00x
▲ VercelNext.js (Turbopack)308.185s (+6948.8% 🔺)309.806s (+4822.6% 🔺)1.621s1117.73x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (-1.0%)2.009s (~)0.748s151.00x
🐘 PostgresNext.js (Turbopack)1.390s (~)2.007s (~)0.617s151.10x
🐘 PostgresExpress1.447s (+16.4% 🔺)2.082s (+3.5%)0.635s151.15x
💻 LocalExpress1.896s (-10.8% 🟢)2.294s (-11.4% 🟢)0.398s141.50x
💻 LocalNext.js (Turbopack)1.978s (-2.1%)2.316s (-7.6% 🟢)0.338s131.57x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.577s (-12.6% 🟢)4.853s (-19.2% 🟢)1.276s71.00x
▲ VercelNext.js (Turbopack)3.827s (-15.3% 🟢)5.471s (-11.3% 🟢)1.644s61.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.391s (-4.8%)2.008s (~)0.617s151.00x
🐘 PostgresExpress1.549s (+3.2%)2.331s (-2.8%)0.783s131.11x
🐘 PostgresNext.js (Turbopack)1.691s (+2.1%)2.316s (+11.7% 🔺)0.625s131.22x
💻 LocalNext.js (Turbopack)4.898s (-5.1% 🟢)5.513s (-8.4% 🟢)0.615s63.52x
💻 LocalExpress5.004s (-14.7% 🟢)5.680s (-8.6% 🟢)0.676s63.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.902s (+19.2% 🔺)9.075s (+19.8% 🔺)2.172s41.00x
▲ VercelNitro81.134s (+1545.9% 🔺)82.628s (+1126.6% 🔺)1.494s411.75x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.539s (-19.2% 🟢)1.029s (-1.8%)0.490s591.00x
🐘 PostgresNitro0.566s (-7.0% 🟢)1.024s (+1.6%)0.457s591.05x
💻 LocalExpress0.660s (+0.5%)1.005s (-3.3%)0.346s601.22x
🐘 PostgresNext.js (Turbopack)0.825s (+1.6%)1.023s (+1.7%)0.199s591.53x
💻 LocalNext.js (Turbopack)0.861s (+23.7% 🔺)1.039s (+3.4%)0.178s581.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.557s (+10.1% 🔺)7.193s (+5.0% 🔺)1.636s91.00x
▲ VercelNitro6.289s (-15.7% 🟢)7.902s (-17.0% 🟢)1.614s81.13x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (-3.6%)2.007s (~)0.641s451.00x
🐘 PostgresExpress1.494s (-2.8%)2.108s (-2.0%)0.615s431.09x
💻 LocalExpress1.619s (+1.7%)2.029s (~)0.410s451.18x
🐘 PostgresNext.js (Turbopack)1.968s (+1.3%)2.284s (+5.0% 🔺)0.316s401.44x
💻 LocalNext.js (Turbopack)2.067s (+19.6% 🔺)2.882s (+42.1% 🔺)0.815s321.51x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.292s (+8.1% 🔺)15.666s (+1.2%)1.374s61.00x
▲ VercelNext.js (Turbopack)16.308s (+10.4% 🔺)18.593s (+9.8% 🔺)2.285s51.14x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.649s (-9.1% 🟢)3.085s (-5.9% 🟢)0.436s391.00x
🐘 PostgresExpress3.135s (+7.7% 🔺)3.682s (+4.0%)0.547s331.18x
💻 LocalExpress3.545s (+3.7%)4.010s (~)0.465s301.34x
🐘 PostgresNext.js (Turbopack)3.876s (~)4.042s (-0.8%)0.166s301.46x
💻 LocalNext.js (Turbopack)4.417s (+18.5% 🔺)5.052s (+22.9% 🔺)0.635s241.67x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.414s (+5.5% 🔺)32.492s (+1.6%)1.078s41.00x
▲ VercelNext.js (Turbopack)32.338s (+19.8% 🔺)34.168s (+16.7% 🔺)1.829s41.03x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.212s (-10.0% 🟢)1.006s (~)0.794s601.00x
🐘 PostgresExpress0.248s (-1.5%)1.011s (~)0.763s601.17x
🐘 PostgresNext.js (Turbopack)0.266s (-5.5% 🟢)1.006s (~)0.740s601.26x
💻 LocalNext.js (Turbopack)0.578s (-5.3% 🟢)1.057s (+1.8%)0.479s572.73x
💻 LocalExpress0.622s (+33.9% 🔺)1.160s (+15.5% 🔺)0.538s522.94x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.746s (+24.9% 🔺)4.424s (+4.8%)1.678s141.00x
▲ VercelNitro78.584s (+3251.3% 🔺)79.773s (+1740.7% 🔺)1.188s428.62x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.364s (-1.8%)1.018s (+1.1%)0.654s891.00x
🐘 PostgresExpress0.465s (+37.0% 🔺)1.025s (+1.8%)0.560s881.28x
🐘 PostgresNext.js (Turbopack)0.491s (+1.6%)1.018s (+1.1%)0.527s891.35x
💻 LocalNext.js (Turbopack)2.222s (-9.3% 🟢)3.010s (-3.2%)0.788s306.10x
💻 LocalExpress2.224s (+1.5%)2.854s (+1.1%)0.630s326.11x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.160s (-85.9% 🟢)7.805s (-83.0% 🟢)1.645s121.00x
▲ VercelNext.js (Turbopack)40.420s (+544.6% 🔺)42.078s (+395.2% 🔺)1.658s96.56x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.682s (-3.5%)1.006s (~)0.324s1201.00x
🐘 PostgresExpress0.855s (+36.6% 🔺)1.256s (+19.0% 🔺)0.401s961.25x
🐘 PostgresNext.js (Turbopack)1.000s (+2.1%)1.814s (+12.8% 🔺)0.814s671.47x
💻 LocalNext.js (Turbopack)10.521s (+2.0%)11.483s (+1.6%)0.962s1115.42x
💻 LocalExpress10.523s (+2.0%)11.126s (+2.4%)0.603s1115.42x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.195s (-94.5% 🟢)18.692s (-94.0% 🟢)1.497s71.00x
▲ VercelNext.js (Turbopack)58.866s (-63.2% 🟢)60.845s (-62.5% 🟢)1.979s73.42x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.163s (-1.5%)2.002s (~)0.001s (-14.3% 🟢)2.010s (~)0.847s101.00x
💻 LocalExpress1.164s (~)2.005s (~)0.011s (-16.4% 🟢)2.019s (~)0.855s101.00x
💻 LocalNext.js (Turbopack)1.199s (+2.6%)2.003s (~)0.010s (+2.0%)2.017s (~)0.818s101.03x
🐘 PostgresNext.js (Turbopack)1.231s (~)2.001s (~)0.001s (-33.3% 🟢)2.011s (~)0.779s101.06x
🐘 PostgresExpress1.243s (+4.1%)1.999s (~)0.001s (-97.7% 🟢)2.012s (-1.5%)0.769s101.07x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.331s (+7.7% 🔺)3.071s (-10.1% 🟢)2.051s (+297.6% 🔺)5.565s (+24.5% 🔺)3.234s101.00x
▲ VercelNext.js (Turbopack)2.494s (+11.3% 🔺)3.775s (+6.3% 🔺)2.256s (-81.8% 🟢)6.587s (-60.1% 🟢)4.092s101.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.595s (-4.1%)2.006s (~)0.004s (+1.7%)2.025s (~)0.431s301.00x
💻 LocalExpress1.596s (-2.5%)2.009s (~)0.011s (+1.5%)2.023s (~)0.427s301.00x
💻 LocalNext.js (Turbopack)1.709s (+4.2%)2.007s (~)0.011s (+5.3% 🔺)2.021s (~)0.312s301.07x
🐘 PostgresNext.js (Turbopack)1.770s (~)2.011s (~)0.004s (-1.7%)2.027s (~)0.257s301.11x
🐘 PostgresExpress1.919s (+10.5% 🔺)2.313s (+7.9% 🔺)0.003s (+1.6%)2.341s (+8.4% 🔺)0.422s261.20x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.971s (+9.4% 🔺)8.581s (+8.1% 🔺)0.274s (-24.3% 🟢)9.398s (+6.0% 🔺)2.426s71.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.743s (+2.8%)1.029s (~)0.000s (-83.3% 🟢)1.049s (~)0.306s581.00x
🐘 PostgresNext.js (Turbopack)0.801s (-3.7%)1.053s (-1.7%)0.000s (+96.5% 🔺)1.060s (-1.8%)0.259s571.08x
🐘 PostgresExpress1.081s (+10.6% 🔺)1.498s (+4.9%)0.000s (-30.0% 🟢)1.518s (+4.8%)0.438s401.45x
💻 LocalNext.js (Turbopack)1.447s (~)2.012s (~)0.000s (-75.0% 🟢)2.015s (~)0.569s301.95x
💻 LocalExpress1.470s (-0.9%)2.013s (~)0.001s (+73.3% 🔺)2.016s (~)0.547s301.98x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.750s (+4.1%)4.851s (-4.4%)0.001s (+Infinity% 🔺)5.276s (-6.3% 🟢)1.527s121.00x
▲ VercelNext.js (Turbopack)4.000s (+20.9% 🔺)5.605s (+14.5% 🔺)0.000s (+Infinity% 🔺)6.117s (+12.0% 🔺)2.117s111.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.384s (-1.9%)2.099s (+5.2% 🔺)0.000s (+Infinity% 🔺)2.114s (+5.1% 🔺)0.731s291.00x
🐘 PostgresNext.js (Turbopack)1.784s (+3.2%)2.310s (+2.2%)0.000s (+Infinity% 🔺)2.333s (+2.8%)0.550s261.29x
🐘 PostgresExpress2.429s (+50.4% 🔺)3.004s (+33.3% 🔺)0.000s (+Infinity% 🔺)3.031s (+33.0% 🔺)0.602s211.76x
💻 LocalNext.js (Turbopack)2.896s (-9.9% 🟢)3.471s (-10.9% 🟢)0.001s (+77.8% 🔺)3.476s (-10.9% 🟢)0.580s182.09x
💻 LocalExpress3.239s (-2.8%)3.837s (~)0.001s (+183.3% 🔺)3.841s (~)0.602s162.34x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.053s (-14.1% 🟢)7.287s (-15.0% 🟢)0.000s (+Infinity% 🔺)7.727s (-15.0% 🟢)1.675s81.00x
▲ VercelNext.js (Turbopack)7.030s (+36.1% 🔺)8.492s (+27.2% 🔺)0.000s (-100.0% 🟢)8.993s (+24.9% 🔺)1.963s71.16x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress14/21
🐘 PostgresNitro20/21
▲ VercelNitro16/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres21/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: failure
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@github-actions

github-actionsBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production126602191485
✅ 💻 Local Development167102191890
✅ 📦 Local Production167102191890
✅ 🐘 Local Postgres167102191890
✅ 🪟 Windows13500135
✅ 📋 Other7690176945
Total7183010528235

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro109026
✅ example109026
✅ express109026
✅ fastify109026
✅ hono109026
✅ nextjs-turbopack13302
✅ nextjs-webpack13302
✅ nitro109026
✅ nuxt109026
✅ sveltekit12807
✅ vite109026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable110025
✅ e2e-local-dev-tanstack-start-110025
✅ e2e-local-postgres-nest-stable110025
✅ e2e-local-postgres-tanstack-start-110025
✅ e2e-local-prod-nest-stable110025
✅ e2e-local-prod-tanstack-start-110025
✅ e2e-vercel-prod-tanstack-start109026

📋 View full workflow run

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

Adds defensive validation to @workflow/world-vercel ref resolution so empty/truncated ref payloads don’t propagate into replay/hydration and wedge runs during deterministic replay.

Changes:

  • Validate GET /v2/runs/:runId/refs bodies for zero-length and Content-Length mismatches, throwing WorkflowWorldError with specific error codes.
  • Add a new Vitest suite covering happy paths, zero-byte 200s, Content-Length mismatch, non-2xx passthrough, and inline dbrf behavior.
  • Add a patch changeset for @workflow/world-vercel.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/refs.tsAdds response body validation (empty body and Content-Length mismatch) before decoding/returning ref payloads.
packages/world-vercel/src/refs.test.tsIntroduces unit tests for ref resolution behavior across success/error/edge cases.
.changeset/world-vercel-reject-empty-ref-payload.mdDeclares a patch release for the new validation behavior.

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

Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.test.ts
Three review changes:
1. Reject any body shorter than the 4-byte format-prefix length, not
just zero-byte bodies. The SDK guarantees every stored ref payload
starts with a 4-byte format prefix (FORMAT_PREFIX_LENGTH in
@workflow/core), so a 1-3 byte body would also fail downstream
replay with the same 'Data too short to contain format prefix'
error this PR exists to prevent.
2. Parse Content-Length safely with parseInt + Number.isFinite +
non-negative checks instead of bare Number(). A non-numeric value
like 'abc' would otherwise produce NaN and silently surface as a
'truncated' error, masking the real cause. Malformed values are
treated as absent; the minimum-length check still defends against
actual truncation in that case.
3. Add tests for the truncated-body-without-Content-Length case
(chunked transfer where Content-Length validation can't see the
truncation), and for a malformed Content-Length header that should
be ignored rather than misreported as truncation.
The validation logic also moves into a small assertValidRefBody
helper to keep the inner trace function under the noExcessiveCognitiveComplexity limit.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential blocker in PR-version packages/world-vercel/src/refs.ts:143: the new Content-Length check compares response.arrayBuffer().byteLength to the header without accounting for Content-Encoding. Node fetch transparently decompresses gzip bodies while preserving the encoded Content-Length, so a valid compressed ref response can now throw ref-body-length-mismatch. I’d skip this check when Content-Encoding is present and not identity, or force/request identity encoding.

fetch/undici transparently decompresses gzip/br bodies but leaves
Content-Length describing the encoded (compressed) size, so comparing it
against the decompressed byteLength would reject valid compressed refs as
a phantom 'ref-body-length-mismatch'. Skip the comparison when a
non-identity Content-Encoding is present; an absent or 'identity' encoding
is still validated. Adds regression tests for both cases.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@karthikscale3 great catch — fixed in c8f5dce.

You're right: fetch/undici transparently decompresses the body but leaves Content-Length describing the encoded (compressed) size, so a gzip/br ref response would have Content-Length (compressed) ≠ decompressed byteLength and falsely throw ref-body-length-mismatch.

The length comparison is now skipped whenever a non-identityContent-Encoding is present (gzip, br, etc.). An absent or identity encoding means no transform was applied, so the lengths remain directly comparable and that path is still validated. The zero-byte and binary 4-byte-minimum checks are unaffected (they operate on the decompressed body and apply regardless of encoding).

Added two regression tests:

  • skips the length check for compressed (Content-Encoding) responsesContent-Length: 20 + Content-Encoding: gzip with a larger decompressed body still decodes successfully.
  • still enforces the length check for identity Content-EncodingContent-Encoding: identity with a real mismatch still throws.

pnpm --filter @workflow/world-vercel test / typecheck / biome all pass.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2297. (backport job run)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

[world-vercel] Validate ref resolve responses before use - #2035

Merged
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload
Jun 8, 2026
Merged

[world-vercel] Validate ref resolve responses before use#2035
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Adds defense-in-depth validation in resolveRefDescriptor so a zero-byte or truncated ref response from GET /v2/runs/:runId/refs no longer corrupts the in-memory event log during workflow replay.

The workflow runtime feeds ref payload bytes directly into decodeFormatPrefix, which requires at least the 4-byte format prefix that the SDK always writes (encodeWithFormatPrefix in @workflow/core). A zero-byte response is therefore never a valid stored value.

Before this change, resolveRefDescriptor checked response.ok and then handed the empty arrayBuffer() downstream verbatim. Multiple users have reported the resulting failure:

Data too short to contain format prefix: expected at least 4 bytes, got 0

That error surfaces deep inside deterministic replay. Because the SDK has already populated its in-memory event snapshot with the empty payload at that point, the run is wedged: every subsequent invocation deterministically replays the same failure, any downstream resumeHook() then surfaces as Hook not found, and the run only clears when stale-run cleanup terminates the sandbox.

Changes

  • packages/world-vercel/src/refs.ts:
    • After await response.arrayBuffer(), validate buffer.byteLength > 0. Empty bodies throw a WorkflowWorldError with code: 'empty-ref-body'.
    • When Content-Length is present, validate it matches the actual body length. Truncated responses (proxy abort, upstream stream cut mid-flight) throw WorkflowWorldError with code: 'ref-body-length-mismatch'.
    • Both errors record on the span via recordException and tag ErrorType for o11y.
  • packages/world-vercel/src/refs.test.ts (new):
    • Happy path coverage (CBOR + application/octet-stream).
    • Zero-byte 200 with both content types.
    • Content-Length mismatch (truncated stream).
    • Absent Content-Length (chunked transfer encoding) — happy path only.
    • Non-2xx status passes through as HTTP <code>.
    • Inline dbrf refs decode without making a network request.
  • Changeset: @workflow/world-vercel patch.

Why both ends validate

This is the SDK-side companion to vercel/workflow-server#432, which rejects zero-byte payloads at the storage boundary. Both layers are necessary because they catch failures at different points in the stack:

  • Server-side catches storage anomalies (S3 inconsistency, Redis empty key, corrupted write).
  • Client-side catches transport anomalies between the server and the SDK (proxy drop, edge cache miss returning truncated content, undici quirks, etc.).

WorkflowWorldError is treated as a retryable transport-level error by the runtime, which is the desired behavior here: surface the empty/truncated body up to the retry layer instead of poisoning event-log replay.

Validation

  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm --filter @workflow/world-vercel test (77 tests pass, including 8 new in refs.test.ts)
  • pnpm --filter @workflow/world-vercel build
  • pnpm biome check --files-ignore-unknown=true packages/world-vercel/src/refs.ts packages/world-vercel/src/refs.test.ts
  • pnpm changeset status --since=main (confirms @workflow/world-vercel patch bump)

When workflow-server returns a ref body to the SDK, the bytes are
fed into the workflow runtime's event log and deserialized via
`decodeFormatPrefix`. The SDK always writes ref payloads with at
least a 4-byte format prefix (see `encodeWithFormatPrefix` in
`@workflow/core`), so a zero-byte response — or one whose length
disagrees with `Content-Length` — is never a valid stored value.
Before this change, `resolveRefDescriptor` had no validation: a
200 with an empty body would be passed downstream as a zero-length
Uint8Array, which then failed deep inside replay with:
Data too short to contain format prefix: expected at least 4 bytes, got 0
By that point the workflow's in-memory event snapshot is already
poisoned with the empty payload, so every subsequent replay
deterministically reproduces the same failure, downstream
`resumeHook()` calls surface as `Hook not found`, and the run
only unsticks when stale-run cleanup terminates the sandbox.
This catches the failure at the transport boundary instead, where
it can be retried as a `WorkflowWorldError`. Both an empty body
and a length mismatch (truncated streaming response) are rejected.
This is the SDK-side companion to vercel/workflow-server#432, which
adds the same validation on the server side.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 20, 2026 17:13
CopilotAI review requested due to automatic review settings May 20, 2026 17:13
@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c8f5dce

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

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

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

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

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 20, 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🥇 Express0.049s (+13.7% 🔺)1.007s (~)0.958s101.00x
💻 LocalNext.js (Turbopack)0.059s (+6.4% 🔺)1.005s (~)0.947s101.19x
🐘 PostgresNitro0.061s (-2.7%)1.012s (~)0.951s101.25x
🐘 PostgresNext.js (Turbopack)0.067s (-3.7%)1.012s (~)0.945s101.37x
🐘 PostgresExpress0.079s (+18.9% 🔺)1.031s (+1.6%)0.952s101.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.291s (-11.6% 🟢)2.378s (+1.6%)2.087s101.00x
▲ VercelNitro0.360s (+37.9% 🔺)2.201s (-9.7% 🟢)1.840s101.24x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.101s (~)2.006s (~)0.905s101.00x
🐘 PostgresNitro1.116s (~)2.010s (~)0.894s101.01x
💻 LocalNext.js (Turbopack)1.130s (+2.0%)2.006s (~)0.876s101.03x
🐘 PostgresNext.js (Turbopack)1.151s (+0.8%)2.009s (~)0.859s101.04x
🐘 PostgresExpress1.170s (+7.1% 🔺)2.027s (+0.8%)0.857s101.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.698s (+8.4% 🔺)3.490s (-1.1%)1.792s101.00x
▲ VercelNext.js (Turbopack)1.817s (+9.1% 🔺)3.995s (-2.0%)2.177s101.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.555s (~)11.020s (~)0.465s31.00x
💻 LocalExpress10.631s (+0.6%)11.024s (~)0.393s31.01x
🐘 PostgresExpress10.684s (~)11.023s (-2.9%)0.339s31.01x
💻 LocalNext.js (Turbopack)10.763s (+1.5%)11.021s (~)0.258s31.02x
🐘 PostgresNext.js (Turbopack)10.827s (~)11.020s (~)0.193s31.03x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.033s (+5.3% 🔺)15.218s (-2.4%)1.186s21.00x
▲ VercelNext.js (Turbopack)15.723s (+16.5% 🔺)17.581s (+9.2% 🔺)1.858s21.12x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.725s (-0.9%)14.018s (~)0.293s51.00x
💻 LocalExpress13.845s (~)14.029s (-1.4%)0.184s51.01x
🐘 PostgresExpress14.191s (+2.6%)14.618s (+2.7%)0.427s51.03x
🐘 PostgresNext.js (Turbopack)14.421s (-1.2%)15.018s (~)0.597s41.05x
💻 LocalNext.js (Turbopack)14.569s (+3.9%)15.028s (+2.7%)0.459s41.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro23.236s (-8.9% 🟢)24.542s (-11.1% 🟢)1.306s31.00x
▲ VercelNext.js (Turbopack)24.439s (-4.2%)26.713s (-1.8%)2.273s31.05x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro12.500s (-1.9%)13.021s (~)0.521s71.00x
💻 LocalExpress12.697s (+0.8%)13.026s (~)0.329s71.02x
🐘 PostgresExpress13.420s (+5.4% 🔺)14.021s (+5.3% 🔺)0.601s71.07x
💻 LocalNext.js (Turbopack)13.706s (+5.8% 🔺)14.169s (+6.5% 🔺)0.464s71.10x
🐘 PostgresNext.js (Turbopack)14.007s (+1.8%)14.450s (+3.1%)0.443s71.12x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro30.766s (+5.9% 🔺)32.566s (+3.7%)1.800s31.00x
▲ VercelNext.js (Turbopack)33.943s (+14.6% 🔺)36.480s (+14.5% 🔺)2.537s31.10x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.174s (-2.1%)2.008s (~)0.834s151.00x
🐘 PostgresExpress1.234s (+2.8%)2.007s (-0.7%)0.774s151.05x
💻 LocalExpress1.235s (-2.3%)2.007s (~)0.772s151.05x
🐘 PostgresNext.js (Turbopack)1.252s (~)2.007s (~)0.755s151.07x
💻 LocalNext.js (Turbopack)1.377s (+12.6% 🔺)2.007s (~)0.630s151.17x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.525s (-8.8% 🟢)4.139s (-4.7%)1.613s81.00x
▲ VercelNext.js (Turbopack)2.534s (-5.7% 🟢)4.174s (-1.0%)1.641s81.00x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.252s (-0.5%)2.007s (~)0.755s151.00x
🐘 PostgresNext.js (Turbopack)1.401s (-0.6%)2.008s (~)0.607s151.12x
🐘 PostgresExpress1.546s (+16.5% 🔺)2.099s (+4.1%)0.554s151.23x
💻 LocalExpress1.681s (-10.7% 🟢)2.006s (-9.7% 🟢)0.325s151.34x
💻 LocalNext.js (Turbopack)1.798s (+4.2%)2.074s (+3.4%)0.276s151.44x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.382s (-16.9% 🟢)4.751s (-23.5% 🟢)1.369s71.00x
▲ VercelNext.js (Turbopack)3.982s (-3.0%)5.923s (-3.5%)1.941s61.18x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.409s (~)2.008s (~)0.599s151.00x
🐘 PostgresNext.js (Turbopack)1.727s (-1.9%)2.317s (+1.0%)0.590s131.23x
🐘 PostgresExpress1.734s (+24.4% 🔺)2.416s (+20.2% 🔺)0.682s131.23x
💻 LocalExpress4.668s (-11.9% 🟢)5.013s (-16.7% 🟢)0.345s63.31x
💻 LocalNext.js (Turbopack)4.701s (+6.9% 🔺)5.180s (+3.4%)0.478s63.34x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.417s (-19.6% 🟢)7.151s (-17.9% 🟢)1.734s51.00x
▲ VercelNext.js (Turbopack)5.831s (-0.7%)7.666s (-1.4%)1.835s51.08x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.195s (~)2.009s (~)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (-1.2%)2.009s (~)0.758s151.05x
🐘 PostgresExpress1.253s (+4.1%)2.026s (+0.9%)0.774s151.05x
💻 LocalNext.js (Turbopack)1.363s (+4.4%)2.006s (~)0.644s151.14x
💻 LocalExpress1.614s (-2.5%)2.007s (-3.2%)0.392s151.35x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.618s (-2.0%)4.137s (-4.5%)1.519s81.00x
▲ VercelNext.js (Turbopack)308.185s (+6948.8% 🔺)309.806s (+4822.6% 🔺)1.621s1117.73x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (-1.0%)2.009s (~)0.748s151.00x
🐘 PostgresNext.js (Turbopack)1.390s (~)2.007s (~)0.617s151.10x
🐘 PostgresExpress1.447s (+16.4% 🔺)2.082s (+3.5%)0.635s151.15x
💻 LocalExpress1.896s (-10.8% 🟢)2.294s (-11.4% 🟢)0.398s141.50x
💻 LocalNext.js (Turbopack)1.978s (-2.1%)2.316s (-7.6% 🟢)0.338s131.57x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.577s (-12.6% 🟢)4.853s (-19.2% 🟢)1.276s71.00x
▲ VercelNext.js (Turbopack)3.827s (-15.3% 🟢)5.471s (-11.3% 🟢)1.644s61.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.391s (-4.8%)2.008s (~)0.617s151.00x
🐘 PostgresExpress1.549s (+3.2%)2.331s (-2.8%)0.783s131.11x
🐘 PostgresNext.js (Turbopack)1.691s (+2.1%)2.316s (+11.7% 🔺)0.625s131.22x
💻 LocalNext.js (Turbopack)4.898s (-5.1% 🟢)5.513s (-8.4% 🟢)0.615s63.52x
💻 LocalExpress5.004s (-14.7% 🟢)5.680s (-8.6% 🟢)0.676s63.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.902s (+19.2% 🔺)9.075s (+19.8% 🔺)2.172s41.00x
▲ VercelNitro81.134s (+1545.9% 🔺)82.628s (+1126.6% 🔺)1.494s411.75x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.539s (-19.2% 🟢)1.029s (-1.8%)0.490s591.00x
🐘 PostgresNitro0.566s (-7.0% 🟢)1.024s (+1.6%)0.457s591.05x
💻 LocalExpress0.660s (+0.5%)1.005s (-3.3%)0.346s601.22x
🐘 PostgresNext.js (Turbopack)0.825s (+1.6%)1.023s (+1.7%)0.199s591.53x
💻 LocalNext.js (Turbopack)0.861s (+23.7% 🔺)1.039s (+3.4%)0.178s581.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.557s (+10.1% 🔺)7.193s (+5.0% 🔺)1.636s91.00x
▲ VercelNitro6.289s (-15.7% 🟢)7.902s (-17.0% 🟢)1.614s81.13x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (-3.6%)2.007s (~)0.641s451.00x
🐘 PostgresExpress1.494s (-2.8%)2.108s (-2.0%)0.615s431.09x
💻 LocalExpress1.619s (+1.7%)2.029s (~)0.410s451.18x
🐘 PostgresNext.js (Turbopack)1.968s (+1.3%)2.284s (+5.0% 🔺)0.316s401.44x
💻 LocalNext.js (Turbopack)2.067s (+19.6% 🔺)2.882s (+42.1% 🔺)0.815s321.51x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.292s (+8.1% 🔺)15.666s (+1.2%)1.374s61.00x
▲ VercelNext.js (Turbopack)16.308s (+10.4% 🔺)18.593s (+9.8% 🔺)2.285s51.14x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.649s (-9.1% 🟢)3.085s (-5.9% 🟢)0.436s391.00x
🐘 PostgresExpress3.135s (+7.7% 🔺)3.682s (+4.0%)0.547s331.18x
💻 LocalExpress3.545s (+3.7%)4.010s (~)0.465s301.34x
🐘 PostgresNext.js (Turbopack)3.876s (~)4.042s (-0.8%)0.166s301.46x
💻 LocalNext.js (Turbopack)4.417s (+18.5% 🔺)5.052s (+22.9% 🔺)0.635s241.67x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.414s (+5.5% 🔺)32.492s (+1.6%)1.078s41.00x
▲ VercelNext.js (Turbopack)32.338s (+19.8% 🔺)34.168s (+16.7% 🔺)1.829s41.03x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.212s (-10.0% 🟢)1.006s (~)0.794s601.00x
🐘 PostgresExpress0.248s (-1.5%)1.011s (~)0.763s601.17x
🐘 PostgresNext.js (Turbopack)0.266s (-5.5% 🟢)1.006s (~)0.740s601.26x
💻 LocalNext.js (Turbopack)0.578s (-5.3% 🟢)1.057s (+1.8%)0.479s572.73x
💻 LocalExpress0.622s (+33.9% 🔺)1.160s (+15.5% 🔺)0.538s522.94x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.746s (+24.9% 🔺)4.424s (+4.8%)1.678s141.00x
▲ VercelNitro78.584s (+3251.3% 🔺)79.773s (+1740.7% 🔺)1.188s428.62x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.364s (-1.8%)1.018s (+1.1%)0.654s891.00x
🐘 PostgresExpress0.465s (+37.0% 🔺)1.025s (+1.8%)0.560s881.28x
🐘 PostgresNext.js (Turbopack)0.491s (+1.6%)1.018s (+1.1%)0.527s891.35x
💻 LocalNext.js (Turbopack)2.222s (-9.3% 🟢)3.010s (-3.2%)0.788s306.10x
💻 LocalExpress2.224s (+1.5%)2.854s (+1.1%)0.630s326.11x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.160s (-85.9% 🟢)7.805s (-83.0% 🟢)1.645s121.00x
▲ VercelNext.js (Turbopack)40.420s (+544.6% 🔺)42.078s (+395.2% 🔺)1.658s96.56x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.682s (-3.5%)1.006s (~)0.324s1201.00x
🐘 PostgresExpress0.855s (+36.6% 🔺)1.256s (+19.0% 🔺)0.401s961.25x
🐘 PostgresNext.js (Turbopack)1.000s (+2.1%)1.814s (+12.8% 🔺)0.814s671.47x
💻 LocalNext.js (Turbopack)10.521s (+2.0%)11.483s (+1.6%)0.962s1115.42x
💻 LocalExpress10.523s (+2.0%)11.126s (+2.4%)0.603s1115.42x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.195s (-94.5% 🟢)18.692s (-94.0% 🟢)1.497s71.00x
▲ VercelNext.js (Turbopack)58.866s (-63.2% 🟢)60.845s (-62.5% 🟢)1.979s73.42x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.163s (-1.5%)2.002s (~)0.001s (-14.3% 🟢)2.010s (~)0.847s101.00x
💻 LocalExpress1.164s (~)2.005s (~)0.011s (-16.4% 🟢)2.019s (~)0.855s101.00x
💻 LocalNext.js (Turbopack)1.199s (+2.6%)2.003s (~)0.010s (+2.0%)2.017s (~)0.818s101.03x
🐘 PostgresNext.js (Turbopack)1.231s (~)2.001s (~)0.001s (-33.3% 🟢)2.011s (~)0.779s101.06x
🐘 PostgresExpress1.243s (+4.1%)1.999s (~)0.001s (-97.7% 🟢)2.012s (-1.5%)0.769s101.07x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.331s (+7.7% 🔺)3.071s (-10.1% 🟢)2.051s (+297.6% 🔺)5.565s (+24.5% 🔺)3.234s101.00x
▲ VercelNext.js (Turbopack)2.494s (+11.3% 🔺)3.775s (+6.3% 🔺)2.256s (-81.8% 🟢)6.587s (-60.1% 🟢)4.092s101.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.595s (-4.1%)2.006s (~)0.004s (+1.7%)2.025s (~)0.431s301.00x
💻 LocalExpress1.596s (-2.5%)2.009s (~)0.011s (+1.5%)2.023s (~)0.427s301.00x
💻 LocalNext.js (Turbopack)1.709s (+4.2%)2.007s (~)0.011s (+5.3% 🔺)2.021s (~)0.312s301.07x
🐘 PostgresNext.js (Turbopack)1.770s (~)2.011s (~)0.004s (-1.7%)2.027s (~)0.257s301.11x
🐘 PostgresExpress1.919s (+10.5% 🔺)2.313s (+7.9% 🔺)0.003s (+1.6%)2.341s (+8.4% 🔺)0.422s261.20x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.971s (+9.4% 🔺)8.581s (+8.1% 🔺)0.274s (-24.3% 🟢)9.398s (+6.0% 🔺)2.426s71.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.743s (+2.8%)1.029s (~)0.000s (-83.3% 🟢)1.049s (~)0.306s581.00x
🐘 PostgresNext.js (Turbopack)0.801s (-3.7%)1.053s (-1.7%)0.000s (+96.5% 🔺)1.060s (-1.8%)0.259s571.08x
🐘 PostgresExpress1.081s (+10.6% 🔺)1.498s (+4.9%)0.000s (-30.0% 🟢)1.518s (+4.8%)0.438s401.45x
💻 LocalNext.js (Turbopack)1.447s (~)2.012s (~)0.000s (-75.0% 🟢)2.015s (~)0.569s301.95x
💻 LocalExpress1.470s (-0.9%)2.013s (~)0.001s (+73.3% 🔺)2.016s (~)0.547s301.98x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.750s (+4.1%)4.851s (-4.4%)0.001s (+Infinity% 🔺)5.276s (-6.3% 🟢)1.527s121.00x
▲ VercelNext.js (Turbopack)4.000s (+20.9% 🔺)5.605s (+14.5% 🔺)0.000s (+Infinity% 🔺)6.117s (+12.0% 🔺)2.117s111.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.384s (-1.9%)2.099s (+5.2% 🔺)0.000s (+Infinity% 🔺)2.114s (+5.1% 🔺)0.731s291.00x
🐘 PostgresNext.js (Turbopack)1.784s (+3.2%)2.310s (+2.2%)0.000s (+Infinity% 🔺)2.333s (+2.8%)0.550s261.29x
🐘 PostgresExpress2.429s (+50.4% 🔺)3.004s (+33.3% 🔺)0.000s (+Infinity% 🔺)3.031s (+33.0% 🔺)0.602s211.76x
💻 LocalNext.js (Turbopack)2.896s (-9.9% 🟢)3.471s (-10.9% 🟢)0.001s (+77.8% 🔺)3.476s (-10.9% 🟢)0.580s182.09x
💻 LocalExpress3.239s (-2.8%)3.837s (~)0.001s (+183.3% 🔺)3.841s (~)0.602s162.34x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.053s (-14.1% 🟢)7.287s (-15.0% 🟢)0.000s (+Infinity% 🔺)7.727s (-15.0% 🟢)1.675s81.00x
▲ VercelNext.js (Turbopack)7.030s (+36.1% 🔺)8.492s (+27.2% 🔺)0.000s (-100.0% 🟢)8.993s (+24.9% 🔺)1.963s71.16x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress14/21
🐘 PostgresNitro20/21
▲ VercelNitro16/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres21/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: failure
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@github-actions

github-actionsBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production126602191485
✅ 💻 Local Development167102191890
✅ 📦 Local Production167102191890
✅ 🐘 Local Postgres167102191890
✅ 🪟 Windows13500135
✅ 📋 Other7690176945
Total7183010528235

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro109026
✅ example109026
✅ express109026
✅ fastify109026
✅ hono109026
✅ nextjs-turbopack13302
✅ nextjs-webpack13302
✅ nitro109026
✅ nuxt109026
✅ sveltekit12807
✅ vite109026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable110025
✅ e2e-local-dev-tanstack-start-110025
✅ e2e-local-postgres-nest-stable110025
✅ e2e-local-postgres-tanstack-start-110025
✅ e2e-local-prod-nest-stable110025
✅ e2e-local-prod-tanstack-start-110025
✅ e2e-vercel-prod-tanstack-start109026

📋 View full workflow run

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

Adds defensive validation to @workflow/world-vercel ref resolution so empty/truncated ref payloads don’t propagate into replay/hydration and wedge runs during deterministic replay.

Changes:

  • Validate GET /v2/runs/:runId/refs bodies for zero-length and Content-Length mismatches, throwing WorkflowWorldError with specific error codes.
  • Add a new Vitest suite covering happy paths, zero-byte 200s, Content-Length mismatch, non-2xx passthrough, and inline dbrf behavior.
  • Add a patch changeset for @workflow/world-vercel.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/refs.tsAdds response body validation (empty body and Content-Length mismatch) before decoding/returning ref payloads.
packages/world-vercel/src/refs.test.tsIntroduces unit tests for ref resolution behavior across success/error/edge cases.
.changeset/world-vercel-reject-empty-ref-payload.mdDeclares a patch release for the new validation behavior.

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

Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.test.ts
Three review changes:
1. Reject any body shorter than the 4-byte format-prefix length, not
just zero-byte bodies. The SDK guarantees every stored ref payload
starts with a 4-byte format prefix (FORMAT_PREFIX_LENGTH in
@workflow/core), so a 1-3 byte body would also fail downstream
replay with the same 'Data too short to contain format prefix'
error this PR exists to prevent.
2. Parse Content-Length safely with parseInt + Number.isFinite +
non-negative checks instead of bare Number(). A non-numeric value
like 'abc' would otherwise produce NaN and silently surface as a
'truncated' error, masking the real cause. Malformed values are
treated as absent; the minimum-length check still defends against
actual truncation in that case.
3. Add tests for the truncated-body-without-Content-Length case
(chunked transfer where Content-Length validation can't see the
truncation), and for a malformed Content-Length header that should
be ignored rather than misreported as truncation.
The validation logic also moves into a small assertValidRefBody
helper to keep the inner trace function under the noExcessiveCognitiveComplexity limit.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential blocker in PR-version packages/world-vercel/src/refs.ts:143: the new Content-Length check compares response.arrayBuffer().byteLength to the header without accounting for Content-Encoding. Node fetch transparently decompresses gzip bodies while preserving the encoded Content-Length, so a valid compressed ref response can now throw ref-body-length-mismatch. I’d skip this check when Content-Encoding is present and not identity, or force/request identity encoding.

fetch/undici transparently decompresses gzip/br bodies but leaves
Content-Length describing the encoded (compressed) size, so comparing it
against the decompressed byteLength would reject valid compressed refs as
a phantom 'ref-body-length-mismatch'. Skip the comparison when a
non-identity Content-Encoding is present; an absent or 'identity' encoding
is still validated. Adds regression tests for both cases.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@karthikscale3 great catch — fixed in c8f5dce.

You're right: fetch/undici transparently decompresses the body but leaves Content-Length describing the encoded (compressed) size, so a gzip/br ref response would have Content-Length (compressed) ≠ decompressed byteLength and falsely throw ref-body-length-mismatch.

The length comparison is now skipped whenever a non-identityContent-Encoding is present (gzip, br, etc.). An absent or identity encoding means no transform was applied, so the lengths remain directly comparable and that path is still validated. The zero-byte and binary 4-byte-minimum checks are unaffected (they operate on the decompressed body and apply regardless of encoding).

Added two regression tests:

  • skips the length check for compressed (Content-Encoding) responsesContent-Length: 20 + Content-Encoding: gzip with a larger decompressed body still decodes successfully.
  • still enforces the length check for identity Content-EncodingContent-Encoding: identity with a real mismatch still throws.

pnpm --filter @workflow/world-vercel test / typecheck / biome all pass.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2297. (backport job run)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

[world-vercel] Validate ref resolve responses before use - #2035

Merged
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload
Jun 8, 2026
Merged

[world-vercel] Validate ref resolve responses before use#2035
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Adds defense-in-depth validation in resolveRefDescriptor so a zero-byte or truncated ref response from GET /v2/runs/:runId/refs no longer corrupts the in-memory event log during workflow replay.

The workflow runtime feeds ref payload bytes directly into decodeFormatPrefix, which requires at least the 4-byte format prefix that the SDK always writes (encodeWithFormatPrefix in @workflow/core). A zero-byte response is therefore never a valid stored value.

Before this change, resolveRefDescriptor checked response.ok and then handed the empty arrayBuffer() downstream verbatim. Multiple users have reported the resulting failure:

Data too short to contain format prefix: expected at least 4 bytes, got 0

That error surfaces deep inside deterministic replay. Because the SDK has already populated its in-memory event snapshot with the empty payload at that point, the run is wedged: every subsequent invocation deterministically replays the same failure, any downstream resumeHook() then surfaces as Hook not found, and the run only clears when stale-run cleanup terminates the sandbox.

Changes

  • packages/world-vercel/src/refs.ts:
    • After await response.arrayBuffer(), validate buffer.byteLength > 0. Empty bodies throw a WorkflowWorldError with code: 'empty-ref-body'.
    • When Content-Length is present, validate it matches the actual body length. Truncated responses (proxy abort, upstream stream cut mid-flight) throw WorkflowWorldError with code: 'ref-body-length-mismatch'.
    • Both errors record on the span via recordException and tag ErrorType for o11y.
  • packages/world-vercel/src/refs.test.ts (new):
    • Happy path coverage (CBOR + application/octet-stream).
    • Zero-byte 200 with both content types.
    • Content-Length mismatch (truncated stream).
    • Absent Content-Length (chunked transfer encoding) — happy path only.
    • Non-2xx status passes through as HTTP <code>.
    • Inline dbrf refs decode without making a network request.
  • Changeset: @workflow/world-vercel patch.

Why both ends validate

This is the SDK-side companion to vercel/workflow-server#432, which rejects zero-byte payloads at the storage boundary. Both layers are necessary because they catch failures at different points in the stack:

  • Server-side catches storage anomalies (S3 inconsistency, Redis empty key, corrupted write).
  • Client-side catches transport anomalies between the server and the SDK (proxy drop, edge cache miss returning truncated content, undici quirks, etc.).

WorkflowWorldError is treated as a retryable transport-level error by the runtime, which is the desired behavior here: surface the empty/truncated body up to the retry layer instead of poisoning event-log replay.

Validation

  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm --filter @workflow/world-vercel test (77 tests pass, including 8 new in refs.test.ts)
  • pnpm --filter @workflow/world-vercel build
  • pnpm biome check --files-ignore-unknown=true packages/world-vercel/src/refs.ts packages/world-vercel/src/refs.test.ts
  • pnpm changeset status --since=main (confirms @workflow/world-vercel patch bump)

When workflow-server returns a ref body to the SDK, the bytes are
fed into the workflow runtime's event log and deserialized via
`decodeFormatPrefix`. The SDK always writes ref payloads with at
least a 4-byte format prefix (see `encodeWithFormatPrefix` in
`@workflow/core`), so a zero-byte response — or one whose length
disagrees with `Content-Length` — is never a valid stored value.
Before this change, `resolveRefDescriptor` had no validation: a
200 with an empty body would be passed downstream as a zero-length
Uint8Array, which then failed deep inside replay with:
Data too short to contain format prefix: expected at least 4 bytes, got 0
By that point the workflow's in-memory event snapshot is already
poisoned with the empty payload, so every subsequent replay
deterministically reproduces the same failure, downstream
`resumeHook()` calls surface as `Hook not found`, and the run
only unsticks when stale-run cleanup terminates the sandbox.
This catches the failure at the transport boundary instead, where
it can be retried as a `WorkflowWorldError`. Both an empty body
and a length mismatch (truncated streaming response) are rejected.
This is the SDK-side companion to vercel/workflow-server#432, which
adds the same validation on the server side.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 20, 2026 17:13
CopilotAI review requested due to automatic review settings May 20, 2026 17:13
@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c8f5dce

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

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

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

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

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 20, 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🥇 Express0.049s (+13.7% 🔺)1.007s (~)0.958s101.00x
💻 LocalNext.js (Turbopack)0.059s (+6.4% 🔺)1.005s (~)0.947s101.19x
🐘 PostgresNitro0.061s (-2.7%)1.012s (~)0.951s101.25x
🐘 PostgresNext.js (Turbopack)0.067s (-3.7%)1.012s (~)0.945s101.37x
🐘 PostgresExpress0.079s (+18.9% 🔺)1.031s (+1.6%)0.952s101.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.291s (-11.6% 🟢)2.378s (+1.6%)2.087s101.00x
▲ VercelNitro0.360s (+37.9% 🔺)2.201s (-9.7% 🟢)1.840s101.24x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.101s (~)2.006s (~)0.905s101.00x
🐘 PostgresNitro1.116s (~)2.010s (~)0.894s101.01x
💻 LocalNext.js (Turbopack)1.130s (+2.0%)2.006s (~)0.876s101.03x
🐘 PostgresNext.js (Turbopack)1.151s (+0.8%)2.009s (~)0.859s101.04x
🐘 PostgresExpress1.170s (+7.1% 🔺)2.027s (+0.8%)0.857s101.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.698s (+8.4% 🔺)3.490s (-1.1%)1.792s101.00x
▲ VercelNext.js (Turbopack)1.817s (+9.1% 🔺)3.995s (-2.0%)2.177s101.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.555s (~)11.020s (~)0.465s31.00x
💻 LocalExpress10.631s (+0.6%)11.024s (~)0.393s31.01x
🐘 PostgresExpress10.684s (~)11.023s (-2.9%)0.339s31.01x
💻 LocalNext.js (Turbopack)10.763s (+1.5%)11.021s (~)0.258s31.02x
🐘 PostgresNext.js (Turbopack)10.827s (~)11.020s (~)0.193s31.03x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.033s (+5.3% 🔺)15.218s (-2.4%)1.186s21.00x
▲ VercelNext.js (Turbopack)15.723s (+16.5% 🔺)17.581s (+9.2% 🔺)1.858s21.12x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.725s (-0.9%)14.018s (~)0.293s51.00x
💻 LocalExpress13.845s (~)14.029s (-1.4%)0.184s51.01x
🐘 PostgresExpress14.191s (+2.6%)14.618s (+2.7%)0.427s51.03x
🐘 PostgresNext.js (Turbopack)14.421s (-1.2%)15.018s (~)0.597s41.05x
💻 LocalNext.js (Turbopack)14.569s (+3.9%)15.028s (+2.7%)0.459s41.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro23.236s (-8.9% 🟢)24.542s (-11.1% 🟢)1.306s31.00x
▲ VercelNext.js (Turbopack)24.439s (-4.2%)26.713s (-1.8%)2.273s31.05x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro12.500s (-1.9%)13.021s (~)0.521s71.00x
💻 LocalExpress12.697s (+0.8%)13.026s (~)0.329s71.02x
🐘 PostgresExpress13.420s (+5.4% 🔺)14.021s (+5.3% 🔺)0.601s71.07x
💻 LocalNext.js (Turbopack)13.706s (+5.8% 🔺)14.169s (+6.5% 🔺)0.464s71.10x
🐘 PostgresNext.js (Turbopack)14.007s (+1.8%)14.450s (+3.1%)0.443s71.12x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro30.766s (+5.9% 🔺)32.566s (+3.7%)1.800s31.00x
▲ VercelNext.js (Turbopack)33.943s (+14.6% 🔺)36.480s (+14.5% 🔺)2.537s31.10x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.174s (-2.1%)2.008s (~)0.834s151.00x
🐘 PostgresExpress1.234s (+2.8%)2.007s (-0.7%)0.774s151.05x
💻 LocalExpress1.235s (-2.3%)2.007s (~)0.772s151.05x
🐘 PostgresNext.js (Turbopack)1.252s (~)2.007s (~)0.755s151.07x
💻 LocalNext.js (Turbopack)1.377s (+12.6% 🔺)2.007s (~)0.630s151.17x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.525s (-8.8% 🟢)4.139s (-4.7%)1.613s81.00x
▲ VercelNext.js (Turbopack)2.534s (-5.7% 🟢)4.174s (-1.0%)1.641s81.00x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.252s (-0.5%)2.007s (~)0.755s151.00x
🐘 PostgresNext.js (Turbopack)1.401s (-0.6%)2.008s (~)0.607s151.12x
🐘 PostgresExpress1.546s (+16.5% 🔺)2.099s (+4.1%)0.554s151.23x
💻 LocalExpress1.681s (-10.7% 🟢)2.006s (-9.7% 🟢)0.325s151.34x
💻 LocalNext.js (Turbopack)1.798s (+4.2%)2.074s (+3.4%)0.276s151.44x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.382s (-16.9% 🟢)4.751s (-23.5% 🟢)1.369s71.00x
▲ VercelNext.js (Turbopack)3.982s (-3.0%)5.923s (-3.5%)1.941s61.18x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.409s (~)2.008s (~)0.599s151.00x
🐘 PostgresNext.js (Turbopack)1.727s (-1.9%)2.317s (+1.0%)0.590s131.23x
🐘 PostgresExpress1.734s (+24.4% 🔺)2.416s (+20.2% 🔺)0.682s131.23x
💻 LocalExpress4.668s (-11.9% 🟢)5.013s (-16.7% 🟢)0.345s63.31x
💻 LocalNext.js (Turbopack)4.701s (+6.9% 🔺)5.180s (+3.4%)0.478s63.34x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.417s (-19.6% 🟢)7.151s (-17.9% 🟢)1.734s51.00x
▲ VercelNext.js (Turbopack)5.831s (-0.7%)7.666s (-1.4%)1.835s51.08x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.195s (~)2.009s (~)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (-1.2%)2.009s (~)0.758s151.05x
🐘 PostgresExpress1.253s (+4.1%)2.026s (+0.9%)0.774s151.05x
💻 LocalNext.js (Turbopack)1.363s (+4.4%)2.006s (~)0.644s151.14x
💻 LocalExpress1.614s (-2.5%)2.007s (-3.2%)0.392s151.35x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.618s (-2.0%)4.137s (-4.5%)1.519s81.00x
▲ VercelNext.js (Turbopack)308.185s (+6948.8% 🔺)309.806s (+4822.6% 🔺)1.621s1117.73x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (-1.0%)2.009s (~)0.748s151.00x
🐘 PostgresNext.js (Turbopack)1.390s (~)2.007s (~)0.617s151.10x
🐘 PostgresExpress1.447s (+16.4% 🔺)2.082s (+3.5%)0.635s151.15x
💻 LocalExpress1.896s (-10.8% 🟢)2.294s (-11.4% 🟢)0.398s141.50x
💻 LocalNext.js (Turbopack)1.978s (-2.1%)2.316s (-7.6% 🟢)0.338s131.57x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.577s (-12.6% 🟢)4.853s (-19.2% 🟢)1.276s71.00x
▲ VercelNext.js (Turbopack)3.827s (-15.3% 🟢)5.471s (-11.3% 🟢)1.644s61.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.391s (-4.8%)2.008s (~)0.617s151.00x
🐘 PostgresExpress1.549s (+3.2%)2.331s (-2.8%)0.783s131.11x
🐘 PostgresNext.js (Turbopack)1.691s (+2.1%)2.316s (+11.7% 🔺)0.625s131.22x
💻 LocalNext.js (Turbopack)4.898s (-5.1% 🟢)5.513s (-8.4% 🟢)0.615s63.52x
💻 LocalExpress5.004s (-14.7% 🟢)5.680s (-8.6% 🟢)0.676s63.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.902s (+19.2% 🔺)9.075s (+19.8% 🔺)2.172s41.00x
▲ VercelNitro81.134s (+1545.9% 🔺)82.628s (+1126.6% 🔺)1.494s411.75x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.539s (-19.2% 🟢)1.029s (-1.8%)0.490s591.00x
🐘 PostgresNitro0.566s (-7.0% 🟢)1.024s (+1.6%)0.457s591.05x
💻 LocalExpress0.660s (+0.5%)1.005s (-3.3%)0.346s601.22x
🐘 PostgresNext.js (Turbopack)0.825s (+1.6%)1.023s (+1.7%)0.199s591.53x
💻 LocalNext.js (Turbopack)0.861s (+23.7% 🔺)1.039s (+3.4%)0.178s581.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.557s (+10.1% 🔺)7.193s (+5.0% 🔺)1.636s91.00x
▲ VercelNitro6.289s (-15.7% 🟢)7.902s (-17.0% 🟢)1.614s81.13x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (-3.6%)2.007s (~)0.641s451.00x
🐘 PostgresExpress1.494s (-2.8%)2.108s (-2.0%)0.615s431.09x
💻 LocalExpress1.619s (+1.7%)2.029s (~)0.410s451.18x
🐘 PostgresNext.js (Turbopack)1.968s (+1.3%)2.284s (+5.0% 🔺)0.316s401.44x
💻 LocalNext.js (Turbopack)2.067s (+19.6% 🔺)2.882s (+42.1% 🔺)0.815s321.51x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.292s (+8.1% 🔺)15.666s (+1.2%)1.374s61.00x
▲ VercelNext.js (Turbopack)16.308s (+10.4% 🔺)18.593s (+9.8% 🔺)2.285s51.14x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.649s (-9.1% 🟢)3.085s (-5.9% 🟢)0.436s391.00x
🐘 PostgresExpress3.135s (+7.7% 🔺)3.682s (+4.0%)0.547s331.18x
💻 LocalExpress3.545s (+3.7%)4.010s (~)0.465s301.34x
🐘 PostgresNext.js (Turbopack)3.876s (~)4.042s (-0.8%)0.166s301.46x
💻 LocalNext.js (Turbopack)4.417s (+18.5% 🔺)5.052s (+22.9% 🔺)0.635s241.67x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.414s (+5.5% 🔺)32.492s (+1.6%)1.078s41.00x
▲ VercelNext.js (Turbopack)32.338s (+19.8% 🔺)34.168s (+16.7% 🔺)1.829s41.03x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.212s (-10.0% 🟢)1.006s (~)0.794s601.00x
🐘 PostgresExpress0.248s (-1.5%)1.011s (~)0.763s601.17x
🐘 PostgresNext.js (Turbopack)0.266s (-5.5% 🟢)1.006s (~)0.740s601.26x
💻 LocalNext.js (Turbopack)0.578s (-5.3% 🟢)1.057s (+1.8%)0.479s572.73x
💻 LocalExpress0.622s (+33.9% 🔺)1.160s (+15.5% 🔺)0.538s522.94x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.746s (+24.9% 🔺)4.424s (+4.8%)1.678s141.00x
▲ VercelNitro78.584s (+3251.3% 🔺)79.773s (+1740.7% 🔺)1.188s428.62x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.364s (-1.8%)1.018s (+1.1%)0.654s891.00x
🐘 PostgresExpress0.465s (+37.0% 🔺)1.025s (+1.8%)0.560s881.28x
🐘 PostgresNext.js (Turbopack)0.491s (+1.6%)1.018s (+1.1%)0.527s891.35x
💻 LocalNext.js (Turbopack)2.222s (-9.3% 🟢)3.010s (-3.2%)0.788s306.10x
💻 LocalExpress2.224s (+1.5%)2.854s (+1.1%)0.630s326.11x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.160s (-85.9% 🟢)7.805s (-83.0% 🟢)1.645s121.00x
▲ VercelNext.js (Turbopack)40.420s (+544.6% 🔺)42.078s (+395.2% 🔺)1.658s96.56x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.682s (-3.5%)1.006s (~)0.324s1201.00x
🐘 PostgresExpress0.855s (+36.6% 🔺)1.256s (+19.0% 🔺)0.401s961.25x
🐘 PostgresNext.js (Turbopack)1.000s (+2.1%)1.814s (+12.8% 🔺)0.814s671.47x
💻 LocalNext.js (Turbopack)10.521s (+2.0%)11.483s (+1.6%)0.962s1115.42x
💻 LocalExpress10.523s (+2.0%)11.126s (+2.4%)0.603s1115.42x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.195s (-94.5% 🟢)18.692s (-94.0% 🟢)1.497s71.00x
▲ VercelNext.js (Turbopack)58.866s (-63.2% 🟢)60.845s (-62.5% 🟢)1.979s73.42x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.163s (-1.5%)2.002s (~)0.001s (-14.3% 🟢)2.010s (~)0.847s101.00x
💻 LocalExpress1.164s (~)2.005s (~)0.011s (-16.4% 🟢)2.019s (~)0.855s101.00x
💻 LocalNext.js (Turbopack)1.199s (+2.6%)2.003s (~)0.010s (+2.0%)2.017s (~)0.818s101.03x
🐘 PostgresNext.js (Turbopack)1.231s (~)2.001s (~)0.001s (-33.3% 🟢)2.011s (~)0.779s101.06x
🐘 PostgresExpress1.243s (+4.1%)1.999s (~)0.001s (-97.7% 🟢)2.012s (-1.5%)0.769s101.07x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.331s (+7.7% 🔺)3.071s (-10.1% 🟢)2.051s (+297.6% 🔺)5.565s (+24.5% 🔺)3.234s101.00x
▲ VercelNext.js (Turbopack)2.494s (+11.3% 🔺)3.775s (+6.3% 🔺)2.256s (-81.8% 🟢)6.587s (-60.1% 🟢)4.092s101.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.595s (-4.1%)2.006s (~)0.004s (+1.7%)2.025s (~)0.431s301.00x
💻 LocalExpress1.596s (-2.5%)2.009s (~)0.011s (+1.5%)2.023s (~)0.427s301.00x
💻 LocalNext.js (Turbopack)1.709s (+4.2%)2.007s (~)0.011s (+5.3% 🔺)2.021s (~)0.312s301.07x
🐘 PostgresNext.js (Turbopack)1.770s (~)2.011s (~)0.004s (-1.7%)2.027s (~)0.257s301.11x
🐘 PostgresExpress1.919s (+10.5% 🔺)2.313s (+7.9% 🔺)0.003s (+1.6%)2.341s (+8.4% 🔺)0.422s261.20x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.971s (+9.4% 🔺)8.581s (+8.1% 🔺)0.274s (-24.3% 🟢)9.398s (+6.0% 🔺)2.426s71.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.743s (+2.8%)1.029s (~)0.000s (-83.3% 🟢)1.049s (~)0.306s581.00x
🐘 PostgresNext.js (Turbopack)0.801s (-3.7%)1.053s (-1.7%)0.000s (+96.5% 🔺)1.060s (-1.8%)0.259s571.08x
🐘 PostgresExpress1.081s (+10.6% 🔺)1.498s (+4.9%)0.000s (-30.0% 🟢)1.518s (+4.8%)0.438s401.45x
💻 LocalNext.js (Turbopack)1.447s (~)2.012s (~)0.000s (-75.0% 🟢)2.015s (~)0.569s301.95x
💻 LocalExpress1.470s (-0.9%)2.013s (~)0.001s (+73.3% 🔺)2.016s (~)0.547s301.98x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.750s (+4.1%)4.851s (-4.4%)0.001s (+Infinity% 🔺)5.276s (-6.3% 🟢)1.527s121.00x
▲ VercelNext.js (Turbopack)4.000s (+20.9% 🔺)5.605s (+14.5% 🔺)0.000s (+Infinity% 🔺)6.117s (+12.0% 🔺)2.117s111.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.384s (-1.9%)2.099s (+5.2% 🔺)0.000s (+Infinity% 🔺)2.114s (+5.1% 🔺)0.731s291.00x
🐘 PostgresNext.js (Turbopack)1.784s (+3.2%)2.310s (+2.2%)0.000s (+Infinity% 🔺)2.333s (+2.8%)0.550s261.29x
🐘 PostgresExpress2.429s (+50.4% 🔺)3.004s (+33.3% 🔺)0.000s (+Infinity% 🔺)3.031s (+33.0% 🔺)0.602s211.76x
💻 LocalNext.js (Turbopack)2.896s (-9.9% 🟢)3.471s (-10.9% 🟢)0.001s (+77.8% 🔺)3.476s (-10.9% 🟢)0.580s182.09x
💻 LocalExpress3.239s (-2.8%)3.837s (~)0.001s (+183.3% 🔺)3.841s (~)0.602s162.34x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.053s (-14.1% 🟢)7.287s (-15.0% 🟢)0.000s (+Infinity% 🔺)7.727s (-15.0% 🟢)1.675s81.00x
▲ VercelNext.js (Turbopack)7.030s (+36.1% 🔺)8.492s (+27.2% 🔺)0.000s (-100.0% 🟢)8.993s (+24.9% 🔺)1.963s71.16x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress14/21
🐘 PostgresNitro20/21
▲ VercelNitro16/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres21/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: failure
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@github-actions

github-actionsBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production126602191485
✅ 💻 Local Development167102191890
✅ 📦 Local Production167102191890
✅ 🐘 Local Postgres167102191890
✅ 🪟 Windows13500135
✅ 📋 Other7690176945
Total7183010528235

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro109026
✅ example109026
✅ express109026
✅ fastify109026
✅ hono109026
✅ nextjs-turbopack13302
✅ nextjs-webpack13302
✅ nitro109026
✅ nuxt109026
✅ sveltekit12807
✅ vite109026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable110025
✅ e2e-local-dev-tanstack-start-110025
✅ e2e-local-postgres-nest-stable110025
✅ e2e-local-postgres-tanstack-start-110025
✅ e2e-local-prod-nest-stable110025
✅ e2e-local-prod-tanstack-start-110025
✅ e2e-vercel-prod-tanstack-start109026

📋 View full workflow run

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

Adds defensive validation to @workflow/world-vercel ref resolution so empty/truncated ref payloads don’t propagate into replay/hydration and wedge runs during deterministic replay.

Changes:

  • Validate GET /v2/runs/:runId/refs bodies for zero-length and Content-Length mismatches, throwing WorkflowWorldError with specific error codes.
  • Add a new Vitest suite covering happy paths, zero-byte 200s, Content-Length mismatch, non-2xx passthrough, and inline dbrf behavior.
  • Add a patch changeset for @workflow/world-vercel.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/refs.tsAdds response body validation (empty body and Content-Length mismatch) before decoding/returning ref payloads.
packages/world-vercel/src/refs.test.tsIntroduces unit tests for ref resolution behavior across success/error/edge cases.
.changeset/world-vercel-reject-empty-ref-payload.mdDeclares a patch release for the new validation behavior.

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

Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.test.ts
Three review changes:
1. Reject any body shorter than the 4-byte format-prefix length, not
just zero-byte bodies. The SDK guarantees every stored ref payload
starts with a 4-byte format prefix (FORMAT_PREFIX_LENGTH in
@workflow/core), so a 1-3 byte body would also fail downstream
replay with the same 'Data too short to contain format prefix'
error this PR exists to prevent.
2. Parse Content-Length safely with parseInt + Number.isFinite +
non-negative checks instead of bare Number(). A non-numeric value
like 'abc' would otherwise produce NaN and silently surface as a
'truncated' error, masking the real cause. Malformed values are
treated as absent; the minimum-length check still defends against
actual truncation in that case.
3. Add tests for the truncated-body-without-Content-Length case
(chunked transfer where Content-Length validation can't see the
truncation), and for a malformed Content-Length header that should
be ignored rather than misreported as truncation.
The validation logic also moves into a small assertValidRefBody
helper to keep the inner trace function under the noExcessiveCognitiveComplexity limit.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential blocker in PR-version packages/world-vercel/src/refs.ts:143: the new Content-Length check compares response.arrayBuffer().byteLength to the header without accounting for Content-Encoding. Node fetch transparently decompresses gzip bodies while preserving the encoded Content-Length, so a valid compressed ref response can now throw ref-body-length-mismatch. I’d skip this check when Content-Encoding is present and not identity, or force/request identity encoding.

fetch/undici transparently decompresses gzip/br bodies but leaves
Content-Length describing the encoded (compressed) size, so comparing it
against the decompressed byteLength would reject valid compressed refs as
a phantom 'ref-body-length-mismatch'. Skip the comparison when a
non-identity Content-Encoding is present; an absent or 'identity' encoding
is still validated. Adds regression tests for both cases.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@karthikscale3 great catch — fixed in c8f5dce.

You're right: fetch/undici transparently decompresses the body but leaves Content-Length describing the encoded (compressed) size, so a gzip/br ref response would have Content-Length (compressed) ≠ decompressed byteLength and falsely throw ref-body-length-mismatch.

The length comparison is now skipped whenever a non-identityContent-Encoding is present (gzip, br, etc.). An absent or identity encoding means no transform was applied, so the lengths remain directly comparable and that path is still validated. The zero-byte and binary 4-byte-minimum checks are unaffected (they operate on the decompressed body and apply regardless of encoding).

Added two regression tests:

  • skips the length check for compressed (Content-Encoding) responsesContent-Length: 20 + Content-Encoding: gzip with a larger decompressed body still decodes successfully.
  • still enforces the length check for identity Content-EncodingContent-Encoding: identity with a real mismatch still throws.

pnpm --filter @workflow/world-vercel test / typecheck / biome all pass.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2297. (backport job run)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

[world-vercel] Validate ref resolve responses before use - #2035

Merged
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload
Jun 8, 2026
Merged

[world-vercel] Validate ref resolve responses before use#2035
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Adds defense-in-depth validation in resolveRefDescriptor so a zero-byte or truncated ref response from GET /v2/runs/:runId/refs no longer corrupts the in-memory event log during workflow replay.

The workflow runtime feeds ref payload bytes directly into decodeFormatPrefix, which requires at least the 4-byte format prefix that the SDK always writes (encodeWithFormatPrefix in @workflow/core). A zero-byte response is therefore never a valid stored value.

Before this change, resolveRefDescriptor checked response.ok and then handed the empty arrayBuffer() downstream verbatim. Multiple users have reported the resulting failure:

Data too short to contain format prefix: expected at least 4 bytes, got 0

That error surfaces deep inside deterministic replay. Because the SDK has already populated its in-memory event snapshot with the empty payload at that point, the run is wedged: every subsequent invocation deterministically replays the same failure, any downstream resumeHook() then surfaces as Hook not found, and the run only clears when stale-run cleanup terminates the sandbox.

Changes

  • packages/world-vercel/src/refs.ts:
    • After await response.arrayBuffer(), validate buffer.byteLength > 0. Empty bodies throw a WorkflowWorldError with code: 'empty-ref-body'.
    • When Content-Length is present, validate it matches the actual body length. Truncated responses (proxy abort, upstream stream cut mid-flight) throw WorkflowWorldError with code: 'ref-body-length-mismatch'.
    • Both errors record on the span via recordException and tag ErrorType for o11y.
  • packages/world-vercel/src/refs.test.ts (new):
    • Happy path coverage (CBOR + application/octet-stream).
    • Zero-byte 200 with both content types.
    • Content-Length mismatch (truncated stream).
    • Absent Content-Length (chunked transfer encoding) — happy path only.
    • Non-2xx status passes through as HTTP <code>.
    • Inline dbrf refs decode without making a network request.
  • Changeset: @workflow/world-vercel patch.

Why both ends validate

This is the SDK-side companion to vercel/workflow-server#432, which rejects zero-byte payloads at the storage boundary. Both layers are necessary because they catch failures at different points in the stack:

  • Server-side catches storage anomalies (S3 inconsistency, Redis empty key, corrupted write).
  • Client-side catches transport anomalies between the server and the SDK (proxy drop, edge cache miss returning truncated content, undici quirks, etc.).

WorkflowWorldError is treated as a retryable transport-level error by the runtime, which is the desired behavior here: surface the empty/truncated body up to the retry layer instead of poisoning event-log replay.

Validation

  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm --filter @workflow/world-vercel test (77 tests pass, including 8 new in refs.test.ts)
  • pnpm --filter @workflow/world-vercel build
  • pnpm biome check --files-ignore-unknown=true packages/world-vercel/src/refs.ts packages/world-vercel/src/refs.test.ts
  • pnpm changeset status --since=main (confirms @workflow/world-vercel patch bump)

When workflow-server returns a ref body to the SDK, the bytes are
fed into the workflow runtime's event log and deserialized via
`decodeFormatPrefix`. The SDK always writes ref payloads with at
least a 4-byte format prefix (see `encodeWithFormatPrefix` in
`@workflow/core`), so a zero-byte response — or one whose length
disagrees with `Content-Length` — is never a valid stored value.
Before this change, `resolveRefDescriptor` had no validation: a
200 with an empty body would be passed downstream as a zero-length
Uint8Array, which then failed deep inside replay with:
Data too short to contain format prefix: expected at least 4 bytes, got 0
By that point the workflow's in-memory event snapshot is already
poisoned with the empty payload, so every subsequent replay
deterministically reproduces the same failure, downstream
`resumeHook()` calls surface as `Hook not found`, and the run
only unsticks when stale-run cleanup terminates the sandbox.
This catches the failure at the transport boundary instead, where
it can be retried as a `WorkflowWorldError`. Both an empty body
and a length mismatch (truncated streaming response) are rejected.
This is the SDK-side companion to vercel/workflow-server#432, which
adds the same validation on the server side.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 20, 2026 17:13
CopilotAI review requested due to automatic review settings May 20, 2026 17:13
@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c8f5dce

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

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

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

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

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 20, 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🥇 Express0.049s (+13.7% 🔺)1.007s (~)0.958s101.00x
💻 LocalNext.js (Turbopack)0.059s (+6.4% 🔺)1.005s (~)0.947s101.19x
🐘 PostgresNitro0.061s (-2.7%)1.012s (~)0.951s101.25x
🐘 PostgresNext.js (Turbopack)0.067s (-3.7%)1.012s (~)0.945s101.37x
🐘 PostgresExpress0.079s (+18.9% 🔺)1.031s (+1.6%)0.952s101.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.291s (-11.6% 🟢)2.378s (+1.6%)2.087s101.00x
▲ VercelNitro0.360s (+37.9% 🔺)2.201s (-9.7% 🟢)1.840s101.24x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.101s (~)2.006s (~)0.905s101.00x
🐘 PostgresNitro1.116s (~)2.010s (~)0.894s101.01x
💻 LocalNext.js (Turbopack)1.130s (+2.0%)2.006s (~)0.876s101.03x
🐘 PostgresNext.js (Turbopack)1.151s (+0.8%)2.009s (~)0.859s101.04x
🐘 PostgresExpress1.170s (+7.1% 🔺)2.027s (+0.8%)0.857s101.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.698s (+8.4% 🔺)3.490s (-1.1%)1.792s101.00x
▲ VercelNext.js (Turbopack)1.817s (+9.1% 🔺)3.995s (-2.0%)2.177s101.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.555s (~)11.020s (~)0.465s31.00x
💻 LocalExpress10.631s (+0.6%)11.024s (~)0.393s31.01x
🐘 PostgresExpress10.684s (~)11.023s (-2.9%)0.339s31.01x
💻 LocalNext.js (Turbopack)10.763s (+1.5%)11.021s (~)0.258s31.02x
🐘 PostgresNext.js (Turbopack)10.827s (~)11.020s (~)0.193s31.03x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.033s (+5.3% 🔺)15.218s (-2.4%)1.186s21.00x
▲ VercelNext.js (Turbopack)15.723s (+16.5% 🔺)17.581s (+9.2% 🔺)1.858s21.12x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.725s (-0.9%)14.018s (~)0.293s51.00x
💻 LocalExpress13.845s (~)14.029s (-1.4%)0.184s51.01x
🐘 PostgresExpress14.191s (+2.6%)14.618s (+2.7%)0.427s51.03x
🐘 PostgresNext.js (Turbopack)14.421s (-1.2%)15.018s (~)0.597s41.05x
💻 LocalNext.js (Turbopack)14.569s (+3.9%)15.028s (+2.7%)0.459s41.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro23.236s (-8.9% 🟢)24.542s (-11.1% 🟢)1.306s31.00x
▲ VercelNext.js (Turbopack)24.439s (-4.2%)26.713s (-1.8%)2.273s31.05x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro12.500s (-1.9%)13.021s (~)0.521s71.00x
💻 LocalExpress12.697s (+0.8%)13.026s (~)0.329s71.02x
🐘 PostgresExpress13.420s (+5.4% 🔺)14.021s (+5.3% 🔺)0.601s71.07x
💻 LocalNext.js (Turbopack)13.706s (+5.8% 🔺)14.169s (+6.5% 🔺)0.464s71.10x
🐘 PostgresNext.js (Turbopack)14.007s (+1.8%)14.450s (+3.1%)0.443s71.12x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro30.766s (+5.9% 🔺)32.566s (+3.7%)1.800s31.00x
▲ VercelNext.js (Turbopack)33.943s (+14.6% 🔺)36.480s (+14.5% 🔺)2.537s31.10x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.174s (-2.1%)2.008s (~)0.834s151.00x
🐘 PostgresExpress1.234s (+2.8%)2.007s (-0.7%)0.774s151.05x
💻 LocalExpress1.235s (-2.3%)2.007s (~)0.772s151.05x
🐘 PostgresNext.js (Turbopack)1.252s (~)2.007s (~)0.755s151.07x
💻 LocalNext.js (Turbopack)1.377s (+12.6% 🔺)2.007s (~)0.630s151.17x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.525s (-8.8% 🟢)4.139s (-4.7%)1.613s81.00x
▲ VercelNext.js (Turbopack)2.534s (-5.7% 🟢)4.174s (-1.0%)1.641s81.00x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.252s (-0.5%)2.007s (~)0.755s151.00x
🐘 PostgresNext.js (Turbopack)1.401s (-0.6%)2.008s (~)0.607s151.12x
🐘 PostgresExpress1.546s (+16.5% 🔺)2.099s (+4.1%)0.554s151.23x
💻 LocalExpress1.681s (-10.7% 🟢)2.006s (-9.7% 🟢)0.325s151.34x
💻 LocalNext.js (Turbopack)1.798s (+4.2%)2.074s (+3.4%)0.276s151.44x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.382s (-16.9% 🟢)4.751s (-23.5% 🟢)1.369s71.00x
▲ VercelNext.js (Turbopack)3.982s (-3.0%)5.923s (-3.5%)1.941s61.18x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.409s (~)2.008s (~)0.599s151.00x
🐘 PostgresNext.js (Turbopack)1.727s (-1.9%)2.317s (+1.0%)0.590s131.23x
🐘 PostgresExpress1.734s (+24.4% 🔺)2.416s (+20.2% 🔺)0.682s131.23x
💻 LocalExpress4.668s (-11.9% 🟢)5.013s (-16.7% 🟢)0.345s63.31x
💻 LocalNext.js (Turbopack)4.701s (+6.9% 🔺)5.180s (+3.4%)0.478s63.34x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.417s (-19.6% 🟢)7.151s (-17.9% 🟢)1.734s51.00x
▲ VercelNext.js (Turbopack)5.831s (-0.7%)7.666s (-1.4%)1.835s51.08x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.195s (~)2.009s (~)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (-1.2%)2.009s (~)0.758s151.05x
🐘 PostgresExpress1.253s (+4.1%)2.026s (+0.9%)0.774s151.05x
💻 LocalNext.js (Turbopack)1.363s (+4.4%)2.006s (~)0.644s151.14x
💻 LocalExpress1.614s (-2.5%)2.007s (-3.2%)0.392s151.35x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.618s (-2.0%)4.137s (-4.5%)1.519s81.00x
▲ VercelNext.js (Turbopack)308.185s (+6948.8% 🔺)309.806s (+4822.6% 🔺)1.621s1117.73x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (-1.0%)2.009s (~)0.748s151.00x
🐘 PostgresNext.js (Turbopack)1.390s (~)2.007s (~)0.617s151.10x
🐘 PostgresExpress1.447s (+16.4% 🔺)2.082s (+3.5%)0.635s151.15x
💻 LocalExpress1.896s (-10.8% 🟢)2.294s (-11.4% 🟢)0.398s141.50x
💻 LocalNext.js (Turbopack)1.978s (-2.1%)2.316s (-7.6% 🟢)0.338s131.57x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.577s (-12.6% 🟢)4.853s (-19.2% 🟢)1.276s71.00x
▲ VercelNext.js (Turbopack)3.827s (-15.3% 🟢)5.471s (-11.3% 🟢)1.644s61.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.391s (-4.8%)2.008s (~)0.617s151.00x
🐘 PostgresExpress1.549s (+3.2%)2.331s (-2.8%)0.783s131.11x
🐘 PostgresNext.js (Turbopack)1.691s (+2.1%)2.316s (+11.7% 🔺)0.625s131.22x
💻 LocalNext.js (Turbopack)4.898s (-5.1% 🟢)5.513s (-8.4% 🟢)0.615s63.52x
💻 LocalExpress5.004s (-14.7% 🟢)5.680s (-8.6% 🟢)0.676s63.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.902s (+19.2% 🔺)9.075s (+19.8% 🔺)2.172s41.00x
▲ VercelNitro81.134s (+1545.9% 🔺)82.628s (+1126.6% 🔺)1.494s411.75x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.539s (-19.2% 🟢)1.029s (-1.8%)0.490s591.00x
🐘 PostgresNitro0.566s (-7.0% 🟢)1.024s (+1.6%)0.457s591.05x
💻 LocalExpress0.660s (+0.5%)1.005s (-3.3%)0.346s601.22x
🐘 PostgresNext.js (Turbopack)0.825s (+1.6%)1.023s (+1.7%)0.199s591.53x
💻 LocalNext.js (Turbopack)0.861s (+23.7% 🔺)1.039s (+3.4%)0.178s581.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.557s (+10.1% 🔺)7.193s (+5.0% 🔺)1.636s91.00x
▲ VercelNitro6.289s (-15.7% 🟢)7.902s (-17.0% 🟢)1.614s81.13x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (-3.6%)2.007s (~)0.641s451.00x
🐘 PostgresExpress1.494s (-2.8%)2.108s (-2.0%)0.615s431.09x
💻 LocalExpress1.619s (+1.7%)2.029s (~)0.410s451.18x
🐘 PostgresNext.js (Turbopack)1.968s (+1.3%)2.284s (+5.0% 🔺)0.316s401.44x
💻 LocalNext.js (Turbopack)2.067s (+19.6% 🔺)2.882s (+42.1% 🔺)0.815s321.51x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.292s (+8.1% 🔺)15.666s (+1.2%)1.374s61.00x
▲ VercelNext.js (Turbopack)16.308s (+10.4% 🔺)18.593s (+9.8% 🔺)2.285s51.14x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.649s (-9.1% 🟢)3.085s (-5.9% 🟢)0.436s391.00x
🐘 PostgresExpress3.135s (+7.7% 🔺)3.682s (+4.0%)0.547s331.18x
💻 LocalExpress3.545s (+3.7%)4.010s (~)0.465s301.34x
🐘 PostgresNext.js (Turbopack)3.876s (~)4.042s (-0.8%)0.166s301.46x
💻 LocalNext.js (Turbopack)4.417s (+18.5% 🔺)5.052s (+22.9% 🔺)0.635s241.67x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.414s (+5.5% 🔺)32.492s (+1.6%)1.078s41.00x
▲ VercelNext.js (Turbopack)32.338s (+19.8% 🔺)34.168s (+16.7% 🔺)1.829s41.03x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.212s (-10.0% 🟢)1.006s (~)0.794s601.00x
🐘 PostgresExpress0.248s (-1.5%)1.011s (~)0.763s601.17x
🐘 PostgresNext.js (Turbopack)0.266s (-5.5% 🟢)1.006s (~)0.740s601.26x
💻 LocalNext.js (Turbopack)0.578s (-5.3% 🟢)1.057s (+1.8%)0.479s572.73x
💻 LocalExpress0.622s (+33.9% 🔺)1.160s (+15.5% 🔺)0.538s522.94x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.746s (+24.9% 🔺)4.424s (+4.8%)1.678s141.00x
▲ VercelNitro78.584s (+3251.3% 🔺)79.773s (+1740.7% 🔺)1.188s428.62x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.364s (-1.8%)1.018s (+1.1%)0.654s891.00x
🐘 PostgresExpress0.465s (+37.0% 🔺)1.025s (+1.8%)0.560s881.28x
🐘 PostgresNext.js (Turbopack)0.491s (+1.6%)1.018s (+1.1%)0.527s891.35x
💻 LocalNext.js (Turbopack)2.222s (-9.3% 🟢)3.010s (-3.2%)0.788s306.10x
💻 LocalExpress2.224s (+1.5%)2.854s (+1.1%)0.630s326.11x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.160s (-85.9% 🟢)7.805s (-83.0% 🟢)1.645s121.00x
▲ VercelNext.js (Turbopack)40.420s (+544.6% 🔺)42.078s (+395.2% 🔺)1.658s96.56x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.682s (-3.5%)1.006s (~)0.324s1201.00x
🐘 PostgresExpress0.855s (+36.6% 🔺)1.256s (+19.0% 🔺)0.401s961.25x
🐘 PostgresNext.js (Turbopack)1.000s (+2.1%)1.814s (+12.8% 🔺)0.814s671.47x
💻 LocalNext.js (Turbopack)10.521s (+2.0%)11.483s (+1.6%)0.962s1115.42x
💻 LocalExpress10.523s (+2.0%)11.126s (+2.4%)0.603s1115.42x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.195s (-94.5% 🟢)18.692s (-94.0% 🟢)1.497s71.00x
▲ VercelNext.js (Turbopack)58.866s (-63.2% 🟢)60.845s (-62.5% 🟢)1.979s73.42x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.163s (-1.5%)2.002s (~)0.001s (-14.3% 🟢)2.010s (~)0.847s101.00x
💻 LocalExpress1.164s (~)2.005s (~)0.011s (-16.4% 🟢)2.019s (~)0.855s101.00x
💻 LocalNext.js (Turbopack)1.199s (+2.6%)2.003s (~)0.010s (+2.0%)2.017s (~)0.818s101.03x
🐘 PostgresNext.js (Turbopack)1.231s (~)2.001s (~)0.001s (-33.3% 🟢)2.011s (~)0.779s101.06x
🐘 PostgresExpress1.243s (+4.1%)1.999s (~)0.001s (-97.7% 🟢)2.012s (-1.5%)0.769s101.07x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.331s (+7.7% 🔺)3.071s (-10.1% 🟢)2.051s (+297.6% 🔺)5.565s (+24.5% 🔺)3.234s101.00x
▲ VercelNext.js (Turbopack)2.494s (+11.3% 🔺)3.775s (+6.3% 🔺)2.256s (-81.8% 🟢)6.587s (-60.1% 🟢)4.092s101.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.595s (-4.1%)2.006s (~)0.004s (+1.7%)2.025s (~)0.431s301.00x
💻 LocalExpress1.596s (-2.5%)2.009s (~)0.011s (+1.5%)2.023s (~)0.427s301.00x
💻 LocalNext.js (Turbopack)1.709s (+4.2%)2.007s (~)0.011s (+5.3% 🔺)2.021s (~)0.312s301.07x
🐘 PostgresNext.js (Turbopack)1.770s (~)2.011s (~)0.004s (-1.7%)2.027s (~)0.257s301.11x
🐘 PostgresExpress1.919s (+10.5% 🔺)2.313s (+7.9% 🔺)0.003s (+1.6%)2.341s (+8.4% 🔺)0.422s261.20x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.971s (+9.4% 🔺)8.581s (+8.1% 🔺)0.274s (-24.3% 🟢)9.398s (+6.0% 🔺)2.426s71.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.743s (+2.8%)1.029s (~)0.000s (-83.3% 🟢)1.049s (~)0.306s581.00x
🐘 PostgresNext.js (Turbopack)0.801s (-3.7%)1.053s (-1.7%)0.000s (+96.5% 🔺)1.060s (-1.8%)0.259s571.08x
🐘 PostgresExpress1.081s (+10.6% 🔺)1.498s (+4.9%)0.000s (-30.0% 🟢)1.518s (+4.8%)0.438s401.45x
💻 LocalNext.js (Turbopack)1.447s (~)2.012s (~)0.000s (-75.0% 🟢)2.015s (~)0.569s301.95x
💻 LocalExpress1.470s (-0.9%)2.013s (~)0.001s (+73.3% 🔺)2.016s (~)0.547s301.98x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.750s (+4.1%)4.851s (-4.4%)0.001s (+Infinity% 🔺)5.276s (-6.3% 🟢)1.527s121.00x
▲ VercelNext.js (Turbopack)4.000s (+20.9% 🔺)5.605s (+14.5% 🔺)0.000s (+Infinity% 🔺)6.117s (+12.0% 🔺)2.117s111.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.384s (-1.9%)2.099s (+5.2% 🔺)0.000s (+Infinity% 🔺)2.114s (+5.1% 🔺)0.731s291.00x
🐘 PostgresNext.js (Turbopack)1.784s (+3.2%)2.310s (+2.2%)0.000s (+Infinity% 🔺)2.333s (+2.8%)0.550s261.29x
🐘 PostgresExpress2.429s (+50.4% 🔺)3.004s (+33.3% 🔺)0.000s (+Infinity% 🔺)3.031s (+33.0% 🔺)0.602s211.76x
💻 LocalNext.js (Turbopack)2.896s (-9.9% 🟢)3.471s (-10.9% 🟢)0.001s (+77.8% 🔺)3.476s (-10.9% 🟢)0.580s182.09x
💻 LocalExpress3.239s (-2.8%)3.837s (~)0.001s (+183.3% 🔺)3.841s (~)0.602s162.34x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.053s (-14.1% 🟢)7.287s (-15.0% 🟢)0.000s (+Infinity% 🔺)7.727s (-15.0% 🟢)1.675s81.00x
▲ VercelNext.js (Turbopack)7.030s (+36.1% 🔺)8.492s (+27.2% 🔺)0.000s (-100.0% 🟢)8.993s (+24.9% 🔺)1.963s71.16x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress14/21
🐘 PostgresNitro20/21
▲ VercelNitro16/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres21/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: failure
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@github-actions

github-actionsBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production126602191485
✅ 💻 Local Development167102191890
✅ 📦 Local Production167102191890
✅ 🐘 Local Postgres167102191890
✅ 🪟 Windows13500135
✅ 📋 Other7690176945
Total7183010528235

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro109026
✅ example109026
✅ express109026
✅ fastify109026
✅ hono109026
✅ nextjs-turbopack13302
✅ nextjs-webpack13302
✅ nitro109026
✅ nuxt109026
✅ sveltekit12807
✅ vite109026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable110025
✅ e2e-local-dev-tanstack-start-110025
✅ e2e-local-postgres-nest-stable110025
✅ e2e-local-postgres-tanstack-start-110025
✅ e2e-local-prod-nest-stable110025
✅ e2e-local-prod-tanstack-start-110025
✅ e2e-vercel-prod-tanstack-start109026

📋 View full workflow run

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

Adds defensive validation to @workflow/world-vercel ref resolution so empty/truncated ref payloads don’t propagate into replay/hydration and wedge runs during deterministic replay.

Changes:

  • Validate GET /v2/runs/:runId/refs bodies for zero-length and Content-Length mismatches, throwing WorkflowWorldError with specific error codes.
  • Add a new Vitest suite covering happy paths, zero-byte 200s, Content-Length mismatch, non-2xx passthrough, and inline dbrf behavior.
  • Add a patch changeset for @workflow/world-vercel.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/refs.tsAdds response body validation (empty body and Content-Length mismatch) before decoding/returning ref payloads.
packages/world-vercel/src/refs.test.tsIntroduces unit tests for ref resolution behavior across success/error/edge cases.
.changeset/world-vercel-reject-empty-ref-payload.mdDeclares a patch release for the new validation behavior.

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

Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.test.ts
Three review changes:
1. Reject any body shorter than the 4-byte format-prefix length, not
just zero-byte bodies. The SDK guarantees every stored ref payload
starts with a 4-byte format prefix (FORMAT_PREFIX_LENGTH in
@workflow/core), so a 1-3 byte body would also fail downstream
replay with the same 'Data too short to contain format prefix'
error this PR exists to prevent.
2. Parse Content-Length safely with parseInt + Number.isFinite +
non-negative checks instead of bare Number(). A non-numeric value
like 'abc' would otherwise produce NaN and silently surface as a
'truncated' error, masking the real cause. Malformed values are
treated as absent; the minimum-length check still defends against
actual truncation in that case.
3. Add tests for the truncated-body-without-Content-Length case
(chunked transfer where Content-Length validation can't see the
truncation), and for a malformed Content-Length header that should
be ignored rather than misreported as truncation.
The validation logic also moves into a small assertValidRefBody
helper to keep the inner trace function under the noExcessiveCognitiveComplexity limit.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential blocker in PR-version packages/world-vercel/src/refs.ts:143: the new Content-Length check compares response.arrayBuffer().byteLength to the header without accounting for Content-Encoding. Node fetch transparently decompresses gzip bodies while preserving the encoded Content-Length, so a valid compressed ref response can now throw ref-body-length-mismatch. I’d skip this check when Content-Encoding is present and not identity, or force/request identity encoding.

fetch/undici transparently decompresses gzip/br bodies but leaves
Content-Length describing the encoded (compressed) size, so comparing it
against the decompressed byteLength would reject valid compressed refs as
a phantom 'ref-body-length-mismatch'. Skip the comparison when a
non-identity Content-Encoding is present; an absent or 'identity' encoding
is still validated. Adds regression tests for both cases.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@karthikscale3 great catch — fixed in c8f5dce.

You're right: fetch/undici transparently decompresses the body but leaves Content-Length describing the encoded (compressed) size, so a gzip/br ref response would have Content-Length (compressed) ≠ decompressed byteLength and falsely throw ref-body-length-mismatch.

The length comparison is now skipped whenever a non-identityContent-Encoding is present (gzip, br, etc.). An absent or identity encoding means no transform was applied, so the lengths remain directly comparable and that path is still validated. The zero-byte and binary 4-byte-minimum checks are unaffected (they operate on the decompressed body and apply regardless of encoding).

Added two regression tests:

  • skips the length check for compressed (Content-Encoding) responsesContent-Length: 20 + Content-Encoding: gzip with a larger decompressed body still decodes successfully.
  • still enforces the length check for identity Content-EncodingContent-Encoding: identity with a real mismatch still throws.

pnpm --filter @workflow/world-vercel test / typecheck / biome all pass.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2297. (backport job run)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

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

[world-vercel] Validate ref resolve responses before use - #2035

Merged
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload
Jun 8, 2026
Merged

[world-vercel] Validate ref resolve responses before use#2035
TooTallNate merged 6 commits into
mainfrom
refs-validate-empty-payload

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Adds defense-in-depth validation in resolveRefDescriptor so a zero-byte or truncated ref response from GET /v2/runs/:runId/refs no longer corrupts the in-memory event log during workflow replay.

The workflow runtime feeds ref payload bytes directly into decodeFormatPrefix, which requires at least the 4-byte format prefix that the SDK always writes (encodeWithFormatPrefix in @workflow/core). A zero-byte response is therefore never a valid stored value.

Before this change, resolveRefDescriptor checked response.ok and then handed the empty arrayBuffer() downstream verbatim. Multiple users have reported the resulting failure:

Data too short to contain format prefix: expected at least 4 bytes, got 0

That error surfaces deep inside deterministic replay. Because the SDK has already populated its in-memory event snapshot with the empty payload at that point, the run is wedged: every subsequent invocation deterministically replays the same failure, any downstream resumeHook() then surfaces as Hook not found, and the run only clears when stale-run cleanup terminates the sandbox.

Changes

  • packages/world-vercel/src/refs.ts:
    • After await response.arrayBuffer(), validate buffer.byteLength > 0. Empty bodies throw a WorkflowWorldError with code: 'empty-ref-body'.
    • When Content-Length is present, validate it matches the actual body length. Truncated responses (proxy abort, upstream stream cut mid-flight) throw WorkflowWorldError with code: 'ref-body-length-mismatch'.
    • Both errors record on the span via recordException and tag ErrorType for o11y.
  • packages/world-vercel/src/refs.test.ts (new):
    • Happy path coverage (CBOR + application/octet-stream).
    • Zero-byte 200 with both content types.
    • Content-Length mismatch (truncated stream).
    • Absent Content-Length (chunked transfer encoding) — happy path only.
    • Non-2xx status passes through as HTTP <code>.
    • Inline dbrf refs decode without making a network request.
  • Changeset: @workflow/world-vercel patch.

Why both ends validate

This is the SDK-side companion to vercel/workflow-server#432, which rejects zero-byte payloads at the storage boundary. Both layers are necessary because they catch failures at different points in the stack:

  • Server-side catches storage anomalies (S3 inconsistency, Redis empty key, corrupted write).
  • Client-side catches transport anomalies between the server and the SDK (proxy drop, edge cache miss returning truncated content, undici quirks, etc.).

WorkflowWorldError is treated as a retryable transport-level error by the runtime, which is the desired behavior here: surface the empty/truncated body up to the retry layer instead of poisoning event-log replay.

Validation

  • pnpm --filter @workflow/world-vercel typecheck
  • pnpm --filter @workflow/world-vercel test (77 tests pass, including 8 new in refs.test.ts)
  • pnpm --filter @workflow/world-vercel build
  • pnpm biome check --files-ignore-unknown=true packages/world-vercel/src/refs.ts packages/world-vercel/src/refs.test.ts
  • pnpm changeset status --since=main (confirms @workflow/world-vercel patch bump)

When workflow-server returns a ref body to the SDK, the bytes are
fed into the workflow runtime's event log and deserialized via
`decodeFormatPrefix`. The SDK always writes ref payloads with at
least a 4-byte format prefix (see `encodeWithFormatPrefix` in
`@workflow/core`), so a zero-byte response — or one whose length
disagrees with `Content-Length` — is never a valid stored value.
Before this change, `resolveRefDescriptor` had no validation: a
200 with an empty body would be passed downstream as a zero-length
Uint8Array, which then failed deep inside replay with:
Data too short to contain format prefix: expected at least 4 bytes, got 0
By that point the workflow's in-memory event snapshot is already
poisoned with the empty payload, so every subsequent replay
deterministically reproduces the same failure, downstream
`resumeHook()` calls surface as `Hook not found`, and the run
only unsticks when stale-run cleanup terminates the sandbox.
This catches the failure at the transport boundary instead, where
it can be retried as a `WorkflowWorldError`. Both an empty body
and a length mismatch (truncated streaming response) are rejected.
This is the SDK-side companion to vercel/workflow-server#432, which
adds the same validation on the server side.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 20, 2026 17:13
CopilotAI review requested due to automatic review settings May 20, 2026 17:13
@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c8f5dce

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

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

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

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

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 20, 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🥇 Express0.049s (+13.7% 🔺)1.007s (~)0.958s101.00x
💻 LocalNext.js (Turbopack)0.059s (+6.4% 🔺)1.005s (~)0.947s101.19x
🐘 PostgresNitro0.061s (-2.7%)1.012s (~)0.951s101.25x
🐘 PostgresNext.js (Turbopack)0.067s (-3.7%)1.012s (~)0.945s101.37x
🐘 PostgresExpress0.079s (+18.9% 🔺)1.031s (+1.6%)0.952s101.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.291s (-11.6% 🟢)2.378s (+1.6%)2.087s101.00x
▲ VercelNitro0.360s (+37.9% 🔺)2.201s (-9.7% 🟢)1.840s101.24x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.101s (~)2.006s (~)0.905s101.00x
🐘 PostgresNitro1.116s (~)2.010s (~)0.894s101.01x
💻 LocalNext.js (Turbopack)1.130s (+2.0%)2.006s (~)0.876s101.03x
🐘 PostgresNext.js (Turbopack)1.151s (+0.8%)2.009s (~)0.859s101.04x
🐘 PostgresExpress1.170s (+7.1% 🔺)2.027s (+0.8%)0.857s101.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.698s (+8.4% 🔺)3.490s (-1.1%)1.792s101.00x
▲ VercelNext.js (Turbopack)1.817s (+9.1% 🔺)3.995s (-2.0%)2.177s101.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.555s (~)11.020s (~)0.465s31.00x
💻 LocalExpress10.631s (+0.6%)11.024s (~)0.393s31.01x
🐘 PostgresExpress10.684s (~)11.023s (-2.9%)0.339s31.01x
💻 LocalNext.js (Turbopack)10.763s (+1.5%)11.021s (~)0.258s31.02x
🐘 PostgresNext.js (Turbopack)10.827s (~)11.020s (~)0.193s31.03x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.033s (+5.3% 🔺)15.218s (-2.4%)1.186s21.00x
▲ VercelNext.js (Turbopack)15.723s (+16.5% 🔺)17.581s (+9.2% 🔺)1.858s21.12x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro13.725s (-0.9%)14.018s (~)0.293s51.00x
💻 LocalExpress13.845s (~)14.029s (-1.4%)0.184s51.01x
🐘 PostgresExpress14.191s (+2.6%)14.618s (+2.7%)0.427s51.03x
🐘 PostgresNext.js (Turbopack)14.421s (-1.2%)15.018s (~)0.597s41.05x
💻 LocalNext.js (Turbopack)14.569s (+3.9%)15.028s (+2.7%)0.459s41.06x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro23.236s (-8.9% 🟢)24.542s (-11.1% 🟢)1.306s31.00x
▲ VercelNext.js (Turbopack)24.439s (-4.2%)26.713s (-1.8%)2.273s31.05x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro12.500s (-1.9%)13.021s (~)0.521s71.00x
💻 LocalExpress12.697s (+0.8%)13.026s (~)0.329s71.02x
🐘 PostgresExpress13.420s (+5.4% 🔺)14.021s (+5.3% 🔺)0.601s71.07x
💻 LocalNext.js (Turbopack)13.706s (+5.8% 🔺)14.169s (+6.5% 🔺)0.464s71.10x
🐘 PostgresNext.js (Turbopack)14.007s (+1.8%)14.450s (+3.1%)0.443s71.12x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro30.766s (+5.9% 🔺)32.566s (+3.7%)1.800s31.00x
▲ VercelNext.js (Turbopack)33.943s (+14.6% 🔺)36.480s (+14.5% 🔺)2.537s31.10x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.174s (-2.1%)2.008s (~)0.834s151.00x
🐘 PostgresExpress1.234s (+2.8%)2.007s (-0.7%)0.774s151.05x
💻 LocalExpress1.235s (-2.3%)2.007s (~)0.772s151.05x
🐘 PostgresNext.js (Turbopack)1.252s (~)2.007s (~)0.755s151.07x
💻 LocalNext.js (Turbopack)1.377s (+12.6% 🔺)2.007s (~)0.630s151.17x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.525s (-8.8% 🟢)4.139s (-4.7%)1.613s81.00x
▲ VercelNext.js (Turbopack)2.534s (-5.7% 🟢)4.174s (-1.0%)1.641s81.00x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.252s (-0.5%)2.007s (~)0.755s151.00x
🐘 PostgresNext.js (Turbopack)1.401s (-0.6%)2.008s (~)0.607s151.12x
🐘 PostgresExpress1.546s (+16.5% 🔺)2.099s (+4.1%)0.554s151.23x
💻 LocalExpress1.681s (-10.7% 🟢)2.006s (-9.7% 🟢)0.325s151.34x
💻 LocalNext.js (Turbopack)1.798s (+4.2%)2.074s (+3.4%)0.276s151.44x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.382s (-16.9% 🟢)4.751s (-23.5% 🟢)1.369s71.00x
▲ VercelNext.js (Turbopack)3.982s (-3.0%)5.923s (-3.5%)1.941s61.18x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.409s (~)2.008s (~)0.599s151.00x
🐘 PostgresNext.js (Turbopack)1.727s (-1.9%)2.317s (+1.0%)0.590s131.23x
🐘 PostgresExpress1.734s (+24.4% 🔺)2.416s (+20.2% 🔺)0.682s131.23x
💻 LocalExpress4.668s (-11.9% 🟢)5.013s (-16.7% 🟢)0.345s63.31x
💻 LocalNext.js (Turbopack)4.701s (+6.9% 🔺)5.180s (+3.4%)0.478s63.34x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.417s (-19.6% 🟢)7.151s (-17.9% 🟢)1.734s51.00x
▲ VercelNext.js (Turbopack)5.831s (-0.7%)7.666s (-1.4%)1.835s51.08x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.195s (~)2.009s (~)0.814s151.00x
🐘 PostgresNext.js (Turbopack)1.251s (-1.2%)2.009s (~)0.758s151.05x
🐘 PostgresExpress1.253s (+4.1%)2.026s (+0.9%)0.774s151.05x
💻 LocalNext.js (Turbopack)1.363s (+4.4%)2.006s (~)0.644s151.14x
💻 LocalExpress1.614s (-2.5%)2.007s (-3.2%)0.392s151.35x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.618s (-2.0%)4.137s (-4.5%)1.519s81.00x
▲ VercelNext.js (Turbopack)308.185s (+6948.8% 🔺)309.806s (+4822.6% 🔺)1.621s1117.73x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.261s (-1.0%)2.009s (~)0.748s151.00x
🐘 PostgresNext.js (Turbopack)1.390s (~)2.007s (~)0.617s151.10x
🐘 PostgresExpress1.447s (+16.4% 🔺)2.082s (+3.5%)0.635s151.15x
💻 LocalExpress1.896s (-10.8% 🟢)2.294s (-11.4% 🟢)0.398s141.50x
💻 LocalNext.js (Turbopack)1.978s (-2.1%)2.316s (-7.6% 🟢)0.338s131.57x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.577s (-12.6% 🟢)4.853s (-19.2% 🟢)1.276s71.00x
▲ VercelNext.js (Turbopack)3.827s (-15.3% 🟢)5.471s (-11.3% 🟢)1.644s61.07x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.391s (-4.8%)2.008s (~)0.617s151.00x
🐘 PostgresExpress1.549s (+3.2%)2.331s (-2.8%)0.783s131.11x
🐘 PostgresNext.js (Turbopack)1.691s (+2.1%)2.316s (+11.7% 🔺)0.625s131.22x
💻 LocalNext.js (Turbopack)4.898s (-5.1% 🟢)5.513s (-8.4% 🟢)0.615s63.52x
💻 LocalExpress5.004s (-14.7% 🟢)5.680s (-8.6% 🟢)0.676s63.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.902s (+19.2% 🔺)9.075s (+19.8% 🔺)2.172s41.00x
▲ VercelNitro81.134s (+1545.9% 🔺)82.628s (+1126.6% 🔺)1.494s411.75x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.539s (-19.2% 🟢)1.029s (-1.8%)0.490s591.00x
🐘 PostgresNitro0.566s (-7.0% 🟢)1.024s (+1.6%)0.457s591.05x
💻 LocalExpress0.660s (+0.5%)1.005s (-3.3%)0.346s601.22x
🐘 PostgresNext.js (Turbopack)0.825s (+1.6%)1.023s (+1.7%)0.199s591.53x
💻 LocalNext.js (Turbopack)0.861s (+23.7% 🔺)1.039s (+3.4%)0.178s581.60x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.557s (+10.1% 🔺)7.193s (+5.0% 🔺)1.636s91.00x
▲ VercelNitro6.289s (-15.7% 🟢)7.902s (-17.0% 🟢)1.614s81.13x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (-3.6%)2.007s (~)0.641s451.00x
🐘 PostgresExpress1.494s (-2.8%)2.108s (-2.0%)0.615s431.09x
💻 LocalExpress1.619s (+1.7%)2.029s (~)0.410s451.18x
🐘 PostgresNext.js (Turbopack)1.968s (+1.3%)2.284s (+5.0% 🔺)0.316s401.44x
💻 LocalNext.js (Turbopack)2.067s (+19.6% 🔺)2.882s (+42.1% 🔺)0.815s321.51x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.292s (+8.1% 🔺)15.666s (+1.2%)1.374s61.00x
▲ VercelNext.js (Turbopack)16.308s (+10.4% 🔺)18.593s (+9.8% 🔺)2.285s51.14x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.649s (-9.1% 🟢)3.085s (-5.9% 🟢)0.436s391.00x
🐘 PostgresExpress3.135s (+7.7% 🔺)3.682s (+4.0%)0.547s331.18x
💻 LocalExpress3.545s (+3.7%)4.010s (~)0.465s301.34x
🐘 PostgresNext.js (Turbopack)3.876s (~)4.042s (-0.8%)0.166s301.46x
💻 LocalNext.js (Turbopack)4.417s (+18.5% 🔺)5.052s (+22.9% 🔺)0.635s241.67x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.414s (+5.5% 🔺)32.492s (+1.6%)1.078s41.00x
▲ VercelNext.js (Turbopack)32.338s (+19.8% 🔺)34.168s (+16.7% 🔺)1.829s41.03x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.212s (-10.0% 🟢)1.006s (~)0.794s601.00x
🐘 PostgresExpress0.248s (-1.5%)1.011s (~)0.763s601.17x
🐘 PostgresNext.js (Turbopack)0.266s (-5.5% 🟢)1.006s (~)0.740s601.26x
💻 LocalNext.js (Turbopack)0.578s (-5.3% 🟢)1.057s (+1.8%)0.479s572.73x
💻 LocalExpress0.622s (+33.9% 🔺)1.160s (+15.5% 🔺)0.538s522.94x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.746s (+24.9% 🔺)4.424s (+4.8%)1.678s141.00x
▲ VercelNitro78.584s (+3251.3% 🔺)79.773s (+1740.7% 🔺)1.188s428.62x
▲ VercelExpress⚠️missing----

🔍 Observability: Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.364s (-1.8%)1.018s (+1.1%)0.654s891.00x
🐘 PostgresExpress0.465s (+37.0% 🔺)1.025s (+1.8%)0.560s881.28x
🐘 PostgresNext.js (Turbopack)0.491s (+1.6%)1.018s (+1.1%)0.527s891.35x
💻 LocalNext.js (Turbopack)2.222s (-9.3% 🟢)3.010s (-3.2%)0.788s306.10x
💻 LocalExpress2.224s (+1.5%)2.854s (+1.1%)0.630s326.11x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.160s (-85.9% 🟢)7.805s (-83.0% 🟢)1.645s121.00x
▲ VercelNext.js (Turbopack)40.420s (+544.6% 🔺)42.078s (+395.2% 🔺)1.658s96.56x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.682s (-3.5%)1.006s (~)0.324s1201.00x
🐘 PostgresExpress0.855s (+36.6% 🔺)1.256s (+19.0% 🔺)0.401s961.25x
🐘 PostgresNext.js (Turbopack)1.000s (+2.1%)1.814s (+12.8% 🔺)0.814s671.47x
💻 LocalNext.js (Turbopack)10.521s (+2.0%)11.483s (+1.6%)0.962s1115.42x
💻 LocalExpress10.523s (+2.0%)11.126s (+2.4%)0.603s1115.42x
💻 LocalNitro⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.195s (-94.5% 🟢)18.692s (-94.0% 🟢)1.497s71.00x
▲ VercelNext.js (Turbopack)58.866s (-63.2% 🟢)60.845s (-62.5% 🟢)1.979s73.42x
▲ VercelExpress⚠️missing----

🔍 Observability: Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.163s (-1.5%)2.002s (~)0.001s (-14.3% 🟢)2.010s (~)0.847s101.00x
💻 LocalExpress1.164s (~)2.005s (~)0.011s (-16.4% 🟢)2.019s (~)0.855s101.00x
💻 LocalNext.js (Turbopack)1.199s (+2.6%)2.003s (~)0.010s (+2.0%)2.017s (~)0.818s101.03x
🐘 PostgresNext.js (Turbopack)1.231s (~)2.001s (~)0.001s (-33.3% 🟢)2.011s (~)0.779s101.06x
🐘 PostgresExpress1.243s (+4.1%)1.999s (~)0.001s (-97.7% 🟢)2.012s (-1.5%)0.769s101.07x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.331s (+7.7% 🔺)3.071s (-10.1% 🟢)2.051s (+297.6% 🔺)5.565s (+24.5% 🔺)3.234s101.00x
▲ VercelNext.js (Turbopack)2.494s (+11.3% 🔺)3.775s (+6.3% 🔺)2.256s (-81.8% 🟢)6.587s (-60.1% 🟢)4.092s101.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.595s (-4.1%)2.006s (~)0.004s (+1.7%)2.025s (~)0.431s301.00x
💻 LocalExpress1.596s (-2.5%)2.009s (~)0.011s (+1.5%)2.023s (~)0.427s301.00x
💻 LocalNext.js (Turbopack)1.709s (+4.2%)2.007s (~)0.011s (+5.3% 🔺)2.021s (~)0.312s301.07x
🐘 PostgresNext.js (Turbopack)1.770s (~)2.011s (~)0.004s (-1.7%)2.027s (~)0.257s301.11x
🐘 PostgresExpress1.919s (+10.5% 🔺)2.313s (+7.9% 🔺)0.003s (+1.6%)2.341s (+8.4% 🔺)0.422s261.20x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.971s (+9.4% 🔺)8.581s (+8.1% 🔺)0.274s (-24.3% 🟢)9.398s (+6.0% 🔺)2.426s71.00x
▲ VercelExpress⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.743s (+2.8%)1.029s (~)0.000s (-83.3% 🟢)1.049s (~)0.306s581.00x
🐘 PostgresNext.js (Turbopack)0.801s (-3.7%)1.053s (-1.7%)0.000s (+96.5% 🔺)1.060s (-1.8%)0.259s571.08x
🐘 PostgresExpress1.081s (+10.6% 🔺)1.498s (+4.9%)0.000s (-30.0% 🟢)1.518s (+4.8%)0.438s401.45x
💻 LocalNext.js (Turbopack)1.447s (~)2.012s (~)0.000s (-75.0% 🟢)2.015s (~)0.569s301.95x
💻 LocalExpress1.470s (-0.9%)2.013s (~)0.001s (+73.3% 🔺)2.016s (~)0.547s301.98x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.750s (+4.1%)4.851s (-4.4%)0.001s (+Infinity% 🔺)5.276s (-6.3% 🟢)1.527s121.00x
▲ VercelNext.js (Turbopack)4.000s (+20.9% 🔺)5.605s (+14.5% 🔺)0.000s (+Infinity% 🔺)6.117s (+12.0% 🔺)2.117s111.07x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.384s (-1.9%)2.099s (+5.2% 🔺)0.000s (+Infinity% 🔺)2.114s (+5.1% 🔺)0.731s291.00x
🐘 PostgresNext.js (Turbopack)1.784s (+3.2%)2.310s (+2.2%)0.000s (+Infinity% 🔺)2.333s (+2.8%)0.550s261.29x
🐘 PostgresExpress2.429s (+50.4% 🔺)3.004s (+33.3% 🔺)0.000s (+Infinity% 🔺)3.031s (+33.0% 🔺)0.602s211.76x
💻 LocalNext.js (Turbopack)2.896s (-9.9% 🟢)3.471s (-10.9% 🟢)0.001s (+77.8% 🔺)3.476s (-10.9% 🟢)0.580s182.09x
💻 LocalExpress3.239s (-2.8%)3.837s (~)0.001s (+183.3% 🔺)3.841s (~)0.602s162.34x
💻 LocalNitro⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.053s (-14.1% 🟢)7.287s (-15.0% 🟢)0.000s (+Infinity% 🔺)7.727s (-15.0% 🟢)1.675s81.00x
▲ VercelNext.js (Turbopack)7.030s (+36.1% 🔺)8.492s (+27.2% 🔺)0.000s (-100.0% 🟢)8.993s (+24.9% 🔺)1.963s71.16x
▲ VercelExpress⚠️missing-----

🔍 Observability: Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress14/21
🐘 PostgresNitro20/21
▲ VercelNitro16/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres21/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: failure
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@github-actions

github-actionsBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production126602191485
✅ 💻 Local Development167102191890
✅ 📦 Local Production167102191890
✅ 🐘 Local Postgres167102191890
✅ 🪟 Windows13500135
✅ 📋 Other7690176945
Total7183010528235

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro109026
✅ example109026
✅ express109026
✅ fastify109026
✅ hono109026
✅ nextjs-turbopack13302
✅ nextjs-webpack13302
✅ nitro109026
✅ nuxt109026
✅ sveltekit12807
✅ vite109026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable110025
✅ express-stable110025
✅ fastify-stable110025
✅ hono-stable110025
✅ nextjs-turbopack-canary116019
✅ nextjs-turbopack-stable-lazy-discovery-disabled13500
✅ nextjs-turbopack-stable-lazy-discovery-enabled13500
✅ nextjs-webpack-canary116019
✅ nextjs-webpack-stable-lazy-discovery-disabled13500
✅ nextjs-webpack-stable-lazy-discovery-enabled13500
✅ nitro-stable110025
✅ nuxt-stable110025
✅ sveltekit-stable12906
✅ vite-stable110025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack13500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable110025
✅ e2e-local-dev-tanstack-start-110025
✅ e2e-local-postgres-nest-stable110025
✅ e2e-local-postgres-tanstack-start-110025
✅ e2e-local-prod-nest-stable110025
✅ e2e-local-prod-tanstack-start-110025
✅ e2e-vercel-prod-tanstack-start109026

📋 View full workflow run

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

Adds defensive validation to @workflow/world-vercel ref resolution so empty/truncated ref payloads don’t propagate into replay/hydration and wedge runs during deterministic replay.

Changes:

  • Validate GET /v2/runs/:runId/refs bodies for zero-length and Content-Length mismatches, throwing WorkflowWorldError with specific error codes.
  • Add a new Vitest suite covering happy paths, zero-byte 200s, Content-Length mismatch, non-2xx passthrough, and inline dbrf behavior.
  • Add a patch changeset for @workflow/world-vercel.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

FileDescription
packages/world-vercel/src/refs.tsAdds response body validation (empty body and Content-Length mismatch) before decoding/returning ref payloads.
packages/world-vercel/src/refs.test.tsIntroduces unit tests for ref resolution behavior across success/error/edge cases.
.changeset/world-vercel-reject-empty-ref-payload.mdDeclares a patch release for the new validation behavior.

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

Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.ts Outdated
Comment threadpackages/world-vercel/src/refs.test.ts
Three review changes:
1. Reject any body shorter than the 4-byte format-prefix length, not
just zero-byte bodies. The SDK guarantees every stored ref payload
starts with a 4-byte format prefix (FORMAT_PREFIX_LENGTH in
@workflow/core), so a 1-3 byte body would also fail downstream
replay with the same 'Data too short to contain format prefix'
error this PR exists to prevent.
2. Parse Content-Length safely with parseInt + Number.isFinite +
non-negative checks instead of bare Number(). A non-numeric value
like 'abc' would otherwise produce NaN and silently surface as a
'truncated' error, masking the real cause. Malformed values are
treated as absent; the minimum-length check still defends against
actual truncation in that case.
3. Add tests for the truncated-body-without-Content-Length case
(chunked transfer where Content-Length validation can't see the
truncation), and for a malformed Content-Length header that should
be ignored rather than misreported as truncation.
The validation logic also moves into a small assertValidRefBody
helper to keep the inner trace function under the noExcessiveCognitiveComplexity limit.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential blocker in PR-version packages/world-vercel/src/refs.ts:143: the new Content-Length check compares response.arrayBuffer().byteLength to the header without accounting for Content-Encoding. Node fetch transparently decompresses gzip bodies while preserving the encoded Content-Length, so a valid compressed ref response can now throw ref-body-length-mismatch. I’d skip this check when Content-Encoding is present and not identity, or force/request identity encoding.

fetch/undici transparently decompresses gzip/br bodies but leaves
Content-Length describing the encoded (compressed) size, so comparing it
against the decompressed byteLength would reject valid compressed refs as
a phantom 'ref-body-length-mismatch'. Skip the comparison when a
non-identity Content-Encoding is present; an absent or 'identity' encoding
is still validated. Adds regression tests for both cases.
@TooTallNate

Copy link
Copy Markdown
MemberAuthor

@karthikscale3 great catch — fixed in c8f5dce.

You're right: fetch/undici transparently decompresses the body but leaves Content-Length describing the encoded (compressed) size, so a gzip/br ref response would have Content-Length (compressed) ≠ decompressed byteLength and falsely throw ref-body-length-mismatch.

The length comparison is now skipped whenever a non-identityContent-Encoding is present (gzip, br, etc.). An absent or identity encoding means no transform was applied, so the lengths remain directly comparable and that path is still validated. The zero-byte and binary 4-byte-minimum checks are unaffected (they operate on the decompressed body and apply regardless of encoding).

Added two regression tests:

  • skips the length check for compressed (Content-Encoding) responsesContent-Length: 20 + Content-Encoding: gzip with a larger decompressed body still decodes successfully.
  • still enforces the length check for identity Content-EncodingContent-Encoding: identity with a real mismatch still throws.

pnpm --filter @workflow/world-vercel test / typecheck / biome all pass.

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2297. (backport job run)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TooTallNate@pranaygp@karthikscale3