Skip to content

feat(core): bounded queue-redelivery retry on decryption failure during replay - #2166

Open
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure
Open

feat(core): bounded queue-redelivery retry on decryption failure during replay#2166
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Follow-up to #2145 (the RuntimeDecryptionError attribution fix), implementing the bounded-redelivery behavior @pranaygp described in this comment.

An AES-GCM authentication failure is terminal for the bytes/key of the current attempt — we must never continue executing the workflow on top of data we couldn't decrypt. But the failure is often not terminal for the run: when the ciphertext came from a transiently truncated or corrupted read of remotely-persisted data (a partial /refs response, an edge-cache miss returning a partial 200, a proxy drop during streaming), a fresh queue delivery re-fetches the event log and ref payloads from scratch and can succeed.

Previously we committed run_failed immediately, turning a potentially recoverable read failure into a terminal workflow failure.

What changed

Mirrors the existing replay-timeout bounded-redelivery precedent (handleReplayBudgetExhausted):

  • Managed worlds (processExitTriggersQueueRedelivery === true, e.g. world-vercel): on attempts ≤ DECRYPTION_FAILURE_MAX_RETRIES (3), the run handler exits the process — which the platform turns into a queue redelivery — so replay restarts from freshly-fetched persisted data. Once the retry budget is exhausted, it commits run_failed with RUNTIME_ERROR.
  • In-process worlds (world-local, dev servers, custom in-process worlds): no queue to re-fetch from, and process.exit() would kill the user's host, so the run fails immediately with RUNTIME_ERROR.

New shouldRedriveOnDecryptionFailure() helper (runtime/decryption-failure.ts) is pure (logging only) and returns whether the caller should redrive, keeping the process.exit / run_failed decision at the single existing call site in the run handler.

Test coverage

  • runtime/decryption-failure.test.ts — unit tests for the helper across managed/in-process worlds and the retry-budget boundary.
  • runtime.test.ts — end-to-end tests driving a real key mismatch (input encrypted with key A, run-key resolves to key B → auth-tag failure during input hydration):
    • early attempt on a managed world → process.exit(1), no run_failed
    • past the retry budget on a managed world → run_failed with RUNTIME_ERROR
    • in-process world → run_failed immediately, no exit

All @workflow/core tests pass (1073), full repo typecheck passes (40/40).

Notes

The longer-term improvement @pranaygp mentioned — detecting response truncation / integrity failure at the /refs transport boundary to classify the retryable case more directly — is out of scope here and would be a separate change on the world / workflow-server side.

…ng replay
Follow-up to #2145. An AES-GCM auth failure (RuntimeDecryptionError) is
terminal for the current attempt's bytes, but often transient at the run
level: when the ciphertext came from a truncated/corrupted read of
remotely-persisted data (e.g. a partial /refs response), a fresh queue
delivery re-fetches the event log + ref payloads and can succeed.
Mirror the replay-timeout bounded-redelivery precedent: on managed worlds
(processExitTriggersQueueRedelivery), exit the process to trigger queue
redelivery for up to DECRYPTION_FAILURE_MAX_RETRIES attempts, then commit
run_failed as RUNTIME_ERROR. In-process worlds fail immediately (no queue
to re-fetch from, and exiting would kill the host).
CopilotAI review requested due to automatic review settings May 29, 2026 19:24
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 29, 2026 19:24
@changeset-bot

changeset-botBot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 558cc3f

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

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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 29, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+1.7%)1.006s (~)0.963s101.00x
💻 LocalExpress0.043s (-10.3% 🟢)1.007s (~)0.964s101.02x
🐘 PostgresNitro0.064s (-1.5%)1.011s (~)0.948s101.50x
🐘 PostgresNext.js (Turbopack)0.068s (-7.0% 🟢)1.011s (~)0.944s101.59x
🐘 PostgresExpress0.068s (+11.0% 🔺)1.012s (~)0.944s101.59x
💻 LocalNext.js (Turbopack)0.069s (+13.3% 🔺)1.006s (~)0.937s101.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.279s (-74.8% 🟢)2.020s (-43.5% 🟢)1.741s101.00x
▲ VercelNext.js (Turbopack)0.313s (-84.2% 🟢)2.337s (-43.1% 🟢)2.024s101.12x
▲ VercelNitro0.400s (-33.4% 🟢)2.213s (-16.8% 🟢)1.813s101.43x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.094s (~)2.006s (~)0.912s101.00x
💻 LocalExpress1.095s (~)2.007s (~)0.912s101.00x
🐘 PostgresExpress1.115s (~)2.010s (~)0.894s101.02x
🐘 PostgresNitro1.135s (+1.4%)2.009s (~)0.875s101.04x
🐘 PostgresNext.js (Turbopack)1.142s (-1.4%)2.011s (~)0.869s101.04x
💻 LocalNext.js (Turbopack)1.145s (~)2.007s (~)0.862s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.704s (-36.5% 🟢)3.836s (-15.2% 🟢)2.132s101.00x
▲ VercelExpress1.800s (-32.8% 🟢)3.830s (-23.4% 🟢)2.030s101.06x
▲ VercelNitro1.865s (-33.9% 🟢)3.713s (-18.3% 🟢)1.847s101.09x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.503s (~)11.022s (~)0.519s31.00x
💻 LocalExpress10.545s (~)11.023s (~)0.478s31.00x
🐘 PostgresExpress10.584s (~)11.018s (~)0.435s31.01x
🐘 PostgresNitro10.588s (~)11.015s (~)0.427s31.01x
💻 LocalNext.js (Turbopack)10.868s (+0.8%)11.023s (~)0.155s31.03x
🐘 PostgresNext.js (Turbopack)10.901s (~)11.016s (~)0.115s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)14.283s (-33.3% 🟢)16.662s (-28.4% 🟢)2.379s21.00x
▲ VercelNitro14.988s (-17.5% 🟢)17.110s (-13.9% 🟢)2.122s21.05x
▲ VercelExpress15.147s (-6.9% 🟢)17.654s (-5.2% 🟢)2.507s21.06x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.779s (~)14.029s (~)0.250s51.00x
🐘 PostgresNitro13.817s (~)14.020s (~)0.203s51.00x
💻 LocalExpress13.835s (~)14.027s (~)0.192s51.00x
🐘 PostgresExpress13.855s (~)14.020s (~)0.165s51.01x
🐘 PostgresNext.js (Turbopack)14.544s (~)15.019s (~)0.475s41.06x
💻 LocalNext.js (Turbopack)14.605s (+1.6%)15.030s (~)0.425s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.570s (-35.3% 🟢)24.870s (-32.6% 🟢)2.300s31.00x
▲ VercelNitro23.905s (-27.5% 🟢)25.993s (-25.4% 🟢)2.088s31.06x
▲ VercelNext.js (Turbopack)24.384s (-20.8% 🟢)26.434s (-19.6% 🟢)2.050s31.08x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.403s (-1.3%)13.024s (~)0.621s71.00x
💻 LocalExpress12.512s (-0.8%)13.024s (~)0.511s71.01x
🐘 PostgresNitro12.647s (~)13.023s (~)0.376s71.02x
🐘 PostgresExpress12.680s (+0.7%)13.019s (~)0.339s71.02x
🐘 PostgresNext.js (Turbopack)13.856s (-0.8%)14.020s (-1.0%)0.164s71.12x
💻 LocalNext.js (Turbopack)14.047s (+3.2%)14.744s (+5.1% 🔺)0.697s71.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express29.958s (-13.8% 🟢)32.678s (-11.9% 🟢)2.720s31.00x
▲ VercelNitro30.838s (-18.6% 🟢)33.293s (-15.7% 🟢)2.455s31.03x
▲ VercelNext.js (Turbopack)31.778s (-9.5% 🟢)33.997s (-9.4% 🟢)2.218s31.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.207s (+1.2%)2.006s (~)0.800s151.00x
💻 LocalNitro1.207s (+3.8%)2.006s (~)0.799s151.00x
🐘 PostgresNitro1.214s (~)2.008s (~)0.794s151.01x
🐘 PostgresExpress1.214s (+0.7%)2.009s (~)0.795s151.01x
🐘 PostgresNext.js (Turbopack)1.277s (-5.9% 🟢)2.007s (-3.3%)0.731s151.06x
💻 LocalNext.js (Turbopack)1.340s (+0.8%)2.007s (~)0.667s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.289s (-26.0% 🟢)3.763s (-17.9% 🟢)1.474s81.00x
▲ VercelExpress2.513s (-1.5%)4.117s (+1.1%)1.604s81.10x
▲ VercelNext.js (Turbopack)3.039s (+29.5% 🔺)4.619s (+15.0% 🔺)1.580s71.33x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.367s (+1.9%)2.393s (-4.6%)1.026s131.00x
🐘 PostgresExpress1.387s (+2.2%)2.223s (-11.3% 🟢)0.836s141.01x
🐘 PostgresNext.js (Turbopack)1.662s (+1.4%)2.295s (-8.5% 🟢)0.633s141.22x
💻 LocalNitro1.705s (+4.7%)2.006s (~)0.301s151.25x
💻 LocalExpress1.838s (-3.0%)2.150s (~)0.313s141.34x
💻 LocalNext.js (Turbopack)1.860s (+16.7% 🔺)2.075s (+3.4%)0.215s151.36x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.813s (-31.6% 🟢)4.734s (-21.8% 🟢)1.920s71.00x
▲ VercelNitro2.996s (-20.5% 🟢)4.799s (-11.1% 🟢)1.803s71.06x
▲ VercelNext.js (Turbopack)3.230s (-7.9% 🟢)5.201s (-1.0%)1.971s61.15x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.821s (+12.1% 🔺)3.887s (-6.1% 🟢)2.066s81.00x
🐘 PostgresExpress1.827s (+12.6% 🔺)4.011s (+3.2%)2.183s81.00x
🐘 PostgresNext.js (Turbopack)3.481s (-9.1% 🟢)4.299s (-3.3%)0.818s71.91x
💻 LocalNitro4.569s (+13.6% 🔺)5.015s (+9.5% 🔺)0.446s72.51x
💻 LocalExpress5.321s (+1.7%)5.846s (~)0.525s62.92x
💻 LocalNext.js (Turbopack)5.870s (+28.6% 🔺)6.348s (+22.6% 🔺)0.478s63.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.964s (-13.3% 🟢)6.243s (-4.8%)2.278s51.00x
▲ VercelNitro4.013s (-5.5% 🟢)6.018s (+5.1% 🔺)2.005s51.01x
▲ VercelNext.js (Turbopack)4.065s (+7.3% 🔺)6.166s (+14.0% 🔺)2.101s51.03x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.218s (~)2.007s (~)0.789s151.00x
🐘 PostgresExpress1.220s (+0.8%)2.008s (~)0.789s151.00x
🐘 PostgresNext.js (Turbopack)1.272s (-1.1%)2.008s (~)0.736s151.04x
💻 LocalNext.js (Turbopack)1.424s (+3.3%)2.008s (~)0.584s151.17x
💻 LocalNitro1.560s (+3.2%)2.007s (~)0.447s151.28x
💻 LocalExpress1.593s (-1.3%)2.007s (~)0.414s151.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.290s (-0.7%)3.776s (-4.3%)1.486s81.00x
▲ VercelNitro2.297s (-64.6% 🟢)3.691s (-54.0% 🟢)1.394s91.00x
▲ VercelExpress2.406s (+5.8% 🔺)3.962s (+2.8%)1.556s81.05x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.357s (~)2.315s (-10.7% 🟢)0.958s131.00x
🐘 PostgresExpress1.406s (+0.8%)2.592s (+4.9%)1.186s121.04x
🐘 PostgresNext.js (Turbopack)1.583s (+3.0%)2.152s (-3.3%)0.569s141.17x
💻 LocalNext.js (Turbopack)2.095s (+8.5% 🔺)2.918s (+31.3% 🔺)0.823s111.54x
💻 LocalNitro2.187s (+22.2% 🔺)2.676s (+29.1% 🔺)0.489s121.61x
💻 LocalExpress2.349s (-8.8% 🟢)2.827s (-9.0% 🟢)0.479s111.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.794s (-25.4% 🟢)4.495s (-17.7% 🟢)1.700s71.00x
▲ VercelNext.js (Turbopack)3.325s (-17.1% 🟢)5.058s (-12.0% 🟢)1.733s61.19x
▲ VercelExpress3.330s (~)5.327s (~)1.997s61.19x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.572s (-9.0% 🟢)4.011s (+3.2%)2.439s81.00x
🐘 PostgresExpress1.646s (-6.1% 🟢)4.014s (-2.9%)2.368s81.05x
🐘 PostgresNext.js (Turbopack)2.971s (-22.6% 🟢)3.764s (-15.3% 🟢)0.793s81.89x
💻 LocalNitro5.218s (+9.5% 🔺)5.684s (+6.3% 🔺)0.467s63.32x
💻 LocalExpress6.467s (+2.0%)7.216s (+2.8%)0.749s54.11x
💻 LocalNext.js (Turbopack)6.475s (+22.3% 🔺)7.018s (+16.7% 🔺)0.543s54.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.355s (-33.9% 🟢)6.132s (-26.5% 🟢)1.777s61.00x
▲ VercelNext.js (Turbopack)4.600s (-6.6% 🟢)6.524s (~)1.924s51.06x
▲ VercelExpress4.640s (-11.4% 🟢)6.693s (-3.9%)2.053s51.07x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.593s (-8.0% 🟢)1.023s (-1.8%)0.430s591.00x
💻 LocalNitro0.596s (+2.2%)1.005s (~)0.409s601.00x
🐘 PostgresExpress0.609s (+2.8%)1.023s (+1.8%)0.415s591.03x
💻 LocalExpress0.625s (-5.3% 🟢)1.005s (-3.3%)0.380s601.05x
🐘 PostgresNext.js (Turbopack)0.819s (-3.7%)1.006s (-1.7%)0.187s601.38x
💻 LocalNext.js (Turbopack)0.942s (+6.4% 🔺)1.096s (+3.7%)0.154s551.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.725s (-13.0% 🟢)7.514s (-12.9% 🟢)1.790s91.00x
▲ VercelNext.js (Turbopack)5.798s (-21.9% 🟢)7.584s (-18.8% 🟢)1.786s81.01x
▲ VercelNitro5.831s (-31.1% 🟢)7.298s (-27.8% 🟢)1.467s91.02x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.384s (-3.6%)2.007s (-1.1%)0.623s451.00x
🐘 PostgresNitro1.385s (-4.2%)2.008s (~)0.623s451.00x
💻 LocalNitro1.502s (+1.7%)2.006s (~)0.504s451.09x
💻 LocalExpress1.575s (-0.6%)2.007s (~)0.432s451.14x
🐘 PostgresNext.js (Turbopack)1.935s (-5.0% 🟢)2.150s (-16.1% 🟢)0.216s421.40x
💻 LocalNext.js (Turbopack)2.277s (+7.0% 🔺)3.009s (~)0.732s301.65x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.711s (-31.9% 🟢)16.057s (-26.5% 🟢)2.346s61.00x
▲ VercelNitro14.119s (-24.8% 🟢)16.038s (-22.7% 🟢)1.919s61.03x
▲ VercelNext.js (Turbopack)14.780s (-25.8% 🟢)16.884s (-24.6% 🟢)2.104s61.08x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.788s (-1.2%)3.166s (+0.9%)0.379s381.00x
🐘 PostgresExpress2.860s (+2.9%)3.252s (+3.7%)0.392s371.03x
💻 LocalNitro3.279s (+3.5%)4.009s (+2.5%)0.731s301.18x
💻 LocalExpress3.391s (+1.4%)4.010s (~)0.619s301.22x
🐘 PostgresNext.js (Turbopack)3.810s (-5.3% 🟢)4.043s (-8.2% 🟢)0.233s301.37x
💻 LocalNext.js (Turbopack)4.748s (+4.6%)5.054s (~)0.306s241.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.736s (-18.0% 🟢)32.583s (-14.7% 🟢)2.847s41.00x
▲ VercelExpress29.831s (-22.9% 🟢)32.492s (-19.6% 🟢)2.660s41.00x
▲ VercelNext.js (Turbopack)32.381s (-27.8% 🟢)35.293s (-24.4% 🟢)2.912s41.09x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.253s (-3.1%)1.006s (~)0.753s601.00x
🐘 PostgresExpress0.264s (+1.1%)1.023s (+1.7%)0.760s591.04x
🐘 PostgresNext.js (Turbopack)0.301s (-2.4%)1.006s (~)0.705s601.19x
💻 LocalExpress0.409s (-4.4%)1.005s (~)0.595s601.62x
💻 LocalNitro0.425s (-1.5%)1.005s (~)0.579s601.68x
💻 LocalNext.js (Turbopack)0.594s (-8.2% 🟢)1.006s (-4.9%)0.413s602.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.844s (-36.1% 🟢)3.799s (-24.5% 🟢)1.954s161.00x
▲ VercelExpress1.910s (-46.5% 🟢)3.900s (-30.2% 🟢)1.990s161.04x
▲ VercelNext.js (Turbopack)1.931s (-37.1% 🟢)3.841s (-23.4% 🟢)1.910s161.05x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.410s (-10.2% 🟢)1.029s (-4.6%)0.619s881.00x
🐘 PostgresExpress0.418s (+1.8%)1.078s (+7.1% 🔺)0.659s841.02x
🐘 PostgresNext.js (Turbopack)0.575s (-15.2% 🟢)1.118s (-12.4% 🟢)0.543s811.40x
💻 LocalNitro2.162s (+7.5% 🔺)2.687s (+10.1% 🔺)0.524s345.27x
💻 LocalExpress2.221s (+1.4%)2.769s (-2.9%)0.548s335.41x
💻 LocalNext.js (Turbopack)2.825s (+10.4% 🔺)3.512s (+7.7% 🔺)0.687s266.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.226s (-37.4% 🟢)5.211s (-27.8% 🟢)1.986s181.00x
▲ VercelNitro3.370s (-3.3%)5.242s (+1.4%)1.872s181.04x
▲ VercelNext.js (Turbopack)3.799s (~)5.760s (~)1.961s161.18x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.804s (-4.7%)1.527s (+3.8%)0.724s791.00x
🐘 PostgresExpress0.831s (-2.0%)1.471s (-2.4%)0.640s821.03x
🐘 PostgresNext.js (Turbopack)2.802s (-9.3% 🟢)3.651s (-4.6%)0.849s333.49x
💻 LocalNitro9.753s (+11.8% 🔺)10.447s (+12.9% 🔺)0.694s1212.14x
💻 LocalExpress9.767s (-1.0%)10.280s (~)0.514s1212.15x
💻 LocalNext.js (Turbopack)12.460s (+18.8% 🔺)13.337s (+17.1% 🔺)0.877s1015.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.603s (-17.0% 🟢)7.854s (-9.0% 🟢)2.251s161.00x
▲ VercelExpress5.898s (-13.7% 🟢)8.080s (-6.3% 🟢)2.182s151.05x
▲ VercelNext.js (Turbopack)7.576s (+5.3% 🔺)9.754s (+8.8% 🔺)2.178s131.35x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.163s (~)2.005s (~)0.010s (+1.0%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (-2.4%)1.999s (~)0.002s (+33.3% 🔺)2.011s (~)0.842s101.01x
💻 LocalExpress1.172s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.847s101.01x
🐘 PostgresExpress1.185s (+0.9%)2.000s (~)0.002s (+54.5% 🔺)2.012s (~)0.827s101.02x
🐘 PostgresNext.js (Turbopack)1.228s (-1.3%)2.001s (~)0.001s (-25.0% 🟢)2.011s (~)0.783s101.06x
💻 LocalNext.js (Turbopack)1.228s (-1.7%)2.004s (~)0.012s (+13.1% 🔺)2.021s (~)0.793s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.289s (-3.1%)3.325s (-5.8% 🟢)0.785s (+11.6% 🔺)4.593s (-4.1%)2.304s101.00x
▲ VercelNext.js (Turbopack)2.336s (-12.9% 🟢)3.384s (-9.7% 🟢)0.770s (-8.0% 🟢)4.613s (-9.3% 🟢)2.276s101.02x
▲ VercelNitro2.345s (-8.0% 🟢)3.480s (-6.3% 🟢)0.621s (-16.1% 🟢)4.531s (-8.7% 🟢)2.187s101.02x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.586s (+1.3%)2.009s (~)0.013s (+18.1% 🔺)2.025s (~)0.439s301.00x
💻 LocalExpress1.593s (-1.1%)2.011s (~)0.013s (+2.9%)2.026s (~)0.433s301.00x
🐘 PostgresExpress1.602s (~)2.006s (~)0.005s (+1.4%)2.027s (~)0.425s301.01x
🐘 PostgresNitro1.603s (-2.2%)2.004s (~)0.005s (-0.6%)2.026s (~)0.423s301.01x
🐘 PostgresNext.js (Turbopack)1.753s (-2.2%)2.010s (~)0.005s (-2.4%)2.026s (~)0.273s301.11x
💻 LocalNext.js (Turbopack)1.889s (+8.2% 🔺)2.080s (+3.7%)0.014s (+7.7% 🔺)2.098s (+3.6%)0.209s291.19x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.212s (-9.9% 🟢)7.300s (-12.6% 🟢)0.270s (-21.4% 🟢)8.082s (-12.4% 🟢)1.869s81.00x
▲ VercelNext.js (Turbopack)6.304s (-8.2% 🟢)7.685s (-10.4% 🟢)0.289s (-7.7% 🟢)8.468s (-9.7% 🟢)2.164s81.01x
▲ VercelNitro6.373s (-5.4% 🟢)7.745s (-3.7%)0.208s (-5.5% 🟢)8.596s (-2.4%)2.223s71.03x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.797s (~)1.063s (+1.4%)0.000s (+Infinity% 🔺)1.079s (+1.8%)0.282s561.00x
🐘 PostgresNitro0.798s (-1.8%)1.067s (~)0.000s (+1.8%)1.099s (+1.8%)0.301s551.00x
🐘 PostgresNext.js (Turbopack)1.016s (-1.1%)1.500s (-7.5% 🟢)0.000s (-7.5% 🟢)1.507s (-7.6% 🟢)0.491s401.28x
💻 LocalExpress1.413s (~)2.012s (~)0.000s (~)2.015s (~)0.602s301.77x
💻 LocalNitro1.468s (+8.6% 🔺)2.014s (~)0.000s (-42.9% 🟢)2.017s (~)0.549s301.84x
💻 LocalNext.js (Turbopack)1.592s (+5.3% 🔺)2.015s (~)0.000s (-18.2% 🟢)2.019s (~)0.426s302.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.933s (-28.7% 🟢)4.414s (-23.2% 🟢)0.000s (NaN%)4.919s (-21.2% 🟢)1.986s131.00x
▲ VercelExpress2.951s (-20.1% 🟢)4.593s (-6.3% 🟢)0.000s (+Infinity% 🔺)5.072s (-8.7% 🟢)2.121s121.01x
▲ VercelNext.js (Turbopack)3.232s (-14.2% 🟢)4.626s (-13.3% 🟢)0.000s (-100.0% 🟢)5.093s (-13.4% 🟢)1.860s121.10x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.560s (-7.9% 🟢)2.066s (-5.0% 🟢)0.000s (-3.4%)2.136s (-2.7%)0.576s291.00x
🐘 PostgresExpress1.628s (-1.5%)2.137s (-1.8%)0.000s (+Infinity% 🔺)2.156s (-1.4%)0.528s281.04x
🐘 PostgresNext.js (Turbopack)2.214s (-1.8%)2.776s (+1.7%)0.000s (NaN%)2.782s (+1.6%)0.568s221.42x
💻 LocalExpress3.162s (-4.7%)3.965s (-1.5%)0.000s (-62.5% 🟢)3.968s (-1.6%)0.806s162.03x
💻 LocalNitro3.179s (+3.2%)3.734s (-4.2%)0.001s (+61.3% 🔺)3.737s (-4.2%)0.558s172.04x
💻 LocalNext.js (Turbopack)3.265s (+15.1% 🔺)3.734s (+9.3% 🔺)0.000s (-41.2% 🟢)3.738s (+9.3% 🔺)0.473s172.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.284s (-29.4% 🟢)6.003s (-21.0% 🟢)0.000s (-100.0% 🟢)6.482s (-20.8% 🟢)2.198s101.00x
▲ VercelNitro4.573s (-7.1% 🟢)6.058s (-1.7%)0.000s (NaN%)6.512s (-4.2%)1.939s101.07x
▲ VercelNext.js (Turbopack)5.009s (-29.1% 🟢)6.602s (-19.8% 🟢)0.000s (-100.0% 🟢)7.080s (-19.2% 🟢)2.071s91.17x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro15/21
🐘 PostgresNitro16/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202191661
✅ 💻 Local Development189502192114
✅ 📦 Local Production189502192114
✅ 🐘 Local Postgres188102332114
✅ 🪟 Windows15100151
❌ 📋 Other87811781057
Total8142110689211

❌ Failed Tests

📋 Other (1 failed)

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

  • webhookWorkflow | wrun_01KV7X2YYXA571EE20AYFJ50VB

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125026
✅ example125026
✅ express125026
✅ fastify125026
✅ hono125026
✅ nextjs-turbopack14902
✅ nextjs-webpack14902
✅ nitro125026
✅ nuxt125026
✅ sveltekit14407
✅ vite125026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable125026
✅ express-stable125026
✅ fastify-stable125026
✅ hono-stable125026
✅ nextjs-turbopack-canary131020
✅ nextjs-turbopack-stable-lazy-discovery-disabled15001
✅ nextjs-turbopack-stable-lazy-discovery-enabled15001
✅ nextjs-webpack-canary131020
✅ nextjs-webpack-stable-lazy-discovery-disabled15001
✅ nextjs-webpack-stable-lazy-discovery-enabled15001
✅ nitro-stable125026
✅ nuxt-stable125026
✅ sveltekit-stable14407
✅ vite-stable125026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15100
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable126025
✅ e2e-local-dev-tanstack-start-126025
✅ e2e-local-postgres-nest-stable125026
✅ e2e-local-postgres-tanstack-start-125026
✅ e2e-local-prod-nest-stable126025
✅ e2e-local-prod-tanstack-start-126025
❌ e2e-vercel-prod-tanstack-start124126

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

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

This PR adds bounded queue-redelivery behavior for replay-time RuntimeDecryptionErrors in @workflow/core, allowing managed worlds to retry transient persisted-data read corruption before marking the run failed.

Changes:

  • Adds a decryption-failure retry decision helper and retry budget constant.
  • Integrates the helper into the workflow runtime error path before writing run_failed.
  • Adds unit and runtime coverage plus a patch changeset.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/decryption-failure.tsAdds redelivery decision logic and logging for replay decryption failures.
packages/core/src/runtime/decryption-failure.test.tsCovers managed/in-process world behavior and retry-budget boundaries.
packages/core/src/runtime/constants.tsDefines the decryption-failure retry budget.
packages/core/src/runtime.tsRedrives managed-world runs on eligible RuntimeDecryptionErrors before failing the run.
packages/core/src/runtime.test.tsAdds end-to-end key-mismatch tests for redelivery and failure behavior.
.changeset/requeue-on-decryption-failure.mdAdds a patch changeset for @workflow/core.

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

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: blocking issues found

return false;
}

if (attempt <= DECRYPTION_FAILURE_MAX_RETRIES) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Blocking

RuntimeDecryptionError also covers encrypt-side AES-GCM failures (context.operation === "encrypt"), but this helper redrives every RuntimeDecryptionError on managed worlds. A fresh queue delivery can only help when decrypting persisted bytes, not when serializing/encrypting a new workflow payload. This can re-run workflow code up to 3 times before failing. Please gate redelivery on error.context?.operation === "decrypt".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — shouldRedriveOnDecryptionFailure now returns false immediately when error.context?.operation !== "decrypt", so encrypt-side AES-GCM failures fail the run without redelivery (re-fetching cannot change a payload being serialized fresh). Added a unit test covering the encrypt case on a managed world. Commit 558cc3f.

// queue delivery re-fetches the event log + ref
// payloads and can succeed. On managed Worlds, exit the
// process (which the platform turns into a redelivery)
// for a bounded number of attempts before failing the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

This changes the user-facing behavior for runtime decryption failures on managed worlds: they now redeliver before the terminal run_failed. The existing runtime-decryption-failed docs still say the run fails immediately, so those docs should be updated as part of this behavior change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — updated errors/runtime-decryption-failed.mdx (both v4 and v5) to document the bounded queue-redelivery behavior on managed worlds: decrypt failures are now retried before the terminal run_failed, while encrypt-side failures and in-process worlds still fail immediately. Commit 558cc3f.

Comment threadpackages/core/src/runtime.test.ts Outdated
// Past DECRYPTION_FAILURE_MAX_RETRIES (3) but under MAX_QUEUE_DELIVERIES
// (48), so we exercise the decryption-failure budget, not the queue
// max-deliveries guard.
attempt: 10,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This test duplicates the retry/delivery constants in comments and uses attempt: 10 instead of deriving from the constant it is testing. Please import the constants and use DECRYPTION_FAILURE_MAX_RETRIES + 1, with an assertion that it remains below MAX_QUEUE_DELIVERIES, so future constant changes don't silently stale the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — the past-budget test now derives attempt = DECRYPTION_FAILURE_MAX_RETRIES + 1 from the imported constant (no duplicated magic numbers) and asserts attempt < MAX_QUEUE_DELIVERIES, so a future constant change cannot silently stale the test. Commit 558cc3f.

Resolve conflicts in runtime.ts, runtime.test.ts, and constants.ts
(decryption-failure redelivery + replay-divergence recovery coexist).
Address review feedback:
- Gate decryption-failure redelivery on operation === 'decrypt' so
encrypt-side AES-GCM failures fail immediately (re-fetching cannot help).
- Derive the past-budget test attempt from DECRYPTION_FAILURE_MAX_RETRIES + 1
and assert it stays below MAX_QUEUE_DELIVERIES.
- Document the bounded queue-redelivery behavior in runtime-decryption-failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

feat(core): bounded queue-redelivery retry on decryption failure during replay - #2166

Open
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure
Open

feat(core): bounded queue-redelivery retry on decryption failure during replay#2166
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Follow-up to #2145 (the RuntimeDecryptionError attribution fix), implementing the bounded-redelivery behavior @pranaygp described in this comment.

An AES-GCM authentication failure is terminal for the bytes/key of the current attempt — we must never continue executing the workflow on top of data we couldn't decrypt. But the failure is often not terminal for the run: when the ciphertext came from a transiently truncated or corrupted read of remotely-persisted data (a partial /refs response, an edge-cache miss returning a partial 200, a proxy drop during streaming), a fresh queue delivery re-fetches the event log and ref payloads from scratch and can succeed.

Previously we committed run_failed immediately, turning a potentially recoverable read failure into a terminal workflow failure.

What changed

Mirrors the existing replay-timeout bounded-redelivery precedent (handleReplayBudgetExhausted):

  • Managed worlds (processExitTriggersQueueRedelivery === true, e.g. world-vercel): on attempts ≤ DECRYPTION_FAILURE_MAX_RETRIES (3), the run handler exits the process — which the platform turns into a queue redelivery — so replay restarts from freshly-fetched persisted data. Once the retry budget is exhausted, it commits run_failed with RUNTIME_ERROR.
  • In-process worlds (world-local, dev servers, custom in-process worlds): no queue to re-fetch from, and process.exit() would kill the user's host, so the run fails immediately with RUNTIME_ERROR.

New shouldRedriveOnDecryptionFailure() helper (runtime/decryption-failure.ts) is pure (logging only) and returns whether the caller should redrive, keeping the process.exit / run_failed decision at the single existing call site in the run handler.

Test coverage

  • runtime/decryption-failure.test.ts — unit tests for the helper across managed/in-process worlds and the retry-budget boundary.
  • runtime.test.ts — end-to-end tests driving a real key mismatch (input encrypted with key A, run-key resolves to key B → auth-tag failure during input hydration):
    • early attempt on a managed world → process.exit(1), no run_failed
    • past the retry budget on a managed world → run_failed with RUNTIME_ERROR
    • in-process world → run_failed immediately, no exit

All @workflow/core tests pass (1073), full repo typecheck passes (40/40).

Notes

The longer-term improvement @pranaygp mentioned — detecting response truncation / integrity failure at the /refs transport boundary to classify the retryable case more directly — is out of scope here and would be a separate change on the world / workflow-server side.

…ng replay
Follow-up to #2145. An AES-GCM auth failure (RuntimeDecryptionError) is
terminal for the current attempt's bytes, but often transient at the run
level: when the ciphertext came from a truncated/corrupted read of
remotely-persisted data (e.g. a partial /refs response), a fresh queue
delivery re-fetches the event log + ref payloads and can succeed.
Mirror the replay-timeout bounded-redelivery precedent: on managed worlds
(processExitTriggersQueueRedelivery), exit the process to trigger queue
redelivery for up to DECRYPTION_FAILURE_MAX_RETRIES attempts, then commit
run_failed as RUNTIME_ERROR. In-process worlds fail immediately (no queue
to re-fetch from, and exiting would kill the host).
CopilotAI review requested due to automatic review settings May 29, 2026 19:24
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 29, 2026 19:24
@changeset-bot

changeset-botBot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 558cc3f

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

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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 29, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+1.7%)1.006s (~)0.963s101.00x
💻 LocalExpress0.043s (-10.3% 🟢)1.007s (~)0.964s101.02x
🐘 PostgresNitro0.064s (-1.5%)1.011s (~)0.948s101.50x
🐘 PostgresNext.js (Turbopack)0.068s (-7.0% 🟢)1.011s (~)0.944s101.59x
🐘 PostgresExpress0.068s (+11.0% 🔺)1.012s (~)0.944s101.59x
💻 LocalNext.js (Turbopack)0.069s (+13.3% 🔺)1.006s (~)0.937s101.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.279s (-74.8% 🟢)2.020s (-43.5% 🟢)1.741s101.00x
▲ VercelNext.js (Turbopack)0.313s (-84.2% 🟢)2.337s (-43.1% 🟢)2.024s101.12x
▲ VercelNitro0.400s (-33.4% 🟢)2.213s (-16.8% 🟢)1.813s101.43x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.094s (~)2.006s (~)0.912s101.00x
💻 LocalExpress1.095s (~)2.007s (~)0.912s101.00x
🐘 PostgresExpress1.115s (~)2.010s (~)0.894s101.02x
🐘 PostgresNitro1.135s (+1.4%)2.009s (~)0.875s101.04x
🐘 PostgresNext.js (Turbopack)1.142s (-1.4%)2.011s (~)0.869s101.04x
💻 LocalNext.js (Turbopack)1.145s (~)2.007s (~)0.862s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.704s (-36.5% 🟢)3.836s (-15.2% 🟢)2.132s101.00x
▲ VercelExpress1.800s (-32.8% 🟢)3.830s (-23.4% 🟢)2.030s101.06x
▲ VercelNitro1.865s (-33.9% 🟢)3.713s (-18.3% 🟢)1.847s101.09x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.503s (~)11.022s (~)0.519s31.00x
💻 LocalExpress10.545s (~)11.023s (~)0.478s31.00x
🐘 PostgresExpress10.584s (~)11.018s (~)0.435s31.01x
🐘 PostgresNitro10.588s (~)11.015s (~)0.427s31.01x
💻 LocalNext.js (Turbopack)10.868s (+0.8%)11.023s (~)0.155s31.03x
🐘 PostgresNext.js (Turbopack)10.901s (~)11.016s (~)0.115s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)14.283s (-33.3% 🟢)16.662s (-28.4% 🟢)2.379s21.00x
▲ VercelNitro14.988s (-17.5% 🟢)17.110s (-13.9% 🟢)2.122s21.05x
▲ VercelExpress15.147s (-6.9% 🟢)17.654s (-5.2% 🟢)2.507s21.06x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.779s (~)14.029s (~)0.250s51.00x
🐘 PostgresNitro13.817s (~)14.020s (~)0.203s51.00x
💻 LocalExpress13.835s (~)14.027s (~)0.192s51.00x
🐘 PostgresExpress13.855s (~)14.020s (~)0.165s51.01x
🐘 PostgresNext.js (Turbopack)14.544s (~)15.019s (~)0.475s41.06x
💻 LocalNext.js (Turbopack)14.605s (+1.6%)15.030s (~)0.425s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.570s (-35.3% 🟢)24.870s (-32.6% 🟢)2.300s31.00x
▲ VercelNitro23.905s (-27.5% 🟢)25.993s (-25.4% 🟢)2.088s31.06x
▲ VercelNext.js (Turbopack)24.384s (-20.8% 🟢)26.434s (-19.6% 🟢)2.050s31.08x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.403s (-1.3%)13.024s (~)0.621s71.00x
💻 LocalExpress12.512s (-0.8%)13.024s (~)0.511s71.01x
🐘 PostgresNitro12.647s (~)13.023s (~)0.376s71.02x
🐘 PostgresExpress12.680s (+0.7%)13.019s (~)0.339s71.02x
🐘 PostgresNext.js (Turbopack)13.856s (-0.8%)14.020s (-1.0%)0.164s71.12x
💻 LocalNext.js (Turbopack)14.047s (+3.2%)14.744s (+5.1% 🔺)0.697s71.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express29.958s (-13.8% 🟢)32.678s (-11.9% 🟢)2.720s31.00x
▲ VercelNitro30.838s (-18.6% 🟢)33.293s (-15.7% 🟢)2.455s31.03x
▲ VercelNext.js (Turbopack)31.778s (-9.5% 🟢)33.997s (-9.4% 🟢)2.218s31.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.207s (+1.2%)2.006s (~)0.800s151.00x
💻 LocalNitro1.207s (+3.8%)2.006s (~)0.799s151.00x
🐘 PostgresNitro1.214s (~)2.008s (~)0.794s151.01x
🐘 PostgresExpress1.214s (+0.7%)2.009s (~)0.795s151.01x
🐘 PostgresNext.js (Turbopack)1.277s (-5.9% 🟢)2.007s (-3.3%)0.731s151.06x
💻 LocalNext.js (Turbopack)1.340s (+0.8%)2.007s (~)0.667s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.289s (-26.0% 🟢)3.763s (-17.9% 🟢)1.474s81.00x
▲ VercelExpress2.513s (-1.5%)4.117s (+1.1%)1.604s81.10x
▲ VercelNext.js (Turbopack)3.039s (+29.5% 🔺)4.619s (+15.0% 🔺)1.580s71.33x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.367s (+1.9%)2.393s (-4.6%)1.026s131.00x
🐘 PostgresExpress1.387s (+2.2%)2.223s (-11.3% 🟢)0.836s141.01x
🐘 PostgresNext.js (Turbopack)1.662s (+1.4%)2.295s (-8.5% 🟢)0.633s141.22x
💻 LocalNitro1.705s (+4.7%)2.006s (~)0.301s151.25x
💻 LocalExpress1.838s (-3.0%)2.150s (~)0.313s141.34x
💻 LocalNext.js (Turbopack)1.860s (+16.7% 🔺)2.075s (+3.4%)0.215s151.36x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.813s (-31.6% 🟢)4.734s (-21.8% 🟢)1.920s71.00x
▲ VercelNitro2.996s (-20.5% 🟢)4.799s (-11.1% 🟢)1.803s71.06x
▲ VercelNext.js (Turbopack)3.230s (-7.9% 🟢)5.201s (-1.0%)1.971s61.15x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.821s (+12.1% 🔺)3.887s (-6.1% 🟢)2.066s81.00x
🐘 PostgresExpress1.827s (+12.6% 🔺)4.011s (+3.2%)2.183s81.00x
🐘 PostgresNext.js (Turbopack)3.481s (-9.1% 🟢)4.299s (-3.3%)0.818s71.91x
💻 LocalNitro4.569s (+13.6% 🔺)5.015s (+9.5% 🔺)0.446s72.51x
💻 LocalExpress5.321s (+1.7%)5.846s (~)0.525s62.92x
💻 LocalNext.js (Turbopack)5.870s (+28.6% 🔺)6.348s (+22.6% 🔺)0.478s63.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.964s (-13.3% 🟢)6.243s (-4.8%)2.278s51.00x
▲ VercelNitro4.013s (-5.5% 🟢)6.018s (+5.1% 🔺)2.005s51.01x
▲ VercelNext.js (Turbopack)4.065s (+7.3% 🔺)6.166s (+14.0% 🔺)2.101s51.03x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.218s (~)2.007s (~)0.789s151.00x
🐘 PostgresExpress1.220s (+0.8%)2.008s (~)0.789s151.00x
🐘 PostgresNext.js (Turbopack)1.272s (-1.1%)2.008s (~)0.736s151.04x
💻 LocalNext.js (Turbopack)1.424s (+3.3%)2.008s (~)0.584s151.17x
💻 LocalNitro1.560s (+3.2%)2.007s (~)0.447s151.28x
💻 LocalExpress1.593s (-1.3%)2.007s (~)0.414s151.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.290s (-0.7%)3.776s (-4.3%)1.486s81.00x
▲ VercelNitro2.297s (-64.6% 🟢)3.691s (-54.0% 🟢)1.394s91.00x
▲ VercelExpress2.406s (+5.8% 🔺)3.962s (+2.8%)1.556s81.05x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.357s (~)2.315s (-10.7% 🟢)0.958s131.00x
🐘 PostgresExpress1.406s (+0.8%)2.592s (+4.9%)1.186s121.04x
🐘 PostgresNext.js (Turbopack)1.583s (+3.0%)2.152s (-3.3%)0.569s141.17x
💻 LocalNext.js (Turbopack)2.095s (+8.5% 🔺)2.918s (+31.3% 🔺)0.823s111.54x
💻 LocalNitro2.187s (+22.2% 🔺)2.676s (+29.1% 🔺)0.489s121.61x
💻 LocalExpress2.349s (-8.8% 🟢)2.827s (-9.0% 🟢)0.479s111.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.794s (-25.4% 🟢)4.495s (-17.7% 🟢)1.700s71.00x
▲ VercelNext.js (Turbopack)3.325s (-17.1% 🟢)5.058s (-12.0% 🟢)1.733s61.19x
▲ VercelExpress3.330s (~)5.327s (~)1.997s61.19x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.572s (-9.0% 🟢)4.011s (+3.2%)2.439s81.00x
🐘 PostgresExpress1.646s (-6.1% 🟢)4.014s (-2.9%)2.368s81.05x
🐘 PostgresNext.js (Turbopack)2.971s (-22.6% 🟢)3.764s (-15.3% 🟢)0.793s81.89x
💻 LocalNitro5.218s (+9.5% 🔺)5.684s (+6.3% 🔺)0.467s63.32x
💻 LocalExpress6.467s (+2.0%)7.216s (+2.8%)0.749s54.11x
💻 LocalNext.js (Turbopack)6.475s (+22.3% 🔺)7.018s (+16.7% 🔺)0.543s54.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.355s (-33.9% 🟢)6.132s (-26.5% 🟢)1.777s61.00x
▲ VercelNext.js (Turbopack)4.600s (-6.6% 🟢)6.524s (~)1.924s51.06x
▲ VercelExpress4.640s (-11.4% 🟢)6.693s (-3.9%)2.053s51.07x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.593s (-8.0% 🟢)1.023s (-1.8%)0.430s591.00x
💻 LocalNitro0.596s (+2.2%)1.005s (~)0.409s601.00x
🐘 PostgresExpress0.609s (+2.8%)1.023s (+1.8%)0.415s591.03x
💻 LocalExpress0.625s (-5.3% 🟢)1.005s (-3.3%)0.380s601.05x
🐘 PostgresNext.js (Turbopack)0.819s (-3.7%)1.006s (-1.7%)0.187s601.38x
💻 LocalNext.js (Turbopack)0.942s (+6.4% 🔺)1.096s (+3.7%)0.154s551.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.725s (-13.0% 🟢)7.514s (-12.9% 🟢)1.790s91.00x
▲ VercelNext.js (Turbopack)5.798s (-21.9% 🟢)7.584s (-18.8% 🟢)1.786s81.01x
▲ VercelNitro5.831s (-31.1% 🟢)7.298s (-27.8% 🟢)1.467s91.02x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.384s (-3.6%)2.007s (-1.1%)0.623s451.00x
🐘 PostgresNitro1.385s (-4.2%)2.008s (~)0.623s451.00x
💻 LocalNitro1.502s (+1.7%)2.006s (~)0.504s451.09x
💻 LocalExpress1.575s (-0.6%)2.007s (~)0.432s451.14x
🐘 PostgresNext.js (Turbopack)1.935s (-5.0% 🟢)2.150s (-16.1% 🟢)0.216s421.40x
💻 LocalNext.js (Turbopack)2.277s (+7.0% 🔺)3.009s (~)0.732s301.65x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.711s (-31.9% 🟢)16.057s (-26.5% 🟢)2.346s61.00x
▲ VercelNitro14.119s (-24.8% 🟢)16.038s (-22.7% 🟢)1.919s61.03x
▲ VercelNext.js (Turbopack)14.780s (-25.8% 🟢)16.884s (-24.6% 🟢)2.104s61.08x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.788s (-1.2%)3.166s (+0.9%)0.379s381.00x
🐘 PostgresExpress2.860s (+2.9%)3.252s (+3.7%)0.392s371.03x
💻 LocalNitro3.279s (+3.5%)4.009s (+2.5%)0.731s301.18x
💻 LocalExpress3.391s (+1.4%)4.010s (~)0.619s301.22x
🐘 PostgresNext.js (Turbopack)3.810s (-5.3% 🟢)4.043s (-8.2% 🟢)0.233s301.37x
💻 LocalNext.js (Turbopack)4.748s (+4.6%)5.054s (~)0.306s241.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.736s (-18.0% 🟢)32.583s (-14.7% 🟢)2.847s41.00x
▲ VercelExpress29.831s (-22.9% 🟢)32.492s (-19.6% 🟢)2.660s41.00x
▲ VercelNext.js (Turbopack)32.381s (-27.8% 🟢)35.293s (-24.4% 🟢)2.912s41.09x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.253s (-3.1%)1.006s (~)0.753s601.00x
🐘 PostgresExpress0.264s (+1.1%)1.023s (+1.7%)0.760s591.04x
🐘 PostgresNext.js (Turbopack)0.301s (-2.4%)1.006s (~)0.705s601.19x
💻 LocalExpress0.409s (-4.4%)1.005s (~)0.595s601.62x
💻 LocalNitro0.425s (-1.5%)1.005s (~)0.579s601.68x
💻 LocalNext.js (Turbopack)0.594s (-8.2% 🟢)1.006s (-4.9%)0.413s602.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.844s (-36.1% 🟢)3.799s (-24.5% 🟢)1.954s161.00x
▲ VercelExpress1.910s (-46.5% 🟢)3.900s (-30.2% 🟢)1.990s161.04x
▲ VercelNext.js (Turbopack)1.931s (-37.1% 🟢)3.841s (-23.4% 🟢)1.910s161.05x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.410s (-10.2% 🟢)1.029s (-4.6%)0.619s881.00x
🐘 PostgresExpress0.418s (+1.8%)1.078s (+7.1% 🔺)0.659s841.02x
🐘 PostgresNext.js (Turbopack)0.575s (-15.2% 🟢)1.118s (-12.4% 🟢)0.543s811.40x
💻 LocalNitro2.162s (+7.5% 🔺)2.687s (+10.1% 🔺)0.524s345.27x
💻 LocalExpress2.221s (+1.4%)2.769s (-2.9%)0.548s335.41x
💻 LocalNext.js (Turbopack)2.825s (+10.4% 🔺)3.512s (+7.7% 🔺)0.687s266.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.226s (-37.4% 🟢)5.211s (-27.8% 🟢)1.986s181.00x
▲ VercelNitro3.370s (-3.3%)5.242s (+1.4%)1.872s181.04x
▲ VercelNext.js (Turbopack)3.799s (~)5.760s (~)1.961s161.18x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.804s (-4.7%)1.527s (+3.8%)0.724s791.00x
🐘 PostgresExpress0.831s (-2.0%)1.471s (-2.4%)0.640s821.03x
🐘 PostgresNext.js (Turbopack)2.802s (-9.3% 🟢)3.651s (-4.6%)0.849s333.49x
💻 LocalNitro9.753s (+11.8% 🔺)10.447s (+12.9% 🔺)0.694s1212.14x
💻 LocalExpress9.767s (-1.0%)10.280s (~)0.514s1212.15x
💻 LocalNext.js (Turbopack)12.460s (+18.8% 🔺)13.337s (+17.1% 🔺)0.877s1015.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.603s (-17.0% 🟢)7.854s (-9.0% 🟢)2.251s161.00x
▲ VercelExpress5.898s (-13.7% 🟢)8.080s (-6.3% 🟢)2.182s151.05x
▲ VercelNext.js (Turbopack)7.576s (+5.3% 🔺)9.754s (+8.8% 🔺)2.178s131.35x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.163s (~)2.005s (~)0.010s (+1.0%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (-2.4%)1.999s (~)0.002s (+33.3% 🔺)2.011s (~)0.842s101.01x
💻 LocalExpress1.172s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.847s101.01x
🐘 PostgresExpress1.185s (+0.9%)2.000s (~)0.002s (+54.5% 🔺)2.012s (~)0.827s101.02x
🐘 PostgresNext.js (Turbopack)1.228s (-1.3%)2.001s (~)0.001s (-25.0% 🟢)2.011s (~)0.783s101.06x
💻 LocalNext.js (Turbopack)1.228s (-1.7%)2.004s (~)0.012s (+13.1% 🔺)2.021s (~)0.793s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.289s (-3.1%)3.325s (-5.8% 🟢)0.785s (+11.6% 🔺)4.593s (-4.1%)2.304s101.00x
▲ VercelNext.js (Turbopack)2.336s (-12.9% 🟢)3.384s (-9.7% 🟢)0.770s (-8.0% 🟢)4.613s (-9.3% 🟢)2.276s101.02x
▲ VercelNitro2.345s (-8.0% 🟢)3.480s (-6.3% 🟢)0.621s (-16.1% 🟢)4.531s (-8.7% 🟢)2.187s101.02x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.586s (+1.3%)2.009s (~)0.013s (+18.1% 🔺)2.025s (~)0.439s301.00x
💻 LocalExpress1.593s (-1.1%)2.011s (~)0.013s (+2.9%)2.026s (~)0.433s301.00x
🐘 PostgresExpress1.602s (~)2.006s (~)0.005s (+1.4%)2.027s (~)0.425s301.01x
🐘 PostgresNitro1.603s (-2.2%)2.004s (~)0.005s (-0.6%)2.026s (~)0.423s301.01x
🐘 PostgresNext.js (Turbopack)1.753s (-2.2%)2.010s (~)0.005s (-2.4%)2.026s (~)0.273s301.11x
💻 LocalNext.js (Turbopack)1.889s (+8.2% 🔺)2.080s (+3.7%)0.014s (+7.7% 🔺)2.098s (+3.6%)0.209s291.19x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.212s (-9.9% 🟢)7.300s (-12.6% 🟢)0.270s (-21.4% 🟢)8.082s (-12.4% 🟢)1.869s81.00x
▲ VercelNext.js (Turbopack)6.304s (-8.2% 🟢)7.685s (-10.4% 🟢)0.289s (-7.7% 🟢)8.468s (-9.7% 🟢)2.164s81.01x
▲ VercelNitro6.373s (-5.4% 🟢)7.745s (-3.7%)0.208s (-5.5% 🟢)8.596s (-2.4%)2.223s71.03x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.797s (~)1.063s (+1.4%)0.000s (+Infinity% 🔺)1.079s (+1.8%)0.282s561.00x
🐘 PostgresNitro0.798s (-1.8%)1.067s (~)0.000s (+1.8%)1.099s (+1.8%)0.301s551.00x
🐘 PostgresNext.js (Turbopack)1.016s (-1.1%)1.500s (-7.5% 🟢)0.000s (-7.5% 🟢)1.507s (-7.6% 🟢)0.491s401.28x
💻 LocalExpress1.413s (~)2.012s (~)0.000s (~)2.015s (~)0.602s301.77x
💻 LocalNitro1.468s (+8.6% 🔺)2.014s (~)0.000s (-42.9% 🟢)2.017s (~)0.549s301.84x
💻 LocalNext.js (Turbopack)1.592s (+5.3% 🔺)2.015s (~)0.000s (-18.2% 🟢)2.019s (~)0.426s302.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.933s (-28.7% 🟢)4.414s (-23.2% 🟢)0.000s (NaN%)4.919s (-21.2% 🟢)1.986s131.00x
▲ VercelExpress2.951s (-20.1% 🟢)4.593s (-6.3% 🟢)0.000s (+Infinity% 🔺)5.072s (-8.7% 🟢)2.121s121.01x
▲ VercelNext.js (Turbopack)3.232s (-14.2% 🟢)4.626s (-13.3% 🟢)0.000s (-100.0% 🟢)5.093s (-13.4% 🟢)1.860s121.10x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.560s (-7.9% 🟢)2.066s (-5.0% 🟢)0.000s (-3.4%)2.136s (-2.7%)0.576s291.00x
🐘 PostgresExpress1.628s (-1.5%)2.137s (-1.8%)0.000s (+Infinity% 🔺)2.156s (-1.4%)0.528s281.04x
🐘 PostgresNext.js (Turbopack)2.214s (-1.8%)2.776s (+1.7%)0.000s (NaN%)2.782s (+1.6%)0.568s221.42x
💻 LocalExpress3.162s (-4.7%)3.965s (-1.5%)0.000s (-62.5% 🟢)3.968s (-1.6%)0.806s162.03x
💻 LocalNitro3.179s (+3.2%)3.734s (-4.2%)0.001s (+61.3% 🔺)3.737s (-4.2%)0.558s172.04x
💻 LocalNext.js (Turbopack)3.265s (+15.1% 🔺)3.734s (+9.3% 🔺)0.000s (-41.2% 🟢)3.738s (+9.3% 🔺)0.473s172.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.284s (-29.4% 🟢)6.003s (-21.0% 🟢)0.000s (-100.0% 🟢)6.482s (-20.8% 🟢)2.198s101.00x
▲ VercelNitro4.573s (-7.1% 🟢)6.058s (-1.7%)0.000s (NaN%)6.512s (-4.2%)1.939s101.07x
▲ VercelNext.js (Turbopack)5.009s (-29.1% 🟢)6.602s (-19.8% 🟢)0.000s (-100.0% 🟢)7.080s (-19.2% 🟢)2.071s91.17x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro15/21
🐘 PostgresNitro16/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202191661
✅ 💻 Local Development189502192114
✅ 📦 Local Production189502192114
✅ 🐘 Local Postgres188102332114
✅ 🪟 Windows15100151
❌ 📋 Other87811781057
Total8142110689211

❌ Failed Tests

📋 Other (1 failed)

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

  • webhookWorkflow | wrun_01KV7X2YYXA571EE20AYFJ50VB

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125026
✅ example125026
✅ express125026
✅ fastify125026
✅ hono125026
✅ nextjs-turbopack14902
✅ nextjs-webpack14902
✅ nitro125026
✅ nuxt125026
✅ sveltekit14407
✅ vite125026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable125026
✅ express-stable125026
✅ fastify-stable125026
✅ hono-stable125026
✅ nextjs-turbopack-canary131020
✅ nextjs-turbopack-stable-lazy-discovery-disabled15001
✅ nextjs-turbopack-stable-lazy-discovery-enabled15001
✅ nextjs-webpack-canary131020
✅ nextjs-webpack-stable-lazy-discovery-disabled15001
✅ nextjs-webpack-stable-lazy-discovery-enabled15001
✅ nitro-stable125026
✅ nuxt-stable125026
✅ sveltekit-stable14407
✅ vite-stable125026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15100
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable126025
✅ e2e-local-dev-tanstack-start-126025
✅ e2e-local-postgres-nest-stable125026
✅ e2e-local-postgres-tanstack-start-125026
✅ e2e-local-prod-nest-stable126025
✅ e2e-local-prod-tanstack-start-126025
❌ e2e-vercel-prod-tanstack-start124126

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

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

This PR adds bounded queue-redelivery behavior for replay-time RuntimeDecryptionErrors in @workflow/core, allowing managed worlds to retry transient persisted-data read corruption before marking the run failed.

Changes:

  • Adds a decryption-failure retry decision helper and retry budget constant.
  • Integrates the helper into the workflow runtime error path before writing run_failed.
  • Adds unit and runtime coverage plus a patch changeset.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/decryption-failure.tsAdds redelivery decision logic and logging for replay decryption failures.
packages/core/src/runtime/decryption-failure.test.tsCovers managed/in-process world behavior and retry-budget boundaries.
packages/core/src/runtime/constants.tsDefines the decryption-failure retry budget.
packages/core/src/runtime.tsRedrives managed-world runs on eligible RuntimeDecryptionErrors before failing the run.
packages/core/src/runtime.test.tsAdds end-to-end key-mismatch tests for redelivery and failure behavior.
.changeset/requeue-on-decryption-failure.mdAdds a patch changeset for @workflow/core.

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

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: blocking issues found

return false;
}

if (attempt <= DECRYPTION_FAILURE_MAX_RETRIES) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Blocking

RuntimeDecryptionError also covers encrypt-side AES-GCM failures (context.operation === "encrypt"), but this helper redrives every RuntimeDecryptionError on managed worlds. A fresh queue delivery can only help when decrypting persisted bytes, not when serializing/encrypting a new workflow payload. This can re-run workflow code up to 3 times before failing. Please gate redelivery on error.context?.operation === "decrypt".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — shouldRedriveOnDecryptionFailure now returns false immediately when error.context?.operation !== "decrypt", so encrypt-side AES-GCM failures fail the run without redelivery (re-fetching cannot change a payload being serialized fresh). Added a unit test covering the encrypt case on a managed world. Commit 558cc3f.

// queue delivery re-fetches the event log + ref
// payloads and can succeed. On managed Worlds, exit the
// process (which the platform turns into a redelivery)
// for a bounded number of attempts before failing the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

This changes the user-facing behavior for runtime decryption failures on managed worlds: they now redeliver before the terminal run_failed. The existing runtime-decryption-failed docs still say the run fails immediately, so those docs should be updated as part of this behavior change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — updated errors/runtime-decryption-failed.mdx (both v4 and v5) to document the bounded queue-redelivery behavior on managed worlds: decrypt failures are now retried before the terminal run_failed, while encrypt-side failures and in-process worlds still fail immediately. Commit 558cc3f.

Comment threadpackages/core/src/runtime.test.ts Outdated
// Past DECRYPTION_FAILURE_MAX_RETRIES (3) but under MAX_QUEUE_DELIVERIES
// (48), so we exercise the decryption-failure budget, not the queue
// max-deliveries guard.
attempt: 10,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This test duplicates the retry/delivery constants in comments and uses attempt: 10 instead of deriving from the constant it is testing. Please import the constants and use DECRYPTION_FAILURE_MAX_RETRIES + 1, with an assertion that it remains below MAX_QUEUE_DELIVERIES, so future constant changes don't silently stale the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — the past-budget test now derives attempt = DECRYPTION_FAILURE_MAX_RETRIES + 1 from the imported constant (no duplicated magic numbers) and asserts attempt < MAX_QUEUE_DELIVERIES, so a future constant change cannot silently stale the test. Commit 558cc3f.

Resolve conflicts in runtime.ts, runtime.test.ts, and constants.ts
(decryption-failure redelivery + replay-divergence recovery coexist).
Address review feedback:
- Gate decryption-failure redelivery on operation === 'decrypt' so
encrypt-side AES-GCM failures fail immediately (re-fetching cannot help).
- Derive the past-budget test attempt from DECRYPTION_FAILURE_MAX_RETRIES + 1
and assert it stays below MAX_QUEUE_DELIVERIES.
- Document the bounded queue-redelivery behavior in runtime-decryption-failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

feat(core): bounded queue-redelivery retry on decryption failure during replay - #2166

Open
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure
Open

feat(core): bounded queue-redelivery retry on decryption failure during replay#2166
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Follow-up to #2145 (the RuntimeDecryptionError attribution fix), implementing the bounded-redelivery behavior @pranaygp described in this comment.

An AES-GCM authentication failure is terminal for the bytes/key of the current attempt — we must never continue executing the workflow on top of data we couldn't decrypt. But the failure is often not terminal for the run: when the ciphertext came from a transiently truncated or corrupted read of remotely-persisted data (a partial /refs response, an edge-cache miss returning a partial 200, a proxy drop during streaming), a fresh queue delivery re-fetches the event log and ref payloads from scratch and can succeed.

Previously we committed run_failed immediately, turning a potentially recoverable read failure into a terminal workflow failure.

What changed

Mirrors the existing replay-timeout bounded-redelivery precedent (handleReplayBudgetExhausted):

  • Managed worlds (processExitTriggersQueueRedelivery === true, e.g. world-vercel): on attempts ≤ DECRYPTION_FAILURE_MAX_RETRIES (3), the run handler exits the process — which the platform turns into a queue redelivery — so replay restarts from freshly-fetched persisted data. Once the retry budget is exhausted, it commits run_failed with RUNTIME_ERROR.
  • In-process worlds (world-local, dev servers, custom in-process worlds): no queue to re-fetch from, and process.exit() would kill the user's host, so the run fails immediately with RUNTIME_ERROR.

New shouldRedriveOnDecryptionFailure() helper (runtime/decryption-failure.ts) is pure (logging only) and returns whether the caller should redrive, keeping the process.exit / run_failed decision at the single existing call site in the run handler.

Test coverage

  • runtime/decryption-failure.test.ts — unit tests for the helper across managed/in-process worlds and the retry-budget boundary.
  • runtime.test.ts — end-to-end tests driving a real key mismatch (input encrypted with key A, run-key resolves to key B → auth-tag failure during input hydration):
    • early attempt on a managed world → process.exit(1), no run_failed
    • past the retry budget on a managed world → run_failed with RUNTIME_ERROR
    • in-process world → run_failed immediately, no exit

All @workflow/core tests pass (1073), full repo typecheck passes (40/40).

Notes

The longer-term improvement @pranaygp mentioned — detecting response truncation / integrity failure at the /refs transport boundary to classify the retryable case more directly — is out of scope here and would be a separate change on the world / workflow-server side.

…ng replay
Follow-up to #2145. An AES-GCM auth failure (RuntimeDecryptionError) is
terminal for the current attempt's bytes, but often transient at the run
level: when the ciphertext came from a truncated/corrupted read of
remotely-persisted data (e.g. a partial /refs response), a fresh queue
delivery re-fetches the event log + ref payloads and can succeed.
Mirror the replay-timeout bounded-redelivery precedent: on managed worlds
(processExitTriggersQueueRedelivery), exit the process to trigger queue
redelivery for up to DECRYPTION_FAILURE_MAX_RETRIES attempts, then commit
run_failed as RUNTIME_ERROR. In-process worlds fail immediately (no queue
to re-fetch from, and exiting would kill the host).
CopilotAI review requested due to automatic review settings May 29, 2026 19:24
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 29, 2026 19:24
@changeset-bot

changeset-botBot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 558cc3f

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

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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 29, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+1.7%)1.006s (~)0.963s101.00x
💻 LocalExpress0.043s (-10.3% 🟢)1.007s (~)0.964s101.02x
🐘 PostgresNitro0.064s (-1.5%)1.011s (~)0.948s101.50x
🐘 PostgresNext.js (Turbopack)0.068s (-7.0% 🟢)1.011s (~)0.944s101.59x
🐘 PostgresExpress0.068s (+11.0% 🔺)1.012s (~)0.944s101.59x
💻 LocalNext.js (Turbopack)0.069s (+13.3% 🔺)1.006s (~)0.937s101.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.279s (-74.8% 🟢)2.020s (-43.5% 🟢)1.741s101.00x
▲ VercelNext.js (Turbopack)0.313s (-84.2% 🟢)2.337s (-43.1% 🟢)2.024s101.12x
▲ VercelNitro0.400s (-33.4% 🟢)2.213s (-16.8% 🟢)1.813s101.43x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.094s (~)2.006s (~)0.912s101.00x
💻 LocalExpress1.095s (~)2.007s (~)0.912s101.00x
🐘 PostgresExpress1.115s (~)2.010s (~)0.894s101.02x
🐘 PostgresNitro1.135s (+1.4%)2.009s (~)0.875s101.04x
🐘 PostgresNext.js (Turbopack)1.142s (-1.4%)2.011s (~)0.869s101.04x
💻 LocalNext.js (Turbopack)1.145s (~)2.007s (~)0.862s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.704s (-36.5% 🟢)3.836s (-15.2% 🟢)2.132s101.00x
▲ VercelExpress1.800s (-32.8% 🟢)3.830s (-23.4% 🟢)2.030s101.06x
▲ VercelNitro1.865s (-33.9% 🟢)3.713s (-18.3% 🟢)1.847s101.09x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.503s (~)11.022s (~)0.519s31.00x
💻 LocalExpress10.545s (~)11.023s (~)0.478s31.00x
🐘 PostgresExpress10.584s (~)11.018s (~)0.435s31.01x
🐘 PostgresNitro10.588s (~)11.015s (~)0.427s31.01x
💻 LocalNext.js (Turbopack)10.868s (+0.8%)11.023s (~)0.155s31.03x
🐘 PostgresNext.js (Turbopack)10.901s (~)11.016s (~)0.115s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)14.283s (-33.3% 🟢)16.662s (-28.4% 🟢)2.379s21.00x
▲ VercelNitro14.988s (-17.5% 🟢)17.110s (-13.9% 🟢)2.122s21.05x
▲ VercelExpress15.147s (-6.9% 🟢)17.654s (-5.2% 🟢)2.507s21.06x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.779s (~)14.029s (~)0.250s51.00x
🐘 PostgresNitro13.817s (~)14.020s (~)0.203s51.00x
💻 LocalExpress13.835s (~)14.027s (~)0.192s51.00x
🐘 PostgresExpress13.855s (~)14.020s (~)0.165s51.01x
🐘 PostgresNext.js (Turbopack)14.544s (~)15.019s (~)0.475s41.06x
💻 LocalNext.js (Turbopack)14.605s (+1.6%)15.030s (~)0.425s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.570s (-35.3% 🟢)24.870s (-32.6% 🟢)2.300s31.00x
▲ VercelNitro23.905s (-27.5% 🟢)25.993s (-25.4% 🟢)2.088s31.06x
▲ VercelNext.js (Turbopack)24.384s (-20.8% 🟢)26.434s (-19.6% 🟢)2.050s31.08x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.403s (-1.3%)13.024s (~)0.621s71.00x
💻 LocalExpress12.512s (-0.8%)13.024s (~)0.511s71.01x
🐘 PostgresNitro12.647s (~)13.023s (~)0.376s71.02x
🐘 PostgresExpress12.680s (+0.7%)13.019s (~)0.339s71.02x
🐘 PostgresNext.js (Turbopack)13.856s (-0.8%)14.020s (-1.0%)0.164s71.12x
💻 LocalNext.js (Turbopack)14.047s (+3.2%)14.744s (+5.1% 🔺)0.697s71.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express29.958s (-13.8% 🟢)32.678s (-11.9% 🟢)2.720s31.00x
▲ VercelNitro30.838s (-18.6% 🟢)33.293s (-15.7% 🟢)2.455s31.03x
▲ VercelNext.js (Turbopack)31.778s (-9.5% 🟢)33.997s (-9.4% 🟢)2.218s31.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.207s (+1.2%)2.006s (~)0.800s151.00x
💻 LocalNitro1.207s (+3.8%)2.006s (~)0.799s151.00x
🐘 PostgresNitro1.214s (~)2.008s (~)0.794s151.01x
🐘 PostgresExpress1.214s (+0.7%)2.009s (~)0.795s151.01x
🐘 PostgresNext.js (Turbopack)1.277s (-5.9% 🟢)2.007s (-3.3%)0.731s151.06x
💻 LocalNext.js (Turbopack)1.340s (+0.8%)2.007s (~)0.667s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.289s (-26.0% 🟢)3.763s (-17.9% 🟢)1.474s81.00x
▲ VercelExpress2.513s (-1.5%)4.117s (+1.1%)1.604s81.10x
▲ VercelNext.js (Turbopack)3.039s (+29.5% 🔺)4.619s (+15.0% 🔺)1.580s71.33x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.367s (+1.9%)2.393s (-4.6%)1.026s131.00x
🐘 PostgresExpress1.387s (+2.2%)2.223s (-11.3% 🟢)0.836s141.01x
🐘 PostgresNext.js (Turbopack)1.662s (+1.4%)2.295s (-8.5% 🟢)0.633s141.22x
💻 LocalNitro1.705s (+4.7%)2.006s (~)0.301s151.25x
💻 LocalExpress1.838s (-3.0%)2.150s (~)0.313s141.34x
💻 LocalNext.js (Turbopack)1.860s (+16.7% 🔺)2.075s (+3.4%)0.215s151.36x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.813s (-31.6% 🟢)4.734s (-21.8% 🟢)1.920s71.00x
▲ VercelNitro2.996s (-20.5% 🟢)4.799s (-11.1% 🟢)1.803s71.06x
▲ VercelNext.js (Turbopack)3.230s (-7.9% 🟢)5.201s (-1.0%)1.971s61.15x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.821s (+12.1% 🔺)3.887s (-6.1% 🟢)2.066s81.00x
🐘 PostgresExpress1.827s (+12.6% 🔺)4.011s (+3.2%)2.183s81.00x
🐘 PostgresNext.js (Turbopack)3.481s (-9.1% 🟢)4.299s (-3.3%)0.818s71.91x
💻 LocalNitro4.569s (+13.6% 🔺)5.015s (+9.5% 🔺)0.446s72.51x
💻 LocalExpress5.321s (+1.7%)5.846s (~)0.525s62.92x
💻 LocalNext.js (Turbopack)5.870s (+28.6% 🔺)6.348s (+22.6% 🔺)0.478s63.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.964s (-13.3% 🟢)6.243s (-4.8%)2.278s51.00x
▲ VercelNitro4.013s (-5.5% 🟢)6.018s (+5.1% 🔺)2.005s51.01x
▲ VercelNext.js (Turbopack)4.065s (+7.3% 🔺)6.166s (+14.0% 🔺)2.101s51.03x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.218s (~)2.007s (~)0.789s151.00x
🐘 PostgresExpress1.220s (+0.8%)2.008s (~)0.789s151.00x
🐘 PostgresNext.js (Turbopack)1.272s (-1.1%)2.008s (~)0.736s151.04x
💻 LocalNext.js (Turbopack)1.424s (+3.3%)2.008s (~)0.584s151.17x
💻 LocalNitro1.560s (+3.2%)2.007s (~)0.447s151.28x
💻 LocalExpress1.593s (-1.3%)2.007s (~)0.414s151.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.290s (-0.7%)3.776s (-4.3%)1.486s81.00x
▲ VercelNitro2.297s (-64.6% 🟢)3.691s (-54.0% 🟢)1.394s91.00x
▲ VercelExpress2.406s (+5.8% 🔺)3.962s (+2.8%)1.556s81.05x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.357s (~)2.315s (-10.7% 🟢)0.958s131.00x
🐘 PostgresExpress1.406s (+0.8%)2.592s (+4.9%)1.186s121.04x
🐘 PostgresNext.js (Turbopack)1.583s (+3.0%)2.152s (-3.3%)0.569s141.17x
💻 LocalNext.js (Turbopack)2.095s (+8.5% 🔺)2.918s (+31.3% 🔺)0.823s111.54x
💻 LocalNitro2.187s (+22.2% 🔺)2.676s (+29.1% 🔺)0.489s121.61x
💻 LocalExpress2.349s (-8.8% 🟢)2.827s (-9.0% 🟢)0.479s111.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.794s (-25.4% 🟢)4.495s (-17.7% 🟢)1.700s71.00x
▲ VercelNext.js (Turbopack)3.325s (-17.1% 🟢)5.058s (-12.0% 🟢)1.733s61.19x
▲ VercelExpress3.330s (~)5.327s (~)1.997s61.19x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.572s (-9.0% 🟢)4.011s (+3.2%)2.439s81.00x
🐘 PostgresExpress1.646s (-6.1% 🟢)4.014s (-2.9%)2.368s81.05x
🐘 PostgresNext.js (Turbopack)2.971s (-22.6% 🟢)3.764s (-15.3% 🟢)0.793s81.89x
💻 LocalNitro5.218s (+9.5% 🔺)5.684s (+6.3% 🔺)0.467s63.32x
💻 LocalExpress6.467s (+2.0%)7.216s (+2.8%)0.749s54.11x
💻 LocalNext.js (Turbopack)6.475s (+22.3% 🔺)7.018s (+16.7% 🔺)0.543s54.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.355s (-33.9% 🟢)6.132s (-26.5% 🟢)1.777s61.00x
▲ VercelNext.js (Turbopack)4.600s (-6.6% 🟢)6.524s (~)1.924s51.06x
▲ VercelExpress4.640s (-11.4% 🟢)6.693s (-3.9%)2.053s51.07x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.593s (-8.0% 🟢)1.023s (-1.8%)0.430s591.00x
💻 LocalNitro0.596s (+2.2%)1.005s (~)0.409s601.00x
🐘 PostgresExpress0.609s (+2.8%)1.023s (+1.8%)0.415s591.03x
💻 LocalExpress0.625s (-5.3% 🟢)1.005s (-3.3%)0.380s601.05x
🐘 PostgresNext.js (Turbopack)0.819s (-3.7%)1.006s (-1.7%)0.187s601.38x
💻 LocalNext.js (Turbopack)0.942s (+6.4% 🔺)1.096s (+3.7%)0.154s551.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.725s (-13.0% 🟢)7.514s (-12.9% 🟢)1.790s91.00x
▲ VercelNext.js (Turbopack)5.798s (-21.9% 🟢)7.584s (-18.8% 🟢)1.786s81.01x
▲ VercelNitro5.831s (-31.1% 🟢)7.298s (-27.8% 🟢)1.467s91.02x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.384s (-3.6%)2.007s (-1.1%)0.623s451.00x
🐘 PostgresNitro1.385s (-4.2%)2.008s (~)0.623s451.00x
💻 LocalNitro1.502s (+1.7%)2.006s (~)0.504s451.09x
💻 LocalExpress1.575s (-0.6%)2.007s (~)0.432s451.14x
🐘 PostgresNext.js (Turbopack)1.935s (-5.0% 🟢)2.150s (-16.1% 🟢)0.216s421.40x
💻 LocalNext.js (Turbopack)2.277s (+7.0% 🔺)3.009s (~)0.732s301.65x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.711s (-31.9% 🟢)16.057s (-26.5% 🟢)2.346s61.00x
▲ VercelNitro14.119s (-24.8% 🟢)16.038s (-22.7% 🟢)1.919s61.03x
▲ VercelNext.js (Turbopack)14.780s (-25.8% 🟢)16.884s (-24.6% 🟢)2.104s61.08x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.788s (-1.2%)3.166s (+0.9%)0.379s381.00x
🐘 PostgresExpress2.860s (+2.9%)3.252s (+3.7%)0.392s371.03x
💻 LocalNitro3.279s (+3.5%)4.009s (+2.5%)0.731s301.18x
💻 LocalExpress3.391s (+1.4%)4.010s (~)0.619s301.22x
🐘 PostgresNext.js (Turbopack)3.810s (-5.3% 🟢)4.043s (-8.2% 🟢)0.233s301.37x
💻 LocalNext.js (Turbopack)4.748s (+4.6%)5.054s (~)0.306s241.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.736s (-18.0% 🟢)32.583s (-14.7% 🟢)2.847s41.00x
▲ VercelExpress29.831s (-22.9% 🟢)32.492s (-19.6% 🟢)2.660s41.00x
▲ VercelNext.js (Turbopack)32.381s (-27.8% 🟢)35.293s (-24.4% 🟢)2.912s41.09x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.253s (-3.1%)1.006s (~)0.753s601.00x
🐘 PostgresExpress0.264s (+1.1%)1.023s (+1.7%)0.760s591.04x
🐘 PostgresNext.js (Turbopack)0.301s (-2.4%)1.006s (~)0.705s601.19x
💻 LocalExpress0.409s (-4.4%)1.005s (~)0.595s601.62x
💻 LocalNitro0.425s (-1.5%)1.005s (~)0.579s601.68x
💻 LocalNext.js (Turbopack)0.594s (-8.2% 🟢)1.006s (-4.9%)0.413s602.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.844s (-36.1% 🟢)3.799s (-24.5% 🟢)1.954s161.00x
▲ VercelExpress1.910s (-46.5% 🟢)3.900s (-30.2% 🟢)1.990s161.04x
▲ VercelNext.js (Turbopack)1.931s (-37.1% 🟢)3.841s (-23.4% 🟢)1.910s161.05x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.410s (-10.2% 🟢)1.029s (-4.6%)0.619s881.00x
🐘 PostgresExpress0.418s (+1.8%)1.078s (+7.1% 🔺)0.659s841.02x
🐘 PostgresNext.js (Turbopack)0.575s (-15.2% 🟢)1.118s (-12.4% 🟢)0.543s811.40x
💻 LocalNitro2.162s (+7.5% 🔺)2.687s (+10.1% 🔺)0.524s345.27x
💻 LocalExpress2.221s (+1.4%)2.769s (-2.9%)0.548s335.41x
💻 LocalNext.js (Turbopack)2.825s (+10.4% 🔺)3.512s (+7.7% 🔺)0.687s266.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.226s (-37.4% 🟢)5.211s (-27.8% 🟢)1.986s181.00x
▲ VercelNitro3.370s (-3.3%)5.242s (+1.4%)1.872s181.04x
▲ VercelNext.js (Turbopack)3.799s (~)5.760s (~)1.961s161.18x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.804s (-4.7%)1.527s (+3.8%)0.724s791.00x
🐘 PostgresExpress0.831s (-2.0%)1.471s (-2.4%)0.640s821.03x
🐘 PostgresNext.js (Turbopack)2.802s (-9.3% 🟢)3.651s (-4.6%)0.849s333.49x
💻 LocalNitro9.753s (+11.8% 🔺)10.447s (+12.9% 🔺)0.694s1212.14x
💻 LocalExpress9.767s (-1.0%)10.280s (~)0.514s1212.15x
💻 LocalNext.js (Turbopack)12.460s (+18.8% 🔺)13.337s (+17.1% 🔺)0.877s1015.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.603s (-17.0% 🟢)7.854s (-9.0% 🟢)2.251s161.00x
▲ VercelExpress5.898s (-13.7% 🟢)8.080s (-6.3% 🟢)2.182s151.05x
▲ VercelNext.js (Turbopack)7.576s (+5.3% 🔺)9.754s (+8.8% 🔺)2.178s131.35x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.163s (~)2.005s (~)0.010s (+1.0%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (-2.4%)1.999s (~)0.002s (+33.3% 🔺)2.011s (~)0.842s101.01x
💻 LocalExpress1.172s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.847s101.01x
🐘 PostgresExpress1.185s (+0.9%)2.000s (~)0.002s (+54.5% 🔺)2.012s (~)0.827s101.02x
🐘 PostgresNext.js (Turbopack)1.228s (-1.3%)2.001s (~)0.001s (-25.0% 🟢)2.011s (~)0.783s101.06x
💻 LocalNext.js (Turbopack)1.228s (-1.7%)2.004s (~)0.012s (+13.1% 🔺)2.021s (~)0.793s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.289s (-3.1%)3.325s (-5.8% 🟢)0.785s (+11.6% 🔺)4.593s (-4.1%)2.304s101.00x
▲ VercelNext.js (Turbopack)2.336s (-12.9% 🟢)3.384s (-9.7% 🟢)0.770s (-8.0% 🟢)4.613s (-9.3% 🟢)2.276s101.02x
▲ VercelNitro2.345s (-8.0% 🟢)3.480s (-6.3% 🟢)0.621s (-16.1% 🟢)4.531s (-8.7% 🟢)2.187s101.02x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.586s (+1.3%)2.009s (~)0.013s (+18.1% 🔺)2.025s (~)0.439s301.00x
💻 LocalExpress1.593s (-1.1%)2.011s (~)0.013s (+2.9%)2.026s (~)0.433s301.00x
🐘 PostgresExpress1.602s (~)2.006s (~)0.005s (+1.4%)2.027s (~)0.425s301.01x
🐘 PostgresNitro1.603s (-2.2%)2.004s (~)0.005s (-0.6%)2.026s (~)0.423s301.01x
🐘 PostgresNext.js (Turbopack)1.753s (-2.2%)2.010s (~)0.005s (-2.4%)2.026s (~)0.273s301.11x
💻 LocalNext.js (Turbopack)1.889s (+8.2% 🔺)2.080s (+3.7%)0.014s (+7.7% 🔺)2.098s (+3.6%)0.209s291.19x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.212s (-9.9% 🟢)7.300s (-12.6% 🟢)0.270s (-21.4% 🟢)8.082s (-12.4% 🟢)1.869s81.00x
▲ VercelNext.js (Turbopack)6.304s (-8.2% 🟢)7.685s (-10.4% 🟢)0.289s (-7.7% 🟢)8.468s (-9.7% 🟢)2.164s81.01x
▲ VercelNitro6.373s (-5.4% 🟢)7.745s (-3.7%)0.208s (-5.5% 🟢)8.596s (-2.4%)2.223s71.03x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.797s (~)1.063s (+1.4%)0.000s (+Infinity% 🔺)1.079s (+1.8%)0.282s561.00x
🐘 PostgresNitro0.798s (-1.8%)1.067s (~)0.000s (+1.8%)1.099s (+1.8%)0.301s551.00x
🐘 PostgresNext.js (Turbopack)1.016s (-1.1%)1.500s (-7.5% 🟢)0.000s (-7.5% 🟢)1.507s (-7.6% 🟢)0.491s401.28x
💻 LocalExpress1.413s (~)2.012s (~)0.000s (~)2.015s (~)0.602s301.77x
💻 LocalNitro1.468s (+8.6% 🔺)2.014s (~)0.000s (-42.9% 🟢)2.017s (~)0.549s301.84x
💻 LocalNext.js (Turbopack)1.592s (+5.3% 🔺)2.015s (~)0.000s (-18.2% 🟢)2.019s (~)0.426s302.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.933s (-28.7% 🟢)4.414s (-23.2% 🟢)0.000s (NaN%)4.919s (-21.2% 🟢)1.986s131.00x
▲ VercelExpress2.951s (-20.1% 🟢)4.593s (-6.3% 🟢)0.000s (+Infinity% 🔺)5.072s (-8.7% 🟢)2.121s121.01x
▲ VercelNext.js (Turbopack)3.232s (-14.2% 🟢)4.626s (-13.3% 🟢)0.000s (-100.0% 🟢)5.093s (-13.4% 🟢)1.860s121.10x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.560s (-7.9% 🟢)2.066s (-5.0% 🟢)0.000s (-3.4%)2.136s (-2.7%)0.576s291.00x
🐘 PostgresExpress1.628s (-1.5%)2.137s (-1.8%)0.000s (+Infinity% 🔺)2.156s (-1.4%)0.528s281.04x
🐘 PostgresNext.js (Turbopack)2.214s (-1.8%)2.776s (+1.7%)0.000s (NaN%)2.782s (+1.6%)0.568s221.42x
💻 LocalExpress3.162s (-4.7%)3.965s (-1.5%)0.000s (-62.5% 🟢)3.968s (-1.6%)0.806s162.03x
💻 LocalNitro3.179s (+3.2%)3.734s (-4.2%)0.001s (+61.3% 🔺)3.737s (-4.2%)0.558s172.04x
💻 LocalNext.js (Turbopack)3.265s (+15.1% 🔺)3.734s (+9.3% 🔺)0.000s (-41.2% 🟢)3.738s (+9.3% 🔺)0.473s172.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.284s (-29.4% 🟢)6.003s (-21.0% 🟢)0.000s (-100.0% 🟢)6.482s (-20.8% 🟢)2.198s101.00x
▲ VercelNitro4.573s (-7.1% 🟢)6.058s (-1.7%)0.000s (NaN%)6.512s (-4.2%)1.939s101.07x
▲ VercelNext.js (Turbopack)5.009s (-29.1% 🟢)6.602s (-19.8% 🟢)0.000s (-100.0% 🟢)7.080s (-19.2% 🟢)2.071s91.17x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro15/21
🐘 PostgresNitro16/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202191661
✅ 💻 Local Development189502192114
✅ 📦 Local Production189502192114
✅ 🐘 Local Postgres188102332114
✅ 🪟 Windows15100151
❌ 📋 Other87811781057
Total8142110689211

❌ Failed Tests

📋 Other (1 failed)

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

  • webhookWorkflow | wrun_01KV7X2YYXA571EE20AYFJ50VB

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125026
✅ example125026
✅ express125026
✅ fastify125026
✅ hono125026
✅ nextjs-turbopack14902
✅ nextjs-webpack14902
✅ nitro125026
✅ nuxt125026
✅ sveltekit14407
✅ vite125026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable125026
✅ express-stable125026
✅ fastify-stable125026
✅ hono-stable125026
✅ nextjs-turbopack-canary131020
✅ nextjs-turbopack-stable-lazy-discovery-disabled15001
✅ nextjs-turbopack-stable-lazy-discovery-enabled15001
✅ nextjs-webpack-canary131020
✅ nextjs-webpack-stable-lazy-discovery-disabled15001
✅ nextjs-webpack-stable-lazy-discovery-enabled15001
✅ nitro-stable125026
✅ nuxt-stable125026
✅ sveltekit-stable14407
✅ vite-stable125026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15100
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable126025
✅ e2e-local-dev-tanstack-start-126025
✅ e2e-local-postgres-nest-stable125026
✅ e2e-local-postgres-tanstack-start-125026
✅ e2e-local-prod-nest-stable126025
✅ e2e-local-prod-tanstack-start-126025
❌ e2e-vercel-prod-tanstack-start124126

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

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

This PR adds bounded queue-redelivery behavior for replay-time RuntimeDecryptionErrors in @workflow/core, allowing managed worlds to retry transient persisted-data read corruption before marking the run failed.

Changes:

  • Adds a decryption-failure retry decision helper and retry budget constant.
  • Integrates the helper into the workflow runtime error path before writing run_failed.
  • Adds unit and runtime coverage plus a patch changeset.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/decryption-failure.tsAdds redelivery decision logic and logging for replay decryption failures.
packages/core/src/runtime/decryption-failure.test.tsCovers managed/in-process world behavior and retry-budget boundaries.
packages/core/src/runtime/constants.tsDefines the decryption-failure retry budget.
packages/core/src/runtime.tsRedrives managed-world runs on eligible RuntimeDecryptionErrors before failing the run.
packages/core/src/runtime.test.tsAdds end-to-end key-mismatch tests for redelivery and failure behavior.
.changeset/requeue-on-decryption-failure.mdAdds a patch changeset for @workflow/core.

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

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: blocking issues found

return false;
}

if (attempt <= DECRYPTION_FAILURE_MAX_RETRIES) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Blocking

RuntimeDecryptionError also covers encrypt-side AES-GCM failures (context.operation === "encrypt"), but this helper redrives every RuntimeDecryptionError on managed worlds. A fresh queue delivery can only help when decrypting persisted bytes, not when serializing/encrypting a new workflow payload. This can re-run workflow code up to 3 times before failing. Please gate redelivery on error.context?.operation === "decrypt".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — shouldRedriveOnDecryptionFailure now returns false immediately when error.context?.operation !== "decrypt", so encrypt-side AES-GCM failures fail the run without redelivery (re-fetching cannot change a payload being serialized fresh). Added a unit test covering the encrypt case on a managed world. Commit 558cc3f.

// queue delivery re-fetches the event log + ref
// payloads and can succeed. On managed Worlds, exit the
// process (which the platform turns into a redelivery)
// for a bounded number of attempts before failing the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

This changes the user-facing behavior for runtime decryption failures on managed worlds: they now redeliver before the terminal run_failed. The existing runtime-decryption-failed docs still say the run fails immediately, so those docs should be updated as part of this behavior change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — updated errors/runtime-decryption-failed.mdx (both v4 and v5) to document the bounded queue-redelivery behavior on managed worlds: decrypt failures are now retried before the terminal run_failed, while encrypt-side failures and in-process worlds still fail immediately. Commit 558cc3f.

Comment threadpackages/core/src/runtime.test.ts Outdated
// Past DECRYPTION_FAILURE_MAX_RETRIES (3) but under MAX_QUEUE_DELIVERIES
// (48), so we exercise the decryption-failure budget, not the queue
// max-deliveries guard.
attempt: 10,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This test duplicates the retry/delivery constants in comments and uses attempt: 10 instead of deriving from the constant it is testing. Please import the constants and use DECRYPTION_FAILURE_MAX_RETRIES + 1, with an assertion that it remains below MAX_QUEUE_DELIVERIES, so future constant changes don't silently stale the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — the past-budget test now derives attempt = DECRYPTION_FAILURE_MAX_RETRIES + 1 from the imported constant (no duplicated magic numbers) and asserts attempt < MAX_QUEUE_DELIVERIES, so a future constant change cannot silently stale the test. Commit 558cc3f.

Resolve conflicts in runtime.ts, runtime.test.ts, and constants.ts
(decryption-failure redelivery + replay-divergence recovery coexist).
Address review feedback:
- Gate decryption-failure redelivery on operation === 'decrypt' so
encrypt-side AES-GCM failures fail immediately (re-fetching cannot help).
- Derive the past-budget test attempt from DECRYPTION_FAILURE_MAX_RETRIES + 1
and assert it stays below MAX_QUEUE_DELIVERIES.
- Document the bounded queue-redelivery behavior in runtime-decryption-failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

feat(core): bounded queue-redelivery retry on decryption failure during replay - #2166

Open
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure
Open

feat(core): bounded queue-redelivery retry on decryption failure during replay#2166
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Follow-up to #2145 (the RuntimeDecryptionError attribution fix), implementing the bounded-redelivery behavior @pranaygp described in this comment.

An AES-GCM authentication failure is terminal for the bytes/key of the current attempt — we must never continue executing the workflow on top of data we couldn't decrypt. But the failure is often not terminal for the run: when the ciphertext came from a transiently truncated or corrupted read of remotely-persisted data (a partial /refs response, an edge-cache miss returning a partial 200, a proxy drop during streaming), a fresh queue delivery re-fetches the event log and ref payloads from scratch and can succeed.

Previously we committed run_failed immediately, turning a potentially recoverable read failure into a terminal workflow failure.

What changed

Mirrors the existing replay-timeout bounded-redelivery precedent (handleReplayBudgetExhausted):

  • Managed worlds (processExitTriggersQueueRedelivery === true, e.g. world-vercel): on attempts ≤ DECRYPTION_FAILURE_MAX_RETRIES (3), the run handler exits the process — which the platform turns into a queue redelivery — so replay restarts from freshly-fetched persisted data. Once the retry budget is exhausted, it commits run_failed with RUNTIME_ERROR.
  • In-process worlds (world-local, dev servers, custom in-process worlds): no queue to re-fetch from, and process.exit() would kill the user's host, so the run fails immediately with RUNTIME_ERROR.

New shouldRedriveOnDecryptionFailure() helper (runtime/decryption-failure.ts) is pure (logging only) and returns whether the caller should redrive, keeping the process.exit / run_failed decision at the single existing call site in the run handler.

Test coverage

  • runtime/decryption-failure.test.ts — unit tests for the helper across managed/in-process worlds and the retry-budget boundary.
  • runtime.test.ts — end-to-end tests driving a real key mismatch (input encrypted with key A, run-key resolves to key B → auth-tag failure during input hydration):
    • early attempt on a managed world → process.exit(1), no run_failed
    • past the retry budget on a managed world → run_failed with RUNTIME_ERROR
    • in-process world → run_failed immediately, no exit

All @workflow/core tests pass (1073), full repo typecheck passes (40/40).

Notes

The longer-term improvement @pranaygp mentioned — detecting response truncation / integrity failure at the /refs transport boundary to classify the retryable case more directly — is out of scope here and would be a separate change on the world / workflow-server side.

…ng replay
Follow-up to #2145. An AES-GCM auth failure (RuntimeDecryptionError) is
terminal for the current attempt's bytes, but often transient at the run
level: when the ciphertext came from a truncated/corrupted read of
remotely-persisted data (e.g. a partial /refs response), a fresh queue
delivery re-fetches the event log + ref payloads and can succeed.
Mirror the replay-timeout bounded-redelivery precedent: on managed worlds
(processExitTriggersQueueRedelivery), exit the process to trigger queue
redelivery for up to DECRYPTION_FAILURE_MAX_RETRIES attempts, then commit
run_failed as RUNTIME_ERROR. In-process worlds fail immediately (no queue
to re-fetch from, and exiting would kill the host).
CopilotAI review requested due to automatic review settings May 29, 2026 19:24
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 29, 2026 19:24
@changeset-bot

changeset-botBot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 558cc3f

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

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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 29, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+1.7%)1.006s (~)0.963s101.00x
💻 LocalExpress0.043s (-10.3% 🟢)1.007s (~)0.964s101.02x
🐘 PostgresNitro0.064s (-1.5%)1.011s (~)0.948s101.50x
🐘 PostgresNext.js (Turbopack)0.068s (-7.0% 🟢)1.011s (~)0.944s101.59x
🐘 PostgresExpress0.068s (+11.0% 🔺)1.012s (~)0.944s101.59x
💻 LocalNext.js (Turbopack)0.069s (+13.3% 🔺)1.006s (~)0.937s101.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.279s (-74.8% 🟢)2.020s (-43.5% 🟢)1.741s101.00x
▲ VercelNext.js (Turbopack)0.313s (-84.2% 🟢)2.337s (-43.1% 🟢)2.024s101.12x
▲ VercelNitro0.400s (-33.4% 🟢)2.213s (-16.8% 🟢)1.813s101.43x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.094s (~)2.006s (~)0.912s101.00x
💻 LocalExpress1.095s (~)2.007s (~)0.912s101.00x
🐘 PostgresExpress1.115s (~)2.010s (~)0.894s101.02x
🐘 PostgresNitro1.135s (+1.4%)2.009s (~)0.875s101.04x
🐘 PostgresNext.js (Turbopack)1.142s (-1.4%)2.011s (~)0.869s101.04x
💻 LocalNext.js (Turbopack)1.145s (~)2.007s (~)0.862s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.704s (-36.5% 🟢)3.836s (-15.2% 🟢)2.132s101.00x
▲ VercelExpress1.800s (-32.8% 🟢)3.830s (-23.4% 🟢)2.030s101.06x
▲ VercelNitro1.865s (-33.9% 🟢)3.713s (-18.3% 🟢)1.847s101.09x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.503s (~)11.022s (~)0.519s31.00x
💻 LocalExpress10.545s (~)11.023s (~)0.478s31.00x
🐘 PostgresExpress10.584s (~)11.018s (~)0.435s31.01x
🐘 PostgresNitro10.588s (~)11.015s (~)0.427s31.01x
💻 LocalNext.js (Turbopack)10.868s (+0.8%)11.023s (~)0.155s31.03x
🐘 PostgresNext.js (Turbopack)10.901s (~)11.016s (~)0.115s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)14.283s (-33.3% 🟢)16.662s (-28.4% 🟢)2.379s21.00x
▲ VercelNitro14.988s (-17.5% 🟢)17.110s (-13.9% 🟢)2.122s21.05x
▲ VercelExpress15.147s (-6.9% 🟢)17.654s (-5.2% 🟢)2.507s21.06x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.779s (~)14.029s (~)0.250s51.00x
🐘 PostgresNitro13.817s (~)14.020s (~)0.203s51.00x
💻 LocalExpress13.835s (~)14.027s (~)0.192s51.00x
🐘 PostgresExpress13.855s (~)14.020s (~)0.165s51.01x
🐘 PostgresNext.js (Turbopack)14.544s (~)15.019s (~)0.475s41.06x
💻 LocalNext.js (Turbopack)14.605s (+1.6%)15.030s (~)0.425s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.570s (-35.3% 🟢)24.870s (-32.6% 🟢)2.300s31.00x
▲ VercelNitro23.905s (-27.5% 🟢)25.993s (-25.4% 🟢)2.088s31.06x
▲ VercelNext.js (Turbopack)24.384s (-20.8% 🟢)26.434s (-19.6% 🟢)2.050s31.08x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.403s (-1.3%)13.024s (~)0.621s71.00x
💻 LocalExpress12.512s (-0.8%)13.024s (~)0.511s71.01x
🐘 PostgresNitro12.647s (~)13.023s (~)0.376s71.02x
🐘 PostgresExpress12.680s (+0.7%)13.019s (~)0.339s71.02x
🐘 PostgresNext.js (Turbopack)13.856s (-0.8%)14.020s (-1.0%)0.164s71.12x
💻 LocalNext.js (Turbopack)14.047s (+3.2%)14.744s (+5.1% 🔺)0.697s71.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express29.958s (-13.8% 🟢)32.678s (-11.9% 🟢)2.720s31.00x
▲ VercelNitro30.838s (-18.6% 🟢)33.293s (-15.7% 🟢)2.455s31.03x
▲ VercelNext.js (Turbopack)31.778s (-9.5% 🟢)33.997s (-9.4% 🟢)2.218s31.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.207s (+1.2%)2.006s (~)0.800s151.00x
💻 LocalNitro1.207s (+3.8%)2.006s (~)0.799s151.00x
🐘 PostgresNitro1.214s (~)2.008s (~)0.794s151.01x
🐘 PostgresExpress1.214s (+0.7%)2.009s (~)0.795s151.01x
🐘 PostgresNext.js (Turbopack)1.277s (-5.9% 🟢)2.007s (-3.3%)0.731s151.06x
💻 LocalNext.js (Turbopack)1.340s (+0.8%)2.007s (~)0.667s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.289s (-26.0% 🟢)3.763s (-17.9% 🟢)1.474s81.00x
▲ VercelExpress2.513s (-1.5%)4.117s (+1.1%)1.604s81.10x
▲ VercelNext.js (Turbopack)3.039s (+29.5% 🔺)4.619s (+15.0% 🔺)1.580s71.33x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.367s (+1.9%)2.393s (-4.6%)1.026s131.00x
🐘 PostgresExpress1.387s (+2.2%)2.223s (-11.3% 🟢)0.836s141.01x
🐘 PostgresNext.js (Turbopack)1.662s (+1.4%)2.295s (-8.5% 🟢)0.633s141.22x
💻 LocalNitro1.705s (+4.7%)2.006s (~)0.301s151.25x
💻 LocalExpress1.838s (-3.0%)2.150s (~)0.313s141.34x
💻 LocalNext.js (Turbopack)1.860s (+16.7% 🔺)2.075s (+3.4%)0.215s151.36x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.813s (-31.6% 🟢)4.734s (-21.8% 🟢)1.920s71.00x
▲ VercelNitro2.996s (-20.5% 🟢)4.799s (-11.1% 🟢)1.803s71.06x
▲ VercelNext.js (Turbopack)3.230s (-7.9% 🟢)5.201s (-1.0%)1.971s61.15x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.821s (+12.1% 🔺)3.887s (-6.1% 🟢)2.066s81.00x
🐘 PostgresExpress1.827s (+12.6% 🔺)4.011s (+3.2%)2.183s81.00x
🐘 PostgresNext.js (Turbopack)3.481s (-9.1% 🟢)4.299s (-3.3%)0.818s71.91x
💻 LocalNitro4.569s (+13.6% 🔺)5.015s (+9.5% 🔺)0.446s72.51x
💻 LocalExpress5.321s (+1.7%)5.846s (~)0.525s62.92x
💻 LocalNext.js (Turbopack)5.870s (+28.6% 🔺)6.348s (+22.6% 🔺)0.478s63.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.964s (-13.3% 🟢)6.243s (-4.8%)2.278s51.00x
▲ VercelNitro4.013s (-5.5% 🟢)6.018s (+5.1% 🔺)2.005s51.01x
▲ VercelNext.js (Turbopack)4.065s (+7.3% 🔺)6.166s (+14.0% 🔺)2.101s51.03x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.218s (~)2.007s (~)0.789s151.00x
🐘 PostgresExpress1.220s (+0.8%)2.008s (~)0.789s151.00x
🐘 PostgresNext.js (Turbopack)1.272s (-1.1%)2.008s (~)0.736s151.04x
💻 LocalNext.js (Turbopack)1.424s (+3.3%)2.008s (~)0.584s151.17x
💻 LocalNitro1.560s (+3.2%)2.007s (~)0.447s151.28x
💻 LocalExpress1.593s (-1.3%)2.007s (~)0.414s151.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.290s (-0.7%)3.776s (-4.3%)1.486s81.00x
▲ VercelNitro2.297s (-64.6% 🟢)3.691s (-54.0% 🟢)1.394s91.00x
▲ VercelExpress2.406s (+5.8% 🔺)3.962s (+2.8%)1.556s81.05x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.357s (~)2.315s (-10.7% 🟢)0.958s131.00x
🐘 PostgresExpress1.406s (+0.8%)2.592s (+4.9%)1.186s121.04x
🐘 PostgresNext.js (Turbopack)1.583s (+3.0%)2.152s (-3.3%)0.569s141.17x
💻 LocalNext.js (Turbopack)2.095s (+8.5% 🔺)2.918s (+31.3% 🔺)0.823s111.54x
💻 LocalNitro2.187s (+22.2% 🔺)2.676s (+29.1% 🔺)0.489s121.61x
💻 LocalExpress2.349s (-8.8% 🟢)2.827s (-9.0% 🟢)0.479s111.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.794s (-25.4% 🟢)4.495s (-17.7% 🟢)1.700s71.00x
▲ VercelNext.js (Turbopack)3.325s (-17.1% 🟢)5.058s (-12.0% 🟢)1.733s61.19x
▲ VercelExpress3.330s (~)5.327s (~)1.997s61.19x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.572s (-9.0% 🟢)4.011s (+3.2%)2.439s81.00x
🐘 PostgresExpress1.646s (-6.1% 🟢)4.014s (-2.9%)2.368s81.05x
🐘 PostgresNext.js (Turbopack)2.971s (-22.6% 🟢)3.764s (-15.3% 🟢)0.793s81.89x
💻 LocalNitro5.218s (+9.5% 🔺)5.684s (+6.3% 🔺)0.467s63.32x
💻 LocalExpress6.467s (+2.0%)7.216s (+2.8%)0.749s54.11x
💻 LocalNext.js (Turbopack)6.475s (+22.3% 🔺)7.018s (+16.7% 🔺)0.543s54.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.355s (-33.9% 🟢)6.132s (-26.5% 🟢)1.777s61.00x
▲ VercelNext.js (Turbopack)4.600s (-6.6% 🟢)6.524s (~)1.924s51.06x
▲ VercelExpress4.640s (-11.4% 🟢)6.693s (-3.9%)2.053s51.07x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.593s (-8.0% 🟢)1.023s (-1.8%)0.430s591.00x
💻 LocalNitro0.596s (+2.2%)1.005s (~)0.409s601.00x
🐘 PostgresExpress0.609s (+2.8%)1.023s (+1.8%)0.415s591.03x
💻 LocalExpress0.625s (-5.3% 🟢)1.005s (-3.3%)0.380s601.05x
🐘 PostgresNext.js (Turbopack)0.819s (-3.7%)1.006s (-1.7%)0.187s601.38x
💻 LocalNext.js (Turbopack)0.942s (+6.4% 🔺)1.096s (+3.7%)0.154s551.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.725s (-13.0% 🟢)7.514s (-12.9% 🟢)1.790s91.00x
▲ VercelNext.js (Turbopack)5.798s (-21.9% 🟢)7.584s (-18.8% 🟢)1.786s81.01x
▲ VercelNitro5.831s (-31.1% 🟢)7.298s (-27.8% 🟢)1.467s91.02x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.384s (-3.6%)2.007s (-1.1%)0.623s451.00x
🐘 PostgresNitro1.385s (-4.2%)2.008s (~)0.623s451.00x
💻 LocalNitro1.502s (+1.7%)2.006s (~)0.504s451.09x
💻 LocalExpress1.575s (-0.6%)2.007s (~)0.432s451.14x
🐘 PostgresNext.js (Turbopack)1.935s (-5.0% 🟢)2.150s (-16.1% 🟢)0.216s421.40x
💻 LocalNext.js (Turbopack)2.277s (+7.0% 🔺)3.009s (~)0.732s301.65x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.711s (-31.9% 🟢)16.057s (-26.5% 🟢)2.346s61.00x
▲ VercelNitro14.119s (-24.8% 🟢)16.038s (-22.7% 🟢)1.919s61.03x
▲ VercelNext.js (Turbopack)14.780s (-25.8% 🟢)16.884s (-24.6% 🟢)2.104s61.08x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.788s (-1.2%)3.166s (+0.9%)0.379s381.00x
🐘 PostgresExpress2.860s (+2.9%)3.252s (+3.7%)0.392s371.03x
💻 LocalNitro3.279s (+3.5%)4.009s (+2.5%)0.731s301.18x
💻 LocalExpress3.391s (+1.4%)4.010s (~)0.619s301.22x
🐘 PostgresNext.js (Turbopack)3.810s (-5.3% 🟢)4.043s (-8.2% 🟢)0.233s301.37x
💻 LocalNext.js (Turbopack)4.748s (+4.6%)5.054s (~)0.306s241.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.736s (-18.0% 🟢)32.583s (-14.7% 🟢)2.847s41.00x
▲ VercelExpress29.831s (-22.9% 🟢)32.492s (-19.6% 🟢)2.660s41.00x
▲ VercelNext.js (Turbopack)32.381s (-27.8% 🟢)35.293s (-24.4% 🟢)2.912s41.09x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.253s (-3.1%)1.006s (~)0.753s601.00x
🐘 PostgresExpress0.264s (+1.1%)1.023s (+1.7%)0.760s591.04x
🐘 PostgresNext.js (Turbopack)0.301s (-2.4%)1.006s (~)0.705s601.19x
💻 LocalExpress0.409s (-4.4%)1.005s (~)0.595s601.62x
💻 LocalNitro0.425s (-1.5%)1.005s (~)0.579s601.68x
💻 LocalNext.js (Turbopack)0.594s (-8.2% 🟢)1.006s (-4.9%)0.413s602.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.844s (-36.1% 🟢)3.799s (-24.5% 🟢)1.954s161.00x
▲ VercelExpress1.910s (-46.5% 🟢)3.900s (-30.2% 🟢)1.990s161.04x
▲ VercelNext.js (Turbopack)1.931s (-37.1% 🟢)3.841s (-23.4% 🟢)1.910s161.05x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.410s (-10.2% 🟢)1.029s (-4.6%)0.619s881.00x
🐘 PostgresExpress0.418s (+1.8%)1.078s (+7.1% 🔺)0.659s841.02x
🐘 PostgresNext.js (Turbopack)0.575s (-15.2% 🟢)1.118s (-12.4% 🟢)0.543s811.40x
💻 LocalNitro2.162s (+7.5% 🔺)2.687s (+10.1% 🔺)0.524s345.27x
💻 LocalExpress2.221s (+1.4%)2.769s (-2.9%)0.548s335.41x
💻 LocalNext.js (Turbopack)2.825s (+10.4% 🔺)3.512s (+7.7% 🔺)0.687s266.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.226s (-37.4% 🟢)5.211s (-27.8% 🟢)1.986s181.00x
▲ VercelNitro3.370s (-3.3%)5.242s (+1.4%)1.872s181.04x
▲ VercelNext.js (Turbopack)3.799s (~)5.760s (~)1.961s161.18x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.804s (-4.7%)1.527s (+3.8%)0.724s791.00x
🐘 PostgresExpress0.831s (-2.0%)1.471s (-2.4%)0.640s821.03x
🐘 PostgresNext.js (Turbopack)2.802s (-9.3% 🟢)3.651s (-4.6%)0.849s333.49x
💻 LocalNitro9.753s (+11.8% 🔺)10.447s (+12.9% 🔺)0.694s1212.14x
💻 LocalExpress9.767s (-1.0%)10.280s (~)0.514s1212.15x
💻 LocalNext.js (Turbopack)12.460s (+18.8% 🔺)13.337s (+17.1% 🔺)0.877s1015.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.603s (-17.0% 🟢)7.854s (-9.0% 🟢)2.251s161.00x
▲ VercelExpress5.898s (-13.7% 🟢)8.080s (-6.3% 🟢)2.182s151.05x
▲ VercelNext.js (Turbopack)7.576s (+5.3% 🔺)9.754s (+8.8% 🔺)2.178s131.35x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.163s (~)2.005s (~)0.010s (+1.0%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (-2.4%)1.999s (~)0.002s (+33.3% 🔺)2.011s (~)0.842s101.01x
💻 LocalExpress1.172s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.847s101.01x
🐘 PostgresExpress1.185s (+0.9%)2.000s (~)0.002s (+54.5% 🔺)2.012s (~)0.827s101.02x
🐘 PostgresNext.js (Turbopack)1.228s (-1.3%)2.001s (~)0.001s (-25.0% 🟢)2.011s (~)0.783s101.06x
💻 LocalNext.js (Turbopack)1.228s (-1.7%)2.004s (~)0.012s (+13.1% 🔺)2.021s (~)0.793s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.289s (-3.1%)3.325s (-5.8% 🟢)0.785s (+11.6% 🔺)4.593s (-4.1%)2.304s101.00x
▲ VercelNext.js (Turbopack)2.336s (-12.9% 🟢)3.384s (-9.7% 🟢)0.770s (-8.0% 🟢)4.613s (-9.3% 🟢)2.276s101.02x
▲ VercelNitro2.345s (-8.0% 🟢)3.480s (-6.3% 🟢)0.621s (-16.1% 🟢)4.531s (-8.7% 🟢)2.187s101.02x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.586s (+1.3%)2.009s (~)0.013s (+18.1% 🔺)2.025s (~)0.439s301.00x
💻 LocalExpress1.593s (-1.1%)2.011s (~)0.013s (+2.9%)2.026s (~)0.433s301.00x
🐘 PostgresExpress1.602s (~)2.006s (~)0.005s (+1.4%)2.027s (~)0.425s301.01x
🐘 PostgresNitro1.603s (-2.2%)2.004s (~)0.005s (-0.6%)2.026s (~)0.423s301.01x
🐘 PostgresNext.js (Turbopack)1.753s (-2.2%)2.010s (~)0.005s (-2.4%)2.026s (~)0.273s301.11x
💻 LocalNext.js (Turbopack)1.889s (+8.2% 🔺)2.080s (+3.7%)0.014s (+7.7% 🔺)2.098s (+3.6%)0.209s291.19x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.212s (-9.9% 🟢)7.300s (-12.6% 🟢)0.270s (-21.4% 🟢)8.082s (-12.4% 🟢)1.869s81.00x
▲ VercelNext.js (Turbopack)6.304s (-8.2% 🟢)7.685s (-10.4% 🟢)0.289s (-7.7% 🟢)8.468s (-9.7% 🟢)2.164s81.01x
▲ VercelNitro6.373s (-5.4% 🟢)7.745s (-3.7%)0.208s (-5.5% 🟢)8.596s (-2.4%)2.223s71.03x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.797s (~)1.063s (+1.4%)0.000s (+Infinity% 🔺)1.079s (+1.8%)0.282s561.00x
🐘 PostgresNitro0.798s (-1.8%)1.067s (~)0.000s (+1.8%)1.099s (+1.8%)0.301s551.00x
🐘 PostgresNext.js (Turbopack)1.016s (-1.1%)1.500s (-7.5% 🟢)0.000s (-7.5% 🟢)1.507s (-7.6% 🟢)0.491s401.28x
💻 LocalExpress1.413s (~)2.012s (~)0.000s (~)2.015s (~)0.602s301.77x
💻 LocalNitro1.468s (+8.6% 🔺)2.014s (~)0.000s (-42.9% 🟢)2.017s (~)0.549s301.84x
💻 LocalNext.js (Turbopack)1.592s (+5.3% 🔺)2.015s (~)0.000s (-18.2% 🟢)2.019s (~)0.426s302.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.933s (-28.7% 🟢)4.414s (-23.2% 🟢)0.000s (NaN%)4.919s (-21.2% 🟢)1.986s131.00x
▲ VercelExpress2.951s (-20.1% 🟢)4.593s (-6.3% 🟢)0.000s (+Infinity% 🔺)5.072s (-8.7% 🟢)2.121s121.01x
▲ VercelNext.js (Turbopack)3.232s (-14.2% 🟢)4.626s (-13.3% 🟢)0.000s (-100.0% 🟢)5.093s (-13.4% 🟢)1.860s121.10x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.560s (-7.9% 🟢)2.066s (-5.0% 🟢)0.000s (-3.4%)2.136s (-2.7%)0.576s291.00x
🐘 PostgresExpress1.628s (-1.5%)2.137s (-1.8%)0.000s (+Infinity% 🔺)2.156s (-1.4%)0.528s281.04x
🐘 PostgresNext.js (Turbopack)2.214s (-1.8%)2.776s (+1.7%)0.000s (NaN%)2.782s (+1.6%)0.568s221.42x
💻 LocalExpress3.162s (-4.7%)3.965s (-1.5%)0.000s (-62.5% 🟢)3.968s (-1.6%)0.806s162.03x
💻 LocalNitro3.179s (+3.2%)3.734s (-4.2%)0.001s (+61.3% 🔺)3.737s (-4.2%)0.558s172.04x
💻 LocalNext.js (Turbopack)3.265s (+15.1% 🔺)3.734s (+9.3% 🔺)0.000s (-41.2% 🟢)3.738s (+9.3% 🔺)0.473s172.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.284s (-29.4% 🟢)6.003s (-21.0% 🟢)0.000s (-100.0% 🟢)6.482s (-20.8% 🟢)2.198s101.00x
▲ VercelNitro4.573s (-7.1% 🟢)6.058s (-1.7%)0.000s (NaN%)6.512s (-4.2%)1.939s101.07x
▲ VercelNext.js (Turbopack)5.009s (-29.1% 🟢)6.602s (-19.8% 🟢)0.000s (-100.0% 🟢)7.080s (-19.2% 🟢)2.071s91.17x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro15/21
🐘 PostgresNitro16/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202191661
✅ 💻 Local Development189502192114
✅ 📦 Local Production189502192114
✅ 🐘 Local Postgres188102332114
✅ 🪟 Windows15100151
❌ 📋 Other87811781057
Total8142110689211

❌ Failed Tests

📋 Other (1 failed)

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

  • webhookWorkflow | wrun_01KV7X2YYXA571EE20AYFJ50VB

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125026
✅ example125026
✅ express125026
✅ fastify125026
✅ hono125026
✅ nextjs-turbopack14902
✅ nextjs-webpack14902
✅ nitro125026
✅ nuxt125026
✅ sveltekit14407
✅ vite125026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable125026
✅ express-stable125026
✅ fastify-stable125026
✅ hono-stable125026
✅ nextjs-turbopack-canary131020
✅ nextjs-turbopack-stable-lazy-discovery-disabled15001
✅ nextjs-turbopack-stable-lazy-discovery-enabled15001
✅ nextjs-webpack-canary131020
✅ nextjs-webpack-stable-lazy-discovery-disabled15001
✅ nextjs-webpack-stable-lazy-discovery-enabled15001
✅ nitro-stable125026
✅ nuxt-stable125026
✅ sveltekit-stable14407
✅ vite-stable125026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15100
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable126025
✅ e2e-local-dev-tanstack-start-126025
✅ e2e-local-postgres-nest-stable125026
✅ e2e-local-postgres-tanstack-start-125026
✅ e2e-local-prod-nest-stable126025
✅ e2e-local-prod-tanstack-start-126025
❌ e2e-vercel-prod-tanstack-start124126

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

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

This PR adds bounded queue-redelivery behavior for replay-time RuntimeDecryptionErrors in @workflow/core, allowing managed worlds to retry transient persisted-data read corruption before marking the run failed.

Changes:

  • Adds a decryption-failure retry decision helper and retry budget constant.
  • Integrates the helper into the workflow runtime error path before writing run_failed.
  • Adds unit and runtime coverage plus a patch changeset.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/decryption-failure.tsAdds redelivery decision logic and logging for replay decryption failures.
packages/core/src/runtime/decryption-failure.test.tsCovers managed/in-process world behavior and retry-budget boundaries.
packages/core/src/runtime/constants.tsDefines the decryption-failure retry budget.
packages/core/src/runtime.tsRedrives managed-world runs on eligible RuntimeDecryptionErrors before failing the run.
packages/core/src/runtime.test.tsAdds end-to-end key-mismatch tests for redelivery and failure behavior.
.changeset/requeue-on-decryption-failure.mdAdds a patch changeset for @workflow/core.

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

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: blocking issues found

return false;
}

if (attempt <= DECRYPTION_FAILURE_MAX_RETRIES) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Blocking

RuntimeDecryptionError also covers encrypt-side AES-GCM failures (context.operation === "encrypt"), but this helper redrives every RuntimeDecryptionError on managed worlds. A fresh queue delivery can only help when decrypting persisted bytes, not when serializing/encrypting a new workflow payload. This can re-run workflow code up to 3 times before failing. Please gate redelivery on error.context?.operation === "decrypt".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — shouldRedriveOnDecryptionFailure now returns false immediately when error.context?.operation !== "decrypt", so encrypt-side AES-GCM failures fail the run without redelivery (re-fetching cannot change a payload being serialized fresh). Added a unit test covering the encrypt case on a managed world. Commit 558cc3f.

// queue delivery re-fetches the event log + ref
// payloads and can succeed. On managed Worlds, exit the
// process (which the platform turns into a redelivery)
// for a bounded number of attempts before failing the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

This changes the user-facing behavior for runtime decryption failures on managed worlds: they now redeliver before the terminal run_failed. The existing runtime-decryption-failed docs still say the run fails immediately, so those docs should be updated as part of this behavior change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — updated errors/runtime-decryption-failed.mdx (both v4 and v5) to document the bounded queue-redelivery behavior on managed worlds: decrypt failures are now retried before the terminal run_failed, while encrypt-side failures and in-process worlds still fail immediately. Commit 558cc3f.

Comment threadpackages/core/src/runtime.test.ts Outdated
// Past DECRYPTION_FAILURE_MAX_RETRIES (3) but under MAX_QUEUE_DELIVERIES
// (48), so we exercise the decryption-failure budget, not the queue
// max-deliveries guard.
attempt: 10,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This test duplicates the retry/delivery constants in comments and uses attempt: 10 instead of deriving from the constant it is testing. Please import the constants and use DECRYPTION_FAILURE_MAX_RETRIES + 1, with an assertion that it remains below MAX_QUEUE_DELIVERIES, so future constant changes don't silently stale the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — the past-budget test now derives attempt = DECRYPTION_FAILURE_MAX_RETRIES + 1 from the imported constant (no duplicated magic numbers) and asserts attempt < MAX_QUEUE_DELIVERIES, so a future constant change cannot silently stale the test. Commit 558cc3f.

Resolve conflicts in runtime.ts, runtime.test.ts, and constants.ts
(decryption-failure redelivery + replay-divergence recovery coexist).
Address review feedback:
- Gate decryption-failure redelivery on operation === 'decrypt' so
encrypt-side AES-GCM failures fail immediately (re-fetching cannot help).
- Derive the past-budget test attempt from DECRYPTION_FAILURE_MAX_RETRIES + 1
and assert it stays below MAX_QUEUE_DELIVERIES.
- Document the bounded queue-redelivery behavior in runtime-decryption-failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

feat(core): bounded queue-redelivery retry on decryption failure during replay - #2166

Open
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure
Open

feat(core): bounded queue-redelivery retry on decryption failure during replay#2166
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Follow-up to #2145 (the RuntimeDecryptionError attribution fix), implementing the bounded-redelivery behavior @pranaygp described in this comment.

An AES-GCM authentication failure is terminal for the bytes/key of the current attempt — we must never continue executing the workflow on top of data we couldn't decrypt. But the failure is often not terminal for the run: when the ciphertext came from a transiently truncated or corrupted read of remotely-persisted data (a partial /refs response, an edge-cache miss returning a partial 200, a proxy drop during streaming), a fresh queue delivery re-fetches the event log and ref payloads from scratch and can succeed.

Previously we committed run_failed immediately, turning a potentially recoverable read failure into a terminal workflow failure.

What changed

Mirrors the existing replay-timeout bounded-redelivery precedent (handleReplayBudgetExhausted):

  • Managed worlds (processExitTriggersQueueRedelivery === true, e.g. world-vercel): on attempts ≤ DECRYPTION_FAILURE_MAX_RETRIES (3), the run handler exits the process — which the platform turns into a queue redelivery — so replay restarts from freshly-fetched persisted data. Once the retry budget is exhausted, it commits run_failed with RUNTIME_ERROR.
  • In-process worlds (world-local, dev servers, custom in-process worlds): no queue to re-fetch from, and process.exit() would kill the user's host, so the run fails immediately with RUNTIME_ERROR.

New shouldRedriveOnDecryptionFailure() helper (runtime/decryption-failure.ts) is pure (logging only) and returns whether the caller should redrive, keeping the process.exit / run_failed decision at the single existing call site in the run handler.

Test coverage

  • runtime/decryption-failure.test.ts — unit tests for the helper across managed/in-process worlds and the retry-budget boundary.
  • runtime.test.ts — end-to-end tests driving a real key mismatch (input encrypted with key A, run-key resolves to key B → auth-tag failure during input hydration):
    • early attempt on a managed world → process.exit(1), no run_failed
    • past the retry budget on a managed world → run_failed with RUNTIME_ERROR
    • in-process world → run_failed immediately, no exit

All @workflow/core tests pass (1073), full repo typecheck passes (40/40).

Notes

The longer-term improvement @pranaygp mentioned — detecting response truncation / integrity failure at the /refs transport boundary to classify the retryable case more directly — is out of scope here and would be a separate change on the world / workflow-server side.

…ng replay
Follow-up to #2145. An AES-GCM auth failure (RuntimeDecryptionError) is
terminal for the current attempt's bytes, but often transient at the run
level: when the ciphertext came from a truncated/corrupted read of
remotely-persisted data (e.g. a partial /refs response), a fresh queue
delivery re-fetches the event log + ref payloads and can succeed.
Mirror the replay-timeout bounded-redelivery precedent: on managed worlds
(processExitTriggersQueueRedelivery), exit the process to trigger queue
redelivery for up to DECRYPTION_FAILURE_MAX_RETRIES attempts, then commit
run_failed as RUNTIME_ERROR. In-process worlds fail immediately (no queue
to re-fetch from, and exiting would kill the host).
CopilotAI review requested due to automatic review settings May 29, 2026 19:24
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 29, 2026 19:24
@changeset-bot

changeset-botBot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 558cc3f

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

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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 29, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+1.7%)1.006s (~)0.963s101.00x
💻 LocalExpress0.043s (-10.3% 🟢)1.007s (~)0.964s101.02x
🐘 PostgresNitro0.064s (-1.5%)1.011s (~)0.948s101.50x
🐘 PostgresNext.js (Turbopack)0.068s (-7.0% 🟢)1.011s (~)0.944s101.59x
🐘 PostgresExpress0.068s (+11.0% 🔺)1.012s (~)0.944s101.59x
💻 LocalNext.js (Turbopack)0.069s (+13.3% 🔺)1.006s (~)0.937s101.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.279s (-74.8% 🟢)2.020s (-43.5% 🟢)1.741s101.00x
▲ VercelNext.js (Turbopack)0.313s (-84.2% 🟢)2.337s (-43.1% 🟢)2.024s101.12x
▲ VercelNitro0.400s (-33.4% 🟢)2.213s (-16.8% 🟢)1.813s101.43x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.094s (~)2.006s (~)0.912s101.00x
💻 LocalExpress1.095s (~)2.007s (~)0.912s101.00x
🐘 PostgresExpress1.115s (~)2.010s (~)0.894s101.02x
🐘 PostgresNitro1.135s (+1.4%)2.009s (~)0.875s101.04x
🐘 PostgresNext.js (Turbopack)1.142s (-1.4%)2.011s (~)0.869s101.04x
💻 LocalNext.js (Turbopack)1.145s (~)2.007s (~)0.862s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.704s (-36.5% 🟢)3.836s (-15.2% 🟢)2.132s101.00x
▲ VercelExpress1.800s (-32.8% 🟢)3.830s (-23.4% 🟢)2.030s101.06x
▲ VercelNitro1.865s (-33.9% 🟢)3.713s (-18.3% 🟢)1.847s101.09x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.503s (~)11.022s (~)0.519s31.00x
💻 LocalExpress10.545s (~)11.023s (~)0.478s31.00x
🐘 PostgresExpress10.584s (~)11.018s (~)0.435s31.01x
🐘 PostgresNitro10.588s (~)11.015s (~)0.427s31.01x
💻 LocalNext.js (Turbopack)10.868s (+0.8%)11.023s (~)0.155s31.03x
🐘 PostgresNext.js (Turbopack)10.901s (~)11.016s (~)0.115s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)14.283s (-33.3% 🟢)16.662s (-28.4% 🟢)2.379s21.00x
▲ VercelNitro14.988s (-17.5% 🟢)17.110s (-13.9% 🟢)2.122s21.05x
▲ VercelExpress15.147s (-6.9% 🟢)17.654s (-5.2% 🟢)2.507s21.06x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.779s (~)14.029s (~)0.250s51.00x
🐘 PostgresNitro13.817s (~)14.020s (~)0.203s51.00x
💻 LocalExpress13.835s (~)14.027s (~)0.192s51.00x
🐘 PostgresExpress13.855s (~)14.020s (~)0.165s51.01x
🐘 PostgresNext.js (Turbopack)14.544s (~)15.019s (~)0.475s41.06x
💻 LocalNext.js (Turbopack)14.605s (+1.6%)15.030s (~)0.425s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.570s (-35.3% 🟢)24.870s (-32.6% 🟢)2.300s31.00x
▲ VercelNitro23.905s (-27.5% 🟢)25.993s (-25.4% 🟢)2.088s31.06x
▲ VercelNext.js (Turbopack)24.384s (-20.8% 🟢)26.434s (-19.6% 🟢)2.050s31.08x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.403s (-1.3%)13.024s (~)0.621s71.00x
💻 LocalExpress12.512s (-0.8%)13.024s (~)0.511s71.01x
🐘 PostgresNitro12.647s (~)13.023s (~)0.376s71.02x
🐘 PostgresExpress12.680s (+0.7%)13.019s (~)0.339s71.02x
🐘 PostgresNext.js (Turbopack)13.856s (-0.8%)14.020s (-1.0%)0.164s71.12x
💻 LocalNext.js (Turbopack)14.047s (+3.2%)14.744s (+5.1% 🔺)0.697s71.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express29.958s (-13.8% 🟢)32.678s (-11.9% 🟢)2.720s31.00x
▲ VercelNitro30.838s (-18.6% 🟢)33.293s (-15.7% 🟢)2.455s31.03x
▲ VercelNext.js (Turbopack)31.778s (-9.5% 🟢)33.997s (-9.4% 🟢)2.218s31.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.207s (+1.2%)2.006s (~)0.800s151.00x
💻 LocalNitro1.207s (+3.8%)2.006s (~)0.799s151.00x
🐘 PostgresNitro1.214s (~)2.008s (~)0.794s151.01x
🐘 PostgresExpress1.214s (+0.7%)2.009s (~)0.795s151.01x
🐘 PostgresNext.js (Turbopack)1.277s (-5.9% 🟢)2.007s (-3.3%)0.731s151.06x
💻 LocalNext.js (Turbopack)1.340s (+0.8%)2.007s (~)0.667s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.289s (-26.0% 🟢)3.763s (-17.9% 🟢)1.474s81.00x
▲ VercelExpress2.513s (-1.5%)4.117s (+1.1%)1.604s81.10x
▲ VercelNext.js (Turbopack)3.039s (+29.5% 🔺)4.619s (+15.0% 🔺)1.580s71.33x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.367s (+1.9%)2.393s (-4.6%)1.026s131.00x
🐘 PostgresExpress1.387s (+2.2%)2.223s (-11.3% 🟢)0.836s141.01x
🐘 PostgresNext.js (Turbopack)1.662s (+1.4%)2.295s (-8.5% 🟢)0.633s141.22x
💻 LocalNitro1.705s (+4.7%)2.006s (~)0.301s151.25x
💻 LocalExpress1.838s (-3.0%)2.150s (~)0.313s141.34x
💻 LocalNext.js (Turbopack)1.860s (+16.7% 🔺)2.075s (+3.4%)0.215s151.36x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.813s (-31.6% 🟢)4.734s (-21.8% 🟢)1.920s71.00x
▲ VercelNitro2.996s (-20.5% 🟢)4.799s (-11.1% 🟢)1.803s71.06x
▲ VercelNext.js (Turbopack)3.230s (-7.9% 🟢)5.201s (-1.0%)1.971s61.15x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.821s (+12.1% 🔺)3.887s (-6.1% 🟢)2.066s81.00x
🐘 PostgresExpress1.827s (+12.6% 🔺)4.011s (+3.2%)2.183s81.00x
🐘 PostgresNext.js (Turbopack)3.481s (-9.1% 🟢)4.299s (-3.3%)0.818s71.91x
💻 LocalNitro4.569s (+13.6% 🔺)5.015s (+9.5% 🔺)0.446s72.51x
💻 LocalExpress5.321s (+1.7%)5.846s (~)0.525s62.92x
💻 LocalNext.js (Turbopack)5.870s (+28.6% 🔺)6.348s (+22.6% 🔺)0.478s63.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.964s (-13.3% 🟢)6.243s (-4.8%)2.278s51.00x
▲ VercelNitro4.013s (-5.5% 🟢)6.018s (+5.1% 🔺)2.005s51.01x
▲ VercelNext.js (Turbopack)4.065s (+7.3% 🔺)6.166s (+14.0% 🔺)2.101s51.03x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.218s (~)2.007s (~)0.789s151.00x
🐘 PostgresExpress1.220s (+0.8%)2.008s (~)0.789s151.00x
🐘 PostgresNext.js (Turbopack)1.272s (-1.1%)2.008s (~)0.736s151.04x
💻 LocalNext.js (Turbopack)1.424s (+3.3%)2.008s (~)0.584s151.17x
💻 LocalNitro1.560s (+3.2%)2.007s (~)0.447s151.28x
💻 LocalExpress1.593s (-1.3%)2.007s (~)0.414s151.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.290s (-0.7%)3.776s (-4.3%)1.486s81.00x
▲ VercelNitro2.297s (-64.6% 🟢)3.691s (-54.0% 🟢)1.394s91.00x
▲ VercelExpress2.406s (+5.8% 🔺)3.962s (+2.8%)1.556s81.05x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.357s (~)2.315s (-10.7% 🟢)0.958s131.00x
🐘 PostgresExpress1.406s (+0.8%)2.592s (+4.9%)1.186s121.04x
🐘 PostgresNext.js (Turbopack)1.583s (+3.0%)2.152s (-3.3%)0.569s141.17x
💻 LocalNext.js (Turbopack)2.095s (+8.5% 🔺)2.918s (+31.3% 🔺)0.823s111.54x
💻 LocalNitro2.187s (+22.2% 🔺)2.676s (+29.1% 🔺)0.489s121.61x
💻 LocalExpress2.349s (-8.8% 🟢)2.827s (-9.0% 🟢)0.479s111.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.794s (-25.4% 🟢)4.495s (-17.7% 🟢)1.700s71.00x
▲ VercelNext.js (Turbopack)3.325s (-17.1% 🟢)5.058s (-12.0% 🟢)1.733s61.19x
▲ VercelExpress3.330s (~)5.327s (~)1.997s61.19x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.572s (-9.0% 🟢)4.011s (+3.2%)2.439s81.00x
🐘 PostgresExpress1.646s (-6.1% 🟢)4.014s (-2.9%)2.368s81.05x
🐘 PostgresNext.js (Turbopack)2.971s (-22.6% 🟢)3.764s (-15.3% 🟢)0.793s81.89x
💻 LocalNitro5.218s (+9.5% 🔺)5.684s (+6.3% 🔺)0.467s63.32x
💻 LocalExpress6.467s (+2.0%)7.216s (+2.8%)0.749s54.11x
💻 LocalNext.js (Turbopack)6.475s (+22.3% 🔺)7.018s (+16.7% 🔺)0.543s54.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.355s (-33.9% 🟢)6.132s (-26.5% 🟢)1.777s61.00x
▲ VercelNext.js (Turbopack)4.600s (-6.6% 🟢)6.524s (~)1.924s51.06x
▲ VercelExpress4.640s (-11.4% 🟢)6.693s (-3.9%)2.053s51.07x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.593s (-8.0% 🟢)1.023s (-1.8%)0.430s591.00x
💻 LocalNitro0.596s (+2.2%)1.005s (~)0.409s601.00x
🐘 PostgresExpress0.609s (+2.8%)1.023s (+1.8%)0.415s591.03x
💻 LocalExpress0.625s (-5.3% 🟢)1.005s (-3.3%)0.380s601.05x
🐘 PostgresNext.js (Turbopack)0.819s (-3.7%)1.006s (-1.7%)0.187s601.38x
💻 LocalNext.js (Turbopack)0.942s (+6.4% 🔺)1.096s (+3.7%)0.154s551.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.725s (-13.0% 🟢)7.514s (-12.9% 🟢)1.790s91.00x
▲ VercelNext.js (Turbopack)5.798s (-21.9% 🟢)7.584s (-18.8% 🟢)1.786s81.01x
▲ VercelNitro5.831s (-31.1% 🟢)7.298s (-27.8% 🟢)1.467s91.02x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.384s (-3.6%)2.007s (-1.1%)0.623s451.00x
🐘 PostgresNitro1.385s (-4.2%)2.008s (~)0.623s451.00x
💻 LocalNitro1.502s (+1.7%)2.006s (~)0.504s451.09x
💻 LocalExpress1.575s (-0.6%)2.007s (~)0.432s451.14x
🐘 PostgresNext.js (Turbopack)1.935s (-5.0% 🟢)2.150s (-16.1% 🟢)0.216s421.40x
💻 LocalNext.js (Turbopack)2.277s (+7.0% 🔺)3.009s (~)0.732s301.65x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.711s (-31.9% 🟢)16.057s (-26.5% 🟢)2.346s61.00x
▲ VercelNitro14.119s (-24.8% 🟢)16.038s (-22.7% 🟢)1.919s61.03x
▲ VercelNext.js (Turbopack)14.780s (-25.8% 🟢)16.884s (-24.6% 🟢)2.104s61.08x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.788s (-1.2%)3.166s (+0.9%)0.379s381.00x
🐘 PostgresExpress2.860s (+2.9%)3.252s (+3.7%)0.392s371.03x
💻 LocalNitro3.279s (+3.5%)4.009s (+2.5%)0.731s301.18x
💻 LocalExpress3.391s (+1.4%)4.010s (~)0.619s301.22x
🐘 PostgresNext.js (Turbopack)3.810s (-5.3% 🟢)4.043s (-8.2% 🟢)0.233s301.37x
💻 LocalNext.js (Turbopack)4.748s (+4.6%)5.054s (~)0.306s241.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.736s (-18.0% 🟢)32.583s (-14.7% 🟢)2.847s41.00x
▲ VercelExpress29.831s (-22.9% 🟢)32.492s (-19.6% 🟢)2.660s41.00x
▲ VercelNext.js (Turbopack)32.381s (-27.8% 🟢)35.293s (-24.4% 🟢)2.912s41.09x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.253s (-3.1%)1.006s (~)0.753s601.00x
🐘 PostgresExpress0.264s (+1.1%)1.023s (+1.7%)0.760s591.04x
🐘 PostgresNext.js (Turbopack)0.301s (-2.4%)1.006s (~)0.705s601.19x
💻 LocalExpress0.409s (-4.4%)1.005s (~)0.595s601.62x
💻 LocalNitro0.425s (-1.5%)1.005s (~)0.579s601.68x
💻 LocalNext.js (Turbopack)0.594s (-8.2% 🟢)1.006s (-4.9%)0.413s602.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.844s (-36.1% 🟢)3.799s (-24.5% 🟢)1.954s161.00x
▲ VercelExpress1.910s (-46.5% 🟢)3.900s (-30.2% 🟢)1.990s161.04x
▲ VercelNext.js (Turbopack)1.931s (-37.1% 🟢)3.841s (-23.4% 🟢)1.910s161.05x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.410s (-10.2% 🟢)1.029s (-4.6%)0.619s881.00x
🐘 PostgresExpress0.418s (+1.8%)1.078s (+7.1% 🔺)0.659s841.02x
🐘 PostgresNext.js (Turbopack)0.575s (-15.2% 🟢)1.118s (-12.4% 🟢)0.543s811.40x
💻 LocalNitro2.162s (+7.5% 🔺)2.687s (+10.1% 🔺)0.524s345.27x
💻 LocalExpress2.221s (+1.4%)2.769s (-2.9%)0.548s335.41x
💻 LocalNext.js (Turbopack)2.825s (+10.4% 🔺)3.512s (+7.7% 🔺)0.687s266.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.226s (-37.4% 🟢)5.211s (-27.8% 🟢)1.986s181.00x
▲ VercelNitro3.370s (-3.3%)5.242s (+1.4%)1.872s181.04x
▲ VercelNext.js (Turbopack)3.799s (~)5.760s (~)1.961s161.18x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.804s (-4.7%)1.527s (+3.8%)0.724s791.00x
🐘 PostgresExpress0.831s (-2.0%)1.471s (-2.4%)0.640s821.03x
🐘 PostgresNext.js (Turbopack)2.802s (-9.3% 🟢)3.651s (-4.6%)0.849s333.49x
💻 LocalNitro9.753s (+11.8% 🔺)10.447s (+12.9% 🔺)0.694s1212.14x
💻 LocalExpress9.767s (-1.0%)10.280s (~)0.514s1212.15x
💻 LocalNext.js (Turbopack)12.460s (+18.8% 🔺)13.337s (+17.1% 🔺)0.877s1015.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.603s (-17.0% 🟢)7.854s (-9.0% 🟢)2.251s161.00x
▲ VercelExpress5.898s (-13.7% 🟢)8.080s (-6.3% 🟢)2.182s151.05x
▲ VercelNext.js (Turbopack)7.576s (+5.3% 🔺)9.754s (+8.8% 🔺)2.178s131.35x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.163s (~)2.005s (~)0.010s (+1.0%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (-2.4%)1.999s (~)0.002s (+33.3% 🔺)2.011s (~)0.842s101.01x
💻 LocalExpress1.172s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.847s101.01x
🐘 PostgresExpress1.185s (+0.9%)2.000s (~)0.002s (+54.5% 🔺)2.012s (~)0.827s101.02x
🐘 PostgresNext.js (Turbopack)1.228s (-1.3%)2.001s (~)0.001s (-25.0% 🟢)2.011s (~)0.783s101.06x
💻 LocalNext.js (Turbopack)1.228s (-1.7%)2.004s (~)0.012s (+13.1% 🔺)2.021s (~)0.793s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.289s (-3.1%)3.325s (-5.8% 🟢)0.785s (+11.6% 🔺)4.593s (-4.1%)2.304s101.00x
▲ VercelNext.js (Turbopack)2.336s (-12.9% 🟢)3.384s (-9.7% 🟢)0.770s (-8.0% 🟢)4.613s (-9.3% 🟢)2.276s101.02x
▲ VercelNitro2.345s (-8.0% 🟢)3.480s (-6.3% 🟢)0.621s (-16.1% 🟢)4.531s (-8.7% 🟢)2.187s101.02x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.586s (+1.3%)2.009s (~)0.013s (+18.1% 🔺)2.025s (~)0.439s301.00x
💻 LocalExpress1.593s (-1.1%)2.011s (~)0.013s (+2.9%)2.026s (~)0.433s301.00x
🐘 PostgresExpress1.602s (~)2.006s (~)0.005s (+1.4%)2.027s (~)0.425s301.01x
🐘 PostgresNitro1.603s (-2.2%)2.004s (~)0.005s (-0.6%)2.026s (~)0.423s301.01x
🐘 PostgresNext.js (Turbopack)1.753s (-2.2%)2.010s (~)0.005s (-2.4%)2.026s (~)0.273s301.11x
💻 LocalNext.js (Turbopack)1.889s (+8.2% 🔺)2.080s (+3.7%)0.014s (+7.7% 🔺)2.098s (+3.6%)0.209s291.19x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.212s (-9.9% 🟢)7.300s (-12.6% 🟢)0.270s (-21.4% 🟢)8.082s (-12.4% 🟢)1.869s81.00x
▲ VercelNext.js (Turbopack)6.304s (-8.2% 🟢)7.685s (-10.4% 🟢)0.289s (-7.7% 🟢)8.468s (-9.7% 🟢)2.164s81.01x
▲ VercelNitro6.373s (-5.4% 🟢)7.745s (-3.7%)0.208s (-5.5% 🟢)8.596s (-2.4%)2.223s71.03x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.797s (~)1.063s (+1.4%)0.000s (+Infinity% 🔺)1.079s (+1.8%)0.282s561.00x
🐘 PostgresNitro0.798s (-1.8%)1.067s (~)0.000s (+1.8%)1.099s (+1.8%)0.301s551.00x
🐘 PostgresNext.js (Turbopack)1.016s (-1.1%)1.500s (-7.5% 🟢)0.000s (-7.5% 🟢)1.507s (-7.6% 🟢)0.491s401.28x
💻 LocalExpress1.413s (~)2.012s (~)0.000s (~)2.015s (~)0.602s301.77x
💻 LocalNitro1.468s (+8.6% 🔺)2.014s (~)0.000s (-42.9% 🟢)2.017s (~)0.549s301.84x
💻 LocalNext.js (Turbopack)1.592s (+5.3% 🔺)2.015s (~)0.000s (-18.2% 🟢)2.019s (~)0.426s302.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.933s (-28.7% 🟢)4.414s (-23.2% 🟢)0.000s (NaN%)4.919s (-21.2% 🟢)1.986s131.00x
▲ VercelExpress2.951s (-20.1% 🟢)4.593s (-6.3% 🟢)0.000s (+Infinity% 🔺)5.072s (-8.7% 🟢)2.121s121.01x
▲ VercelNext.js (Turbopack)3.232s (-14.2% 🟢)4.626s (-13.3% 🟢)0.000s (-100.0% 🟢)5.093s (-13.4% 🟢)1.860s121.10x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.560s (-7.9% 🟢)2.066s (-5.0% 🟢)0.000s (-3.4%)2.136s (-2.7%)0.576s291.00x
🐘 PostgresExpress1.628s (-1.5%)2.137s (-1.8%)0.000s (+Infinity% 🔺)2.156s (-1.4%)0.528s281.04x
🐘 PostgresNext.js (Turbopack)2.214s (-1.8%)2.776s (+1.7%)0.000s (NaN%)2.782s (+1.6%)0.568s221.42x
💻 LocalExpress3.162s (-4.7%)3.965s (-1.5%)0.000s (-62.5% 🟢)3.968s (-1.6%)0.806s162.03x
💻 LocalNitro3.179s (+3.2%)3.734s (-4.2%)0.001s (+61.3% 🔺)3.737s (-4.2%)0.558s172.04x
💻 LocalNext.js (Turbopack)3.265s (+15.1% 🔺)3.734s (+9.3% 🔺)0.000s (-41.2% 🟢)3.738s (+9.3% 🔺)0.473s172.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.284s (-29.4% 🟢)6.003s (-21.0% 🟢)0.000s (-100.0% 🟢)6.482s (-20.8% 🟢)2.198s101.00x
▲ VercelNitro4.573s (-7.1% 🟢)6.058s (-1.7%)0.000s (NaN%)6.512s (-4.2%)1.939s101.07x
▲ VercelNext.js (Turbopack)5.009s (-29.1% 🟢)6.602s (-19.8% 🟢)0.000s (-100.0% 🟢)7.080s (-19.2% 🟢)2.071s91.17x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro15/21
🐘 PostgresNitro16/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202191661
✅ 💻 Local Development189502192114
✅ 📦 Local Production189502192114
✅ 🐘 Local Postgres188102332114
✅ 🪟 Windows15100151
❌ 📋 Other87811781057
Total8142110689211

❌ Failed Tests

📋 Other (1 failed)

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

  • webhookWorkflow | wrun_01KV7X2YYXA571EE20AYFJ50VB

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125026
✅ example125026
✅ express125026
✅ fastify125026
✅ hono125026
✅ nextjs-turbopack14902
✅ nextjs-webpack14902
✅ nitro125026
✅ nuxt125026
✅ sveltekit14407
✅ vite125026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable125026
✅ express-stable125026
✅ fastify-stable125026
✅ hono-stable125026
✅ nextjs-turbopack-canary131020
✅ nextjs-turbopack-stable-lazy-discovery-disabled15001
✅ nextjs-turbopack-stable-lazy-discovery-enabled15001
✅ nextjs-webpack-canary131020
✅ nextjs-webpack-stable-lazy-discovery-disabled15001
✅ nextjs-webpack-stable-lazy-discovery-enabled15001
✅ nitro-stable125026
✅ nuxt-stable125026
✅ sveltekit-stable14407
✅ vite-stable125026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15100
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable126025
✅ e2e-local-dev-tanstack-start-126025
✅ e2e-local-postgres-nest-stable125026
✅ e2e-local-postgres-tanstack-start-125026
✅ e2e-local-prod-nest-stable126025
✅ e2e-local-prod-tanstack-start-126025
❌ e2e-vercel-prod-tanstack-start124126

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

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

This PR adds bounded queue-redelivery behavior for replay-time RuntimeDecryptionErrors in @workflow/core, allowing managed worlds to retry transient persisted-data read corruption before marking the run failed.

Changes:

  • Adds a decryption-failure retry decision helper and retry budget constant.
  • Integrates the helper into the workflow runtime error path before writing run_failed.
  • Adds unit and runtime coverage plus a patch changeset.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/decryption-failure.tsAdds redelivery decision logic and logging for replay decryption failures.
packages/core/src/runtime/decryption-failure.test.tsCovers managed/in-process world behavior and retry-budget boundaries.
packages/core/src/runtime/constants.tsDefines the decryption-failure retry budget.
packages/core/src/runtime.tsRedrives managed-world runs on eligible RuntimeDecryptionErrors before failing the run.
packages/core/src/runtime.test.tsAdds end-to-end key-mismatch tests for redelivery and failure behavior.
.changeset/requeue-on-decryption-failure.mdAdds a patch changeset for @workflow/core.

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

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: blocking issues found

return false;
}

if (attempt <= DECRYPTION_FAILURE_MAX_RETRIES) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Blocking

RuntimeDecryptionError also covers encrypt-side AES-GCM failures (context.operation === "encrypt"), but this helper redrives every RuntimeDecryptionError on managed worlds. A fresh queue delivery can only help when decrypting persisted bytes, not when serializing/encrypting a new workflow payload. This can re-run workflow code up to 3 times before failing. Please gate redelivery on error.context?.operation === "decrypt".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — shouldRedriveOnDecryptionFailure now returns false immediately when error.context?.operation !== "decrypt", so encrypt-side AES-GCM failures fail the run without redelivery (re-fetching cannot change a payload being serialized fresh). Added a unit test covering the encrypt case on a managed world. Commit 558cc3f.

// queue delivery re-fetches the event log + ref
// payloads and can succeed. On managed Worlds, exit the
// process (which the platform turns into a redelivery)
// for a bounded number of attempts before failing the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

This changes the user-facing behavior for runtime decryption failures on managed worlds: they now redeliver before the terminal run_failed. The existing runtime-decryption-failed docs still say the run fails immediately, so those docs should be updated as part of this behavior change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — updated errors/runtime-decryption-failed.mdx (both v4 and v5) to document the bounded queue-redelivery behavior on managed worlds: decrypt failures are now retried before the terminal run_failed, while encrypt-side failures and in-process worlds still fail immediately. Commit 558cc3f.

Comment threadpackages/core/src/runtime.test.ts Outdated
// Past DECRYPTION_FAILURE_MAX_RETRIES (3) but under MAX_QUEUE_DELIVERIES
// (48), so we exercise the decryption-failure budget, not the queue
// max-deliveries guard.
attempt: 10,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This test duplicates the retry/delivery constants in comments and uses attempt: 10 instead of deriving from the constant it is testing. Please import the constants and use DECRYPTION_FAILURE_MAX_RETRIES + 1, with an assertion that it remains below MAX_QUEUE_DELIVERIES, so future constant changes don't silently stale the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — the past-budget test now derives attempt = DECRYPTION_FAILURE_MAX_RETRIES + 1 from the imported constant (no duplicated magic numbers) and asserts attempt < MAX_QUEUE_DELIVERIES, so a future constant change cannot silently stale the test. Commit 558cc3f.

Resolve conflicts in runtime.ts, runtime.test.ts, and constants.ts
(decryption-failure redelivery + replay-divergence recovery coexist).
Address review feedback:
- Gate decryption-failure redelivery on operation === 'decrypt' so
encrypt-side AES-GCM failures fail immediately (re-fetching cannot help).
- Derive the past-budget test attempt from DECRYPTION_FAILURE_MAX_RETRIES + 1
and assert it stays below MAX_QUEUE_DELIVERIES.
- Document the bounded queue-redelivery behavior in runtime-decryption-failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

feat(core): bounded queue-redelivery retry on decryption failure during replay - #2166

Open
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure
Open

feat(core): bounded queue-redelivery retry on decryption failure during replay#2166
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Follow-up to #2145 (the RuntimeDecryptionError attribution fix), implementing the bounded-redelivery behavior @pranaygp described in this comment.

An AES-GCM authentication failure is terminal for the bytes/key of the current attempt — we must never continue executing the workflow on top of data we couldn't decrypt. But the failure is often not terminal for the run: when the ciphertext came from a transiently truncated or corrupted read of remotely-persisted data (a partial /refs response, an edge-cache miss returning a partial 200, a proxy drop during streaming), a fresh queue delivery re-fetches the event log and ref payloads from scratch and can succeed.

Previously we committed run_failed immediately, turning a potentially recoverable read failure into a terminal workflow failure.

What changed

Mirrors the existing replay-timeout bounded-redelivery precedent (handleReplayBudgetExhausted):

  • Managed worlds (processExitTriggersQueueRedelivery === true, e.g. world-vercel): on attempts ≤ DECRYPTION_FAILURE_MAX_RETRIES (3), the run handler exits the process — which the platform turns into a queue redelivery — so replay restarts from freshly-fetched persisted data. Once the retry budget is exhausted, it commits run_failed with RUNTIME_ERROR.
  • In-process worlds (world-local, dev servers, custom in-process worlds): no queue to re-fetch from, and process.exit() would kill the user's host, so the run fails immediately with RUNTIME_ERROR.

New shouldRedriveOnDecryptionFailure() helper (runtime/decryption-failure.ts) is pure (logging only) and returns whether the caller should redrive, keeping the process.exit / run_failed decision at the single existing call site in the run handler.

Test coverage

  • runtime/decryption-failure.test.ts — unit tests for the helper across managed/in-process worlds and the retry-budget boundary.
  • runtime.test.ts — end-to-end tests driving a real key mismatch (input encrypted with key A, run-key resolves to key B → auth-tag failure during input hydration):
    • early attempt on a managed world → process.exit(1), no run_failed
    • past the retry budget on a managed world → run_failed with RUNTIME_ERROR
    • in-process world → run_failed immediately, no exit

All @workflow/core tests pass (1073), full repo typecheck passes (40/40).

Notes

The longer-term improvement @pranaygp mentioned — detecting response truncation / integrity failure at the /refs transport boundary to classify the retryable case more directly — is out of scope here and would be a separate change on the world / workflow-server side.

…ng replay
Follow-up to #2145. An AES-GCM auth failure (RuntimeDecryptionError) is
terminal for the current attempt's bytes, but often transient at the run
level: when the ciphertext came from a truncated/corrupted read of
remotely-persisted data (e.g. a partial /refs response), a fresh queue
delivery re-fetches the event log + ref payloads and can succeed.
Mirror the replay-timeout bounded-redelivery precedent: on managed worlds
(processExitTriggersQueueRedelivery), exit the process to trigger queue
redelivery for up to DECRYPTION_FAILURE_MAX_RETRIES attempts, then commit
run_failed as RUNTIME_ERROR. In-process worlds fail immediately (no queue
to re-fetch from, and exiting would kill the host).
CopilotAI review requested due to automatic review settings May 29, 2026 19:24
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 29, 2026 19:24
@changeset-bot

changeset-botBot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 558cc3f

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

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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 29, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+1.7%)1.006s (~)0.963s101.00x
💻 LocalExpress0.043s (-10.3% 🟢)1.007s (~)0.964s101.02x
🐘 PostgresNitro0.064s (-1.5%)1.011s (~)0.948s101.50x
🐘 PostgresNext.js (Turbopack)0.068s (-7.0% 🟢)1.011s (~)0.944s101.59x
🐘 PostgresExpress0.068s (+11.0% 🔺)1.012s (~)0.944s101.59x
💻 LocalNext.js (Turbopack)0.069s (+13.3% 🔺)1.006s (~)0.937s101.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.279s (-74.8% 🟢)2.020s (-43.5% 🟢)1.741s101.00x
▲ VercelNext.js (Turbopack)0.313s (-84.2% 🟢)2.337s (-43.1% 🟢)2.024s101.12x
▲ VercelNitro0.400s (-33.4% 🟢)2.213s (-16.8% 🟢)1.813s101.43x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.094s (~)2.006s (~)0.912s101.00x
💻 LocalExpress1.095s (~)2.007s (~)0.912s101.00x
🐘 PostgresExpress1.115s (~)2.010s (~)0.894s101.02x
🐘 PostgresNitro1.135s (+1.4%)2.009s (~)0.875s101.04x
🐘 PostgresNext.js (Turbopack)1.142s (-1.4%)2.011s (~)0.869s101.04x
💻 LocalNext.js (Turbopack)1.145s (~)2.007s (~)0.862s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.704s (-36.5% 🟢)3.836s (-15.2% 🟢)2.132s101.00x
▲ VercelExpress1.800s (-32.8% 🟢)3.830s (-23.4% 🟢)2.030s101.06x
▲ VercelNitro1.865s (-33.9% 🟢)3.713s (-18.3% 🟢)1.847s101.09x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.503s (~)11.022s (~)0.519s31.00x
💻 LocalExpress10.545s (~)11.023s (~)0.478s31.00x
🐘 PostgresExpress10.584s (~)11.018s (~)0.435s31.01x
🐘 PostgresNitro10.588s (~)11.015s (~)0.427s31.01x
💻 LocalNext.js (Turbopack)10.868s (+0.8%)11.023s (~)0.155s31.03x
🐘 PostgresNext.js (Turbopack)10.901s (~)11.016s (~)0.115s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)14.283s (-33.3% 🟢)16.662s (-28.4% 🟢)2.379s21.00x
▲ VercelNitro14.988s (-17.5% 🟢)17.110s (-13.9% 🟢)2.122s21.05x
▲ VercelExpress15.147s (-6.9% 🟢)17.654s (-5.2% 🟢)2.507s21.06x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.779s (~)14.029s (~)0.250s51.00x
🐘 PostgresNitro13.817s (~)14.020s (~)0.203s51.00x
💻 LocalExpress13.835s (~)14.027s (~)0.192s51.00x
🐘 PostgresExpress13.855s (~)14.020s (~)0.165s51.01x
🐘 PostgresNext.js (Turbopack)14.544s (~)15.019s (~)0.475s41.06x
💻 LocalNext.js (Turbopack)14.605s (+1.6%)15.030s (~)0.425s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.570s (-35.3% 🟢)24.870s (-32.6% 🟢)2.300s31.00x
▲ VercelNitro23.905s (-27.5% 🟢)25.993s (-25.4% 🟢)2.088s31.06x
▲ VercelNext.js (Turbopack)24.384s (-20.8% 🟢)26.434s (-19.6% 🟢)2.050s31.08x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.403s (-1.3%)13.024s (~)0.621s71.00x
💻 LocalExpress12.512s (-0.8%)13.024s (~)0.511s71.01x
🐘 PostgresNitro12.647s (~)13.023s (~)0.376s71.02x
🐘 PostgresExpress12.680s (+0.7%)13.019s (~)0.339s71.02x
🐘 PostgresNext.js (Turbopack)13.856s (-0.8%)14.020s (-1.0%)0.164s71.12x
💻 LocalNext.js (Turbopack)14.047s (+3.2%)14.744s (+5.1% 🔺)0.697s71.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express29.958s (-13.8% 🟢)32.678s (-11.9% 🟢)2.720s31.00x
▲ VercelNitro30.838s (-18.6% 🟢)33.293s (-15.7% 🟢)2.455s31.03x
▲ VercelNext.js (Turbopack)31.778s (-9.5% 🟢)33.997s (-9.4% 🟢)2.218s31.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.207s (+1.2%)2.006s (~)0.800s151.00x
💻 LocalNitro1.207s (+3.8%)2.006s (~)0.799s151.00x
🐘 PostgresNitro1.214s (~)2.008s (~)0.794s151.01x
🐘 PostgresExpress1.214s (+0.7%)2.009s (~)0.795s151.01x
🐘 PostgresNext.js (Turbopack)1.277s (-5.9% 🟢)2.007s (-3.3%)0.731s151.06x
💻 LocalNext.js (Turbopack)1.340s (+0.8%)2.007s (~)0.667s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.289s (-26.0% 🟢)3.763s (-17.9% 🟢)1.474s81.00x
▲ VercelExpress2.513s (-1.5%)4.117s (+1.1%)1.604s81.10x
▲ VercelNext.js (Turbopack)3.039s (+29.5% 🔺)4.619s (+15.0% 🔺)1.580s71.33x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.367s (+1.9%)2.393s (-4.6%)1.026s131.00x
🐘 PostgresExpress1.387s (+2.2%)2.223s (-11.3% 🟢)0.836s141.01x
🐘 PostgresNext.js (Turbopack)1.662s (+1.4%)2.295s (-8.5% 🟢)0.633s141.22x
💻 LocalNitro1.705s (+4.7%)2.006s (~)0.301s151.25x
💻 LocalExpress1.838s (-3.0%)2.150s (~)0.313s141.34x
💻 LocalNext.js (Turbopack)1.860s (+16.7% 🔺)2.075s (+3.4%)0.215s151.36x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.813s (-31.6% 🟢)4.734s (-21.8% 🟢)1.920s71.00x
▲ VercelNitro2.996s (-20.5% 🟢)4.799s (-11.1% 🟢)1.803s71.06x
▲ VercelNext.js (Turbopack)3.230s (-7.9% 🟢)5.201s (-1.0%)1.971s61.15x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.821s (+12.1% 🔺)3.887s (-6.1% 🟢)2.066s81.00x
🐘 PostgresExpress1.827s (+12.6% 🔺)4.011s (+3.2%)2.183s81.00x
🐘 PostgresNext.js (Turbopack)3.481s (-9.1% 🟢)4.299s (-3.3%)0.818s71.91x
💻 LocalNitro4.569s (+13.6% 🔺)5.015s (+9.5% 🔺)0.446s72.51x
💻 LocalExpress5.321s (+1.7%)5.846s (~)0.525s62.92x
💻 LocalNext.js (Turbopack)5.870s (+28.6% 🔺)6.348s (+22.6% 🔺)0.478s63.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.964s (-13.3% 🟢)6.243s (-4.8%)2.278s51.00x
▲ VercelNitro4.013s (-5.5% 🟢)6.018s (+5.1% 🔺)2.005s51.01x
▲ VercelNext.js (Turbopack)4.065s (+7.3% 🔺)6.166s (+14.0% 🔺)2.101s51.03x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.218s (~)2.007s (~)0.789s151.00x
🐘 PostgresExpress1.220s (+0.8%)2.008s (~)0.789s151.00x
🐘 PostgresNext.js (Turbopack)1.272s (-1.1%)2.008s (~)0.736s151.04x
💻 LocalNext.js (Turbopack)1.424s (+3.3%)2.008s (~)0.584s151.17x
💻 LocalNitro1.560s (+3.2%)2.007s (~)0.447s151.28x
💻 LocalExpress1.593s (-1.3%)2.007s (~)0.414s151.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.290s (-0.7%)3.776s (-4.3%)1.486s81.00x
▲ VercelNitro2.297s (-64.6% 🟢)3.691s (-54.0% 🟢)1.394s91.00x
▲ VercelExpress2.406s (+5.8% 🔺)3.962s (+2.8%)1.556s81.05x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.357s (~)2.315s (-10.7% 🟢)0.958s131.00x
🐘 PostgresExpress1.406s (+0.8%)2.592s (+4.9%)1.186s121.04x
🐘 PostgresNext.js (Turbopack)1.583s (+3.0%)2.152s (-3.3%)0.569s141.17x
💻 LocalNext.js (Turbopack)2.095s (+8.5% 🔺)2.918s (+31.3% 🔺)0.823s111.54x
💻 LocalNitro2.187s (+22.2% 🔺)2.676s (+29.1% 🔺)0.489s121.61x
💻 LocalExpress2.349s (-8.8% 🟢)2.827s (-9.0% 🟢)0.479s111.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.794s (-25.4% 🟢)4.495s (-17.7% 🟢)1.700s71.00x
▲ VercelNext.js (Turbopack)3.325s (-17.1% 🟢)5.058s (-12.0% 🟢)1.733s61.19x
▲ VercelExpress3.330s (~)5.327s (~)1.997s61.19x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.572s (-9.0% 🟢)4.011s (+3.2%)2.439s81.00x
🐘 PostgresExpress1.646s (-6.1% 🟢)4.014s (-2.9%)2.368s81.05x
🐘 PostgresNext.js (Turbopack)2.971s (-22.6% 🟢)3.764s (-15.3% 🟢)0.793s81.89x
💻 LocalNitro5.218s (+9.5% 🔺)5.684s (+6.3% 🔺)0.467s63.32x
💻 LocalExpress6.467s (+2.0%)7.216s (+2.8%)0.749s54.11x
💻 LocalNext.js (Turbopack)6.475s (+22.3% 🔺)7.018s (+16.7% 🔺)0.543s54.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.355s (-33.9% 🟢)6.132s (-26.5% 🟢)1.777s61.00x
▲ VercelNext.js (Turbopack)4.600s (-6.6% 🟢)6.524s (~)1.924s51.06x
▲ VercelExpress4.640s (-11.4% 🟢)6.693s (-3.9%)2.053s51.07x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.593s (-8.0% 🟢)1.023s (-1.8%)0.430s591.00x
💻 LocalNitro0.596s (+2.2%)1.005s (~)0.409s601.00x
🐘 PostgresExpress0.609s (+2.8%)1.023s (+1.8%)0.415s591.03x
💻 LocalExpress0.625s (-5.3% 🟢)1.005s (-3.3%)0.380s601.05x
🐘 PostgresNext.js (Turbopack)0.819s (-3.7%)1.006s (-1.7%)0.187s601.38x
💻 LocalNext.js (Turbopack)0.942s (+6.4% 🔺)1.096s (+3.7%)0.154s551.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.725s (-13.0% 🟢)7.514s (-12.9% 🟢)1.790s91.00x
▲ VercelNext.js (Turbopack)5.798s (-21.9% 🟢)7.584s (-18.8% 🟢)1.786s81.01x
▲ VercelNitro5.831s (-31.1% 🟢)7.298s (-27.8% 🟢)1.467s91.02x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.384s (-3.6%)2.007s (-1.1%)0.623s451.00x
🐘 PostgresNitro1.385s (-4.2%)2.008s (~)0.623s451.00x
💻 LocalNitro1.502s (+1.7%)2.006s (~)0.504s451.09x
💻 LocalExpress1.575s (-0.6%)2.007s (~)0.432s451.14x
🐘 PostgresNext.js (Turbopack)1.935s (-5.0% 🟢)2.150s (-16.1% 🟢)0.216s421.40x
💻 LocalNext.js (Turbopack)2.277s (+7.0% 🔺)3.009s (~)0.732s301.65x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.711s (-31.9% 🟢)16.057s (-26.5% 🟢)2.346s61.00x
▲ VercelNitro14.119s (-24.8% 🟢)16.038s (-22.7% 🟢)1.919s61.03x
▲ VercelNext.js (Turbopack)14.780s (-25.8% 🟢)16.884s (-24.6% 🟢)2.104s61.08x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.788s (-1.2%)3.166s (+0.9%)0.379s381.00x
🐘 PostgresExpress2.860s (+2.9%)3.252s (+3.7%)0.392s371.03x
💻 LocalNitro3.279s (+3.5%)4.009s (+2.5%)0.731s301.18x
💻 LocalExpress3.391s (+1.4%)4.010s (~)0.619s301.22x
🐘 PostgresNext.js (Turbopack)3.810s (-5.3% 🟢)4.043s (-8.2% 🟢)0.233s301.37x
💻 LocalNext.js (Turbopack)4.748s (+4.6%)5.054s (~)0.306s241.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.736s (-18.0% 🟢)32.583s (-14.7% 🟢)2.847s41.00x
▲ VercelExpress29.831s (-22.9% 🟢)32.492s (-19.6% 🟢)2.660s41.00x
▲ VercelNext.js (Turbopack)32.381s (-27.8% 🟢)35.293s (-24.4% 🟢)2.912s41.09x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.253s (-3.1%)1.006s (~)0.753s601.00x
🐘 PostgresExpress0.264s (+1.1%)1.023s (+1.7%)0.760s591.04x
🐘 PostgresNext.js (Turbopack)0.301s (-2.4%)1.006s (~)0.705s601.19x
💻 LocalExpress0.409s (-4.4%)1.005s (~)0.595s601.62x
💻 LocalNitro0.425s (-1.5%)1.005s (~)0.579s601.68x
💻 LocalNext.js (Turbopack)0.594s (-8.2% 🟢)1.006s (-4.9%)0.413s602.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.844s (-36.1% 🟢)3.799s (-24.5% 🟢)1.954s161.00x
▲ VercelExpress1.910s (-46.5% 🟢)3.900s (-30.2% 🟢)1.990s161.04x
▲ VercelNext.js (Turbopack)1.931s (-37.1% 🟢)3.841s (-23.4% 🟢)1.910s161.05x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.410s (-10.2% 🟢)1.029s (-4.6%)0.619s881.00x
🐘 PostgresExpress0.418s (+1.8%)1.078s (+7.1% 🔺)0.659s841.02x
🐘 PostgresNext.js (Turbopack)0.575s (-15.2% 🟢)1.118s (-12.4% 🟢)0.543s811.40x
💻 LocalNitro2.162s (+7.5% 🔺)2.687s (+10.1% 🔺)0.524s345.27x
💻 LocalExpress2.221s (+1.4%)2.769s (-2.9%)0.548s335.41x
💻 LocalNext.js (Turbopack)2.825s (+10.4% 🔺)3.512s (+7.7% 🔺)0.687s266.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.226s (-37.4% 🟢)5.211s (-27.8% 🟢)1.986s181.00x
▲ VercelNitro3.370s (-3.3%)5.242s (+1.4%)1.872s181.04x
▲ VercelNext.js (Turbopack)3.799s (~)5.760s (~)1.961s161.18x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.804s (-4.7%)1.527s (+3.8%)0.724s791.00x
🐘 PostgresExpress0.831s (-2.0%)1.471s (-2.4%)0.640s821.03x
🐘 PostgresNext.js (Turbopack)2.802s (-9.3% 🟢)3.651s (-4.6%)0.849s333.49x
💻 LocalNitro9.753s (+11.8% 🔺)10.447s (+12.9% 🔺)0.694s1212.14x
💻 LocalExpress9.767s (-1.0%)10.280s (~)0.514s1212.15x
💻 LocalNext.js (Turbopack)12.460s (+18.8% 🔺)13.337s (+17.1% 🔺)0.877s1015.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.603s (-17.0% 🟢)7.854s (-9.0% 🟢)2.251s161.00x
▲ VercelExpress5.898s (-13.7% 🟢)8.080s (-6.3% 🟢)2.182s151.05x
▲ VercelNext.js (Turbopack)7.576s (+5.3% 🔺)9.754s (+8.8% 🔺)2.178s131.35x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.163s (~)2.005s (~)0.010s (+1.0%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (-2.4%)1.999s (~)0.002s (+33.3% 🔺)2.011s (~)0.842s101.01x
💻 LocalExpress1.172s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.847s101.01x
🐘 PostgresExpress1.185s (+0.9%)2.000s (~)0.002s (+54.5% 🔺)2.012s (~)0.827s101.02x
🐘 PostgresNext.js (Turbopack)1.228s (-1.3%)2.001s (~)0.001s (-25.0% 🟢)2.011s (~)0.783s101.06x
💻 LocalNext.js (Turbopack)1.228s (-1.7%)2.004s (~)0.012s (+13.1% 🔺)2.021s (~)0.793s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.289s (-3.1%)3.325s (-5.8% 🟢)0.785s (+11.6% 🔺)4.593s (-4.1%)2.304s101.00x
▲ VercelNext.js (Turbopack)2.336s (-12.9% 🟢)3.384s (-9.7% 🟢)0.770s (-8.0% 🟢)4.613s (-9.3% 🟢)2.276s101.02x
▲ VercelNitro2.345s (-8.0% 🟢)3.480s (-6.3% 🟢)0.621s (-16.1% 🟢)4.531s (-8.7% 🟢)2.187s101.02x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.586s (+1.3%)2.009s (~)0.013s (+18.1% 🔺)2.025s (~)0.439s301.00x
💻 LocalExpress1.593s (-1.1%)2.011s (~)0.013s (+2.9%)2.026s (~)0.433s301.00x
🐘 PostgresExpress1.602s (~)2.006s (~)0.005s (+1.4%)2.027s (~)0.425s301.01x
🐘 PostgresNitro1.603s (-2.2%)2.004s (~)0.005s (-0.6%)2.026s (~)0.423s301.01x
🐘 PostgresNext.js (Turbopack)1.753s (-2.2%)2.010s (~)0.005s (-2.4%)2.026s (~)0.273s301.11x
💻 LocalNext.js (Turbopack)1.889s (+8.2% 🔺)2.080s (+3.7%)0.014s (+7.7% 🔺)2.098s (+3.6%)0.209s291.19x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.212s (-9.9% 🟢)7.300s (-12.6% 🟢)0.270s (-21.4% 🟢)8.082s (-12.4% 🟢)1.869s81.00x
▲ VercelNext.js (Turbopack)6.304s (-8.2% 🟢)7.685s (-10.4% 🟢)0.289s (-7.7% 🟢)8.468s (-9.7% 🟢)2.164s81.01x
▲ VercelNitro6.373s (-5.4% 🟢)7.745s (-3.7%)0.208s (-5.5% 🟢)8.596s (-2.4%)2.223s71.03x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.797s (~)1.063s (+1.4%)0.000s (+Infinity% 🔺)1.079s (+1.8%)0.282s561.00x
🐘 PostgresNitro0.798s (-1.8%)1.067s (~)0.000s (+1.8%)1.099s (+1.8%)0.301s551.00x
🐘 PostgresNext.js (Turbopack)1.016s (-1.1%)1.500s (-7.5% 🟢)0.000s (-7.5% 🟢)1.507s (-7.6% 🟢)0.491s401.28x
💻 LocalExpress1.413s (~)2.012s (~)0.000s (~)2.015s (~)0.602s301.77x
💻 LocalNitro1.468s (+8.6% 🔺)2.014s (~)0.000s (-42.9% 🟢)2.017s (~)0.549s301.84x
💻 LocalNext.js (Turbopack)1.592s (+5.3% 🔺)2.015s (~)0.000s (-18.2% 🟢)2.019s (~)0.426s302.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.933s (-28.7% 🟢)4.414s (-23.2% 🟢)0.000s (NaN%)4.919s (-21.2% 🟢)1.986s131.00x
▲ VercelExpress2.951s (-20.1% 🟢)4.593s (-6.3% 🟢)0.000s (+Infinity% 🔺)5.072s (-8.7% 🟢)2.121s121.01x
▲ VercelNext.js (Turbopack)3.232s (-14.2% 🟢)4.626s (-13.3% 🟢)0.000s (-100.0% 🟢)5.093s (-13.4% 🟢)1.860s121.10x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.560s (-7.9% 🟢)2.066s (-5.0% 🟢)0.000s (-3.4%)2.136s (-2.7%)0.576s291.00x
🐘 PostgresExpress1.628s (-1.5%)2.137s (-1.8%)0.000s (+Infinity% 🔺)2.156s (-1.4%)0.528s281.04x
🐘 PostgresNext.js (Turbopack)2.214s (-1.8%)2.776s (+1.7%)0.000s (NaN%)2.782s (+1.6%)0.568s221.42x
💻 LocalExpress3.162s (-4.7%)3.965s (-1.5%)0.000s (-62.5% 🟢)3.968s (-1.6%)0.806s162.03x
💻 LocalNitro3.179s (+3.2%)3.734s (-4.2%)0.001s (+61.3% 🔺)3.737s (-4.2%)0.558s172.04x
💻 LocalNext.js (Turbopack)3.265s (+15.1% 🔺)3.734s (+9.3% 🔺)0.000s (-41.2% 🟢)3.738s (+9.3% 🔺)0.473s172.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.284s (-29.4% 🟢)6.003s (-21.0% 🟢)0.000s (-100.0% 🟢)6.482s (-20.8% 🟢)2.198s101.00x
▲ VercelNitro4.573s (-7.1% 🟢)6.058s (-1.7%)0.000s (NaN%)6.512s (-4.2%)1.939s101.07x
▲ VercelNext.js (Turbopack)5.009s (-29.1% 🟢)6.602s (-19.8% 🟢)0.000s (-100.0% 🟢)7.080s (-19.2% 🟢)2.071s91.17x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro15/21
🐘 PostgresNitro16/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202191661
✅ 💻 Local Development189502192114
✅ 📦 Local Production189502192114
✅ 🐘 Local Postgres188102332114
✅ 🪟 Windows15100151
❌ 📋 Other87811781057
Total8142110689211

❌ Failed Tests

📋 Other (1 failed)

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

  • webhookWorkflow | wrun_01KV7X2YYXA571EE20AYFJ50VB

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125026
✅ example125026
✅ express125026
✅ fastify125026
✅ hono125026
✅ nextjs-turbopack14902
✅ nextjs-webpack14902
✅ nitro125026
✅ nuxt125026
✅ sveltekit14407
✅ vite125026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable125026
✅ express-stable125026
✅ fastify-stable125026
✅ hono-stable125026
✅ nextjs-turbopack-canary131020
✅ nextjs-turbopack-stable-lazy-discovery-disabled15001
✅ nextjs-turbopack-stable-lazy-discovery-enabled15001
✅ nextjs-webpack-canary131020
✅ nextjs-webpack-stable-lazy-discovery-disabled15001
✅ nextjs-webpack-stable-lazy-discovery-enabled15001
✅ nitro-stable125026
✅ nuxt-stable125026
✅ sveltekit-stable14407
✅ vite-stable125026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15100
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable126025
✅ e2e-local-dev-tanstack-start-126025
✅ e2e-local-postgres-nest-stable125026
✅ e2e-local-postgres-tanstack-start-125026
✅ e2e-local-prod-nest-stable126025
✅ e2e-local-prod-tanstack-start-126025
❌ e2e-vercel-prod-tanstack-start124126

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

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

This PR adds bounded queue-redelivery behavior for replay-time RuntimeDecryptionErrors in @workflow/core, allowing managed worlds to retry transient persisted-data read corruption before marking the run failed.

Changes:

  • Adds a decryption-failure retry decision helper and retry budget constant.
  • Integrates the helper into the workflow runtime error path before writing run_failed.
  • Adds unit and runtime coverage plus a patch changeset.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/decryption-failure.tsAdds redelivery decision logic and logging for replay decryption failures.
packages/core/src/runtime/decryption-failure.test.tsCovers managed/in-process world behavior and retry-budget boundaries.
packages/core/src/runtime/constants.tsDefines the decryption-failure retry budget.
packages/core/src/runtime.tsRedrives managed-world runs on eligible RuntimeDecryptionErrors before failing the run.
packages/core/src/runtime.test.tsAdds end-to-end key-mismatch tests for redelivery and failure behavior.
.changeset/requeue-on-decryption-failure.mdAdds a patch changeset for @workflow/core.

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

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: blocking issues found

return false;
}

if (attempt <= DECRYPTION_FAILURE_MAX_RETRIES) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Blocking

RuntimeDecryptionError also covers encrypt-side AES-GCM failures (context.operation === "encrypt"), but this helper redrives every RuntimeDecryptionError on managed worlds. A fresh queue delivery can only help when decrypting persisted bytes, not when serializing/encrypting a new workflow payload. This can re-run workflow code up to 3 times before failing. Please gate redelivery on error.context?.operation === "decrypt".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — shouldRedriveOnDecryptionFailure now returns false immediately when error.context?.operation !== "decrypt", so encrypt-side AES-GCM failures fail the run without redelivery (re-fetching cannot change a payload being serialized fresh). Added a unit test covering the encrypt case on a managed world. Commit 558cc3f.

// queue delivery re-fetches the event log + ref
// payloads and can succeed. On managed Worlds, exit the
// process (which the platform turns into a redelivery)
// for a bounded number of attempts before failing the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

This changes the user-facing behavior for runtime decryption failures on managed worlds: they now redeliver before the terminal run_failed. The existing runtime-decryption-failed docs still say the run fails immediately, so those docs should be updated as part of this behavior change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — updated errors/runtime-decryption-failed.mdx (both v4 and v5) to document the bounded queue-redelivery behavior on managed worlds: decrypt failures are now retried before the terminal run_failed, while encrypt-side failures and in-process worlds still fail immediately. Commit 558cc3f.

Comment threadpackages/core/src/runtime.test.ts Outdated
// Past DECRYPTION_FAILURE_MAX_RETRIES (3) but under MAX_QUEUE_DELIVERIES
// (48), so we exercise the decryption-failure budget, not the queue
// max-deliveries guard.
attempt: 10,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This test duplicates the retry/delivery constants in comments and uses attempt: 10 instead of deriving from the constant it is testing. Please import the constants and use DECRYPTION_FAILURE_MAX_RETRIES + 1, with an assertion that it remains below MAX_QUEUE_DELIVERIES, so future constant changes don't silently stale the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — the past-budget test now derives attempt = DECRYPTION_FAILURE_MAX_RETRIES + 1 from the imported constant (no duplicated magic numbers) and asserts attempt < MAX_QUEUE_DELIVERIES, so a future constant change cannot silently stale the test. Commit 558cc3f.

Resolve conflicts in runtime.ts, runtime.test.ts, and constants.ts
(decryption-failure redelivery + replay-divergence recovery coexist).
Address review feedback:
- Gate decryption-failure redelivery on operation === 'decrypt' so
encrypt-side AES-GCM failures fail immediately (re-fetching cannot help).
- Derive the past-budget test attempt from DECRYPTION_FAILURE_MAX_RETRIES + 1
and assert it stays below MAX_QUEUE_DELIVERIES.
- Document the bounded queue-redelivery behavior in runtime-decryption-failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

feat(core): bounded queue-redelivery retry on decryption failure during replay - #2166

Open
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure
Open

feat(core): bounded queue-redelivery retry on decryption failure during replay#2166
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Follow-up to #2145 (the RuntimeDecryptionError attribution fix), implementing the bounded-redelivery behavior @pranaygp described in this comment.

An AES-GCM authentication failure is terminal for the bytes/key of the current attempt — we must never continue executing the workflow on top of data we couldn't decrypt. But the failure is often not terminal for the run: when the ciphertext came from a transiently truncated or corrupted read of remotely-persisted data (a partial /refs response, an edge-cache miss returning a partial 200, a proxy drop during streaming), a fresh queue delivery re-fetches the event log and ref payloads from scratch and can succeed.

Previously we committed run_failed immediately, turning a potentially recoverable read failure into a terminal workflow failure.

What changed

Mirrors the existing replay-timeout bounded-redelivery precedent (handleReplayBudgetExhausted):

  • Managed worlds (processExitTriggersQueueRedelivery === true, e.g. world-vercel): on attempts ≤ DECRYPTION_FAILURE_MAX_RETRIES (3), the run handler exits the process — which the platform turns into a queue redelivery — so replay restarts from freshly-fetched persisted data. Once the retry budget is exhausted, it commits run_failed with RUNTIME_ERROR.
  • In-process worlds (world-local, dev servers, custom in-process worlds): no queue to re-fetch from, and process.exit() would kill the user's host, so the run fails immediately with RUNTIME_ERROR.

New shouldRedriveOnDecryptionFailure() helper (runtime/decryption-failure.ts) is pure (logging only) and returns whether the caller should redrive, keeping the process.exit / run_failed decision at the single existing call site in the run handler.

Test coverage

  • runtime/decryption-failure.test.ts — unit tests for the helper across managed/in-process worlds and the retry-budget boundary.
  • runtime.test.ts — end-to-end tests driving a real key mismatch (input encrypted with key A, run-key resolves to key B → auth-tag failure during input hydration):
    • early attempt on a managed world → process.exit(1), no run_failed
    • past the retry budget on a managed world → run_failed with RUNTIME_ERROR
    • in-process world → run_failed immediately, no exit

All @workflow/core tests pass (1073), full repo typecheck passes (40/40).

Notes

The longer-term improvement @pranaygp mentioned — detecting response truncation / integrity failure at the /refs transport boundary to classify the retryable case more directly — is out of scope here and would be a separate change on the world / workflow-server side.

…ng replay
Follow-up to #2145. An AES-GCM auth failure (RuntimeDecryptionError) is
terminal for the current attempt's bytes, but often transient at the run
level: when the ciphertext came from a truncated/corrupted read of
remotely-persisted data (e.g. a partial /refs response), a fresh queue
delivery re-fetches the event log + ref payloads and can succeed.
Mirror the replay-timeout bounded-redelivery precedent: on managed worlds
(processExitTriggersQueueRedelivery), exit the process to trigger queue
redelivery for up to DECRYPTION_FAILURE_MAX_RETRIES attempts, then commit
run_failed as RUNTIME_ERROR. In-process worlds fail immediately (no queue
to re-fetch from, and exiting would kill the host).
CopilotAI review requested due to automatic review settings May 29, 2026 19:24
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 29, 2026 19:24
@changeset-bot

changeset-botBot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 558cc3f

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

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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 29, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+1.7%)1.006s (~)0.963s101.00x
💻 LocalExpress0.043s (-10.3% 🟢)1.007s (~)0.964s101.02x
🐘 PostgresNitro0.064s (-1.5%)1.011s (~)0.948s101.50x
🐘 PostgresNext.js (Turbopack)0.068s (-7.0% 🟢)1.011s (~)0.944s101.59x
🐘 PostgresExpress0.068s (+11.0% 🔺)1.012s (~)0.944s101.59x
💻 LocalNext.js (Turbopack)0.069s (+13.3% 🔺)1.006s (~)0.937s101.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.279s (-74.8% 🟢)2.020s (-43.5% 🟢)1.741s101.00x
▲ VercelNext.js (Turbopack)0.313s (-84.2% 🟢)2.337s (-43.1% 🟢)2.024s101.12x
▲ VercelNitro0.400s (-33.4% 🟢)2.213s (-16.8% 🟢)1.813s101.43x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.094s (~)2.006s (~)0.912s101.00x
💻 LocalExpress1.095s (~)2.007s (~)0.912s101.00x
🐘 PostgresExpress1.115s (~)2.010s (~)0.894s101.02x
🐘 PostgresNitro1.135s (+1.4%)2.009s (~)0.875s101.04x
🐘 PostgresNext.js (Turbopack)1.142s (-1.4%)2.011s (~)0.869s101.04x
💻 LocalNext.js (Turbopack)1.145s (~)2.007s (~)0.862s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.704s (-36.5% 🟢)3.836s (-15.2% 🟢)2.132s101.00x
▲ VercelExpress1.800s (-32.8% 🟢)3.830s (-23.4% 🟢)2.030s101.06x
▲ VercelNitro1.865s (-33.9% 🟢)3.713s (-18.3% 🟢)1.847s101.09x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.503s (~)11.022s (~)0.519s31.00x
💻 LocalExpress10.545s (~)11.023s (~)0.478s31.00x
🐘 PostgresExpress10.584s (~)11.018s (~)0.435s31.01x
🐘 PostgresNitro10.588s (~)11.015s (~)0.427s31.01x
💻 LocalNext.js (Turbopack)10.868s (+0.8%)11.023s (~)0.155s31.03x
🐘 PostgresNext.js (Turbopack)10.901s (~)11.016s (~)0.115s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)14.283s (-33.3% 🟢)16.662s (-28.4% 🟢)2.379s21.00x
▲ VercelNitro14.988s (-17.5% 🟢)17.110s (-13.9% 🟢)2.122s21.05x
▲ VercelExpress15.147s (-6.9% 🟢)17.654s (-5.2% 🟢)2.507s21.06x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.779s (~)14.029s (~)0.250s51.00x
🐘 PostgresNitro13.817s (~)14.020s (~)0.203s51.00x
💻 LocalExpress13.835s (~)14.027s (~)0.192s51.00x
🐘 PostgresExpress13.855s (~)14.020s (~)0.165s51.01x
🐘 PostgresNext.js (Turbopack)14.544s (~)15.019s (~)0.475s41.06x
💻 LocalNext.js (Turbopack)14.605s (+1.6%)15.030s (~)0.425s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.570s (-35.3% 🟢)24.870s (-32.6% 🟢)2.300s31.00x
▲ VercelNitro23.905s (-27.5% 🟢)25.993s (-25.4% 🟢)2.088s31.06x
▲ VercelNext.js (Turbopack)24.384s (-20.8% 🟢)26.434s (-19.6% 🟢)2.050s31.08x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.403s (-1.3%)13.024s (~)0.621s71.00x
💻 LocalExpress12.512s (-0.8%)13.024s (~)0.511s71.01x
🐘 PostgresNitro12.647s (~)13.023s (~)0.376s71.02x
🐘 PostgresExpress12.680s (+0.7%)13.019s (~)0.339s71.02x
🐘 PostgresNext.js (Turbopack)13.856s (-0.8%)14.020s (-1.0%)0.164s71.12x
💻 LocalNext.js (Turbopack)14.047s (+3.2%)14.744s (+5.1% 🔺)0.697s71.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express29.958s (-13.8% 🟢)32.678s (-11.9% 🟢)2.720s31.00x
▲ VercelNitro30.838s (-18.6% 🟢)33.293s (-15.7% 🟢)2.455s31.03x
▲ VercelNext.js (Turbopack)31.778s (-9.5% 🟢)33.997s (-9.4% 🟢)2.218s31.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.207s (+1.2%)2.006s (~)0.800s151.00x
💻 LocalNitro1.207s (+3.8%)2.006s (~)0.799s151.00x
🐘 PostgresNitro1.214s (~)2.008s (~)0.794s151.01x
🐘 PostgresExpress1.214s (+0.7%)2.009s (~)0.795s151.01x
🐘 PostgresNext.js (Turbopack)1.277s (-5.9% 🟢)2.007s (-3.3%)0.731s151.06x
💻 LocalNext.js (Turbopack)1.340s (+0.8%)2.007s (~)0.667s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.289s (-26.0% 🟢)3.763s (-17.9% 🟢)1.474s81.00x
▲ VercelExpress2.513s (-1.5%)4.117s (+1.1%)1.604s81.10x
▲ VercelNext.js (Turbopack)3.039s (+29.5% 🔺)4.619s (+15.0% 🔺)1.580s71.33x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.367s (+1.9%)2.393s (-4.6%)1.026s131.00x
🐘 PostgresExpress1.387s (+2.2%)2.223s (-11.3% 🟢)0.836s141.01x
🐘 PostgresNext.js (Turbopack)1.662s (+1.4%)2.295s (-8.5% 🟢)0.633s141.22x
💻 LocalNitro1.705s (+4.7%)2.006s (~)0.301s151.25x
💻 LocalExpress1.838s (-3.0%)2.150s (~)0.313s141.34x
💻 LocalNext.js (Turbopack)1.860s (+16.7% 🔺)2.075s (+3.4%)0.215s151.36x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.813s (-31.6% 🟢)4.734s (-21.8% 🟢)1.920s71.00x
▲ VercelNitro2.996s (-20.5% 🟢)4.799s (-11.1% 🟢)1.803s71.06x
▲ VercelNext.js (Turbopack)3.230s (-7.9% 🟢)5.201s (-1.0%)1.971s61.15x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.821s (+12.1% 🔺)3.887s (-6.1% 🟢)2.066s81.00x
🐘 PostgresExpress1.827s (+12.6% 🔺)4.011s (+3.2%)2.183s81.00x
🐘 PostgresNext.js (Turbopack)3.481s (-9.1% 🟢)4.299s (-3.3%)0.818s71.91x
💻 LocalNitro4.569s (+13.6% 🔺)5.015s (+9.5% 🔺)0.446s72.51x
💻 LocalExpress5.321s (+1.7%)5.846s (~)0.525s62.92x
💻 LocalNext.js (Turbopack)5.870s (+28.6% 🔺)6.348s (+22.6% 🔺)0.478s63.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.964s (-13.3% 🟢)6.243s (-4.8%)2.278s51.00x
▲ VercelNitro4.013s (-5.5% 🟢)6.018s (+5.1% 🔺)2.005s51.01x
▲ VercelNext.js (Turbopack)4.065s (+7.3% 🔺)6.166s (+14.0% 🔺)2.101s51.03x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.218s (~)2.007s (~)0.789s151.00x
🐘 PostgresExpress1.220s (+0.8%)2.008s (~)0.789s151.00x
🐘 PostgresNext.js (Turbopack)1.272s (-1.1%)2.008s (~)0.736s151.04x
💻 LocalNext.js (Turbopack)1.424s (+3.3%)2.008s (~)0.584s151.17x
💻 LocalNitro1.560s (+3.2%)2.007s (~)0.447s151.28x
💻 LocalExpress1.593s (-1.3%)2.007s (~)0.414s151.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.290s (-0.7%)3.776s (-4.3%)1.486s81.00x
▲ VercelNitro2.297s (-64.6% 🟢)3.691s (-54.0% 🟢)1.394s91.00x
▲ VercelExpress2.406s (+5.8% 🔺)3.962s (+2.8%)1.556s81.05x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.357s (~)2.315s (-10.7% 🟢)0.958s131.00x
🐘 PostgresExpress1.406s (+0.8%)2.592s (+4.9%)1.186s121.04x
🐘 PostgresNext.js (Turbopack)1.583s (+3.0%)2.152s (-3.3%)0.569s141.17x
💻 LocalNext.js (Turbopack)2.095s (+8.5% 🔺)2.918s (+31.3% 🔺)0.823s111.54x
💻 LocalNitro2.187s (+22.2% 🔺)2.676s (+29.1% 🔺)0.489s121.61x
💻 LocalExpress2.349s (-8.8% 🟢)2.827s (-9.0% 🟢)0.479s111.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.794s (-25.4% 🟢)4.495s (-17.7% 🟢)1.700s71.00x
▲ VercelNext.js (Turbopack)3.325s (-17.1% 🟢)5.058s (-12.0% 🟢)1.733s61.19x
▲ VercelExpress3.330s (~)5.327s (~)1.997s61.19x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.572s (-9.0% 🟢)4.011s (+3.2%)2.439s81.00x
🐘 PostgresExpress1.646s (-6.1% 🟢)4.014s (-2.9%)2.368s81.05x
🐘 PostgresNext.js (Turbopack)2.971s (-22.6% 🟢)3.764s (-15.3% 🟢)0.793s81.89x
💻 LocalNitro5.218s (+9.5% 🔺)5.684s (+6.3% 🔺)0.467s63.32x
💻 LocalExpress6.467s (+2.0%)7.216s (+2.8%)0.749s54.11x
💻 LocalNext.js (Turbopack)6.475s (+22.3% 🔺)7.018s (+16.7% 🔺)0.543s54.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.355s (-33.9% 🟢)6.132s (-26.5% 🟢)1.777s61.00x
▲ VercelNext.js (Turbopack)4.600s (-6.6% 🟢)6.524s (~)1.924s51.06x
▲ VercelExpress4.640s (-11.4% 🟢)6.693s (-3.9%)2.053s51.07x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.593s (-8.0% 🟢)1.023s (-1.8%)0.430s591.00x
💻 LocalNitro0.596s (+2.2%)1.005s (~)0.409s601.00x
🐘 PostgresExpress0.609s (+2.8%)1.023s (+1.8%)0.415s591.03x
💻 LocalExpress0.625s (-5.3% 🟢)1.005s (-3.3%)0.380s601.05x
🐘 PostgresNext.js (Turbopack)0.819s (-3.7%)1.006s (-1.7%)0.187s601.38x
💻 LocalNext.js (Turbopack)0.942s (+6.4% 🔺)1.096s (+3.7%)0.154s551.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.725s (-13.0% 🟢)7.514s (-12.9% 🟢)1.790s91.00x
▲ VercelNext.js (Turbopack)5.798s (-21.9% 🟢)7.584s (-18.8% 🟢)1.786s81.01x
▲ VercelNitro5.831s (-31.1% 🟢)7.298s (-27.8% 🟢)1.467s91.02x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.384s (-3.6%)2.007s (-1.1%)0.623s451.00x
🐘 PostgresNitro1.385s (-4.2%)2.008s (~)0.623s451.00x
💻 LocalNitro1.502s (+1.7%)2.006s (~)0.504s451.09x
💻 LocalExpress1.575s (-0.6%)2.007s (~)0.432s451.14x
🐘 PostgresNext.js (Turbopack)1.935s (-5.0% 🟢)2.150s (-16.1% 🟢)0.216s421.40x
💻 LocalNext.js (Turbopack)2.277s (+7.0% 🔺)3.009s (~)0.732s301.65x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.711s (-31.9% 🟢)16.057s (-26.5% 🟢)2.346s61.00x
▲ VercelNitro14.119s (-24.8% 🟢)16.038s (-22.7% 🟢)1.919s61.03x
▲ VercelNext.js (Turbopack)14.780s (-25.8% 🟢)16.884s (-24.6% 🟢)2.104s61.08x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.788s (-1.2%)3.166s (+0.9%)0.379s381.00x
🐘 PostgresExpress2.860s (+2.9%)3.252s (+3.7%)0.392s371.03x
💻 LocalNitro3.279s (+3.5%)4.009s (+2.5%)0.731s301.18x
💻 LocalExpress3.391s (+1.4%)4.010s (~)0.619s301.22x
🐘 PostgresNext.js (Turbopack)3.810s (-5.3% 🟢)4.043s (-8.2% 🟢)0.233s301.37x
💻 LocalNext.js (Turbopack)4.748s (+4.6%)5.054s (~)0.306s241.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.736s (-18.0% 🟢)32.583s (-14.7% 🟢)2.847s41.00x
▲ VercelExpress29.831s (-22.9% 🟢)32.492s (-19.6% 🟢)2.660s41.00x
▲ VercelNext.js (Turbopack)32.381s (-27.8% 🟢)35.293s (-24.4% 🟢)2.912s41.09x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.253s (-3.1%)1.006s (~)0.753s601.00x
🐘 PostgresExpress0.264s (+1.1%)1.023s (+1.7%)0.760s591.04x
🐘 PostgresNext.js (Turbopack)0.301s (-2.4%)1.006s (~)0.705s601.19x
💻 LocalExpress0.409s (-4.4%)1.005s (~)0.595s601.62x
💻 LocalNitro0.425s (-1.5%)1.005s (~)0.579s601.68x
💻 LocalNext.js (Turbopack)0.594s (-8.2% 🟢)1.006s (-4.9%)0.413s602.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.844s (-36.1% 🟢)3.799s (-24.5% 🟢)1.954s161.00x
▲ VercelExpress1.910s (-46.5% 🟢)3.900s (-30.2% 🟢)1.990s161.04x
▲ VercelNext.js (Turbopack)1.931s (-37.1% 🟢)3.841s (-23.4% 🟢)1.910s161.05x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.410s (-10.2% 🟢)1.029s (-4.6%)0.619s881.00x
🐘 PostgresExpress0.418s (+1.8%)1.078s (+7.1% 🔺)0.659s841.02x
🐘 PostgresNext.js (Turbopack)0.575s (-15.2% 🟢)1.118s (-12.4% 🟢)0.543s811.40x
💻 LocalNitro2.162s (+7.5% 🔺)2.687s (+10.1% 🔺)0.524s345.27x
💻 LocalExpress2.221s (+1.4%)2.769s (-2.9%)0.548s335.41x
💻 LocalNext.js (Turbopack)2.825s (+10.4% 🔺)3.512s (+7.7% 🔺)0.687s266.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.226s (-37.4% 🟢)5.211s (-27.8% 🟢)1.986s181.00x
▲ VercelNitro3.370s (-3.3%)5.242s (+1.4%)1.872s181.04x
▲ VercelNext.js (Turbopack)3.799s (~)5.760s (~)1.961s161.18x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.804s (-4.7%)1.527s (+3.8%)0.724s791.00x
🐘 PostgresExpress0.831s (-2.0%)1.471s (-2.4%)0.640s821.03x
🐘 PostgresNext.js (Turbopack)2.802s (-9.3% 🟢)3.651s (-4.6%)0.849s333.49x
💻 LocalNitro9.753s (+11.8% 🔺)10.447s (+12.9% 🔺)0.694s1212.14x
💻 LocalExpress9.767s (-1.0%)10.280s (~)0.514s1212.15x
💻 LocalNext.js (Turbopack)12.460s (+18.8% 🔺)13.337s (+17.1% 🔺)0.877s1015.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.603s (-17.0% 🟢)7.854s (-9.0% 🟢)2.251s161.00x
▲ VercelExpress5.898s (-13.7% 🟢)8.080s (-6.3% 🟢)2.182s151.05x
▲ VercelNext.js (Turbopack)7.576s (+5.3% 🔺)9.754s (+8.8% 🔺)2.178s131.35x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.163s (~)2.005s (~)0.010s (+1.0%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (-2.4%)1.999s (~)0.002s (+33.3% 🔺)2.011s (~)0.842s101.01x
💻 LocalExpress1.172s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.847s101.01x
🐘 PostgresExpress1.185s (+0.9%)2.000s (~)0.002s (+54.5% 🔺)2.012s (~)0.827s101.02x
🐘 PostgresNext.js (Turbopack)1.228s (-1.3%)2.001s (~)0.001s (-25.0% 🟢)2.011s (~)0.783s101.06x
💻 LocalNext.js (Turbopack)1.228s (-1.7%)2.004s (~)0.012s (+13.1% 🔺)2.021s (~)0.793s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.289s (-3.1%)3.325s (-5.8% 🟢)0.785s (+11.6% 🔺)4.593s (-4.1%)2.304s101.00x
▲ VercelNext.js (Turbopack)2.336s (-12.9% 🟢)3.384s (-9.7% 🟢)0.770s (-8.0% 🟢)4.613s (-9.3% 🟢)2.276s101.02x
▲ VercelNitro2.345s (-8.0% 🟢)3.480s (-6.3% 🟢)0.621s (-16.1% 🟢)4.531s (-8.7% 🟢)2.187s101.02x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.586s (+1.3%)2.009s (~)0.013s (+18.1% 🔺)2.025s (~)0.439s301.00x
💻 LocalExpress1.593s (-1.1%)2.011s (~)0.013s (+2.9%)2.026s (~)0.433s301.00x
🐘 PostgresExpress1.602s (~)2.006s (~)0.005s (+1.4%)2.027s (~)0.425s301.01x
🐘 PostgresNitro1.603s (-2.2%)2.004s (~)0.005s (-0.6%)2.026s (~)0.423s301.01x
🐘 PostgresNext.js (Turbopack)1.753s (-2.2%)2.010s (~)0.005s (-2.4%)2.026s (~)0.273s301.11x
💻 LocalNext.js (Turbopack)1.889s (+8.2% 🔺)2.080s (+3.7%)0.014s (+7.7% 🔺)2.098s (+3.6%)0.209s291.19x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.212s (-9.9% 🟢)7.300s (-12.6% 🟢)0.270s (-21.4% 🟢)8.082s (-12.4% 🟢)1.869s81.00x
▲ VercelNext.js (Turbopack)6.304s (-8.2% 🟢)7.685s (-10.4% 🟢)0.289s (-7.7% 🟢)8.468s (-9.7% 🟢)2.164s81.01x
▲ VercelNitro6.373s (-5.4% 🟢)7.745s (-3.7%)0.208s (-5.5% 🟢)8.596s (-2.4%)2.223s71.03x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.797s (~)1.063s (+1.4%)0.000s (+Infinity% 🔺)1.079s (+1.8%)0.282s561.00x
🐘 PostgresNitro0.798s (-1.8%)1.067s (~)0.000s (+1.8%)1.099s (+1.8%)0.301s551.00x
🐘 PostgresNext.js (Turbopack)1.016s (-1.1%)1.500s (-7.5% 🟢)0.000s (-7.5% 🟢)1.507s (-7.6% 🟢)0.491s401.28x
💻 LocalExpress1.413s (~)2.012s (~)0.000s (~)2.015s (~)0.602s301.77x
💻 LocalNitro1.468s (+8.6% 🔺)2.014s (~)0.000s (-42.9% 🟢)2.017s (~)0.549s301.84x
💻 LocalNext.js (Turbopack)1.592s (+5.3% 🔺)2.015s (~)0.000s (-18.2% 🟢)2.019s (~)0.426s302.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.933s (-28.7% 🟢)4.414s (-23.2% 🟢)0.000s (NaN%)4.919s (-21.2% 🟢)1.986s131.00x
▲ VercelExpress2.951s (-20.1% 🟢)4.593s (-6.3% 🟢)0.000s (+Infinity% 🔺)5.072s (-8.7% 🟢)2.121s121.01x
▲ VercelNext.js (Turbopack)3.232s (-14.2% 🟢)4.626s (-13.3% 🟢)0.000s (-100.0% 🟢)5.093s (-13.4% 🟢)1.860s121.10x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.560s (-7.9% 🟢)2.066s (-5.0% 🟢)0.000s (-3.4%)2.136s (-2.7%)0.576s291.00x
🐘 PostgresExpress1.628s (-1.5%)2.137s (-1.8%)0.000s (+Infinity% 🔺)2.156s (-1.4%)0.528s281.04x
🐘 PostgresNext.js (Turbopack)2.214s (-1.8%)2.776s (+1.7%)0.000s (NaN%)2.782s (+1.6%)0.568s221.42x
💻 LocalExpress3.162s (-4.7%)3.965s (-1.5%)0.000s (-62.5% 🟢)3.968s (-1.6%)0.806s162.03x
💻 LocalNitro3.179s (+3.2%)3.734s (-4.2%)0.001s (+61.3% 🔺)3.737s (-4.2%)0.558s172.04x
💻 LocalNext.js (Turbopack)3.265s (+15.1% 🔺)3.734s (+9.3% 🔺)0.000s (-41.2% 🟢)3.738s (+9.3% 🔺)0.473s172.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.284s (-29.4% 🟢)6.003s (-21.0% 🟢)0.000s (-100.0% 🟢)6.482s (-20.8% 🟢)2.198s101.00x
▲ VercelNitro4.573s (-7.1% 🟢)6.058s (-1.7%)0.000s (NaN%)6.512s (-4.2%)1.939s101.07x
▲ VercelNext.js (Turbopack)5.009s (-29.1% 🟢)6.602s (-19.8% 🟢)0.000s (-100.0% 🟢)7.080s (-19.2% 🟢)2.071s91.17x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro15/21
🐘 PostgresNitro16/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202191661
✅ 💻 Local Development189502192114
✅ 📦 Local Production189502192114
✅ 🐘 Local Postgres188102332114
✅ 🪟 Windows15100151
❌ 📋 Other87811781057
Total8142110689211

❌ Failed Tests

📋 Other (1 failed)

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

  • webhookWorkflow | wrun_01KV7X2YYXA571EE20AYFJ50VB

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125026
✅ example125026
✅ express125026
✅ fastify125026
✅ hono125026
✅ nextjs-turbopack14902
✅ nextjs-webpack14902
✅ nitro125026
✅ nuxt125026
✅ sveltekit14407
✅ vite125026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable125026
✅ express-stable125026
✅ fastify-stable125026
✅ hono-stable125026
✅ nextjs-turbopack-canary131020
✅ nextjs-turbopack-stable-lazy-discovery-disabled15001
✅ nextjs-turbopack-stable-lazy-discovery-enabled15001
✅ nextjs-webpack-canary131020
✅ nextjs-webpack-stable-lazy-discovery-disabled15001
✅ nextjs-webpack-stable-lazy-discovery-enabled15001
✅ nitro-stable125026
✅ nuxt-stable125026
✅ sveltekit-stable14407
✅ vite-stable125026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15100
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable126025
✅ e2e-local-dev-tanstack-start-126025
✅ e2e-local-postgres-nest-stable125026
✅ e2e-local-postgres-tanstack-start-125026
✅ e2e-local-prod-nest-stable126025
✅ e2e-local-prod-tanstack-start-126025
❌ e2e-vercel-prod-tanstack-start124126

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

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

This PR adds bounded queue-redelivery behavior for replay-time RuntimeDecryptionErrors in @workflow/core, allowing managed worlds to retry transient persisted-data read corruption before marking the run failed.

Changes:

  • Adds a decryption-failure retry decision helper and retry budget constant.
  • Integrates the helper into the workflow runtime error path before writing run_failed.
  • Adds unit and runtime coverage plus a patch changeset.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/decryption-failure.tsAdds redelivery decision logic and logging for replay decryption failures.
packages/core/src/runtime/decryption-failure.test.tsCovers managed/in-process world behavior and retry-budget boundaries.
packages/core/src/runtime/constants.tsDefines the decryption-failure retry budget.
packages/core/src/runtime.tsRedrives managed-world runs on eligible RuntimeDecryptionErrors before failing the run.
packages/core/src/runtime.test.tsAdds end-to-end key-mismatch tests for redelivery and failure behavior.
.changeset/requeue-on-decryption-failure.mdAdds a patch changeset for @workflow/core.

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

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: blocking issues found

return false;
}

if (attempt <= DECRYPTION_FAILURE_MAX_RETRIES) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Blocking

RuntimeDecryptionError also covers encrypt-side AES-GCM failures (context.operation === "encrypt"), but this helper redrives every RuntimeDecryptionError on managed worlds. A fresh queue delivery can only help when decrypting persisted bytes, not when serializing/encrypting a new workflow payload. This can re-run workflow code up to 3 times before failing. Please gate redelivery on error.context?.operation === "decrypt".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — shouldRedriveOnDecryptionFailure now returns false immediately when error.context?.operation !== "decrypt", so encrypt-side AES-GCM failures fail the run without redelivery (re-fetching cannot change a payload being serialized fresh). Added a unit test covering the encrypt case on a managed world. Commit 558cc3f.

// queue delivery re-fetches the event log + ref
// payloads and can succeed. On managed Worlds, exit the
// process (which the platform turns into a redelivery)
// for a bounded number of attempts before failing the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

This changes the user-facing behavior for runtime decryption failures on managed worlds: they now redeliver before the terminal run_failed. The existing runtime-decryption-failed docs still say the run fails immediately, so those docs should be updated as part of this behavior change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — updated errors/runtime-decryption-failed.mdx (both v4 and v5) to document the bounded queue-redelivery behavior on managed worlds: decrypt failures are now retried before the terminal run_failed, while encrypt-side failures and in-process worlds still fail immediately. Commit 558cc3f.

Comment threadpackages/core/src/runtime.test.ts Outdated
// Past DECRYPTION_FAILURE_MAX_RETRIES (3) but under MAX_QUEUE_DELIVERIES
// (48), so we exercise the decryption-failure budget, not the queue
// max-deliveries guard.
attempt: 10,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This test duplicates the retry/delivery constants in comments and uses attempt: 10 instead of deriving from the constant it is testing. Please import the constants and use DECRYPTION_FAILURE_MAX_RETRIES + 1, with an assertion that it remains below MAX_QUEUE_DELIVERIES, so future constant changes don't silently stale the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — the past-budget test now derives attempt = DECRYPTION_FAILURE_MAX_RETRIES + 1 from the imported constant (no duplicated magic numbers) and asserts attempt < MAX_QUEUE_DELIVERIES, so a future constant change cannot silently stale the test. Commit 558cc3f.

Resolve conflicts in runtime.ts, runtime.test.ts, and constants.ts
(decryption-failure redelivery + replay-divergence recovery coexist).
Address review feedback:
- Gate decryption-failure redelivery on operation === 'decrypt' so
encrypt-side AES-GCM failures fail immediately (re-fetching cannot help).
- Derive the past-budget test attempt from DECRYPTION_FAILURE_MAX_RETRIES + 1
and assert it stays below MAX_QUEUE_DELIVERIES.
- Document the bounded queue-redelivery behavior in runtime-decryption-failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

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

feat(core): bounded queue-redelivery retry on decryption failure during replay - #2166

Open
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure
Open

feat(core): bounded queue-redelivery retry on decryption failure during replay#2166
TooTallNate wants to merge 2 commits into
mainfrom
nate/requeue-on-decryption-failure

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Follow-up to #2145 (the RuntimeDecryptionError attribution fix), implementing the bounded-redelivery behavior @pranaygp described in this comment.

An AES-GCM authentication failure is terminal for the bytes/key of the current attempt — we must never continue executing the workflow on top of data we couldn't decrypt. But the failure is often not terminal for the run: when the ciphertext came from a transiently truncated or corrupted read of remotely-persisted data (a partial /refs response, an edge-cache miss returning a partial 200, a proxy drop during streaming), a fresh queue delivery re-fetches the event log and ref payloads from scratch and can succeed.

Previously we committed run_failed immediately, turning a potentially recoverable read failure into a terminal workflow failure.

What changed

Mirrors the existing replay-timeout bounded-redelivery precedent (handleReplayBudgetExhausted):

  • Managed worlds (processExitTriggersQueueRedelivery === true, e.g. world-vercel): on attempts ≤ DECRYPTION_FAILURE_MAX_RETRIES (3), the run handler exits the process — which the platform turns into a queue redelivery — so replay restarts from freshly-fetched persisted data. Once the retry budget is exhausted, it commits run_failed with RUNTIME_ERROR.
  • In-process worlds (world-local, dev servers, custom in-process worlds): no queue to re-fetch from, and process.exit() would kill the user's host, so the run fails immediately with RUNTIME_ERROR.

New shouldRedriveOnDecryptionFailure() helper (runtime/decryption-failure.ts) is pure (logging only) and returns whether the caller should redrive, keeping the process.exit / run_failed decision at the single existing call site in the run handler.

Test coverage

  • runtime/decryption-failure.test.ts — unit tests for the helper across managed/in-process worlds and the retry-budget boundary.
  • runtime.test.ts — end-to-end tests driving a real key mismatch (input encrypted with key A, run-key resolves to key B → auth-tag failure during input hydration):
    • early attempt on a managed world → process.exit(1), no run_failed
    • past the retry budget on a managed world → run_failed with RUNTIME_ERROR
    • in-process world → run_failed immediately, no exit

All @workflow/core tests pass (1073), full repo typecheck passes (40/40).

Notes

The longer-term improvement @pranaygp mentioned — detecting response truncation / integrity failure at the /refs transport boundary to classify the retryable case more directly — is out of scope here and would be a separate change on the world / workflow-server side.

…ng replay
Follow-up to #2145. An AES-GCM auth failure (RuntimeDecryptionError) is
terminal for the current attempt's bytes, but often transient at the run
level: when the ciphertext came from a truncated/corrupted read of
remotely-persisted data (e.g. a partial /refs response), a fresh queue
delivery re-fetches the event log + ref payloads and can succeed.
Mirror the replay-timeout bounded-redelivery precedent: on managed worlds
(processExitTriggersQueueRedelivery), exit the process to trigger queue
redelivery for up to DECRYPTION_FAILURE_MAX_RETRIES attempts, then commit
run_failed as RUNTIME_ERROR. In-process worlds fail immediately (no queue
to re-fetch from, and exiting would kill the host).
CopilotAI review requested due to automatic review settings May 29, 2026 19:24
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 29, 2026 19:24
@changeset-bot

changeset-botBot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 558cc3f

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

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@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 29, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+1.7%)1.006s (~)0.963s101.00x
💻 LocalExpress0.043s (-10.3% 🟢)1.007s (~)0.964s101.02x
🐘 PostgresNitro0.064s (-1.5%)1.011s (~)0.948s101.50x
🐘 PostgresNext.js (Turbopack)0.068s (-7.0% 🟢)1.011s (~)0.944s101.59x
🐘 PostgresExpress0.068s (+11.0% 🔺)1.012s (~)0.944s101.59x
💻 LocalNext.js (Turbopack)0.069s (+13.3% 🔺)1.006s (~)0.937s101.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.279s (-74.8% 🟢)2.020s (-43.5% 🟢)1.741s101.00x
▲ VercelNext.js (Turbopack)0.313s (-84.2% 🟢)2.337s (-43.1% 🟢)2.024s101.12x
▲ VercelNitro0.400s (-33.4% 🟢)2.213s (-16.8% 🟢)1.813s101.43x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.094s (~)2.006s (~)0.912s101.00x
💻 LocalExpress1.095s (~)2.007s (~)0.912s101.00x
🐘 PostgresExpress1.115s (~)2.010s (~)0.894s101.02x
🐘 PostgresNitro1.135s (+1.4%)2.009s (~)0.875s101.04x
🐘 PostgresNext.js (Turbopack)1.142s (-1.4%)2.011s (~)0.869s101.04x
💻 LocalNext.js (Turbopack)1.145s (~)2.007s (~)0.862s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.704s (-36.5% 🟢)3.836s (-15.2% 🟢)2.132s101.00x
▲ VercelExpress1.800s (-32.8% 🟢)3.830s (-23.4% 🟢)2.030s101.06x
▲ VercelNitro1.865s (-33.9% 🟢)3.713s (-18.3% 🟢)1.847s101.09x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.503s (~)11.022s (~)0.519s31.00x
💻 LocalExpress10.545s (~)11.023s (~)0.478s31.00x
🐘 PostgresExpress10.584s (~)11.018s (~)0.435s31.01x
🐘 PostgresNitro10.588s (~)11.015s (~)0.427s31.01x
💻 LocalNext.js (Turbopack)10.868s (+0.8%)11.023s (~)0.155s31.03x
🐘 PostgresNext.js (Turbopack)10.901s (~)11.016s (~)0.115s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)14.283s (-33.3% 🟢)16.662s (-28.4% 🟢)2.379s21.00x
▲ VercelNitro14.988s (-17.5% 🟢)17.110s (-13.9% 🟢)2.122s21.05x
▲ VercelExpress15.147s (-6.9% 🟢)17.654s (-5.2% 🟢)2.507s21.06x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.779s (~)14.029s (~)0.250s51.00x
🐘 PostgresNitro13.817s (~)14.020s (~)0.203s51.00x
💻 LocalExpress13.835s (~)14.027s (~)0.192s51.00x
🐘 PostgresExpress13.855s (~)14.020s (~)0.165s51.01x
🐘 PostgresNext.js (Turbopack)14.544s (~)15.019s (~)0.475s41.06x
💻 LocalNext.js (Turbopack)14.605s (+1.6%)15.030s (~)0.425s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.570s (-35.3% 🟢)24.870s (-32.6% 🟢)2.300s31.00x
▲ VercelNitro23.905s (-27.5% 🟢)25.993s (-25.4% 🟢)2.088s31.06x
▲ VercelNext.js (Turbopack)24.384s (-20.8% 🟢)26.434s (-19.6% 🟢)2.050s31.08x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.403s (-1.3%)13.024s (~)0.621s71.00x
💻 LocalExpress12.512s (-0.8%)13.024s (~)0.511s71.01x
🐘 PostgresNitro12.647s (~)13.023s (~)0.376s71.02x
🐘 PostgresExpress12.680s (+0.7%)13.019s (~)0.339s71.02x
🐘 PostgresNext.js (Turbopack)13.856s (-0.8%)14.020s (-1.0%)0.164s71.12x
💻 LocalNext.js (Turbopack)14.047s (+3.2%)14.744s (+5.1% 🔺)0.697s71.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express29.958s (-13.8% 🟢)32.678s (-11.9% 🟢)2.720s31.00x
▲ VercelNitro30.838s (-18.6% 🟢)33.293s (-15.7% 🟢)2.455s31.03x
▲ VercelNext.js (Turbopack)31.778s (-9.5% 🟢)33.997s (-9.4% 🟢)2.218s31.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.207s (+1.2%)2.006s (~)0.800s151.00x
💻 LocalNitro1.207s (+3.8%)2.006s (~)0.799s151.00x
🐘 PostgresNitro1.214s (~)2.008s (~)0.794s151.01x
🐘 PostgresExpress1.214s (+0.7%)2.009s (~)0.795s151.01x
🐘 PostgresNext.js (Turbopack)1.277s (-5.9% 🟢)2.007s (-3.3%)0.731s151.06x
💻 LocalNext.js (Turbopack)1.340s (+0.8%)2.007s (~)0.667s151.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.289s (-26.0% 🟢)3.763s (-17.9% 🟢)1.474s81.00x
▲ VercelExpress2.513s (-1.5%)4.117s (+1.1%)1.604s81.10x
▲ VercelNext.js (Turbopack)3.039s (+29.5% 🔺)4.619s (+15.0% 🔺)1.580s71.33x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.367s (+1.9%)2.393s (-4.6%)1.026s131.00x
🐘 PostgresExpress1.387s (+2.2%)2.223s (-11.3% 🟢)0.836s141.01x
🐘 PostgresNext.js (Turbopack)1.662s (+1.4%)2.295s (-8.5% 🟢)0.633s141.22x
💻 LocalNitro1.705s (+4.7%)2.006s (~)0.301s151.25x
💻 LocalExpress1.838s (-3.0%)2.150s (~)0.313s141.34x
💻 LocalNext.js (Turbopack)1.860s (+16.7% 🔺)2.075s (+3.4%)0.215s151.36x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.813s (-31.6% 🟢)4.734s (-21.8% 🟢)1.920s71.00x
▲ VercelNitro2.996s (-20.5% 🟢)4.799s (-11.1% 🟢)1.803s71.06x
▲ VercelNext.js (Turbopack)3.230s (-7.9% 🟢)5.201s (-1.0%)1.971s61.15x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.821s (+12.1% 🔺)3.887s (-6.1% 🟢)2.066s81.00x
🐘 PostgresExpress1.827s (+12.6% 🔺)4.011s (+3.2%)2.183s81.00x
🐘 PostgresNext.js (Turbopack)3.481s (-9.1% 🟢)4.299s (-3.3%)0.818s71.91x
💻 LocalNitro4.569s (+13.6% 🔺)5.015s (+9.5% 🔺)0.446s72.51x
💻 LocalExpress5.321s (+1.7%)5.846s (~)0.525s62.92x
💻 LocalNext.js (Turbopack)5.870s (+28.6% 🔺)6.348s (+22.6% 🔺)0.478s63.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.964s (-13.3% 🟢)6.243s (-4.8%)2.278s51.00x
▲ VercelNitro4.013s (-5.5% 🟢)6.018s (+5.1% 🔺)2.005s51.01x
▲ VercelNext.js (Turbopack)4.065s (+7.3% 🔺)6.166s (+14.0% 🔺)2.101s51.03x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.218s (~)2.007s (~)0.789s151.00x
🐘 PostgresExpress1.220s (+0.8%)2.008s (~)0.789s151.00x
🐘 PostgresNext.js (Turbopack)1.272s (-1.1%)2.008s (~)0.736s151.04x
💻 LocalNext.js (Turbopack)1.424s (+3.3%)2.008s (~)0.584s151.17x
💻 LocalNitro1.560s (+3.2%)2.007s (~)0.447s151.28x
💻 LocalExpress1.593s (-1.3%)2.007s (~)0.414s151.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.290s (-0.7%)3.776s (-4.3%)1.486s81.00x
▲ VercelNitro2.297s (-64.6% 🟢)3.691s (-54.0% 🟢)1.394s91.00x
▲ VercelExpress2.406s (+5.8% 🔺)3.962s (+2.8%)1.556s81.05x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.357s (~)2.315s (-10.7% 🟢)0.958s131.00x
🐘 PostgresExpress1.406s (+0.8%)2.592s (+4.9%)1.186s121.04x
🐘 PostgresNext.js (Turbopack)1.583s (+3.0%)2.152s (-3.3%)0.569s141.17x
💻 LocalNext.js (Turbopack)2.095s (+8.5% 🔺)2.918s (+31.3% 🔺)0.823s111.54x
💻 LocalNitro2.187s (+22.2% 🔺)2.676s (+29.1% 🔺)0.489s121.61x
💻 LocalExpress2.349s (-8.8% 🟢)2.827s (-9.0% 🟢)0.479s111.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.794s (-25.4% 🟢)4.495s (-17.7% 🟢)1.700s71.00x
▲ VercelNext.js (Turbopack)3.325s (-17.1% 🟢)5.058s (-12.0% 🟢)1.733s61.19x
▲ VercelExpress3.330s (~)5.327s (~)1.997s61.19x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.572s (-9.0% 🟢)4.011s (+3.2%)2.439s81.00x
🐘 PostgresExpress1.646s (-6.1% 🟢)4.014s (-2.9%)2.368s81.05x
🐘 PostgresNext.js (Turbopack)2.971s (-22.6% 🟢)3.764s (-15.3% 🟢)0.793s81.89x
💻 LocalNitro5.218s (+9.5% 🔺)5.684s (+6.3% 🔺)0.467s63.32x
💻 LocalExpress6.467s (+2.0%)7.216s (+2.8%)0.749s54.11x
💻 LocalNext.js (Turbopack)6.475s (+22.3% 🔺)7.018s (+16.7% 🔺)0.543s54.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.355s (-33.9% 🟢)6.132s (-26.5% 🟢)1.777s61.00x
▲ VercelNext.js (Turbopack)4.600s (-6.6% 🟢)6.524s (~)1.924s51.06x
▲ VercelExpress4.640s (-11.4% 🟢)6.693s (-3.9%)2.053s51.07x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.593s (-8.0% 🟢)1.023s (-1.8%)0.430s591.00x
💻 LocalNitro0.596s (+2.2%)1.005s (~)0.409s601.00x
🐘 PostgresExpress0.609s (+2.8%)1.023s (+1.8%)0.415s591.03x
💻 LocalExpress0.625s (-5.3% 🟢)1.005s (-3.3%)0.380s601.05x
🐘 PostgresNext.js (Turbopack)0.819s (-3.7%)1.006s (-1.7%)0.187s601.38x
💻 LocalNext.js (Turbopack)0.942s (+6.4% 🔺)1.096s (+3.7%)0.154s551.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.725s (-13.0% 🟢)7.514s (-12.9% 🟢)1.790s91.00x
▲ VercelNext.js (Turbopack)5.798s (-21.9% 🟢)7.584s (-18.8% 🟢)1.786s81.01x
▲ VercelNitro5.831s (-31.1% 🟢)7.298s (-27.8% 🟢)1.467s91.02x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.384s (-3.6%)2.007s (-1.1%)0.623s451.00x
🐘 PostgresNitro1.385s (-4.2%)2.008s (~)0.623s451.00x
💻 LocalNitro1.502s (+1.7%)2.006s (~)0.504s451.09x
💻 LocalExpress1.575s (-0.6%)2.007s (~)0.432s451.14x
🐘 PostgresNext.js (Turbopack)1.935s (-5.0% 🟢)2.150s (-16.1% 🟢)0.216s421.40x
💻 LocalNext.js (Turbopack)2.277s (+7.0% 🔺)3.009s (~)0.732s301.65x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.711s (-31.9% 🟢)16.057s (-26.5% 🟢)2.346s61.00x
▲ VercelNitro14.119s (-24.8% 🟢)16.038s (-22.7% 🟢)1.919s61.03x
▲ VercelNext.js (Turbopack)14.780s (-25.8% 🟢)16.884s (-24.6% 🟢)2.104s61.08x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.788s (-1.2%)3.166s (+0.9%)0.379s381.00x
🐘 PostgresExpress2.860s (+2.9%)3.252s (+3.7%)0.392s371.03x
💻 LocalNitro3.279s (+3.5%)4.009s (+2.5%)0.731s301.18x
💻 LocalExpress3.391s (+1.4%)4.010s (~)0.619s301.22x
🐘 PostgresNext.js (Turbopack)3.810s (-5.3% 🟢)4.043s (-8.2% 🟢)0.233s301.37x
💻 LocalNext.js (Turbopack)4.748s (+4.6%)5.054s (~)0.306s241.70x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.736s (-18.0% 🟢)32.583s (-14.7% 🟢)2.847s41.00x
▲ VercelExpress29.831s (-22.9% 🟢)32.492s (-19.6% 🟢)2.660s41.00x
▲ VercelNext.js (Turbopack)32.381s (-27.8% 🟢)35.293s (-24.4% 🟢)2.912s41.09x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.253s (-3.1%)1.006s (~)0.753s601.00x
🐘 PostgresExpress0.264s (+1.1%)1.023s (+1.7%)0.760s591.04x
🐘 PostgresNext.js (Turbopack)0.301s (-2.4%)1.006s (~)0.705s601.19x
💻 LocalExpress0.409s (-4.4%)1.005s (~)0.595s601.62x
💻 LocalNitro0.425s (-1.5%)1.005s (~)0.579s601.68x
💻 LocalNext.js (Turbopack)0.594s (-8.2% 🟢)1.006s (-4.9%)0.413s602.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.844s (-36.1% 🟢)3.799s (-24.5% 🟢)1.954s161.00x
▲ VercelExpress1.910s (-46.5% 🟢)3.900s (-30.2% 🟢)1.990s161.04x
▲ VercelNext.js (Turbopack)1.931s (-37.1% 🟢)3.841s (-23.4% 🟢)1.910s161.05x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.410s (-10.2% 🟢)1.029s (-4.6%)0.619s881.00x
🐘 PostgresExpress0.418s (+1.8%)1.078s (+7.1% 🔺)0.659s841.02x
🐘 PostgresNext.js (Turbopack)0.575s (-15.2% 🟢)1.118s (-12.4% 🟢)0.543s811.40x
💻 LocalNitro2.162s (+7.5% 🔺)2.687s (+10.1% 🔺)0.524s345.27x
💻 LocalExpress2.221s (+1.4%)2.769s (-2.9%)0.548s335.41x
💻 LocalNext.js (Turbopack)2.825s (+10.4% 🔺)3.512s (+7.7% 🔺)0.687s266.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.226s (-37.4% 🟢)5.211s (-27.8% 🟢)1.986s181.00x
▲ VercelNitro3.370s (-3.3%)5.242s (+1.4%)1.872s181.04x
▲ VercelNext.js (Turbopack)3.799s (~)5.760s (~)1.961s161.18x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.804s (-4.7%)1.527s (+3.8%)0.724s791.00x
🐘 PostgresExpress0.831s (-2.0%)1.471s (-2.4%)0.640s821.03x
🐘 PostgresNext.js (Turbopack)2.802s (-9.3% 🟢)3.651s (-4.6%)0.849s333.49x
💻 LocalNitro9.753s (+11.8% 🔺)10.447s (+12.9% 🔺)0.694s1212.14x
💻 LocalExpress9.767s (-1.0%)10.280s (~)0.514s1212.15x
💻 LocalNext.js (Turbopack)12.460s (+18.8% 🔺)13.337s (+17.1% 🔺)0.877s1015.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.603s (-17.0% 🟢)7.854s (-9.0% 🟢)2.251s161.00x
▲ VercelExpress5.898s (-13.7% 🟢)8.080s (-6.3% 🟢)2.182s151.05x
▲ VercelNext.js (Turbopack)7.576s (+5.3% 🔺)9.754s (+8.8% 🔺)2.178s131.35x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.163s (~)2.005s (~)0.010s (+1.0%)2.017s (~)0.855s101.00x
🐘 PostgresNitro1.169s (-2.4%)1.999s (~)0.002s (+33.3% 🔺)2.011s (~)0.842s101.01x
💻 LocalExpress1.172s (~)2.005s (~)0.012s (+0.8%)2.020s (~)0.847s101.01x
🐘 PostgresExpress1.185s (+0.9%)2.000s (~)0.002s (+54.5% 🔺)2.012s (~)0.827s101.02x
🐘 PostgresNext.js (Turbopack)1.228s (-1.3%)2.001s (~)0.001s (-25.0% 🟢)2.011s (~)0.783s101.06x
💻 LocalNext.js (Turbopack)1.228s (-1.7%)2.004s (~)0.012s (+13.1% 🔺)2.021s (~)0.793s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.289s (-3.1%)3.325s (-5.8% 🟢)0.785s (+11.6% 🔺)4.593s (-4.1%)2.304s101.00x
▲ VercelNext.js (Turbopack)2.336s (-12.9% 🟢)3.384s (-9.7% 🟢)0.770s (-8.0% 🟢)4.613s (-9.3% 🟢)2.276s101.02x
▲ VercelNitro2.345s (-8.0% 🟢)3.480s (-6.3% 🟢)0.621s (-16.1% 🟢)4.531s (-8.7% 🟢)2.187s101.02x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.586s (+1.3%)2.009s (~)0.013s (+18.1% 🔺)2.025s (~)0.439s301.00x
💻 LocalExpress1.593s (-1.1%)2.011s (~)0.013s (+2.9%)2.026s (~)0.433s301.00x
🐘 PostgresExpress1.602s (~)2.006s (~)0.005s (+1.4%)2.027s (~)0.425s301.01x
🐘 PostgresNitro1.603s (-2.2%)2.004s (~)0.005s (-0.6%)2.026s (~)0.423s301.01x
🐘 PostgresNext.js (Turbopack)1.753s (-2.2%)2.010s (~)0.005s (-2.4%)2.026s (~)0.273s301.11x
💻 LocalNext.js (Turbopack)1.889s (+8.2% 🔺)2.080s (+3.7%)0.014s (+7.7% 🔺)2.098s (+3.6%)0.209s291.19x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.212s (-9.9% 🟢)7.300s (-12.6% 🟢)0.270s (-21.4% 🟢)8.082s (-12.4% 🟢)1.869s81.00x
▲ VercelNext.js (Turbopack)6.304s (-8.2% 🟢)7.685s (-10.4% 🟢)0.289s (-7.7% 🟢)8.468s (-9.7% 🟢)2.164s81.01x
▲ VercelNitro6.373s (-5.4% 🟢)7.745s (-3.7%)0.208s (-5.5% 🟢)8.596s (-2.4%)2.223s71.03x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.797s (~)1.063s (+1.4%)0.000s (+Infinity% 🔺)1.079s (+1.8%)0.282s561.00x
🐘 PostgresNitro0.798s (-1.8%)1.067s (~)0.000s (+1.8%)1.099s (+1.8%)0.301s551.00x
🐘 PostgresNext.js (Turbopack)1.016s (-1.1%)1.500s (-7.5% 🟢)0.000s (-7.5% 🟢)1.507s (-7.6% 🟢)0.491s401.28x
💻 LocalExpress1.413s (~)2.012s (~)0.000s (~)2.015s (~)0.602s301.77x
💻 LocalNitro1.468s (+8.6% 🔺)2.014s (~)0.000s (-42.9% 🟢)2.017s (~)0.549s301.84x
💻 LocalNext.js (Turbopack)1.592s (+5.3% 🔺)2.015s (~)0.000s (-18.2% 🟢)2.019s (~)0.426s302.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.933s (-28.7% 🟢)4.414s (-23.2% 🟢)0.000s (NaN%)4.919s (-21.2% 🟢)1.986s131.00x
▲ VercelExpress2.951s (-20.1% 🟢)4.593s (-6.3% 🟢)0.000s (+Infinity% 🔺)5.072s (-8.7% 🟢)2.121s121.01x
▲ VercelNext.js (Turbopack)3.232s (-14.2% 🟢)4.626s (-13.3% 🟢)0.000s (-100.0% 🟢)5.093s (-13.4% 🟢)1.860s121.10x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.560s (-7.9% 🟢)2.066s (-5.0% 🟢)0.000s (-3.4%)2.136s (-2.7%)0.576s291.00x
🐘 PostgresExpress1.628s (-1.5%)2.137s (-1.8%)0.000s (+Infinity% 🔺)2.156s (-1.4%)0.528s281.04x
🐘 PostgresNext.js (Turbopack)2.214s (-1.8%)2.776s (+1.7%)0.000s (NaN%)2.782s (+1.6%)0.568s221.42x
💻 LocalExpress3.162s (-4.7%)3.965s (-1.5%)0.000s (-62.5% 🟢)3.968s (-1.6%)0.806s162.03x
💻 LocalNitro3.179s (+3.2%)3.734s (-4.2%)0.001s (+61.3% 🔺)3.737s (-4.2%)0.558s172.04x
💻 LocalNext.js (Turbopack)3.265s (+15.1% 🔺)3.734s (+9.3% 🔺)0.000s (-41.2% 🟢)3.738s (+9.3% 🔺)0.473s172.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.284s (-29.4% 🟢)6.003s (-21.0% 🟢)0.000s (-100.0% 🟢)6.482s (-20.8% 🟢)2.198s101.00x
▲ VercelNitro4.573s (-7.1% 🟢)6.058s (-1.7%)0.000s (NaN%)6.512s (-4.2%)1.939s101.07x
▲ VercelNext.js (Turbopack)5.009s (-29.1% 🟢)6.602s (-19.8% 🟢)0.000s (-100.0% 🟢)7.080s (-19.2% 🟢)2.071s91.17x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro15/21
🐘 PostgresNitro16/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

@github-actions

github-actionsBot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202191661
✅ 💻 Local Development189502192114
✅ 📦 Local Production189502192114
✅ 🐘 Local Postgres188102332114
✅ 🪟 Windows15100151
❌ 📋 Other87811781057
Total8142110689211

❌ Failed Tests

📋 Other (1 failed)

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

  • webhookWorkflow | wrun_01KV7X2YYXA571EE20AYFJ50VB

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125026
✅ example125026
✅ express125026
✅ fastify125026
✅ hono125026
✅ nextjs-turbopack14902
✅ nextjs-webpack14902
✅ nitro125026
✅ nuxt125026
✅ sveltekit14407
✅ vite125026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable126025
✅ express-stable126025
✅ fastify-stable126025
✅ hono-stable126025
✅ nextjs-turbopack-canary132019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15100
✅ nextjs-turbopack-stable-lazy-discovery-enabled15100
✅ nextjs-webpack-canary132019
✅ nextjs-webpack-stable-lazy-discovery-disabled15100
✅ nextjs-webpack-stable-lazy-discovery-enabled15100
✅ nitro-stable126025
✅ nuxt-stable126025
✅ sveltekit-stable14506
✅ vite-stable126025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable125026
✅ express-stable125026
✅ fastify-stable125026
✅ hono-stable125026
✅ nextjs-turbopack-canary131020
✅ nextjs-turbopack-stable-lazy-discovery-disabled15001
✅ nextjs-turbopack-stable-lazy-discovery-enabled15001
✅ nextjs-webpack-canary131020
✅ nextjs-webpack-stable-lazy-discovery-disabled15001
✅ nextjs-webpack-stable-lazy-discovery-enabled15001
✅ nitro-stable125026
✅ nuxt-stable125026
✅ sveltekit-stable14407
✅ vite-stable125026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15100
❌ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable126025
✅ e2e-local-dev-tanstack-start-126025
✅ e2e-local-postgres-nest-stable125026
✅ e2e-local-postgres-tanstack-start-125026
✅ e2e-local-prod-nest-stable126025
✅ e2e-local-prod-tanstack-start-126025
❌ e2e-vercel-prod-tanstack-start124126

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

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

This PR adds bounded queue-redelivery behavior for replay-time RuntimeDecryptionErrors in @workflow/core, allowing managed worlds to retry transient persisted-data read corruption before marking the run failed.

Changes:

  • Adds a decryption-failure retry decision helper and retry budget constant.
  • Integrates the helper into the workflow runtime error path before writing run_failed.
  • Adds unit and runtime coverage plus a patch changeset.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/decryption-failure.tsAdds redelivery decision logic and logging for replay decryption failures.
packages/core/src/runtime/decryption-failure.test.tsCovers managed/in-process world behavior and retry-budget boundaries.
packages/core/src/runtime/constants.tsDefines the decryption-failure retry budget.
packages/core/src/runtime.tsRedrives managed-world runs on eligible RuntimeDecryptionErrors before failing the run.
packages/core/src/runtime.test.tsAdds end-to-end key-mismatch tests for redelivery and failure behavior.
.changeset/requeue-on-decryption-failure.mdAdds a patch changeset for @workflow/core.

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

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI review: blocking issues found

return false;
}

if (attempt <= DECRYPTION_FAILURE_MAX_RETRIES) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Blocking

RuntimeDecryptionError also covers encrypt-side AES-GCM failures (context.operation === "encrypt"), but this helper redrives every RuntimeDecryptionError on managed worlds. A fresh queue delivery can only help when decrypting persisted bytes, not when serializing/encrypting a new workflow payload. This can re-run workflow code up to 3 times before failing. Please gate redelivery on error.context?.operation === "decrypt".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — shouldRedriveOnDecryptionFailure now returns false immediately when error.context?.operation !== "decrypt", so encrypt-side AES-GCM failures fail the run without redelivery (re-fetching cannot change a payload being serialized fresh). Added a unit test covering the encrypt case on a managed world. Commit 558cc3f.

// queue delivery re-fetches the event log + ref
// payloads and can succeed. On managed Worlds, exit the
// process (which the platform turns into a redelivery)
// for a bounded number of attempts before failing the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Note

This changes the user-facing behavior for runtime decryption failures on managed worlds: they now redeliver before the terminal run_failed. The existing runtime-decryption-failed docs still say the run fails immediately, so those docs should be updated as part of this behavior change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — updated errors/runtime-decryption-failed.mdx (both v4 and v5) to document the bounded queue-redelivery behavior on managed worlds: decrypt failures are now retried before the terminal run_failed, while encrypt-side failures and in-process worlds still fail immediately. Commit 558cc3f.

Comment threadpackages/core/src/runtime.test.ts Outdated
// Past DECRYPTION_FAILURE_MAX_RETRIES (3) but under MAX_QUEUE_DELIVERIES
// (48), so we exercise the decryption-failure budget, not the queue
// max-deliveries guard.
attempt: 10,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

AI Review: Nit

This test duplicates the retry/delivery constants in comments and uses attempt: 10 instead of deriving from the constant it is testing. Please import the constants and use DECRYPTION_FAILURE_MAX_RETRIES + 1, with an assertion that it remains below MAX_QUEUE_DELIVERIES, so future constant changes don't silently stale the test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(AI) Done — the past-budget test now derives attempt = DECRYPTION_FAILURE_MAX_RETRIES + 1 from the imported constant (no duplicated magic numbers) and asserts attempt < MAX_QUEUE_DELIVERIES, so a future constant change cannot silently stale the test. Commit 558cc3f.

Resolve conflicts in runtime.ts, runtime.test.ts, and constants.ts
(decryption-failure redelivery + replay-divergence recovery coexist).
Address review feedback:
- Gate decryption-failure redelivery on operation === 'decrypt' so
encrypt-side AES-GCM failures fail immediately (re-fetching cannot help).
- Derive the past-budget test attempt from DECRYPTION_FAILURE_MAX_RETRIES + 1
and assert it stays below MAX_QUEUE_DELIVERIES.
- Document the bounded queue-redelivery behavior in runtime-decryption-failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious