Skip to content

fix(core): resolve forwarded stream keys across deployments#2191

Draft
pranaygp wants to merge 1 commit into
mainfrom
pranaygp/codex/fix-cross-deployment-stream-encryption
Draft

fix(core): resolve forwarded stream keys across deployments#2191
pranaygp wants to merge 1 commit into
mainfrom
pranaygp/codex/fix-cross-deployment-stream-encryption

Conversation

@pranaygp
Copy link
Copy Markdown
Contributor

Summary

  • carry the owning deployment ID on forwarded writable stream descriptors so a child on a newer deployment encrypts parent-stream chunks with the parent key
  • fall back to loading the owning run for descriptors already serialized by older SDK versions
  • add focused regression tests and document the expanded World key-resolution usage

Root Cause

Cross-run writable revivers only carried the parent runId and called getEncryptionKeyForRun(targetRunId). In @workflow/world-vercel, a string lookup without deployment context resolves against the executing deployment, so a child started on a newer deployment encrypted new chunks using its key while the parent stream is read using the older run key.

Context

This upstreams the behavior temporarily patched downstream in https://github.com/vercel/ash/pull/852, while avoiding its extra owner-run lookup for newly serialized descriptors. Legacy or in-flight descriptors retain a correctness fallback.

Validation

  • pnpm turbo build --filter=@workflow/core...
  • pnpm --filter @workflow/core typecheck
  • pnpm --filter @workflow/core exec vitest run src/serialization.test.ts src/step/writable-stream.test.ts
  • pnpm --filter @workflow/core test
  • pnpm biome format <changed files>
  • pnpm changeset status --since=main

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 31, 2026

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

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

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 31, 2026

🦋 Changeset detected

Latest commit: bf73d05

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

This PR includes changesets to release 20 packages
Name Type
@workflow/core Patch
@workflow/world Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/world-vercel Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 31, 2026

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 0.041s (-7.9% 🟢) 1.005s (~) 0.965s 10 1.00x
💻 Local Nitro 0.043s (-0.7%) 1.007s (~) 0.964s 10 1.05x
🐘 Postgres Express 0.060s (+2.8%) 1.012s (~) 0.953s 10 1.46x
🐘 Postgres Nitro 0.060s (-36.7% 🟢) 1.011s (-3.1%) 0.950s 10 1.48x
🐘 Postgres Next.js (Turbopack) 0.068s 1.012s 0.944s 10 1.67x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.235s (~) 6.676s (+212.6% 🔺) 6.441s 10 1.00x
▲ Vercel Next.js (Turbopack) 1.245s (+395.2% 🔺) 4.551s (+95.1% 🔺) 3.306s 10 5.31x
▲ Vercel Nitro 1.694s (+313.6% 🔺) 6.282s (+150.3% 🔺) 4.587s 10 7.22x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.097s (-3.0%) 2.006s (~) 0.910s 10 1.00x
🐘 Postgres Nitro 1.102s (-3.4%) 2.010s (~) 0.908s 10 1.00x
💻 Local Express 1.104s (-1.9%) 2.006s (~) 0.902s 10 1.01x
🐘 Postgres Express 1.105s (-3.6%) 2.008s (~) 0.903s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.149s 2.010s 0.861s 10 1.05x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.544s (-60.3% 🟢) 6.096s (+3.2%) 4.553s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.640s (+29.7% 🔺) 9.936s (+159.4% 🔺) 7.297s 10 1.71x
▲ Vercel Express 2.985s (+59.2% 🔺) 8.072s (+112.0% 🔺) 5.087s 10 1.93x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 10.512s (-4.1%) 11.014s (~) 0.502s 3 1.00x
💻 Local Express 10.522s (-3.7%) 11.023s (~) 0.501s 3 1.00x
🐘 Postgres Nitro 10.540s (-3.0%) 11.018s (~) 0.478s 3 1.00x
💻 Local Nitro 10.557s (-3.6%) 11.024s (~) 0.467s 3 1.00x
🐘 Postgres Next.js (Turbopack) 10.851s 11.022s 0.171s 3 1.03x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 13.250s (-22.0% 🟢) 15.041s (-24.9% 🟢) 1.791s 2 1.00x
▲ Vercel Nitro 13.438s (-43.4% 🟢) 19.808s (-21.1% 🟢) 6.371s 2 1.01x
▲ Vercel Next.js (Turbopack) 13.866s (-19.9% 🟢) 21.243s (+9.5% 🔺) 7.377s 2 1.05x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 13.673s (-8.7% 🟢) 14.026s (-6.7% 🟢) 0.353s 5 1.00x
🐘 Postgres Nitro 13.751s (-5.8% 🟢) 14.018s (-6.7% 🟢) 0.267s 5 1.01x
🐘 Postgres Express 13.761s (-5.6% 🟢) 14.021s (-6.7% 🟢) 0.261s 5 1.01x
💻 Local Nitro 13.864s (-8.0% 🟢) 14.228s (-11.2% 🟢) 0.364s 5 1.01x
🐘 Postgres Next.js (Turbopack) 14.378s 15.016s 0.638s 4 1.05x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 20.291s (-61.4% 🟢) 23.433s (-57.1% 🟢) 3.141s 3 1.00x
▲ Vercel Express 20.391s (-59.5% 🟢) 23.773s (-54.8% 🟢) 3.382s 3 1.00x
▲ Vercel Nitro 20.849s (-67.7% 🟢) 24.664s (-63.0% 🟢) 3.814s 3 1.03x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 12.359s (-25.6% 🟢) 13.024s (-23.5% 🟢) 0.665s 7 1.00x
🐘 Postgres Express 12.378s (-11.6% 🟢) 13.018s (-10.8% 🟢) 0.639s 7 1.00x
💻 Local Nitro 12.484s (-25.6% 🟢) 13.025s (-23.5% 🟢) 0.541s 7 1.01x
🐘 Postgres Nitro 12.599s (-9.8% 🟢) 13.018s (-9.0% 🟢) 0.419s 7 1.02x
🐘 Postgres Next.js (Turbopack) 13.675s 14.022s 0.347s 7 1.11x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 26.939s (-77.8% 🟢) 31.949s (-74.2% 🟢) 5.010s 4 1.00x
▲ Vercel Next.js (Turbopack) 27.298s (-93.1% 🟢) 33.379s (-91.6% 🟢) 6.081s 3 1.01x
▲ Vercel Nitro 28.440s (-93.3% 🟢) 40.144s (-90.5% 🟢) 11.704s 3 1.06x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.175s (-7.8% 🟢) 2.006s (~) 0.831s 15 1.00x
🐘 Postgres Express 1.176s (-6.7% 🟢) 2.007s (~) 0.831s 15 1.00x
💻 Local Nitro 1.192s (-26.9% 🟢) 2.007s (-3.2%) 0.814s 15 1.01x
💻 Local Express 1.222s (-17.9% 🟢) 2.006s (~) 0.783s 15 1.04x
🐘 Postgres Next.js (Turbopack) 1.246s 2.008s 0.762s 15 1.06x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.333s (-31.3% 🟢) 13.198s (+167.6% 🔺) 10.864s 3 1.00x
▲ Vercel Express 4.593s (+60.6% 🔺) 8.716s (+88.5% 🔺) 4.123s 4 1.97x
▲ Vercel Nitro 6.918s (+145.5% 🔺) 15.278s (+253.5% 🔺) 8.361s 2 2.96x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.227s (-47.8% 🟢) 2.008s (-33.3% 🟢) 0.781s 15 1.00x
🐘 Postgres Express 1.238s (-47.6% 🟢) 2.007s (-33.3% 🟢) 0.769s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.379s 2.006s 0.627s 15 1.12x
💻 Local Express 1.702s (-42.4% 🟢) 2.006s (-41.9% 🟢) 0.303s 15 1.39x
💻 Local Nitro 1.893s (-39.8% 🟢) 2.293s (-41.0% 🟢) 0.400s 14 1.54x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 3.408s (-52.0% 🟢) 11.147s (+25.2% 🔺) 7.739s 3 1.00x
▲ Vercel Nitro 5.113s (+26.2% 🔺) 12.019s (+103.0% 🔺) 6.906s 3 1.50x
▲ Vercel Express 6.891s (+90.4% 🔺) 13.353s (+161.3% 🔺) 6.461s 3 2.02x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.400s (-59.8% 🟢) 2.007s (-50.0% 🟢) 0.607s 15 1.00x
🐘 Postgres Nitro 1.416s (-59.3% 🟢) 2.006s (-49.9% 🟢) 0.591s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.652s 2.009s 0.357s 15 1.18x
💻 Local Express 4.373s (-47.6% 🟢) 5.012s (-44.5% 🟢) 0.639s 7 3.12x
💻 Local Nitro 5.530s (-33.8% 🟢) 6.013s (-33.3% 🟢) 0.484s 5 3.95x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.565s (+7.7% 🔺) 11.842s (+93.3% 🔺) 7.277s 3 1.00x
▲ Vercel Next.js (Turbopack) 5.807s (-34.9% 🟢) 12.799s (+16.8% 🔺) 6.992s 3 1.27x
▲ Vercel Nitro 6.626s (+88.0% 🔺) 13.839s (+150.1% 🔺) 7.213s 3 1.45x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.185s (-5.7% 🟢) 2.008s (~) 0.823s 15 1.00x
🐘 Postgres Express 1.187s (-5.6% 🟢) 2.009s (~) 0.822s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.239s 2.006s 0.768s 15 1.05x
💻 Local Express 1.528s (-19.3% 🟢) 2.006s (-15.2% 🟢) 0.478s 15 1.29x
💻 Local Nitro 1.542s (-17.4% 🟢) 2.007s (-14.3% 🟢) 0.465s 15 1.30x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.537s (+3.2%) 6.086s (+46.0% 🔺) 3.549s 5 1.00x
▲ Vercel Next.js (Turbopack) 2.797s (-4.6%) 5.477s (+18.0% 🔺) 2.679s 6 1.10x
▲ Vercel Express 5.441s (+110.8% 🔺) 10.242s (+135.5% 🔺) 4.802s 4 2.14x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.252s (-46.5% 🟢) 2.007s (-33.3% 🟢) 0.755s 15 1.00x
🐘 Postgres Express 1.269s (-45.8% 🟢) 2.009s (-33.3% 🟢) 0.740s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.392s 2.008s 0.616s 15 1.11x
💻 Local Express 1.939s (-38.1% 🟢) 2.393s (-36.4% 🟢) 0.454s 13 1.55x
💻 Local Nitro 2.073s (-32.4% 🟢) 2.591s (-33.3% 🟢) 0.518s 12 1.66x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 3.904s (+20.7% 🔺) 7.649s (+50.7% 🔺) 3.746s 4 1.00x
▲ Vercel Next.js (Turbopack) 6.288s (+100.1% 🔺) 18.479s (+308.6% 🔺) 12.191s 2 1.61x
▲ Vercel Express 7.343s (+130.0% 🔺) 15.137s (+215.9% 🔺) 7.794s 2 1.88x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.376s (-60.5% 🟢) 2.008s (-49.9% 🟢) 0.632s 15 1.00x
🐘 Postgres Express 1.386s (-60.4% 🟢) 2.007s (-50.0% 🟢) 0.622s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.652s 2.079s 0.427s 15 1.20x
💻 Local Express 5.154s (-41.4% 🟢) 5.680s (-38.7% 🟢) 0.526s 6 3.74x
💻 Local Nitro 6.010s (-34.3% 🟢) 6.412s (-36.0% 🟢) 0.402s 5 4.37x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.770s (-25.7% 🟢) 15.483s (+89.3% 🔺) 10.712s 3 1.00x
▲ Vercel Next.js (Turbopack) 5.555s (-17.8% 🟢) 7.931s (-7.2% 🟢) 2.376s 4 1.16x
▲ Vercel Nitro 6.042s (+18.6% 🔺) 16.884s (+147.7% 🔺) 10.842s 3 1.27x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.548s (-33.2% 🟢) 1.007s (~) 0.459s 60 1.00x
🐘 Postgres Express 0.579s (-30.9% 🟢) 1.023s (~) 0.444s 59 1.06x
💻 Local Express 0.597s (-39.3% 🟢) 1.005s (-6.6% 🟢) 0.407s 60 1.09x
💻 Local Nitro 0.619s (-36.9% 🟢) 1.039s (-5.0%) 0.420s 58 1.13x
🐘 Postgres Next.js (Turbopack) 0.827s 1.023s 0.196s 59 1.51x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.723s (-78.6% 🟢) 7.797s (-67.5% 🟢) 3.073s 8 1.00x
▲ Vercel Next.js (Turbopack) 5.283s (-63.6% 🟢) 15.195s (-5.5% 🟢) 9.912s 4 1.12x
▲ Vercel Express 5.984s (-68.5% 🟢) 12.149s (-43.0% 🟢) 6.165s 5 1.27x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.340s (-32.2% 🟢) 2.008s (-11.1% 🟢) 0.668s 45 1.00x
🐘 Postgres Nitro 1.347s (-30.1% 🟢) 2.053s (-2.3%) 0.706s 44 1.01x
💻 Local Express 1.469s (-51.3% 🟢) 2.005s (-44.1% 🟢) 0.536s 45 1.10x
💻 Local Nitro 1.494s (-50.8% 🟢) 2.006s (-46.6% 🟢) 0.512s 45 1.12x
🐘 Postgres Next.js (Turbopack) 1.929s 2.124s 0.195s 43 1.44x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 12.152s (-69.2% 🟢) 17.540s (-57.5% 🟢) 5.389s 6 1.00x
▲ Vercel Next.js (Turbopack) 12.654s (-74.6% 🟢) 15.316s (-70.4% 🟢) 2.662s 6 1.04x
▲ Vercel Express 13.859s (-59.9% 🟢) 20.407s (-44.6% 🟢) 6.548s 6 1.14x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.561s (-37.6% 🟢) 3.008s (-34.7% 🟢) 0.447s 40 1.00x
🐘 Postgres Express 2.736s (-31.4% 🟢) 3.137s (-28.2% 🟢) 0.400s 39 1.07x
💻 Local Nitro 3.231s (-65.3% 🟢) 4.009s (-60.0% 🟢) 0.779s 30 1.26x
💻 Local Express 3.347s (-63.7% 🟢) 4.042s (-59.7% 🟢) 0.695s 30 1.31x
🐘 Postgres Next.js (Turbopack) 3.785s 4.043s 0.258s 30 1.48x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 26.261s (-72.9% 🟢) 35.103s (-64.3% 🟢) 8.843s 4 1.00x
▲ Vercel Express 27.594s (-78.8% 🟢) 33.627s (-74.6% 🟢) 6.034s 4 1.05x
▲ Vercel Next.js (Turbopack) 30.338s (-71.7% 🟢) 35.906s (-67.0% 🟢) 5.568s 4 1.16x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.211s (-25.1% 🟢) 1.006s (~) 0.794s 60 1.00x
🐘 Postgres Nitro 0.213s (-24.9% 🟢) 1.006s (~) 0.793s 60 1.01x
🐘 Postgres Next.js (Turbopack) 0.274s 1.006s 0.732s 60 1.29x
💻 Local Express 0.440s (-21.4% 🟢) 1.004s (~) 0.564s 60 2.08x
💻 Local Nitro 0.454s (-24.9% 🟢) 1.005s (-1.7%) 0.550s 60 2.15x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.597s (+56.3% 🔺) 10.005s (+198.6% 🔺) 7.408s 7 1.00x
▲ Vercel Next.js (Turbopack) 2.895s (+43.2% 🔺) 8.011s (+111.2% 🔺) 5.116s 9 1.11x
▲ Vercel Express 3.455s (+76.8% 🔺) 8.255s (+127.0% 🔺) 4.800s 8 1.33x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.335s (-34.2% 🟢) 1.006s (~) 0.671s 90 1.00x
🐘 Postgres Nitro 0.348s (-29.9% 🟢) 1.018s (+1.1%) 0.670s 89 1.04x
🐘 Postgres Next.js (Turbopack) 0.478s 1.006s 0.528s 90 1.43x
💻 Local Express 2.096s (-16.6% 🟢) 2.655s (-11.8% 🟢) 0.560s 34 6.25x
💻 Local Nitro 2.105s (-17.1% 🟢) 2.685s (-10.8% 🟢) 0.580s 34 6.28x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.859s (+59.5% 🔺) 10.328s (+114.8% 🔺) 5.469s 10 1.00x
▲ Vercel Nitro 5.135s (+59.2% 🔺) 13.840s (+187.0% 🔺) 8.705s 7 1.06x
▲ Vercel Next.js (Turbopack) 68.156s (+1827.9% 🔺) 74.975s (+1343.7% 🔺) 6.819s 5 14.03x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.665s (-15.8% 🟢) 1.007s (~) 0.341s 120 1.00x
🐘 Postgres Express 0.666s (-18.7% 🟢) 1.006s (-1.1%) 0.340s 120 1.00x
🐘 Postgres Next.js (Turbopack) 0.947s 1.371s 0.425s 88 1.42x
💻 Local Express 9.467s (-15.4% 🟢) 10.030s (-16.0% 🟢) 0.563s 13 14.23x
💻 Local Nitro 9.743s (-12.9% 🟢) 10.360s (-11.2% 🟢) 0.616s 12 14.64x
💻 Local Next.js (Turbopack) ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 13.641s (+76.6% 🔺) 20.074s (+113.5% 🔺) 6.433s 7 1.00x
▲ Vercel Next.js (Turbopack) 13.929s (+34.9% 🔺) 17.993s (+46.4% 🔺) 4.064s 7 1.02x
▲ Vercel Express 13.968s (+88.2% 🔺) 20.457s (+121.3% 🔺) 6.490s 6 1.02x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.155s (+463.5% 🔺) 1.999s (+100.0% 🔺) 0.001s (-26.7% 🟢) 2.011s (+98.8% 🔺) 0.855s 10 1.00x
💻 Local Express 1.160s (+482.4% 🔺) 2.005s (+99.6% 🔺) 0.011s (-10.7% 🟢) 2.018s (+98.2% 🔺) 0.858s 10 1.00x
💻 Local Nitro 1.162s (+443.6% 🔺) 2.005s (+99.6% 🔺) 0.012s (-2.4%) 2.019s (+98.2% 🔺) 0.858s 10 1.01x
🐘 Postgres Express 1.163s (+467.1% 🔺) 2.000s (+100.3% 🔺) 0.001s (-25.0% 🟢) 2.011s (+98.8% 🔺) 0.847s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.218s 2.001s 0.001s 2.010s 0.792s 10 1.05x
💻 Local Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.077s (-69.7% 🟢) 7.494s (-13.4% 🟢) 2.095s (+231.5% 🔺) 11.204s (+14.5% 🔺) 9.127s 10 1.00x
▲ Vercel Nitro 2.122s (-44.6% 🟢) 9.725s (+84.3% 🔺) 0.289s (-61.0% 🟢) 12.069s (+86.2% 🔺) 9.947s 10 1.02x
▲ Vercel Express 5.514s (+120.1% 🔺) 6.426s (+57.1% 🔺) 2.652s (+176.0% 🔺) 13.756s (+146.0% 🔺) 8.242s 10 2.65x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.565s (+148.5% 🔺) 2.004s (+99.1% 🔺) 0.004s (+0.9%) 2.025s (+98.0% 🔺) 0.460s 30 1.00x
💻 Local Express 1.570s (+107.4% 🔺) 2.008s (+95.2% 🔺) 0.010s (+10.1% 🔺) 2.021s (+94.3% 🔺) 0.450s 30 1.00x
🐘 Postgres Nitro 1.573s (+152.0% 🔺) 2.004s (+99.0% 🔺) 0.004s (-7.3% 🟢) 2.027s (+98.3% 🔺) 0.455s 30 1.00x
💻 Local Nitro 1.599s (+90.7% 🔺) 2.009s (+98.5% 🔺) 0.011s (+18.1% 🔺) 2.022s (+81.2% 🔺) 0.423s 30 1.02x
🐘 Postgres Next.js (Turbopack) 1.736s 2.011s 0.004s 2.027s 0.291s 30 1.11x
💻 Local Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.949s (-79.8% 🟢) 10.025s (-67.5% 🟢) 0.209s (+86.5% 🔺) 10.895s (-65.7% 🟢) 4.946s 6 1.00x
▲ Vercel Next.js (Turbopack) 6.147s (-63.7% 🟢) 11.288s (-38.1% 🟢) 0.367s (+73.9% 🔺) 12.297s (-35.1% 🟢) 6.151s 6 1.03x
▲ Vercel Express 7.340s (+12.8% 🔺) 13.204s (+64.8% 🔺) 0.236s (-42.3% 🟢) 18.042s (+104.2% 🔺) 10.702s 4 1.23x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.678s (-30.0% 🟢) 1.031s (-17.4% 🟢) 0.000s (-58.6% 🟢) 1.042s (-17.2% 🟢) 0.364s 58 1.00x
🐘 Postgres Express 0.703s (-26.9% 🟢) 1.031s (-19.3% 🟢) 0.000s (-60.3% 🟢) 1.041s (-20.3% 🟢) 0.338s 58 1.04x
🐘 Postgres Next.js (Turbopack) 0.840s 1.073s 0.000s 1.080s 0.241s 56 1.24x
💻 Local Express 1.360s (+11.0% 🔺) 2.014s (~) 0.000s (-80.0% 🟢) 2.016s (~) 0.656s 30 2.01x
💻 Local Nitro 1.387s (+13.4% 🔺) 2.014s (~) 0.001s (+500.0% 🔺) 2.016s (~) 0.629s 30 2.05x
💻 Local Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.874s (+3.6%) 11.632s (+128.0% 🔺) 0.000s (-100.0% 🟢) 12.392s (+124.1% 🔺) 8.518s 6 1.00x
▲ Vercel Nitro 4.243s (+39.1% 🔺) 10.820s (+146.3% 🔺) 0.000s (-100.0% 🟢) 14.136s (+194.0% 🔺) 9.893s 5 1.10x
▲ Vercel Next.js (Turbopack) 313.396s (+2978.2% 🔺) 315.223s (+2636.4% 🔺) 0.000s (NaN%) 320.406s (+2559.0% 🔺) 7.010s 1 80.89x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.497s (-15.5% 🟢) 2.142s (-1.6%) 0.000s (+Infinity% 🔺) 2.172s (-1.2%) 0.675s 28 1.00x
🐘 Postgres Nitro 1.502s (-16.2% 🟢) 2.101s (-1.9%) 0.000s (-100.0% 🟢) 2.125s (-2.3%) 0.623s 29 1.00x
🐘 Postgres Next.js (Turbopack) 1.660s 2.188s 0.000s 2.236s 0.576s 27 1.11x
💻 Local Nitro 3.078s (-9.1% 🟢) 3.840s (-4.8%) 0.000s (-29.7% 🟢) 3.842s (-4.8%) 0.765s 16 2.06x
💻 Local Express 3.174s (-8.5% 🟢) 3.836s (-4.9%) 0.001s (-6.3% 🟢) 3.842s (-4.8%) 0.669s 16 2.12x
💻 Local Next.js (Turbopack) ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.529s (+20.5% 🔺) 9.820s (+63.1% 🔺) 0.000s (NaN%) 12.063s (+86.8% 🔺) 6.534s 5 1.00x
▲ Vercel Nitro 5.605s (+36.9% 🔺) 9.231s (+71.8% 🔺) 0.000s (-100.0% 🟢) 10.845s (+87.2% 🔺) 5.240s 6 1.01x
▲ Vercel Next.js (Turbopack) 6.144s (+9.4% 🔺) 11.464s (+64.2% 🔺) 0.000s (+33.3% 🔺) 12.564s (+66.6% 🔺) 6.419s 6 1.11x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

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

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 31, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 1210 12 219 1441
✅ 💻 Local Development 1615 0 219 1834
❌ 📦 Local Production 1614 1 219 1834
✅ 🐘 Local Postgres 1615 0 219 1834
❌ 📋 Other 739 2 176 917
Total 6793 15 1052 7860

❌ Failed Tests

▲ Vercel Production (12 failed)

express (1 failed):

  • cancelRun via CLI - cancelling a running workflow | wrun_01KSZQ3TENTST8PF529RCE7K21 | 🔍 observability

fastify (3 failed):

  • cancelRun via CLI - cancelling a running workflow | wrun_01KSZQ3TENTST8PF529RCE7K21 | 🔍 observability
  • AbortController abortHookOrderingWorkflow [listener-first-hook-first]: addEventListener → hook.then → resumeHook → abort()
  • AbortController abortHookOrderingWorkflow [hook-first-hook-first]: hook.then → addEventListener → resumeHook → abort()

hono (2 failed):

  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KSZPX5X97S5ZM86J4T161F4V | 🔍 observability
  • AbortController abortHookOrderingWorkflow [hook-first-hook-first]: hook.then → addEventListener → resumeHook → abort()

nextjs-turbopack (1 failed):

  • AbortController abortHookOrderingWorkflow [hook-first-hook-first]: hook.then → addEventListener → resumeHook → abort()

nuxt (3 failed):

  • AbortController abortHookOrderingWorkflow [listener-first-abort-first]: addEventListener → hook.then → abort() → resumeHook
  • AbortController abortHookOrderingWorkflow [listener-first-hook-first]: addEventListener → hook.then → resumeHook → abort()
  • AbortController abortHookOrderingWorkflow [hook-first-hook-first]: hook.then → addEventListener → resumeHook → abort()

sveltekit (1 failed):

  • AbortController abortHookOrderingWorkflow [listener-first-hook-first]: addEventListener → hook.then → resumeHook → abort()

vite (1 failed):

  • AbortController abortHookOrderingWorkflow [listener-first-hook-first]: addEventListener → hook.then → resumeHook → abort()
📦 Local Production (1 failed)

sveltekit-stable (1 failed):

  • AbortController abortHookOrderingWorkflow [hook-first-hook-first]: hook.then → addEventListener → resumeHook → abort()
📋 Other (2 failed)

e2e-vercel-prod-tanstack-start (2 failed):

  • cancelRun via CLI - cancelling a running workflow | wrun_01KSZQ3TENTST8PF529RCE7K21
  • AbortController abortHookOrderingWorkflow [hook-first-hook-first]: hook.then → addEventListener → resumeHook → abort()

Details by Category

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

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

Copy link
Copy Markdown
Member

@TooTallNate TooTallNate left a comment

Choose a reason for hiding this comment

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

Approve — solid fix, with one small consistency concern worth thinking about

The cross-deployment encryption bug is real and the fix is well-targeted: add deploymentId to the writable stream descriptor wire format, stamp it via STREAM_SERVER_DEPLOYMENT_ID_SYMBOL on writables that have it, and use it in getEncryptionKeyForRun(runId, { deploymentId }) instead of the assumed-current-deployment fallback. Legacy descriptors without a deploymentId fall back to loading the owning run via world.runs.get(runId) — that's correctness-preserving for in-flight serialized payloads.

Verified

  • After rebasing onto current main: 12 files, +278/-22 (the scary "removals" of experimentalSetAttributes from interfaces.ts and the encryption error handling in serialization.ts are stale-branch artifacts from being forked before #2134, #2157, and #2145 landed)
  • pnpm install --frozen-lockfile
  • pnpm turbo run build --filter @workflow/core
  • pnpm --filter @workflow/core test ✓ (1083/1083)
  • The two new serialization tests both pass:
    • Happy path — forwarded writable with deploymentId → uses getEncryptionKeyForRun(runId, { deploymentId }) and does NOT call runs.get
    • Legacy fallback — descriptor without deploymentId → calls runs.get(runId) first, then getEncryptionKeyForRun(run)

CI noise

14 failures on the latest run, but they're all cascading from swc-plugin-workflow WASM build failing with undefined symbol: __emit_diagnostics. That's the bug fixed by #2174 (5dabbeeca fix(swc-plugin): allow wasm host imports during link) which landed on main after this branch was forked. A rebase would resolve them all.

One thing worth thinking about — step-handler.ts uses process.env.VERCEL_DEPLOYMENT_ID

The two workflowDeploymentId sites are:

Site Source
runtime.ts (inline step execution) workflowRun.deploymentId / bgRun.deploymentId — the workflow's ACTUAL deployment ✓
step-handler.ts (background step execution) process.env.VERCEL_DEPLOYMENT_ID — the CURRENT runtime deployment

For background steps, these are only the same if the queue routes steps to the workflow's original deployment. If queues route steps to a NEWER deployment, process.env.VERCEL_DEPLOYMENT_ID is the newer one, not the workflow's.

Reading the world-vercel getEncryptionKeyForRun, the existing background-step encryption key resolution ALREADY assumes current-deployment (it calls memoizeEncryptionKey(world, workflowRunId) with no context, which then uses local HKDF when running inside Vercel). So this PR's choice in step-handler.ts is at least consistent with how step encryption already works — if the existing background-step encryption is correct (i.e., queue routes to original deployment, or some other invariant), then the new workflowDeploymentId source is also correct.

If the existing encryption has a latent bug for background steps running on newer deployments, this PR carries that forward — but doesn't make it worse, and the parent/child cross-deployment case (which is what the PR is actually trying to fix) IS handled correctly via the inline runtime.ts paths using workflowRun.deploymentId.

Worth confirming the invariant: do background step queues always route to the workflow's original deployment, or can they route to newer deployments? If the latter, there's a follow-up to also resolve the workflow's actual deployment in step-handler.ts (probably load the run first, similar to the legacy fallback in this PR's getForwardedWritableEncryptionKey).

Not a blocker — the PR's scope is the parent/child writable case, and that's handled correctly.

Bonus: the test design caught a subtle correctness point

The legacy-fallback test asserts the right behavior when descriptors are mid-flight from older SDK versions:

expect(runsGet).toHaveBeenCalledWith('wrun_parent');
expect(getEncryptionKeyForRun).toHaveBeenCalledWith(parentRun);

That confirms the fallback uses the full WorkflowRun overload (which extracts deploymentId from run.deploymentId), not the broken (runId, undefined-context) overload. Good test instinct.

PR status

Currently draft. Approving in spirit pending:

  1. Rebase onto current main (resolves the stale-branch CI cascades + the deletions-in-diff confusion)
  2. Optional: thread an explicit workflowDeploymentId through the background step path if cross-deployment step routing is actually possible

Approving.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants