Skip to content

[core] Combine flow+step bundle and process steps eagerly - #1338

Merged
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow
May 4, 2026
Merged

[core] Combine flow+step bundle and process steps eagerly#1338
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow

Conversation

@changeset-bot

changeset-botBot commented Mar 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 234fce4

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

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

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 Mar 11, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Mar 11, 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.033s (-24.6% 🟢)1.005s (~)0.972s101.00x
💻 LocalExpress0.034s (-23.3% 🟢)1.006s (~)0.972s101.05x
🐘 PostgresNext.js (Turbopack)0.049s1.013s0.964s101.50x
💻 LocalNext.js (Turbopack)0.049s1.005s0.956s101.51x
🐘 PostgresNitro0.051s (-46.8% 🟢)1.012s (-3.0%)0.961s101.56x
🌐 RedisNext.js (Turbopack)0.055s1.005s0.951s101.68x
🌐 MongoDBNext.js (Turbopack)0.109s1.007s0.899s103.35x
🐘 PostgresExpress0.177s (+205.2% 🔺)1.107s (+9.5% 🔺)0.930s105.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.236s (-42.4% 🟢)1.939s (-22.7% 🟢)1.703s101.00x
▲ VercelExpress0.241s (+2.2%)2.009s (-5.9% 🟢)1.768s101.02x
▲ VercelNext.js (Turbopack)0.550s (+118.6% 🔺)2.715s (+16.4% 🔺)2.165s102.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.069s (-5.0% 🟢)2.006s (~)0.937s101.00x
💻 LocalNitro1.073s (-5.1% 🟢)2.006s (~)0.933s101.00x
🐘 PostgresNitro1.084s (-4.9%)2.010s (~)0.927s101.01x
🌐 RedisNext.js (Turbopack)1.113s2.006s0.893s101.04x
🐘 PostgresNext.js (Turbopack)1.114s2.007s0.893s101.04x
💻 LocalNext.js (Turbopack)1.121s2.007s0.886s101.05x
🐘 PostgresExpress1.143s (~)2.021s (+0.5%)0.878s101.07x
🌐 MongoDBNext.js (Turbopack)1.162s2.009s0.847s101.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.498s (-20.1% 🟢)3.691s (-3.1%)2.193s101.00x
▲ VercelNitro1.524s (-60.9% 🟢)3.710s (-37.2% 🟢)2.186s101.02x
▲ VercelNext.js (Turbopack)1.942s (-4.6%)4.457s (+16.4% 🔺)2.516s101.30x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.409s (-4.2%)11.018s (~)0.609s31.00x
💻 LocalNitro10.413s (-4.9%)11.022s (~)0.608s31.00x
💻 LocalExpress10.436s (-4.5%)11.021s (~)0.586s31.00x
🐘 PostgresNext.js (Turbopack)10.568s11.013s0.445s31.02x
🌐 RedisNext.js (Turbopack)10.657s11.024s0.367s31.02x
💻 LocalNext.js (Turbopack)10.685s11.022s0.337s31.03x
🐘 PostgresExpress10.745s (-2.0%)11.419s (+3.6%)0.674s31.03x
🌐 MongoDBNext.js (Turbopack)10.789s11.018s0.229s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.059s (-40.8% 🟢)15.700s (-37.5% 🟢)1.641s21.00x
▲ VercelNext.js (Turbopack)14.845s (-14.3% 🟢)16.872s (-13.0% 🟢)2.027s21.06x
▲ VercelExpress15.996s (-5.8% 🟢)18.196s (-9.1% 🟢)2.200s21.14x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.431s (-10.8% 🟢)14.027s (-12.5% 🟢)0.596s51.00x
🐘 PostgresNitro13.434s (-8.0% 🟢)14.013s (-6.8% 🟢)0.579s51.00x
💻 LocalExpress13.490s (-9.9% 🟢)14.027s (-6.7% 🟢)0.537s51.00x
🐘 PostgresNext.js (Turbopack)13.820s14.018s0.198s51.03x
💻 LocalNext.js (Turbopack)14.076s15.030s0.954s41.05x
🌐 RedisNext.js (Turbopack)14.088s15.030s0.942s41.05x
🌐 MongoDBNext.js (Turbopack)14.283s15.019s0.736s41.06x
🐘 PostgresExpress14.537s (~)15.276s (+1.7%)0.739s41.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.821s (-58.6% 🟢)22.773s (-56.7% 🟢)1.952s31.00x
▲ VercelNitro21.829s (-66.1% 🟢)23.502s (-64.7% 🟢)1.673s31.05x
▲ VercelNext.js (Turbopack)23.199s (-55.9% 🟢)25.174s (-53.9% 🟢)1.975s31.11x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.846s (-29.4% 🟢)12.022s (-29.4% 🟢)0.176s81.00x
🐘 PostgresNitro11.923s (-14.6% 🟢)12.392s (-13.4% 🟢)0.469s81.01x
💻 LocalExpress11.997s (-27.7% 🟢)12.398s (-27.2% 🟢)0.401s81.01x
🌐 RedisNext.js (Turbopack)13.015s13.454s0.439s71.10x
💻 LocalNext.js (Turbopack)13.089s14.026s0.937s71.10x
🐘 PostgresNext.js (Turbopack)13.215s13.585s0.370s71.12x
🌐 MongoDBNext.js (Turbopack)13.301s14.021s0.720s71.12x
🐘 PostgresExpress15.027s (+7.3% 🔺)15.400s (+5.5% 🔺)0.373s61.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.095s (-74.3% 🟢)33.159s (-73.2% 🟢)2.064s31.00x
▲ VercelNitro31.508s (-92.5% 🟢)33.124s (-92.2% 🟢)1.615s31.01x
▲ VercelNext.js (Turbopack)31.987s (-91.9% 🟢)33.996s (-91.4% 🟢)2.009s31.03x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.140s (-10.6% 🟢)2.007s (~)0.868s151.00x
🐘 PostgresNext.js (Turbopack)1.168s2.007s0.838s151.03x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.187s (-20.3% 🟢)2.006s (~)0.820s151.04x
🐘 PostgresExpress1.237s (-1.9%)2.018s (~)0.781s151.09x
🌐 RedisNext.js (Turbopack)1.238s2.006s0.768s151.09x
💻 LocalNext.js (Turbopack)1.279s2.005s0.726s151.12x
🌐 MongoDBNext.js (Turbopack)2.048s2.917s0.869s111.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.551s (-10.8% 🟢)4.187s (-9.4% 🟢)1.636s81.00x
▲ VercelNitro2.674s (-5.1% 🟢)4.258s (-1.5%)1.584s81.05x
▲ VercelNext.js (Turbopack)4.193s (+23.4% 🔺)6.817s (+38.2% 🔺)2.624s51.64x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.225s (-47.9% 🟢)2.007s (-33.3% 🟢)0.782s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.05x
🐘 PostgresExpress1.483s (-37.2% 🟢)2.019s (-32.9% 🟢)0.536s151.21x
💻 LocalNitro1.726s (-45.1% 🟢)2.072s (-46.7% 🟢)0.346s151.41x
💻 LocalNext.js (Turbopack)1.831s2.149s0.318s141.50x
💻 LocalExpress1.905s (-35.5% 🟢)2.150s (-37.8% 🟢)0.244s141.56x
🌐 RedisNext.js (Turbopack)2.364s3.008s0.643s101.93x
🌐 MongoDBNext.js (Turbopack)3.568s4.008s0.440s82.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.844s (-5.1% 🟢)5.284s (-10.8% 🟢)1.440s61.00x
▲ VercelExpress3.884s (+7.3% 🔺)6.116s (+19.7% 🔺)2.232s51.01x
▲ VercelNext.js (Turbopack)5.098s (-28.2% 🟢)7.574s (-15.0% 🟢)2.476s41.33x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.419s (-59.2% 🟢)2.007s (-49.9% 🟢)0.588s151.00x
🐘 PostgresNext.js (Turbopack)1.493s2.007s0.514s151.05x
🐘 PostgresExpress2.153s (-38.2% 🟢)2.861s (-28.7% 🟢)0.708s111.52x
🌐 RedisNext.js (Turbopack)3.634s4.010s0.376s82.56x
💻 LocalNitro4.547s (-45.5% 🟢)5.013s (-44.4% 🟢)0.466s63.20x
💻 LocalExpress4.829s (-42.1% 🟢)5.512s (-38.9% 🟢)0.683s63.40x
💻 LocalNext.js (Turbopack)5.581s6.213s0.631s53.93x
🌐 MongoDBNext.js (Turbopack)6.289s7.012s0.723s54.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.147s (+21.4% 🔺)7.340s (+19.8% 🔺)2.193s51.00x
▲ VercelNitro5.410s (+53.5% 🔺)7.160s (+29.4% 🔺)1.750s51.05x
▲ VercelNext.js (Turbopack)7.157s (-19.7% 🟢)8.877s (-19.0% 🟢)1.721s41.39x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.170s (-6.9% 🟢)2.007s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.173s2.007s0.835s151.00x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.774s151.05x
💻 LocalNext.js (Turbopack)1.327s2.006s0.679s151.13x
💻 LocalNitro1.380s (-26.0% 🟢)2.006s (-14.3% 🟢)0.626s151.18x
💻 LocalExpress1.393s (-26.5% 🟢)2.006s (-15.1% 🟢)0.613s151.19x
🐘 PostgresExpress1.408s (+12.0% 🔺)2.062s (+2.7%)0.654s151.20x
🌐 MongoDBNext.js (Turbopack)2.036s2.916s0.880s111.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.271s (-7.6% 🟢)4.045s (-3.0%)1.774s81.00x
▲ VercelExpress2.336s (-9.5% 🟢)3.766s (-13.4% 🟢)1.430s81.03x
▲ VercelNext.js (Turbopack)4.170s (+42.2% 🔺)6.038s (+30.1% 🔺)1.869s51.84x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.251s (-46.5% 🟢)2.007s (-33.3% 🟢)0.757s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.03x
🐘 PostgresExpress1.393s (-40.5% 🟢)2.035s (-32.4% 🟢)0.642s151.11x
💻 LocalNitro1.957s (-36.2% 🟢)2.316s (-40.4% 🟢)0.359s131.56x
💻 LocalExpress2.022s (-35.4% 🟢)2.508s (-33.3% 🟢)0.485s121.62x
💻 LocalNext.js (Turbopack)2.130s2.826s0.696s111.70x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.88x
🌐 MongoDBNext.js (Turbopack)3.543s4.007s0.464s82.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.338s (+35.9% 🔺)6.011s (+25.4% 🔺)1.673s51.00x
▲ VercelNitro4.664s (+44.2% 🔺)6.658s (+31.1% 🔺)1.994s51.08x
▲ VercelNext.js (Turbopack)4.714s (+50.0% 🔺)6.542s (+44.7% 🔺)1.827s51.09x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.380s (-60.3% 🟢)2.007s (-49.9% 🟢)0.627s151.00x
🐘 PostgresNext.js (Turbopack)1.483s2.008s0.525s151.07x
🐘 PostgresExpress1.982s (-43.4% 🟢)2.737s (-31.8% 🟢)0.755s111.44x
🌐 RedisNext.js (Turbopack)3.571s4.010s0.439s82.59x
💻 LocalNitro4.894s (-46.5% 🟢)5.348s (-46.7% 🟢)0.454s63.55x
💻 LocalExpress5.939s (-32.5% 🟢)6.417s (-30.8% 🟢)0.478s54.30x
💻 LocalNext.js (Turbopack)6.115s6.416s0.301s54.43x
🌐 MongoDBNext.js (Turbopack)6.302s7.013s0.710s54.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.380s (+5.6% 🔺)7.062s (+3.6%)1.682s51.00x
▲ VercelNext.js (Turbopack)5.690s (-15.8% 🟢)7.416s (-13.2% 🟢)1.726s51.06x
▲ VercelExpress5.700s (-11.2% 🟢)7.539s (-7.8% 🟢)1.840s51.06x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.459s (-44.1% 🟢)1.022s (+1.6%)0.564s591.00x
💻 LocalNitro0.472s (-51.8% 🟢)1.021s (-6.7% 🟢)0.548s591.03x
💻 LocalExpress0.482s (-51.1% 🟢)1.004s (-6.7% 🟢)0.522s601.05x
🐘 PostgresNext.js (Turbopack)0.599s1.040s0.442s581.31x
🌐 RedisNext.js (Turbopack)0.619s1.004s0.386s601.35x
🐘 PostgresExpress0.644s (-23.2% 🟢)1.140s (+11.4% 🔺)0.496s531.40x
💻 LocalNext.js (Turbopack)0.719s1.004s0.285s601.57x
🌐 MongoDBNext.js (Turbopack)0.733s1.006s0.272s601.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.661s (-75.5% 🟢)6.339s (-70.3% 🟢)1.677s101.00x
▲ VercelNitro5.218s (-76.3% 🟢)7.258s (-69.8% 🟢)2.039s101.12x
▲ VercelNext.js (Turbopack)6.455s (-55.5% 🟢)7.922s (-50.7% 🟢)1.466s81.38x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.085s (-43.7% 🟢)1.705s (-18.8% 🟢)0.620s531.00x
🐘 PostgresExpress1.162s (-41.2% 🟢)1.588s (-29.7% 🟢)0.426s571.07x
💻 LocalExpress1.197s (-60.3% 🟢)2.006s (-44.1% 🟢)0.809s451.10x
💻 LocalNitro1.220s (-59.8% 🟢)2.005s (-46.6% 🟢)0.786s451.12x
🌐 RedisNext.js (Turbopack)1.476s2.006s0.530s451.36x
🐘 PostgresNext.js (Turbopack)1.707s2.387s0.679s381.57x
💻 LocalNext.js (Turbopack)1.753s2.005s0.253s451.61x
🌐 MongoDBNext.js (Turbopack)1.813s2.007s0.194s451.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.733s (-70.3% 🟢)13.729s (-66.8% 🟢)1.996s71.00x
▲ VercelExpress12.793s (-62.9% 🟢)14.645s (-60.2% 🟢)1.852s71.09x
▲ VercelNext.js (Turbopack)14.268s (-71.4% 🟢)16.580s (-67.9% 🟢)2.313s61.22x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.108s (-48.6% 🟢)2.696s (-41.4% 🟢)0.588s451.00x
🐘 PostgresExpress2.634s (-34.0% 🟢)3.074s (-29.6% 🟢)0.440s401.25x
🐘 PostgresNext.js (Turbopack)2.659s3.084s0.425s391.26x
💻 LocalNitro2.660s (-71.4% 🟢)3.007s (-70.0% 🟢)0.348s401.26x
💻 LocalExpress2.743s (-70.2% 🟢)3.033s (-69.7% 🟢)0.290s401.30x
🌐 RedisNext.js (Turbopack)2.992s3.191s0.200s381.42x
💻 LocalNext.js (Turbopack)3.748s4.007s0.259s301.78x
🌐 MongoDBNext.js (Turbopack)4.178s5.012s0.833s241.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express35.192s (-72.9% 🟢)37.772s (-71.4% 🟢)2.580s41.00x
▲ VercelNitro36.812s (-62.0% 🟢)39.053s (-60.3% 🟢)2.241s41.05x
▲ VercelNext.js (Turbopack)43.007s (-59.9% 🟢)46.755s (-57.1% 🟢)3.748s31.22x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-44.2% 🟢)1.007s (~)0.850s601.00x
🐘 PostgresNitro0.182s (-35.8% 🟢)1.006s (~)0.824s601.15x
🐘 PostgresNext.js (Turbopack)0.190s1.005s0.815s601.21x
🌐 RedisNext.js (Turbopack)0.316s1.021s0.705s592.00x
💻 LocalNitro0.412s (-31.9% 🟢)1.004s (-1.7%)0.592s602.61x
💻 LocalExpress0.452s (-19.3% 🟢)1.004s (~)0.552s602.87x
💻 LocalNext.js (Turbopack)0.603s1.039s0.436s583.83x
🌐 MongoDBNext.js (Turbopack)1.029s1.749s0.720s356.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.178s (+11.5% 🔺)4.588s (+26.2% 🔺)2.410s141.00x
▲ VercelNitro2.258s (+36.0% 🔺)4.485s (+33.9% 🔺)2.227s141.04x
▲ VercelNext.js (Turbopack)3.716s (+83.7% 🔺)5.458s (+43.9% 🔺)1.742s111.71x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.304s (-38.8% 🟢)1.006s (~)0.702s901.00x
🌐 RedisNext.js (Turbopack)0.423s1.004s0.581s901.39x
🐘 PostgresExpress0.520s (+2.0%)1.112s (+10.5% 🔺)0.592s821.71x
🐘 PostgresNext.js (Turbopack)0.705s1.318s0.613s692.32x
💻 LocalNitro2.170s (-14.5% 🟢)2.821s (-6.3% 🟢)0.651s327.14x
💻 LocalExpress2.186s (-13.0% 🟢)2.944s (-2.2%)0.758s317.19x
💻 LocalNext.js (Turbopack)2.424s3.043s0.619s307.97x
🌐 MongoDBNext.js (Turbopack)2.595s3.007s0.412s308.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.618s (+87.2% 🔺)8.356s (+60.9% 🔺)1.738s111.00x
▲ VercelNitro7.134s (+121.1% 🔺)9.355s (+94.0% 🔺)2.221s101.08x
▲ VercelExpress7.527s (+147.1% 🔺)9.552s (+98.7% 🔺)2.025s101.14x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.637s (-19.4% 🟢)1.006s (~)0.369s1201.00x
🐘 PostgresNext.js (Turbopack)0.730s1.013s0.284s1191.15x
🐘 PostgresExpress0.790s (-3.5%)1.192s (+17.1% 🔺)0.402s1011.24x
🌐 RedisNext.js (Turbopack)0.797s1.004s0.207s1201.25x
🌐 MongoDBNext.js (Turbopack)5.387s6.013s0.626s208.46x
💻 LocalNitro9.790s (-12.5% 🟢)10.276s (-11.9% 🟢)0.486s1215.37x
💻 LocalExpress10.085s (-9.9% 🟢)10.782s (-9.7% 🟢)0.697s1215.84x
💻 LocalNext.js (Turbopack)11.478s12.128s0.650s1018.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.821s (+117.8% 🔺)19.141s (+103.6% 🔺)2.319s71.00x
▲ VercelNext.js (Turbopack)18.466s (+78.8% 🔺)20.419s (+66.2% 🔺)1.953s71.10x
▲ VercelExpress20.247s (+172.9% 🔺)22.460s (+143.0% 🔺)2.213s61.20x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.133s (+430.1% 🔺)2.005s (+99.6% 🔺)0.010s (-19.2% 🟢)2.017s (+98.0% 🔺)0.884s101.00x
🐘 PostgresNitro1.134s (+453.4% 🔺)2.001s (+100.2% 🔺)0.001s (-26.7% 🟢)2.010s (+98.7% 🔺)0.875s101.00x
💻 LocalExpress1.136s (+470.5% 🔺)2.005s (+99.6% 🔺)0.012s (~)2.020s (+98.4% 🔺)0.884s101.00x
💻 LocalNext.js (Turbopack)1.172s2.003s0.012s2.019s0.847s101.03x
🐘 PostgresExpress1.232s (+500.5% 🔺)2.005s (+100.8% 🔺)0.001s (-37.5% 🟢)2.041s (+101.8% 🔺)0.810s101.09x
🐘 PostgresNext.js (Turbopack)2.024s2.601s0.220s2.874s0.850s101.79x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.200s (-42.6% 🟢)3.478s (-34.1% 🟢)1.562s (+110.5% 🔺)5.432s (-16.2% 🟢)3.232s101.00x
▲ VercelExpress2.336s (-6.8% 🟢)3.935s (-3.8%)1.087s (+13.1% 🔺)5.425s (-3.0%)3.089s101.06x
▲ VercelNext.js (Turbopack)4.802s (-29.9% 🟢)4.900s (-43.4% 🟢)0.740s (+17.0% 🔺)7.346s (-25.0% 🟢)2.544s102.18x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.512s (+142.2% 🔺)2.005s (+99.2% 🔺)0.004s (-8.9% 🟢)2.024s (+97.9% 🔺)0.512s301.00x
💻 LocalNitro1.514s (+80.5% 🔺)2.011s (+98.7% 🔺)0.010s (+2.8%)2.022s (+81.2% 🔺)0.509s301.00x
💻 LocalExpress1.721s (+127.3% 🔺)2.011s (+95.5% 🔺)0.010s (+2.1%)2.202s (+111.7% 🔺)0.481s281.14x
🐘 PostgresNext.js (Turbopack)1.722s2.107s0.003s2.123s0.401s301.14x
💻 LocalNext.js (Turbopack)1.845s2.011s0.009s2.203s0.358s281.22x
🐘 PostgresExpress1.983s (+214.8% 🔺)2.330s (+131.5% 🔺)0.003s (-20.7% 🟢)2.395s (+134.1% 🔺)0.412s261.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.656s (-13.0% 🟢)7.281s (-9.1% 🟢)0.179s (-56.1% 🟢)8.330s (-5.7% 🟢)2.675s81.00x
▲ VercelNitro5.796s (-80.3% 🟢)7.455s (-75.8% 🟢)0.170s (+52.1% 🔺)8.071s (-74.6% 🟢)2.275s81.02x
▲ VercelNext.js (Turbopack)12.255s (-27.6% 🟢)13.600s (-25.4% 🟢)0.201s (-4.9%)15.301s (-19.2% 🟢)3.046s42.17x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.651s (-32.8% 🟢)1.033s (-17.2% 🟢)0.000s (-100.0% 🟢)1.042s (-17.2% 🟢)0.390s581.00x
🐘 PostgresExpress1.031s (+7.3% 🔺)1.390s (+8.7% 🔺)0.000s (+9.5% 🔺)1.437s (+10.0% 🔺)0.406s421.58x
🐘 PostgresNext.js (Turbopack)1.333s1.618s0.000s1.715s0.382s372.05x
💻 LocalNitro1.337s (+9.4% 🔺)2.015s (~)0.000s (+166.7% 🔺)2.017s (~)0.680s302.05x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-60.0% 🟢)2.017s (~)0.649s302.10x
💻 LocalNext.js (Turbopack)1.436s2.013s0.000s2.016s0.581s302.20x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.922s (+28.6% 🔺)5.598s (+27.4% 🔺)0.000s (-100.0% 🟢)6.027s (+25.3% 🔺)2.105s101.00x
▲ VercelExpress4.013s (+7.3% 🔺)5.879s (+15.2% 🔺)0.000s (-100.0% 🟢)6.416s (+16.0% 🔺)2.404s101.02x
▲ VercelNext.js (Turbopack)5.465s (-46.3% 🟢)6.707s (-41.8% 🟢)0.000s (+Infinity% 🔺)7.645s (-36.6% 🟢)2.181s81.39x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.327s (-25.9% 🟢)2.066s (-3.5%)0.000s (-100.0% 🟢)2.081s (-4.3%)0.754s291.00x
🐘 PostgresNext.js (Turbopack)1.844s2.402s0.000s2.460s0.616s251.39x
🐘 PostgresExpress2.377s (+34.2% 🔺)3.042s (+39.7% 🔺)0.000s (NaN%)3.091s (+40.6% 🔺)0.714s201.79x
💻 LocalNext.js (Turbopack)2.776s3.362s0.000s3.365s0.589s182.09x
💻 LocalNitro3.068s (-9.4% 🟢)3.838s (-4.8%)0.000s (-53.1% 🟢)3.843s (-4.8%)0.775s162.31x
💻 LocalExpress3.103s (-10.5% 🟢)3.775s (-6.4% 🟢)0.000s (-76.6% 🟢)3.781s (-6.3% 🟢)0.679s162.34x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.059s (+10.3% 🔺)6.788s (+12.7% 🔺)0.000s (+Infinity% 🔺)7.571s (+17.3% 🔺)2.512s81.00x
▲ VercelNitro5.089s (+24.3% 🔺)6.536s (+21.6% 🔺)0.000s (-18.5% 🟢)7.002s (+20.9% 🔺)1.913s91.01x
▲ VercelNext.js (Turbopack)8.675s (+54.5% 🔺)10.065s (+44.2% 🔺)0.000s (-100.0% 🟢)11.283s (+49.6% 🔺)2.608s61.71x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro17/21
🐘 PostgresNitro19/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other4380150588
Total4266010265292

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025

📋 View full workflow run

Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/vitest/src/index.ts
Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/core/src/workflow.ts Outdated
Comment threadpackages/builders/src/base-builder.ts
Comment threadpackages/core/src/runtime/get-world-lazy.ts

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Remaining issues:

  • getWorldLazy needs to be validated as a decision
  • packages/core/e2e/utils.ts skip for hasStepSourceMaps needs to be re-evaluated

@shtefcs

Copy link
Copy Markdown

Seems you had a lot of work here. It's not easy as it seems. Hopefully, this gonna solve some issues we are facing with Workflow.

Peace,

@TooTallNateTooTallNate 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.

Withdrawing my prior CHANGES_REQUESTED — the blocker I called out (the stale (err as any).meta?.retryAfter access in step-executor.ts) is fixed at line 169 with err.retryAfter ?? 1. As an aside, my fix recommendation in the prior review was based on a faulty type assumption (I confused TooEarlyError.retryAfter: number with RetryableError.retryAfter: Date). The author's err.retryAfter ?? 1 form is correct: it's a number of seconds with sensible fallback.

My other earlier concerns:

  • getStepNameFromEvent full-event-load: addressed. stepName is now plumbed through WorkflowInvokePayloadSchema so the handler reads it directly from the queue message payload instead of doing a paginated event load. ✓
  • step.error guard for max retries: known limitation acknowledged in-thread. The proposed long-term fix (server-side ne(status, 'running') on step_started) is separate work.
  • Background step → continuation enqueue safety: acknowledged as intentional.

So my specific items are resolved. But I'm not approving outright — there's still substantial unresolved review from @VaguelySerious touching real concerns I'd defer to them on:

  • Removal of the LegacyStreamWorld shim
  • Limiting queue concurrency to 1 in tests
  • Comments removed without explanation in suspension-handler.ts / runtime.ts
  • The builder-deferred.ts Next.js behavior prediction
  • Several "is there a systematic fix instead of conditionally allowing edge cases" questions

Plus the auto-flagged event cache duplication bug from Vercel's review bot is worth confirming was understood and dismissed (or fixed).

Approval-wise this should land when @VaguelySerious's concerns are resolved. From my side: no remaining blockers.

@TooTallNateTooTallNate 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.

Thorough re-review for Peter. Spent significant time tracing four risk surfaces: the Vercel bot's auto-flag, the concurrent-step-execution model, inline-step replay determinism, and the open review threads. Bottom line up front: no ship-blockers from my side. A few coverage gaps and one acknowledged limitation worth being explicit about, but nothing I'd hold this PR on.

Vercel bot's "event cache duplication" auto-flag is a false positive

"wait_completed events pushed locally without advancing eventsCursor cause duplicate events on next incremental fetch."

The author anticipated exactly this scenario. runtime.ts:585–594 has explicit dedupe-by-eventId logic:

constexistingIds=newSet(cachedEvents.map((e)=>e.eventId));for(consteofloaded.events){if(!existingIds.has(e.eventId)){cachedEvents.push(e);}}

Verified end-to-end: locally-pushed wait_completed (line 685) keeps its real server-returned eventId, the next iteration's incremental fetch returns it again, the dedupe filter drops it, the consumer never sees a duplicate. The comment in the code calls out the exact scenario the bot flagged. Safe to dismiss the auto-flag.

Concurrent-step-execution: a partial fix shipping with a known gap

Verified the L198 guard (step.attempt > maxRetries + 1 && step.error) traces correctly for the scenario it targets — concurrent first-attempts before any user-code error has been recorded. Without step.error, the guard correctly skips and the step proceeds normally even with inflated step.attempt.

But there's a symmetric gap in the catch-block path at L469:

if(currentAttempt>=maxRetries+1){/* step_failed */}

This check has no && step.error guard, so it fires on the first real catch-block failure even when currentAttempt has been inflated by concurrent first-attempts. End result: a workflow with maxRetries=3 and 4+ concurrent handlers can permanently fail after a single transient flake, with a misleading log saying "exceeded max retries (4 retries)" when the step body actually only ran once.

This is the only substantive open thread on the PR (step-executor.ts:198), and the author has acknowledged it in a separate resolved thread:

"Agreed on the concurrent-retry limitation. The server-side ne(status, 'running') WHERE clause is the right long-term fix — tracking as a follow-up."

So everyone's eyes-open. Worth being explicit in the changelog or PR description that under high queue back-pressure, default-maxRetries workflows can spuriously fail. Either of these would close the gap fully:

  1. Add the same && step.error guard at L469 (mirrors L198, easy to do, but doesn't fix the L198 case for catch-block).
  2. Server-side ne(status, 'running') on step_started (referenced in the resolved thread; also fixes the world-testing concurrency issue Peter resolved separately).

Not a ship-stopper, but I'd want it tracked as a known issue users can hit, not just an internal todo.

Inline-step replay: ordering subtlety in the dedupe path

The locally-pushed-then-incremental-fetch path can produce non-monotonic-by-eventId ordering in cachedEvents. Specifically: after wait_completed is pushed locally (L685) without advancing the cursor, the next incremental fetch can return concurrent events with eventIds between the local push and earlier events. After dedupe, cachedEvents becomes append-ordered but not eventId-ordered. The EventsConsumer walks by index, so it consumes them in append order, which is not the canonical replay order.

In practice this is unlikely to cause issues because:

  • Within one handler's writes, the same world process generates monotonic ULIDs.
  • Cross-handler events for the same run are gated by the ownership check (step_created ownership at L813).
  • The dangerous case requires concurrent handler activity for the same run + non-trivial cross-process clock skew.

But it's a footgun. The cleanest fix would be: after locally pushing an event, set eventsCursor to that event's eventId so the next fetch skips it. This also eliminates the dedupe pass entirely. Worth considering as a follow-up.

"VaguelySerious" review threads — context for Peter

For other reviewers who may stumble on this: most of the open @VaguelySerious threads are Peter's own AI-review notes posted via tooling. Of 28 such threads, 27 are either resolved or outdated (the diff has moved past them). The single unresolved + non-outdated thread is the concurrent-retry concern above (step-executor.ts:198), which Peter has acknowledged as a known limitation in a sibling resolved thread.

External-reviewer threads (mine, the Vercel bot's) are all resolved or addressed. So the apparent "long list of unresolved concerns" from my prior comment is misleading — it was conflating Peter's self-review notes with external review.

Test coverage gaps (non-blocking)

A few coverage gaps for novel V2 behavior worth tracking even if you ship:

  1. Dedupe path at runtime.ts:585–594 — zero test coverage. The behavior is novel to V2 and load-bearing for incremental-replay correctness. A unit test that:

    • First iteration fetches with cursor C0
    • Manually pushes a wait_completed to cachedEvents
    • Second iteration fetches and gets the same wait_completed plus an interleaved new event
    • Asserts: no duplicates AND consumer sees no orphans
      ...would lock in the contract.
  2. L198 guard's positive case — there's no test asserting that attempt > maxRetries + 1withoutstep.error does NOT fail the step. The guard exists specifically for this case and is currently un-asserted. The two step-handler.test.ts tests at lines 296–340 and 343–388 both set error on the fixture, so they exercise different paths.

  3. Concurrent-handler races — neither Scenario B (concurrent retry with prior error → premature L198 failure) nor Scenario D (catch-block premature failure with no && step.error guard) has a test. Adding these would catch the bug currently in code if a future fix lands.

  4. builder-deferred.ts Next.js manifest reading — Peter agreed in review that reading app-paths-manifest.json was the right approach over regex prediction. Verified that DID land (L1171–1175 reads the manifest with comment "This avoids predicting Next.js conventions with regexes and instead reads from Next.js's own output"). ✅

What looks good

  • The architectural shift is well-executed. The replay loop, ownership-gated inline dispatch, idempotency-key-based queue dedupe, builder unification, and __steps_registered rollup-tree-shaking guard are all clean.
  • The getStepNameFromEvent full-event-load concern from my prior review was addressed — stepName now flows through WorkflowInvokePayloadSchema. ✓
  • The TooEarlyError stale retryAfter access pattern is fixed at step-executor.ts:169. ✓
  • The CJS module.exports collision (ESM format for steps when bundleFinalOutput: true) is a subtle but correct fix.
  • The changelog doc docs/content/docs/changelog/eager-processing.mdx is one of the better architecture docs I've seen in a PR — covers every edge case, failure mode, and platform-specific quirk.

Verdict

No blockers from my side. Ship if Vade and storytime green-light. The concurrent-retry premature-failure case is real but acknowledged, the auto-flagged "event cache duplication" is a non-issue, and the test coverage gaps are real but not regressions (the V2 behaviors they cover are net-new).

If you want one piece of polish before merge, the L469 && step.error mirror would close the most user-visible failure mode at zero cost — but I won't gate on it.

@TooTallNateTooTallNate 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.

Pre-emptively :approved: but my agent really wants @VaguelySerious to resolve the open threads :lol: and there seems to be a few other things pointed out in the latest review comment.

VaguelySeriousand others added 7 commits May 3, 2026 11:38
Merges the V1 split flow/step routes (one queue message per step + one per
flow continuation, two separate function invocations per step) into a
single combined handler at /.well-known/workflow/v1/flow that executes
steps inline within the same invocation as the workflow replay.
A serial workflow with N steps that previously required ~2N+1 function
invocations now completes in 1.
Architecture
------------
- packages/core: workflowEntrypoint() handler with an inline replay loop.
Each loop iteration: load events incrementally, replay workflow,
inline-execute one owned step on suspension, loop. Background steps
(from Promise.all) are queued back to the same handler with stepId so
the in-flight handler advances the loop without queue round-trips
whenever possible.
- packages/builders: createCombinedBundle() emits a single ESM bundle
with workflowEntrypoint(workflowCode) plus a side-effect
__step_registrations bundle. ESM-by-default with createRequire banner
for CJS deps (matches main's #1562 pattern).
- packages/next, nestjs, sveltekit, astro, nitro, nuxt, hono, express,
fastify, vite: framework builders updated to use createCombinedBundle.
The step/ directory is no longer generated.
Correctness invariants (V2 polish)
----------------------------------
- Single inline executor per step. Atomic step_created per correlationId
(per-step in-process mutex in world-local; SQL-level guards in
postgres/vercel) means exactly one handler claims ownership of each
step. Inline execution gates on ownership; queue dispatch is
unconditional with idempotency keys for crash recovery (matches V1).
- onUnconsumedEvent reverts to its original PR #1055 contract: any
unconsumed event fatals as CORRUPTED_EVENT_LOG. Source-level fixes
removed every code path that produced orphaned step lifecycle events
during V2 work, so the skip branch became unnecessary.
- Lock-release polling interval lowered from 100ms to 10ms in
flushable-stream so the V2 step-executor's ops-settle race typically
resolves in ~5ms instead of ~50ms per writable-bearing step.
- Per-iteration runs.get round-trip eliminated: concurrent completion
is detected by scanning the loaded event log for terminal run events
instead.
- Worker-pool deadlock guard removed from Run#pollReturnValue. The
fibonacci/recursive parent→child polling case is now handled by
raising world-postgres default queueConcurrency from 10 to 50, with
the limitation documented at three call sites (fibonacciWorkflow's
JSDoc, queue.ts, eager-processing.mdx).
- getWorldLazy module-scope singleton breaks the step-bundle import
chain to world.ts so step bundles don't transitively pull in
world-vercel + cbor-x + the VQS dev-handler resolver.
World-local atomicity
---------------------
- Per-step in-process mutex serializes events.create for step
lifecycle events on a (runId, correlationId) key. step_started is
rejected only when the step is already terminal — duplicate
step_started on a non-terminal step is allowed (preserves retry
semantics after SIGKILL).
Runtime helpers
---------------
- loadWorkflowRunEvents(runId, afterCursor?) consolidates the
previous getAllWorkflowRunEvents{,WithCursor} + getNewWorkflowRunEvents
into one helper.
Documentation
-------------
- docs/content/docs/changelog/eager-processing.mdx: full record of
architecture, edge cases, and the fixes applied during the V2 work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Two CI failures from the earlier V2 cutover, now resolved:
1. @workflow/vitest unit tests (`packages/vitest/src/index.test.ts`)
were still mocking the V1 BaseBuilder methods (createWorkflowsBundle
+ createStepsBundle) and expecting two world handlers (workflow +
step). Updated the mocks to expose createCombinedBundle and
asserted a single combined handler at __wkf_workflow_, which is
what the V2 vitest builder actually emits.
2. E2E Local Dev Tests (nextjs-webpack) were asserting that step
error stacks contain `99_e2e.ts` / `helpers.ts`, which held in
pre-V2 webpack dev mode (which imported step sources directly).
V2 inlines the step bundle into the combined flow route, and
webpack's re-bundling collapses original step filenames out of the
dev-mode source maps. Extended the existing nextjs-webpack
carve-out in hasStepSourceMaps() to cover dev as well as prod, and
updated the source-map follow-up callout in eager-processing.mdx
to include nextjs-webpack in the matrix that needs source maps
wired up for V2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/e2e/e2e.test.ts` (PR #1879 vs. V2 hookDispose helper)
Drop the locally-defined `waitForHook(expectedRunId)` shadow in the
hookDisposeTestWorkflow test in favor of the new top-level
`waitForHook(token, { runId })` API merged from main. Keep V2's
event-driven `waitForHookDisposal()` helper, which is stricter than
main's `await sleep(3_000)` (it polls `getHookByToken` for the
HookNotFoundError signal rather than waiting a fixed interval).
* `packages/world-local/src/storage/events-storage.ts` (PR #1877 vs.
V2 per-step async mutex)
Both branches were closing race conditions in the local world's
step lifecycle. PR #1877 adds filesystem-level O_CREAT|O_EXCL locks
for `step_created` and `wait_created`; the V2 branch already wraps
step lifecycle events in an in-process `withStepLock` mutex. They
compose: the mutex serializes within a single Node process; the
filesystem lock additionally protects against cross-process races
(multiple pnpm workers, redelivered queue messages). Resolution
takes V2's mutex wrap as the base and patches in main's two
`writeExclusive` claims at the start of the `step_created` and
`wait_created` handlers, with comments noting the dual-layer
guarantee.
`packages/core/e2e/utils.ts` auto-merged: V2's nextjs-webpack +
all-Vercel source-map carve-outs preserved alongside main's
`tanstack-start` addition to `hasWorkflowSourceMaps`.
Verified locally: `@workflow/core` and `@workflow/world-local`
typecheck clean; all 343 world-local unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone tarballs project's `vercel.json` set
`"buildCommand": "pnpm --filter tarballs build"`, which only runs the
tarballs package's `build` script (`node scripts/pack.ts`) and does
NOT trigger turbo's `dependsOn: ["^build"]` chain. As a result, every
workspace package was packed with an empty `dist/` directory — only
`bin/`, `package.json`, `README.md`, and `docs/` made it into the
tarballs. Downstream installs failed with
`Cannot find module '<pkg>/dist/next.cjs'` (and similar for every
other entry point).
The previous docs-based pipeline didn't hit this because Vercel
detected `docs` as a Next.js framework, ran turbo for it, and pack.ts
was wired as a `prebuild` step that ran after deps were already
built.
Fix: switch the buildCommand to `pnpm turbo build --filter=tarballs`
so turbo evaluates the build task for `tarballs`, which fans out via
`^build` to compile every workspace dependency first, and then runs
`tarballs#build` (pack.ts) against the populated `dist/` directories.
Verified locally: a fresh build produces tarballs containing the full
`dist/` tree (including `dist/next.cjs`, the file flight-booking-app
was failing to resolve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/src/logger.ts` (PR #1849 vs. V2 webpack carve-out)
PR #1849 ("Friendlier workflow errors") reintroduced the `debug`
package as a static import and added a structured `Logger`
interface with `child()` / `forRun()` and `composeLogLine`. The V2
branch had previously removed the `debug` static import because it
pulls `debug/src/node` and a dynamic `require('tty')` into the
Next.js webpack flow route, breaking V2 builds with
`Dynamic require of "tty" is not supported`.
Resolution combines both: keep main's `Logger` interface,
`child()` / `forRun()`, and `composeLogLine` integration; replace
the `debug` invocations with the V2 lightweight
`matchesDebugNamespace` + `process.env.DEBUG` matcher so the flow
route still bundles cleanly under webpack. Comment in the file
documents why `debug` is intentionally absent.
* `packages/core/src/runtime.ts` (PR #1849 vs. V2 inline replay loop)
Multiple regions where main's structured-logging refactor and the
V2 inline-execution loop made overlapping changes. Took V2's
control flow as the base — its `if (!workflowRun) { ... }` setup
block, main replay `while (true)` loop with timeout-based
re-scheduling, terminal-event short-circuit, and per-iteration
incremental event load are all preserved as-is — and folded in
main's contributions: the scoped `runLogger = runtimeLogger.forRun(
runId, workflowName)` is created once and used for run-level info
/ error logging. Discarded main's V1 wait-completion loop and
`runWorkflow` user-code error block since V2 handles waits and
user-code errors inside the inline loop body. Updated
`buildWorkflowSuspensionMessage()` call to drop a stray `runId`
first arg that didn't match the helper's 3-param signature.
* `packages/core/e2e/e2e.test.ts` (PR #1849 unrelated to V2)
No conflict from main's friendly-errors PR, auto-merged. (No
hookDispose-style conflict this round.)
Verified locally: all 820 `@workflow/core` unit tests pass; all 343
`@workflow/world-local` unit tests pass; `pnpm turbo typecheck` is
green across all 40 typecheck tasks.
Co-Authored-By: Claude Opus 4.7 (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

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

[core] Combine flow+step bundle and process steps eagerly - #1338

Merged
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow
May 4, 2026
Merged

[core] Combine flow+step bundle and process steps eagerly#1338
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow

Conversation

@changeset-bot

changeset-botBot commented Mar 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 234fce4

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

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

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 Mar 11, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Mar 11, 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.033s (-24.6% 🟢)1.005s (~)0.972s101.00x
💻 LocalExpress0.034s (-23.3% 🟢)1.006s (~)0.972s101.05x
🐘 PostgresNext.js (Turbopack)0.049s1.013s0.964s101.50x
💻 LocalNext.js (Turbopack)0.049s1.005s0.956s101.51x
🐘 PostgresNitro0.051s (-46.8% 🟢)1.012s (-3.0%)0.961s101.56x
🌐 RedisNext.js (Turbopack)0.055s1.005s0.951s101.68x
🌐 MongoDBNext.js (Turbopack)0.109s1.007s0.899s103.35x
🐘 PostgresExpress0.177s (+205.2% 🔺)1.107s (+9.5% 🔺)0.930s105.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.236s (-42.4% 🟢)1.939s (-22.7% 🟢)1.703s101.00x
▲ VercelExpress0.241s (+2.2%)2.009s (-5.9% 🟢)1.768s101.02x
▲ VercelNext.js (Turbopack)0.550s (+118.6% 🔺)2.715s (+16.4% 🔺)2.165s102.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.069s (-5.0% 🟢)2.006s (~)0.937s101.00x
💻 LocalNitro1.073s (-5.1% 🟢)2.006s (~)0.933s101.00x
🐘 PostgresNitro1.084s (-4.9%)2.010s (~)0.927s101.01x
🌐 RedisNext.js (Turbopack)1.113s2.006s0.893s101.04x
🐘 PostgresNext.js (Turbopack)1.114s2.007s0.893s101.04x
💻 LocalNext.js (Turbopack)1.121s2.007s0.886s101.05x
🐘 PostgresExpress1.143s (~)2.021s (+0.5%)0.878s101.07x
🌐 MongoDBNext.js (Turbopack)1.162s2.009s0.847s101.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.498s (-20.1% 🟢)3.691s (-3.1%)2.193s101.00x
▲ VercelNitro1.524s (-60.9% 🟢)3.710s (-37.2% 🟢)2.186s101.02x
▲ VercelNext.js (Turbopack)1.942s (-4.6%)4.457s (+16.4% 🔺)2.516s101.30x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.409s (-4.2%)11.018s (~)0.609s31.00x
💻 LocalNitro10.413s (-4.9%)11.022s (~)0.608s31.00x
💻 LocalExpress10.436s (-4.5%)11.021s (~)0.586s31.00x
🐘 PostgresNext.js (Turbopack)10.568s11.013s0.445s31.02x
🌐 RedisNext.js (Turbopack)10.657s11.024s0.367s31.02x
💻 LocalNext.js (Turbopack)10.685s11.022s0.337s31.03x
🐘 PostgresExpress10.745s (-2.0%)11.419s (+3.6%)0.674s31.03x
🌐 MongoDBNext.js (Turbopack)10.789s11.018s0.229s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.059s (-40.8% 🟢)15.700s (-37.5% 🟢)1.641s21.00x
▲ VercelNext.js (Turbopack)14.845s (-14.3% 🟢)16.872s (-13.0% 🟢)2.027s21.06x
▲ VercelExpress15.996s (-5.8% 🟢)18.196s (-9.1% 🟢)2.200s21.14x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.431s (-10.8% 🟢)14.027s (-12.5% 🟢)0.596s51.00x
🐘 PostgresNitro13.434s (-8.0% 🟢)14.013s (-6.8% 🟢)0.579s51.00x
💻 LocalExpress13.490s (-9.9% 🟢)14.027s (-6.7% 🟢)0.537s51.00x
🐘 PostgresNext.js (Turbopack)13.820s14.018s0.198s51.03x
💻 LocalNext.js (Turbopack)14.076s15.030s0.954s41.05x
🌐 RedisNext.js (Turbopack)14.088s15.030s0.942s41.05x
🌐 MongoDBNext.js (Turbopack)14.283s15.019s0.736s41.06x
🐘 PostgresExpress14.537s (~)15.276s (+1.7%)0.739s41.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.821s (-58.6% 🟢)22.773s (-56.7% 🟢)1.952s31.00x
▲ VercelNitro21.829s (-66.1% 🟢)23.502s (-64.7% 🟢)1.673s31.05x
▲ VercelNext.js (Turbopack)23.199s (-55.9% 🟢)25.174s (-53.9% 🟢)1.975s31.11x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.846s (-29.4% 🟢)12.022s (-29.4% 🟢)0.176s81.00x
🐘 PostgresNitro11.923s (-14.6% 🟢)12.392s (-13.4% 🟢)0.469s81.01x
💻 LocalExpress11.997s (-27.7% 🟢)12.398s (-27.2% 🟢)0.401s81.01x
🌐 RedisNext.js (Turbopack)13.015s13.454s0.439s71.10x
💻 LocalNext.js (Turbopack)13.089s14.026s0.937s71.10x
🐘 PostgresNext.js (Turbopack)13.215s13.585s0.370s71.12x
🌐 MongoDBNext.js (Turbopack)13.301s14.021s0.720s71.12x
🐘 PostgresExpress15.027s (+7.3% 🔺)15.400s (+5.5% 🔺)0.373s61.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.095s (-74.3% 🟢)33.159s (-73.2% 🟢)2.064s31.00x
▲ VercelNitro31.508s (-92.5% 🟢)33.124s (-92.2% 🟢)1.615s31.01x
▲ VercelNext.js (Turbopack)31.987s (-91.9% 🟢)33.996s (-91.4% 🟢)2.009s31.03x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.140s (-10.6% 🟢)2.007s (~)0.868s151.00x
🐘 PostgresNext.js (Turbopack)1.168s2.007s0.838s151.03x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.187s (-20.3% 🟢)2.006s (~)0.820s151.04x
🐘 PostgresExpress1.237s (-1.9%)2.018s (~)0.781s151.09x
🌐 RedisNext.js (Turbopack)1.238s2.006s0.768s151.09x
💻 LocalNext.js (Turbopack)1.279s2.005s0.726s151.12x
🌐 MongoDBNext.js (Turbopack)2.048s2.917s0.869s111.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.551s (-10.8% 🟢)4.187s (-9.4% 🟢)1.636s81.00x
▲ VercelNitro2.674s (-5.1% 🟢)4.258s (-1.5%)1.584s81.05x
▲ VercelNext.js (Turbopack)4.193s (+23.4% 🔺)6.817s (+38.2% 🔺)2.624s51.64x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.225s (-47.9% 🟢)2.007s (-33.3% 🟢)0.782s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.05x
🐘 PostgresExpress1.483s (-37.2% 🟢)2.019s (-32.9% 🟢)0.536s151.21x
💻 LocalNitro1.726s (-45.1% 🟢)2.072s (-46.7% 🟢)0.346s151.41x
💻 LocalNext.js (Turbopack)1.831s2.149s0.318s141.50x
💻 LocalExpress1.905s (-35.5% 🟢)2.150s (-37.8% 🟢)0.244s141.56x
🌐 RedisNext.js (Turbopack)2.364s3.008s0.643s101.93x
🌐 MongoDBNext.js (Turbopack)3.568s4.008s0.440s82.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.844s (-5.1% 🟢)5.284s (-10.8% 🟢)1.440s61.00x
▲ VercelExpress3.884s (+7.3% 🔺)6.116s (+19.7% 🔺)2.232s51.01x
▲ VercelNext.js (Turbopack)5.098s (-28.2% 🟢)7.574s (-15.0% 🟢)2.476s41.33x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.419s (-59.2% 🟢)2.007s (-49.9% 🟢)0.588s151.00x
🐘 PostgresNext.js (Turbopack)1.493s2.007s0.514s151.05x
🐘 PostgresExpress2.153s (-38.2% 🟢)2.861s (-28.7% 🟢)0.708s111.52x
🌐 RedisNext.js (Turbopack)3.634s4.010s0.376s82.56x
💻 LocalNitro4.547s (-45.5% 🟢)5.013s (-44.4% 🟢)0.466s63.20x
💻 LocalExpress4.829s (-42.1% 🟢)5.512s (-38.9% 🟢)0.683s63.40x
💻 LocalNext.js (Turbopack)5.581s6.213s0.631s53.93x
🌐 MongoDBNext.js (Turbopack)6.289s7.012s0.723s54.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.147s (+21.4% 🔺)7.340s (+19.8% 🔺)2.193s51.00x
▲ VercelNitro5.410s (+53.5% 🔺)7.160s (+29.4% 🔺)1.750s51.05x
▲ VercelNext.js (Turbopack)7.157s (-19.7% 🟢)8.877s (-19.0% 🟢)1.721s41.39x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.170s (-6.9% 🟢)2.007s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.173s2.007s0.835s151.00x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.774s151.05x
💻 LocalNext.js (Turbopack)1.327s2.006s0.679s151.13x
💻 LocalNitro1.380s (-26.0% 🟢)2.006s (-14.3% 🟢)0.626s151.18x
💻 LocalExpress1.393s (-26.5% 🟢)2.006s (-15.1% 🟢)0.613s151.19x
🐘 PostgresExpress1.408s (+12.0% 🔺)2.062s (+2.7%)0.654s151.20x
🌐 MongoDBNext.js (Turbopack)2.036s2.916s0.880s111.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.271s (-7.6% 🟢)4.045s (-3.0%)1.774s81.00x
▲ VercelExpress2.336s (-9.5% 🟢)3.766s (-13.4% 🟢)1.430s81.03x
▲ VercelNext.js (Turbopack)4.170s (+42.2% 🔺)6.038s (+30.1% 🔺)1.869s51.84x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.251s (-46.5% 🟢)2.007s (-33.3% 🟢)0.757s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.03x
🐘 PostgresExpress1.393s (-40.5% 🟢)2.035s (-32.4% 🟢)0.642s151.11x
💻 LocalNitro1.957s (-36.2% 🟢)2.316s (-40.4% 🟢)0.359s131.56x
💻 LocalExpress2.022s (-35.4% 🟢)2.508s (-33.3% 🟢)0.485s121.62x
💻 LocalNext.js (Turbopack)2.130s2.826s0.696s111.70x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.88x
🌐 MongoDBNext.js (Turbopack)3.543s4.007s0.464s82.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.338s (+35.9% 🔺)6.011s (+25.4% 🔺)1.673s51.00x
▲ VercelNitro4.664s (+44.2% 🔺)6.658s (+31.1% 🔺)1.994s51.08x
▲ VercelNext.js (Turbopack)4.714s (+50.0% 🔺)6.542s (+44.7% 🔺)1.827s51.09x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.380s (-60.3% 🟢)2.007s (-49.9% 🟢)0.627s151.00x
🐘 PostgresNext.js (Turbopack)1.483s2.008s0.525s151.07x
🐘 PostgresExpress1.982s (-43.4% 🟢)2.737s (-31.8% 🟢)0.755s111.44x
🌐 RedisNext.js (Turbopack)3.571s4.010s0.439s82.59x
💻 LocalNitro4.894s (-46.5% 🟢)5.348s (-46.7% 🟢)0.454s63.55x
💻 LocalExpress5.939s (-32.5% 🟢)6.417s (-30.8% 🟢)0.478s54.30x
💻 LocalNext.js (Turbopack)6.115s6.416s0.301s54.43x
🌐 MongoDBNext.js (Turbopack)6.302s7.013s0.710s54.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.380s (+5.6% 🔺)7.062s (+3.6%)1.682s51.00x
▲ VercelNext.js (Turbopack)5.690s (-15.8% 🟢)7.416s (-13.2% 🟢)1.726s51.06x
▲ VercelExpress5.700s (-11.2% 🟢)7.539s (-7.8% 🟢)1.840s51.06x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.459s (-44.1% 🟢)1.022s (+1.6%)0.564s591.00x
💻 LocalNitro0.472s (-51.8% 🟢)1.021s (-6.7% 🟢)0.548s591.03x
💻 LocalExpress0.482s (-51.1% 🟢)1.004s (-6.7% 🟢)0.522s601.05x
🐘 PostgresNext.js (Turbopack)0.599s1.040s0.442s581.31x
🌐 RedisNext.js (Turbopack)0.619s1.004s0.386s601.35x
🐘 PostgresExpress0.644s (-23.2% 🟢)1.140s (+11.4% 🔺)0.496s531.40x
💻 LocalNext.js (Turbopack)0.719s1.004s0.285s601.57x
🌐 MongoDBNext.js (Turbopack)0.733s1.006s0.272s601.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.661s (-75.5% 🟢)6.339s (-70.3% 🟢)1.677s101.00x
▲ VercelNitro5.218s (-76.3% 🟢)7.258s (-69.8% 🟢)2.039s101.12x
▲ VercelNext.js (Turbopack)6.455s (-55.5% 🟢)7.922s (-50.7% 🟢)1.466s81.38x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.085s (-43.7% 🟢)1.705s (-18.8% 🟢)0.620s531.00x
🐘 PostgresExpress1.162s (-41.2% 🟢)1.588s (-29.7% 🟢)0.426s571.07x
💻 LocalExpress1.197s (-60.3% 🟢)2.006s (-44.1% 🟢)0.809s451.10x
💻 LocalNitro1.220s (-59.8% 🟢)2.005s (-46.6% 🟢)0.786s451.12x
🌐 RedisNext.js (Turbopack)1.476s2.006s0.530s451.36x
🐘 PostgresNext.js (Turbopack)1.707s2.387s0.679s381.57x
💻 LocalNext.js (Turbopack)1.753s2.005s0.253s451.61x
🌐 MongoDBNext.js (Turbopack)1.813s2.007s0.194s451.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.733s (-70.3% 🟢)13.729s (-66.8% 🟢)1.996s71.00x
▲ VercelExpress12.793s (-62.9% 🟢)14.645s (-60.2% 🟢)1.852s71.09x
▲ VercelNext.js (Turbopack)14.268s (-71.4% 🟢)16.580s (-67.9% 🟢)2.313s61.22x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.108s (-48.6% 🟢)2.696s (-41.4% 🟢)0.588s451.00x
🐘 PostgresExpress2.634s (-34.0% 🟢)3.074s (-29.6% 🟢)0.440s401.25x
🐘 PostgresNext.js (Turbopack)2.659s3.084s0.425s391.26x
💻 LocalNitro2.660s (-71.4% 🟢)3.007s (-70.0% 🟢)0.348s401.26x
💻 LocalExpress2.743s (-70.2% 🟢)3.033s (-69.7% 🟢)0.290s401.30x
🌐 RedisNext.js (Turbopack)2.992s3.191s0.200s381.42x
💻 LocalNext.js (Turbopack)3.748s4.007s0.259s301.78x
🌐 MongoDBNext.js (Turbopack)4.178s5.012s0.833s241.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express35.192s (-72.9% 🟢)37.772s (-71.4% 🟢)2.580s41.00x
▲ VercelNitro36.812s (-62.0% 🟢)39.053s (-60.3% 🟢)2.241s41.05x
▲ VercelNext.js (Turbopack)43.007s (-59.9% 🟢)46.755s (-57.1% 🟢)3.748s31.22x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-44.2% 🟢)1.007s (~)0.850s601.00x
🐘 PostgresNitro0.182s (-35.8% 🟢)1.006s (~)0.824s601.15x
🐘 PostgresNext.js (Turbopack)0.190s1.005s0.815s601.21x
🌐 RedisNext.js (Turbopack)0.316s1.021s0.705s592.00x
💻 LocalNitro0.412s (-31.9% 🟢)1.004s (-1.7%)0.592s602.61x
💻 LocalExpress0.452s (-19.3% 🟢)1.004s (~)0.552s602.87x
💻 LocalNext.js (Turbopack)0.603s1.039s0.436s583.83x
🌐 MongoDBNext.js (Turbopack)1.029s1.749s0.720s356.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.178s (+11.5% 🔺)4.588s (+26.2% 🔺)2.410s141.00x
▲ VercelNitro2.258s (+36.0% 🔺)4.485s (+33.9% 🔺)2.227s141.04x
▲ VercelNext.js (Turbopack)3.716s (+83.7% 🔺)5.458s (+43.9% 🔺)1.742s111.71x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.304s (-38.8% 🟢)1.006s (~)0.702s901.00x
🌐 RedisNext.js (Turbopack)0.423s1.004s0.581s901.39x
🐘 PostgresExpress0.520s (+2.0%)1.112s (+10.5% 🔺)0.592s821.71x
🐘 PostgresNext.js (Turbopack)0.705s1.318s0.613s692.32x
💻 LocalNitro2.170s (-14.5% 🟢)2.821s (-6.3% 🟢)0.651s327.14x
💻 LocalExpress2.186s (-13.0% 🟢)2.944s (-2.2%)0.758s317.19x
💻 LocalNext.js (Turbopack)2.424s3.043s0.619s307.97x
🌐 MongoDBNext.js (Turbopack)2.595s3.007s0.412s308.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.618s (+87.2% 🔺)8.356s (+60.9% 🔺)1.738s111.00x
▲ VercelNitro7.134s (+121.1% 🔺)9.355s (+94.0% 🔺)2.221s101.08x
▲ VercelExpress7.527s (+147.1% 🔺)9.552s (+98.7% 🔺)2.025s101.14x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.637s (-19.4% 🟢)1.006s (~)0.369s1201.00x
🐘 PostgresNext.js (Turbopack)0.730s1.013s0.284s1191.15x
🐘 PostgresExpress0.790s (-3.5%)1.192s (+17.1% 🔺)0.402s1011.24x
🌐 RedisNext.js (Turbopack)0.797s1.004s0.207s1201.25x
🌐 MongoDBNext.js (Turbopack)5.387s6.013s0.626s208.46x
💻 LocalNitro9.790s (-12.5% 🟢)10.276s (-11.9% 🟢)0.486s1215.37x
💻 LocalExpress10.085s (-9.9% 🟢)10.782s (-9.7% 🟢)0.697s1215.84x
💻 LocalNext.js (Turbopack)11.478s12.128s0.650s1018.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.821s (+117.8% 🔺)19.141s (+103.6% 🔺)2.319s71.00x
▲ VercelNext.js (Turbopack)18.466s (+78.8% 🔺)20.419s (+66.2% 🔺)1.953s71.10x
▲ VercelExpress20.247s (+172.9% 🔺)22.460s (+143.0% 🔺)2.213s61.20x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.133s (+430.1% 🔺)2.005s (+99.6% 🔺)0.010s (-19.2% 🟢)2.017s (+98.0% 🔺)0.884s101.00x
🐘 PostgresNitro1.134s (+453.4% 🔺)2.001s (+100.2% 🔺)0.001s (-26.7% 🟢)2.010s (+98.7% 🔺)0.875s101.00x
💻 LocalExpress1.136s (+470.5% 🔺)2.005s (+99.6% 🔺)0.012s (~)2.020s (+98.4% 🔺)0.884s101.00x
💻 LocalNext.js (Turbopack)1.172s2.003s0.012s2.019s0.847s101.03x
🐘 PostgresExpress1.232s (+500.5% 🔺)2.005s (+100.8% 🔺)0.001s (-37.5% 🟢)2.041s (+101.8% 🔺)0.810s101.09x
🐘 PostgresNext.js (Turbopack)2.024s2.601s0.220s2.874s0.850s101.79x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.200s (-42.6% 🟢)3.478s (-34.1% 🟢)1.562s (+110.5% 🔺)5.432s (-16.2% 🟢)3.232s101.00x
▲ VercelExpress2.336s (-6.8% 🟢)3.935s (-3.8%)1.087s (+13.1% 🔺)5.425s (-3.0%)3.089s101.06x
▲ VercelNext.js (Turbopack)4.802s (-29.9% 🟢)4.900s (-43.4% 🟢)0.740s (+17.0% 🔺)7.346s (-25.0% 🟢)2.544s102.18x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.512s (+142.2% 🔺)2.005s (+99.2% 🔺)0.004s (-8.9% 🟢)2.024s (+97.9% 🔺)0.512s301.00x
💻 LocalNitro1.514s (+80.5% 🔺)2.011s (+98.7% 🔺)0.010s (+2.8%)2.022s (+81.2% 🔺)0.509s301.00x
💻 LocalExpress1.721s (+127.3% 🔺)2.011s (+95.5% 🔺)0.010s (+2.1%)2.202s (+111.7% 🔺)0.481s281.14x
🐘 PostgresNext.js (Turbopack)1.722s2.107s0.003s2.123s0.401s301.14x
💻 LocalNext.js (Turbopack)1.845s2.011s0.009s2.203s0.358s281.22x
🐘 PostgresExpress1.983s (+214.8% 🔺)2.330s (+131.5% 🔺)0.003s (-20.7% 🟢)2.395s (+134.1% 🔺)0.412s261.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.656s (-13.0% 🟢)7.281s (-9.1% 🟢)0.179s (-56.1% 🟢)8.330s (-5.7% 🟢)2.675s81.00x
▲ VercelNitro5.796s (-80.3% 🟢)7.455s (-75.8% 🟢)0.170s (+52.1% 🔺)8.071s (-74.6% 🟢)2.275s81.02x
▲ VercelNext.js (Turbopack)12.255s (-27.6% 🟢)13.600s (-25.4% 🟢)0.201s (-4.9%)15.301s (-19.2% 🟢)3.046s42.17x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.651s (-32.8% 🟢)1.033s (-17.2% 🟢)0.000s (-100.0% 🟢)1.042s (-17.2% 🟢)0.390s581.00x
🐘 PostgresExpress1.031s (+7.3% 🔺)1.390s (+8.7% 🔺)0.000s (+9.5% 🔺)1.437s (+10.0% 🔺)0.406s421.58x
🐘 PostgresNext.js (Turbopack)1.333s1.618s0.000s1.715s0.382s372.05x
💻 LocalNitro1.337s (+9.4% 🔺)2.015s (~)0.000s (+166.7% 🔺)2.017s (~)0.680s302.05x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-60.0% 🟢)2.017s (~)0.649s302.10x
💻 LocalNext.js (Turbopack)1.436s2.013s0.000s2.016s0.581s302.20x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.922s (+28.6% 🔺)5.598s (+27.4% 🔺)0.000s (-100.0% 🟢)6.027s (+25.3% 🔺)2.105s101.00x
▲ VercelExpress4.013s (+7.3% 🔺)5.879s (+15.2% 🔺)0.000s (-100.0% 🟢)6.416s (+16.0% 🔺)2.404s101.02x
▲ VercelNext.js (Turbopack)5.465s (-46.3% 🟢)6.707s (-41.8% 🟢)0.000s (+Infinity% 🔺)7.645s (-36.6% 🟢)2.181s81.39x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.327s (-25.9% 🟢)2.066s (-3.5%)0.000s (-100.0% 🟢)2.081s (-4.3%)0.754s291.00x
🐘 PostgresNext.js (Turbopack)1.844s2.402s0.000s2.460s0.616s251.39x
🐘 PostgresExpress2.377s (+34.2% 🔺)3.042s (+39.7% 🔺)0.000s (NaN%)3.091s (+40.6% 🔺)0.714s201.79x
💻 LocalNext.js (Turbopack)2.776s3.362s0.000s3.365s0.589s182.09x
💻 LocalNitro3.068s (-9.4% 🟢)3.838s (-4.8%)0.000s (-53.1% 🟢)3.843s (-4.8%)0.775s162.31x
💻 LocalExpress3.103s (-10.5% 🟢)3.775s (-6.4% 🟢)0.000s (-76.6% 🟢)3.781s (-6.3% 🟢)0.679s162.34x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.059s (+10.3% 🔺)6.788s (+12.7% 🔺)0.000s (+Infinity% 🔺)7.571s (+17.3% 🔺)2.512s81.00x
▲ VercelNitro5.089s (+24.3% 🔺)6.536s (+21.6% 🔺)0.000s (-18.5% 🟢)7.002s (+20.9% 🔺)1.913s91.01x
▲ VercelNext.js (Turbopack)8.675s (+54.5% 🔺)10.065s (+44.2% 🔺)0.000s (-100.0% 🟢)11.283s (+49.6% 🔺)2.608s61.71x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro17/21
🐘 PostgresNitro19/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other4380150588
Total4266010265292

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025

📋 View full workflow run

Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/vitest/src/index.ts
Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/core/src/workflow.ts Outdated
Comment threadpackages/builders/src/base-builder.ts
Comment threadpackages/core/src/runtime/get-world-lazy.ts

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Remaining issues:

  • getWorldLazy needs to be validated as a decision
  • packages/core/e2e/utils.ts skip for hasStepSourceMaps needs to be re-evaluated

@shtefcs

Copy link
Copy Markdown

Seems you had a lot of work here. It's not easy as it seems. Hopefully, this gonna solve some issues we are facing with Workflow.

Peace,

@TooTallNateTooTallNate 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.

Withdrawing my prior CHANGES_REQUESTED — the blocker I called out (the stale (err as any).meta?.retryAfter access in step-executor.ts) is fixed at line 169 with err.retryAfter ?? 1. As an aside, my fix recommendation in the prior review was based on a faulty type assumption (I confused TooEarlyError.retryAfter: number with RetryableError.retryAfter: Date). The author's err.retryAfter ?? 1 form is correct: it's a number of seconds with sensible fallback.

My other earlier concerns:

  • getStepNameFromEvent full-event-load: addressed. stepName is now plumbed through WorkflowInvokePayloadSchema so the handler reads it directly from the queue message payload instead of doing a paginated event load. ✓
  • step.error guard for max retries: known limitation acknowledged in-thread. The proposed long-term fix (server-side ne(status, 'running') on step_started) is separate work.
  • Background step → continuation enqueue safety: acknowledged as intentional.

So my specific items are resolved. But I'm not approving outright — there's still substantial unresolved review from @VaguelySerious touching real concerns I'd defer to them on:

  • Removal of the LegacyStreamWorld shim
  • Limiting queue concurrency to 1 in tests
  • Comments removed without explanation in suspension-handler.ts / runtime.ts
  • The builder-deferred.ts Next.js behavior prediction
  • Several "is there a systematic fix instead of conditionally allowing edge cases" questions

Plus the auto-flagged event cache duplication bug from Vercel's review bot is worth confirming was understood and dismissed (or fixed).

Approval-wise this should land when @VaguelySerious's concerns are resolved. From my side: no remaining blockers.

@TooTallNateTooTallNate 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.

Thorough re-review for Peter. Spent significant time tracing four risk surfaces: the Vercel bot's auto-flag, the concurrent-step-execution model, inline-step replay determinism, and the open review threads. Bottom line up front: no ship-blockers from my side. A few coverage gaps and one acknowledged limitation worth being explicit about, but nothing I'd hold this PR on.

Vercel bot's "event cache duplication" auto-flag is a false positive

"wait_completed events pushed locally without advancing eventsCursor cause duplicate events on next incremental fetch."

The author anticipated exactly this scenario. runtime.ts:585–594 has explicit dedupe-by-eventId logic:

constexistingIds=newSet(cachedEvents.map((e)=>e.eventId));for(consteofloaded.events){if(!existingIds.has(e.eventId)){cachedEvents.push(e);}}

Verified end-to-end: locally-pushed wait_completed (line 685) keeps its real server-returned eventId, the next iteration's incremental fetch returns it again, the dedupe filter drops it, the consumer never sees a duplicate. The comment in the code calls out the exact scenario the bot flagged. Safe to dismiss the auto-flag.

Concurrent-step-execution: a partial fix shipping with a known gap

Verified the L198 guard (step.attempt > maxRetries + 1 && step.error) traces correctly for the scenario it targets — concurrent first-attempts before any user-code error has been recorded. Without step.error, the guard correctly skips and the step proceeds normally even with inflated step.attempt.

But there's a symmetric gap in the catch-block path at L469:

if(currentAttempt>=maxRetries+1){/* step_failed */}

This check has no && step.error guard, so it fires on the first real catch-block failure even when currentAttempt has been inflated by concurrent first-attempts. End result: a workflow with maxRetries=3 and 4+ concurrent handlers can permanently fail after a single transient flake, with a misleading log saying "exceeded max retries (4 retries)" when the step body actually only ran once.

This is the only substantive open thread on the PR (step-executor.ts:198), and the author has acknowledged it in a separate resolved thread:

"Agreed on the concurrent-retry limitation. The server-side ne(status, 'running') WHERE clause is the right long-term fix — tracking as a follow-up."

So everyone's eyes-open. Worth being explicit in the changelog or PR description that under high queue back-pressure, default-maxRetries workflows can spuriously fail. Either of these would close the gap fully:

  1. Add the same && step.error guard at L469 (mirrors L198, easy to do, but doesn't fix the L198 case for catch-block).
  2. Server-side ne(status, 'running') on step_started (referenced in the resolved thread; also fixes the world-testing concurrency issue Peter resolved separately).

Not a ship-stopper, but I'd want it tracked as a known issue users can hit, not just an internal todo.

Inline-step replay: ordering subtlety in the dedupe path

The locally-pushed-then-incremental-fetch path can produce non-monotonic-by-eventId ordering in cachedEvents. Specifically: after wait_completed is pushed locally (L685) without advancing the cursor, the next incremental fetch can return concurrent events with eventIds between the local push and earlier events. After dedupe, cachedEvents becomes append-ordered but not eventId-ordered. The EventsConsumer walks by index, so it consumes them in append order, which is not the canonical replay order.

In practice this is unlikely to cause issues because:

  • Within one handler's writes, the same world process generates monotonic ULIDs.
  • Cross-handler events for the same run are gated by the ownership check (step_created ownership at L813).
  • The dangerous case requires concurrent handler activity for the same run + non-trivial cross-process clock skew.

But it's a footgun. The cleanest fix would be: after locally pushing an event, set eventsCursor to that event's eventId so the next fetch skips it. This also eliminates the dedupe pass entirely. Worth considering as a follow-up.

"VaguelySerious" review threads — context for Peter

For other reviewers who may stumble on this: most of the open @VaguelySerious threads are Peter's own AI-review notes posted via tooling. Of 28 such threads, 27 are either resolved or outdated (the diff has moved past them). The single unresolved + non-outdated thread is the concurrent-retry concern above (step-executor.ts:198), which Peter has acknowledged as a known limitation in a sibling resolved thread.

External-reviewer threads (mine, the Vercel bot's) are all resolved or addressed. So the apparent "long list of unresolved concerns" from my prior comment is misleading — it was conflating Peter's self-review notes with external review.

Test coverage gaps (non-blocking)

A few coverage gaps for novel V2 behavior worth tracking even if you ship:

  1. Dedupe path at runtime.ts:585–594 — zero test coverage. The behavior is novel to V2 and load-bearing for incremental-replay correctness. A unit test that:

    • First iteration fetches with cursor C0
    • Manually pushes a wait_completed to cachedEvents
    • Second iteration fetches and gets the same wait_completed plus an interleaved new event
    • Asserts: no duplicates AND consumer sees no orphans
      ...would lock in the contract.
  2. L198 guard's positive case — there's no test asserting that attempt > maxRetries + 1withoutstep.error does NOT fail the step. The guard exists specifically for this case and is currently un-asserted. The two step-handler.test.ts tests at lines 296–340 and 343–388 both set error on the fixture, so they exercise different paths.

  3. Concurrent-handler races — neither Scenario B (concurrent retry with prior error → premature L198 failure) nor Scenario D (catch-block premature failure with no && step.error guard) has a test. Adding these would catch the bug currently in code if a future fix lands.

  4. builder-deferred.ts Next.js manifest reading — Peter agreed in review that reading app-paths-manifest.json was the right approach over regex prediction. Verified that DID land (L1171–1175 reads the manifest with comment "This avoids predicting Next.js conventions with regexes and instead reads from Next.js's own output"). ✅

What looks good

  • The architectural shift is well-executed. The replay loop, ownership-gated inline dispatch, idempotency-key-based queue dedupe, builder unification, and __steps_registered rollup-tree-shaking guard are all clean.
  • The getStepNameFromEvent full-event-load concern from my prior review was addressed — stepName now flows through WorkflowInvokePayloadSchema. ✓
  • The TooEarlyError stale retryAfter access pattern is fixed at step-executor.ts:169. ✓
  • The CJS module.exports collision (ESM format for steps when bundleFinalOutput: true) is a subtle but correct fix.
  • The changelog doc docs/content/docs/changelog/eager-processing.mdx is one of the better architecture docs I've seen in a PR — covers every edge case, failure mode, and platform-specific quirk.

Verdict

No blockers from my side. Ship if Vade and storytime green-light. The concurrent-retry premature-failure case is real but acknowledged, the auto-flagged "event cache duplication" is a non-issue, and the test coverage gaps are real but not regressions (the V2 behaviors they cover are net-new).

If you want one piece of polish before merge, the L469 && step.error mirror would close the most user-visible failure mode at zero cost — but I won't gate on it.

@TooTallNateTooTallNate 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.

Pre-emptively :approved: but my agent really wants @VaguelySerious to resolve the open threads :lol: and there seems to be a few other things pointed out in the latest review comment.

VaguelySeriousand others added 7 commits May 3, 2026 11:38
Merges the V1 split flow/step routes (one queue message per step + one per
flow continuation, two separate function invocations per step) into a
single combined handler at /.well-known/workflow/v1/flow that executes
steps inline within the same invocation as the workflow replay.
A serial workflow with N steps that previously required ~2N+1 function
invocations now completes in 1.
Architecture
------------
- packages/core: workflowEntrypoint() handler with an inline replay loop.
Each loop iteration: load events incrementally, replay workflow,
inline-execute one owned step on suspension, loop. Background steps
(from Promise.all) are queued back to the same handler with stepId so
the in-flight handler advances the loop without queue round-trips
whenever possible.
- packages/builders: createCombinedBundle() emits a single ESM bundle
with workflowEntrypoint(workflowCode) plus a side-effect
__step_registrations bundle. ESM-by-default with createRequire banner
for CJS deps (matches main's #1562 pattern).
- packages/next, nestjs, sveltekit, astro, nitro, nuxt, hono, express,
fastify, vite: framework builders updated to use createCombinedBundle.
The step/ directory is no longer generated.
Correctness invariants (V2 polish)
----------------------------------
- Single inline executor per step. Atomic step_created per correlationId
(per-step in-process mutex in world-local; SQL-level guards in
postgres/vercel) means exactly one handler claims ownership of each
step. Inline execution gates on ownership; queue dispatch is
unconditional with idempotency keys for crash recovery (matches V1).
- onUnconsumedEvent reverts to its original PR #1055 contract: any
unconsumed event fatals as CORRUPTED_EVENT_LOG. Source-level fixes
removed every code path that produced orphaned step lifecycle events
during V2 work, so the skip branch became unnecessary.
- Lock-release polling interval lowered from 100ms to 10ms in
flushable-stream so the V2 step-executor's ops-settle race typically
resolves in ~5ms instead of ~50ms per writable-bearing step.
- Per-iteration runs.get round-trip eliminated: concurrent completion
is detected by scanning the loaded event log for terminal run events
instead.
- Worker-pool deadlock guard removed from Run#pollReturnValue. The
fibonacci/recursive parent→child polling case is now handled by
raising world-postgres default queueConcurrency from 10 to 50, with
the limitation documented at three call sites (fibonacciWorkflow's
JSDoc, queue.ts, eager-processing.mdx).
- getWorldLazy module-scope singleton breaks the step-bundle import
chain to world.ts so step bundles don't transitively pull in
world-vercel + cbor-x + the VQS dev-handler resolver.
World-local atomicity
---------------------
- Per-step in-process mutex serializes events.create for step
lifecycle events on a (runId, correlationId) key. step_started is
rejected only when the step is already terminal — duplicate
step_started on a non-terminal step is allowed (preserves retry
semantics after SIGKILL).
Runtime helpers
---------------
- loadWorkflowRunEvents(runId, afterCursor?) consolidates the
previous getAllWorkflowRunEvents{,WithCursor} + getNewWorkflowRunEvents
into one helper.
Documentation
-------------
- docs/content/docs/changelog/eager-processing.mdx: full record of
architecture, edge cases, and the fixes applied during the V2 work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Two CI failures from the earlier V2 cutover, now resolved:
1. @workflow/vitest unit tests (`packages/vitest/src/index.test.ts`)
were still mocking the V1 BaseBuilder methods (createWorkflowsBundle
+ createStepsBundle) and expecting two world handlers (workflow +
step). Updated the mocks to expose createCombinedBundle and
asserted a single combined handler at __wkf_workflow_, which is
what the V2 vitest builder actually emits.
2. E2E Local Dev Tests (nextjs-webpack) were asserting that step
error stacks contain `99_e2e.ts` / `helpers.ts`, which held in
pre-V2 webpack dev mode (which imported step sources directly).
V2 inlines the step bundle into the combined flow route, and
webpack's re-bundling collapses original step filenames out of the
dev-mode source maps. Extended the existing nextjs-webpack
carve-out in hasStepSourceMaps() to cover dev as well as prod, and
updated the source-map follow-up callout in eager-processing.mdx
to include nextjs-webpack in the matrix that needs source maps
wired up for V2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/e2e/e2e.test.ts` (PR #1879 vs. V2 hookDispose helper)
Drop the locally-defined `waitForHook(expectedRunId)` shadow in the
hookDisposeTestWorkflow test in favor of the new top-level
`waitForHook(token, { runId })` API merged from main. Keep V2's
event-driven `waitForHookDisposal()` helper, which is stricter than
main's `await sleep(3_000)` (it polls `getHookByToken` for the
HookNotFoundError signal rather than waiting a fixed interval).
* `packages/world-local/src/storage/events-storage.ts` (PR #1877 vs.
V2 per-step async mutex)
Both branches were closing race conditions in the local world's
step lifecycle. PR #1877 adds filesystem-level O_CREAT|O_EXCL locks
for `step_created` and `wait_created`; the V2 branch already wraps
step lifecycle events in an in-process `withStepLock` mutex. They
compose: the mutex serializes within a single Node process; the
filesystem lock additionally protects against cross-process races
(multiple pnpm workers, redelivered queue messages). Resolution
takes V2's mutex wrap as the base and patches in main's two
`writeExclusive` claims at the start of the `step_created` and
`wait_created` handlers, with comments noting the dual-layer
guarantee.
`packages/core/e2e/utils.ts` auto-merged: V2's nextjs-webpack +
all-Vercel source-map carve-outs preserved alongside main's
`tanstack-start` addition to `hasWorkflowSourceMaps`.
Verified locally: `@workflow/core` and `@workflow/world-local`
typecheck clean; all 343 world-local unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone tarballs project's `vercel.json` set
`"buildCommand": "pnpm --filter tarballs build"`, which only runs the
tarballs package's `build` script (`node scripts/pack.ts`) and does
NOT trigger turbo's `dependsOn: ["^build"]` chain. As a result, every
workspace package was packed with an empty `dist/` directory — only
`bin/`, `package.json`, `README.md`, and `docs/` made it into the
tarballs. Downstream installs failed with
`Cannot find module '<pkg>/dist/next.cjs'` (and similar for every
other entry point).
The previous docs-based pipeline didn't hit this because Vercel
detected `docs` as a Next.js framework, ran turbo for it, and pack.ts
was wired as a `prebuild` step that ran after deps were already
built.
Fix: switch the buildCommand to `pnpm turbo build --filter=tarballs`
so turbo evaluates the build task for `tarballs`, which fans out via
`^build` to compile every workspace dependency first, and then runs
`tarballs#build` (pack.ts) against the populated `dist/` directories.
Verified locally: a fresh build produces tarballs containing the full
`dist/` tree (including `dist/next.cjs`, the file flight-booking-app
was failing to resolve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/src/logger.ts` (PR #1849 vs. V2 webpack carve-out)
PR #1849 ("Friendlier workflow errors") reintroduced the `debug`
package as a static import and added a structured `Logger`
interface with `child()` / `forRun()` and `composeLogLine`. The V2
branch had previously removed the `debug` static import because it
pulls `debug/src/node` and a dynamic `require('tty')` into the
Next.js webpack flow route, breaking V2 builds with
`Dynamic require of "tty" is not supported`.
Resolution combines both: keep main's `Logger` interface,
`child()` / `forRun()`, and `composeLogLine` integration; replace
the `debug` invocations with the V2 lightweight
`matchesDebugNamespace` + `process.env.DEBUG` matcher so the flow
route still bundles cleanly under webpack. Comment in the file
documents why `debug` is intentionally absent.
* `packages/core/src/runtime.ts` (PR #1849 vs. V2 inline replay loop)
Multiple regions where main's structured-logging refactor and the
V2 inline-execution loop made overlapping changes. Took V2's
control flow as the base — its `if (!workflowRun) { ... }` setup
block, main replay `while (true)` loop with timeout-based
re-scheduling, terminal-event short-circuit, and per-iteration
incremental event load are all preserved as-is — and folded in
main's contributions: the scoped `runLogger = runtimeLogger.forRun(
runId, workflowName)` is created once and used for run-level info
/ error logging. Discarded main's V1 wait-completion loop and
`runWorkflow` user-code error block since V2 handles waits and
user-code errors inside the inline loop body. Updated
`buildWorkflowSuspensionMessage()` call to drop a stray `runId`
first arg that didn't match the helper's 3-param signature.
* `packages/core/e2e/e2e.test.ts` (PR #1849 unrelated to V2)
No conflict from main's friendly-errors PR, auto-merged. (No
hookDispose-style conflict this round.)
Verified locally: all 820 `@workflow/core` unit tests pass; all 343
`@workflow/world-local` unit tests pass; `pnpm turbo typecheck` is
green across all 40 typecheck tasks.
Co-Authored-By: Claude Opus 4.7 (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

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

[core] Combine flow+step bundle and process steps eagerly - #1338

Merged
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow
May 4, 2026
Merged

[core] Combine flow+step bundle and process steps eagerly#1338
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow

Conversation

@changeset-bot

changeset-botBot commented Mar 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 234fce4

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

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

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 Mar 11, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Mar 11, 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.033s (-24.6% 🟢)1.005s (~)0.972s101.00x
💻 LocalExpress0.034s (-23.3% 🟢)1.006s (~)0.972s101.05x
🐘 PostgresNext.js (Turbopack)0.049s1.013s0.964s101.50x
💻 LocalNext.js (Turbopack)0.049s1.005s0.956s101.51x
🐘 PostgresNitro0.051s (-46.8% 🟢)1.012s (-3.0%)0.961s101.56x
🌐 RedisNext.js (Turbopack)0.055s1.005s0.951s101.68x
🌐 MongoDBNext.js (Turbopack)0.109s1.007s0.899s103.35x
🐘 PostgresExpress0.177s (+205.2% 🔺)1.107s (+9.5% 🔺)0.930s105.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.236s (-42.4% 🟢)1.939s (-22.7% 🟢)1.703s101.00x
▲ VercelExpress0.241s (+2.2%)2.009s (-5.9% 🟢)1.768s101.02x
▲ VercelNext.js (Turbopack)0.550s (+118.6% 🔺)2.715s (+16.4% 🔺)2.165s102.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.069s (-5.0% 🟢)2.006s (~)0.937s101.00x
💻 LocalNitro1.073s (-5.1% 🟢)2.006s (~)0.933s101.00x
🐘 PostgresNitro1.084s (-4.9%)2.010s (~)0.927s101.01x
🌐 RedisNext.js (Turbopack)1.113s2.006s0.893s101.04x
🐘 PostgresNext.js (Turbopack)1.114s2.007s0.893s101.04x
💻 LocalNext.js (Turbopack)1.121s2.007s0.886s101.05x
🐘 PostgresExpress1.143s (~)2.021s (+0.5%)0.878s101.07x
🌐 MongoDBNext.js (Turbopack)1.162s2.009s0.847s101.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.498s (-20.1% 🟢)3.691s (-3.1%)2.193s101.00x
▲ VercelNitro1.524s (-60.9% 🟢)3.710s (-37.2% 🟢)2.186s101.02x
▲ VercelNext.js (Turbopack)1.942s (-4.6%)4.457s (+16.4% 🔺)2.516s101.30x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.409s (-4.2%)11.018s (~)0.609s31.00x
💻 LocalNitro10.413s (-4.9%)11.022s (~)0.608s31.00x
💻 LocalExpress10.436s (-4.5%)11.021s (~)0.586s31.00x
🐘 PostgresNext.js (Turbopack)10.568s11.013s0.445s31.02x
🌐 RedisNext.js (Turbopack)10.657s11.024s0.367s31.02x
💻 LocalNext.js (Turbopack)10.685s11.022s0.337s31.03x
🐘 PostgresExpress10.745s (-2.0%)11.419s (+3.6%)0.674s31.03x
🌐 MongoDBNext.js (Turbopack)10.789s11.018s0.229s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.059s (-40.8% 🟢)15.700s (-37.5% 🟢)1.641s21.00x
▲ VercelNext.js (Turbopack)14.845s (-14.3% 🟢)16.872s (-13.0% 🟢)2.027s21.06x
▲ VercelExpress15.996s (-5.8% 🟢)18.196s (-9.1% 🟢)2.200s21.14x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.431s (-10.8% 🟢)14.027s (-12.5% 🟢)0.596s51.00x
🐘 PostgresNitro13.434s (-8.0% 🟢)14.013s (-6.8% 🟢)0.579s51.00x
💻 LocalExpress13.490s (-9.9% 🟢)14.027s (-6.7% 🟢)0.537s51.00x
🐘 PostgresNext.js (Turbopack)13.820s14.018s0.198s51.03x
💻 LocalNext.js (Turbopack)14.076s15.030s0.954s41.05x
🌐 RedisNext.js (Turbopack)14.088s15.030s0.942s41.05x
🌐 MongoDBNext.js (Turbopack)14.283s15.019s0.736s41.06x
🐘 PostgresExpress14.537s (~)15.276s (+1.7%)0.739s41.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.821s (-58.6% 🟢)22.773s (-56.7% 🟢)1.952s31.00x
▲ VercelNitro21.829s (-66.1% 🟢)23.502s (-64.7% 🟢)1.673s31.05x
▲ VercelNext.js (Turbopack)23.199s (-55.9% 🟢)25.174s (-53.9% 🟢)1.975s31.11x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.846s (-29.4% 🟢)12.022s (-29.4% 🟢)0.176s81.00x
🐘 PostgresNitro11.923s (-14.6% 🟢)12.392s (-13.4% 🟢)0.469s81.01x
💻 LocalExpress11.997s (-27.7% 🟢)12.398s (-27.2% 🟢)0.401s81.01x
🌐 RedisNext.js (Turbopack)13.015s13.454s0.439s71.10x
💻 LocalNext.js (Turbopack)13.089s14.026s0.937s71.10x
🐘 PostgresNext.js (Turbopack)13.215s13.585s0.370s71.12x
🌐 MongoDBNext.js (Turbopack)13.301s14.021s0.720s71.12x
🐘 PostgresExpress15.027s (+7.3% 🔺)15.400s (+5.5% 🔺)0.373s61.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.095s (-74.3% 🟢)33.159s (-73.2% 🟢)2.064s31.00x
▲ VercelNitro31.508s (-92.5% 🟢)33.124s (-92.2% 🟢)1.615s31.01x
▲ VercelNext.js (Turbopack)31.987s (-91.9% 🟢)33.996s (-91.4% 🟢)2.009s31.03x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.140s (-10.6% 🟢)2.007s (~)0.868s151.00x
🐘 PostgresNext.js (Turbopack)1.168s2.007s0.838s151.03x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.187s (-20.3% 🟢)2.006s (~)0.820s151.04x
🐘 PostgresExpress1.237s (-1.9%)2.018s (~)0.781s151.09x
🌐 RedisNext.js (Turbopack)1.238s2.006s0.768s151.09x
💻 LocalNext.js (Turbopack)1.279s2.005s0.726s151.12x
🌐 MongoDBNext.js (Turbopack)2.048s2.917s0.869s111.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.551s (-10.8% 🟢)4.187s (-9.4% 🟢)1.636s81.00x
▲ VercelNitro2.674s (-5.1% 🟢)4.258s (-1.5%)1.584s81.05x
▲ VercelNext.js (Turbopack)4.193s (+23.4% 🔺)6.817s (+38.2% 🔺)2.624s51.64x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.225s (-47.9% 🟢)2.007s (-33.3% 🟢)0.782s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.05x
🐘 PostgresExpress1.483s (-37.2% 🟢)2.019s (-32.9% 🟢)0.536s151.21x
💻 LocalNitro1.726s (-45.1% 🟢)2.072s (-46.7% 🟢)0.346s151.41x
💻 LocalNext.js (Turbopack)1.831s2.149s0.318s141.50x
💻 LocalExpress1.905s (-35.5% 🟢)2.150s (-37.8% 🟢)0.244s141.56x
🌐 RedisNext.js (Turbopack)2.364s3.008s0.643s101.93x
🌐 MongoDBNext.js (Turbopack)3.568s4.008s0.440s82.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.844s (-5.1% 🟢)5.284s (-10.8% 🟢)1.440s61.00x
▲ VercelExpress3.884s (+7.3% 🔺)6.116s (+19.7% 🔺)2.232s51.01x
▲ VercelNext.js (Turbopack)5.098s (-28.2% 🟢)7.574s (-15.0% 🟢)2.476s41.33x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.419s (-59.2% 🟢)2.007s (-49.9% 🟢)0.588s151.00x
🐘 PostgresNext.js (Turbopack)1.493s2.007s0.514s151.05x
🐘 PostgresExpress2.153s (-38.2% 🟢)2.861s (-28.7% 🟢)0.708s111.52x
🌐 RedisNext.js (Turbopack)3.634s4.010s0.376s82.56x
💻 LocalNitro4.547s (-45.5% 🟢)5.013s (-44.4% 🟢)0.466s63.20x
💻 LocalExpress4.829s (-42.1% 🟢)5.512s (-38.9% 🟢)0.683s63.40x
💻 LocalNext.js (Turbopack)5.581s6.213s0.631s53.93x
🌐 MongoDBNext.js (Turbopack)6.289s7.012s0.723s54.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.147s (+21.4% 🔺)7.340s (+19.8% 🔺)2.193s51.00x
▲ VercelNitro5.410s (+53.5% 🔺)7.160s (+29.4% 🔺)1.750s51.05x
▲ VercelNext.js (Turbopack)7.157s (-19.7% 🟢)8.877s (-19.0% 🟢)1.721s41.39x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.170s (-6.9% 🟢)2.007s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.173s2.007s0.835s151.00x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.774s151.05x
💻 LocalNext.js (Turbopack)1.327s2.006s0.679s151.13x
💻 LocalNitro1.380s (-26.0% 🟢)2.006s (-14.3% 🟢)0.626s151.18x
💻 LocalExpress1.393s (-26.5% 🟢)2.006s (-15.1% 🟢)0.613s151.19x
🐘 PostgresExpress1.408s (+12.0% 🔺)2.062s (+2.7%)0.654s151.20x
🌐 MongoDBNext.js (Turbopack)2.036s2.916s0.880s111.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.271s (-7.6% 🟢)4.045s (-3.0%)1.774s81.00x
▲ VercelExpress2.336s (-9.5% 🟢)3.766s (-13.4% 🟢)1.430s81.03x
▲ VercelNext.js (Turbopack)4.170s (+42.2% 🔺)6.038s (+30.1% 🔺)1.869s51.84x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.251s (-46.5% 🟢)2.007s (-33.3% 🟢)0.757s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.03x
🐘 PostgresExpress1.393s (-40.5% 🟢)2.035s (-32.4% 🟢)0.642s151.11x
💻 LocalNitro1.957s (-36.2% 🟢)2.316s (-40.4% 🟢)0.359s131.56x
💻 LocalExpress2.022s (-35.4% 🟢)2.508s (-33.3% 🟢)0.485s121.62x
💻 LocalNext.js (Turbopack)2.130s2.826s0.696s111.70x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.88x
🌐 MongoDBNext.js (Turbopack)3.543s4.007s0.464s82.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.338s (+35.9% 🔺)6.011s (+25.4% 🔺)1.673s51.00x
▲ VercelNitro4.664s (+44.2% 🔺)6.658s (+31.1% 🔺)1.994s51.08x
▲ VercelNext.js (Turbopack)4.714s (+50.0% 🔺)6.542s (+44.7% 🔺)1.827s51.09x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.380s (-60.3% 🟢)2.007s (-49.9% 🟢)0.627s151.00x
🐘 PostgresNext.js (Turbopack)1.483s2.008s0.525s151.07x
🐘 PostgresExpress1.982s (-43.4% 🟢)2.737s (-31.8% 🟢)0.755s111.44x
🌐 RedisNext.js (Turbopack)3.571s4.010s0.439s82.59x
💻 LocalNitro4.894s (-46.5% 🟢)5.348s (-46.7% 🟢)0.454s63.55x
💻 LocalExpress5.939s (-32.5% 🟢)6.417s (-30.8% 🟢)0.478s54.30x
💻 LocalNext.js (Turbopack)6.115s6.416s0.301s54.43x
🌐 MongoDBNext.js (Turbopack)6.302s7.013s0.710s54.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.380s (+5.6% 🔺)7.062s (+3.6%)1.682s51.00x
▲ VercelNext.js (Turbopack)5.690s (-15.8% 🟢)7.416s (-13.2% 🟢)1.726s51.06x
▲ VercelExpress5.700s (-11.2% 🟢)7.539s (-7.8% 🟢)1.840s51.06x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.459s (-44.1% 🟢)1.022s (+1.6%)0.564s591.00x
💻 LocalNitro0.472s (-51.8% 🟢)1.021s (-6.7% 🟢)0.548s591.03x
💻 LocalExpress0.482s (-51.1% 🟢)1.004s (-6.7% 🟢)0.522s601.05x
🐘 PostgresNext.js (Turbopack)0.599s1.040s0.442s581.31x
🌐 RedisNext.js (Turbopack)0.619s1.004s0.386s601.35x
🐘 PostgresExpress0.644s (-23.2% 🟢)1.140s (+11.4% 🔺)0.496s531.40x
💻 LocalNext.js (Turbopack)0.719s1.004s0.285s601.57x
🌐 MongoDBNext.js (Turbopack)0.733s1.006s0.272s601.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.661s (-75.5% 🟢)6.339s (-70.3% 🟢)1.677s101.00x
▲ VercelNitro5.218s (-76.3% 🟢)7.258s (-69.8% 🟢)2.039s101.12x
▲ VercelNext.js (Turbopack)6.455s (-55.5% 🟢)7.922s (-50.7% 🟢)1.466s81.38x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.085s (-43.7% 🟢)1.705s (-18.8% 🟢)0.620s531.00x
🐘 PostgresExpress1.162s (-41.2% 🟢)1.588s (-29.7% 🟢)0.426s571.07x
💻 LocalExpress1.197s (-60.3% 🟢)2.006s (-44.1% 🟢)0.809s451.10x
💻 LocalNitro1.220s (-59.8% 🟢)2.005s (-46.6% 🟢)0.786s451.12x
🌐 RedisNext.js (Turbopack)1.476s2.006s0.530s451.36x
🐘 PostgresNext.js (Turbopack)1.707s2.387s0.679s381.57x
💻 LocalNext.js (Turbopack)1.753s2.005s0.253s451.61x
🌐 MongoDBNext.js (Turbopack)1.813s2.007s0.194s451.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.733s (-70.3% 🟢)13.729s (-66.8% 🟢)1.996s71.00x
▲ VercelExpress12.793s (-62.9% 🟢)14.645s (-60.2% 🟢)1.852s71.09x
▲ VercelNext.js (Turbopack)14.268s (-71.4% 🟢)16.580s (-67.9% 🟢)2.313s61.22x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.108s (-48.6% 🟢)2.696s (-41.4% 🟢)0.588s451.00x
🐘 PostgresExpress2.634s (-34.0% 🟢)3.074s (-29.6% 🟢)0.440s401.25x
🐘 PostgresNext.js (Turbopack)2.659s3.084s0.425s391.26x
💻 LocalNitro2.660s (-71.4% 🟢)3.007s (-70.0% 🟢)0.348s401.26x
💻 LocalExpress2.743s (-70.2% 🟢)3.033s (-69.7% 🟢)0.290s401.30x
🌐 RedisNext.js (Turbopack)2.992s3.191s0.200s381.42x
💻 LocalNext.js (Turbopack)3.748s4.007s0.259s301.78x
🌐 MongoDBNext.js (Turbopack)4.178s5.012s0.833s241.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express35.192s (-72.9% 🟢)37.772s (-71.4% 🟢)2.580s41.00x
▲ VercelNitro36.812s (-62.0% 🟢)39.053s (-60.3% 🟢)2.241s41.05x
▲ VercelNext.js (Turbopack)43.007s (-59.9% 🟢)46.755s (-57.1% 🟢)3.748s31.22x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-44.2% 🟢)1.007s (~)0.850s601.00x
🐘 PostgresNitro0.182s (-35.8% 🟢)1.006s (~)0.824s601.15x
🐘 PostgresNext.js (Turbopack)0.190s1.005s0.815s601.21x
🌐 RedisNext.js (Turbopack)0.316s1.021s0.705s592.00x
💻 LocalNitro0.412s (-31.9% 🟢)1.004s (-1.7%)0.592s602.61x
💻 LocalExpress0.452s (-19.3% 🟢)1.004s (~)0.552s602.87x
💻 LocalNext.js (Turbopack)0.603s1.039s0.436s583.83x
🌐 MongoDBNext.js (Turbopack)1.029s1.749s0.720s356.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.178s (+11.5% 🔺)4.588s (+26.2% 🔺)2.410s141.00x
▲ VercelNitro2.258s (+36.0% 🔺)4.485s (+33.9% 🔺)2.227s141.04x
▲ VercelNext.js (Turbopack)3.716s (+83.7% 🔺)5.458s (+43.9% 🔺)1.742s111.71x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.304s (-38.8% 🟢)1.006s (~)0.702s901.00x
🌐 RedisNext.js (Turbopack)0.423s1.004s0.581s901.39x
🐘 PostgresExpress0.520s (+2.0%)1.112s (+10.5% 🔺)0.592s821.71x
🐘 PostgresNext.js (Turbopack)0.705s1.318s0.613s692.32x
💻 LocalNitro2.170s (-14.5% 🟢)2.821s (-6.3% 🟢)0.651s327.14x
💻 LocalExpress2.186s (-13.0% 🟢)2.944s (-2.2%)0.758s317.19x
💻 LocalNext.js (Turbopack)2.424s3.043s0.619s307.97x
🌐 MongoDBNext.js (Turbopack)2.595s3.007s0.412s308.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.618s (+87.2% 🔺)8.356s (+60.9% 🔺)1.738s111.00x
▲ VercelNitro7.134s (+121.1% 🔺)9.355s (+94.0% 🔺)2.221s101.08x
▲ VercelExpress7.527s (+147.1% 🔺)9.552s (+98.7% 🔺)2.025s101.14x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.637s (-19.4% 🟢)1.006s (~)0.369s1201.00x
🐘 PostgresNext.js (Turbopack)0.730s1.013s0.284s1191.15x
🐘 PostgresExpress0.790s (-3.5%)1.192s (+17.1% 🔺)0.402s1011.24x
🌐 RedisNext.js (Turbopack)0.797s1.004s0.207s1201.25x
🌐 MongoDBNext.js (Turbopack)5.387s6.013s0.626s208.46x
💻 LocalNitro9.790s (-12.5% 🟢)10.276s (-11.9% 🟢)0.486s1215.37x
💻 LocalExpress10.085s (-9.9% 🟢)10.782s (-9.7% 🟢)0.697s1215.84x
💻 LocalNext.js (Turbopack)11.478s12.128s0.650s1018.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.821s (+117.8% 🔺)19.141s (+103.6% 🔺)2.319s71.00x
▲ VercelNext.js (Turbopack)18.466s (+78.8% 🔺)20.419s (+66.2% 🔺)1.953s71.10x
▲ VercelExpress20.247s (+172.9% 🔺)22.460s (+143.0% 🔺)2.213s61.20x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.133s (+430.1% 🔺)2.005s (+99.6% 🔺)0.010s (-19.2% 🟢)2.017s (+98.0% 🔺)0.884s101.00x
🐘 PostgresNitro1.134s (+453.4% 🔺)2.001s (+100.2% 🔺)0.001s (-26.7% 🟢)2.010s (+98.7% 🔺)0.875s101.00x
💻 LocalExpress1.136s (+470.5% 🔺)2.005s (+99.6% 🔺)0.012s (~)2.020s (+98.4% 🔺)0.884s101.00x
💻 LocalNext.js (Turbopack)1.172s2.003s0.012s2.019s0.847s101.03x
🐘 PostgresExpress1.232s (+500.5% 🔺)2.005s (+100.8% 🔺)0.001s (-37.5% 🟢)2.041s (+101.8% 🔺)0.810s101.09x
🐘 PostgresNext.js (Turbopack)2.024s2.601s0.220s2.874s0.850s101.79x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.200s (-42.6% 🟢)3.478s (-34.1% 🟢)1.562s (+110.5% 🔺)5.432s (-16.2% 🟢)3.232s101.00x
▲ VercelExpress2.336s (-6.8% 🟢)3.935s (-3.8%)1.087s (+13.1% 🔺)5.425s (-3.0%)3.089s101.06x
▲ VercelNext.js (Turbopack)4.802s (-29.9% 🟢)4.900s (-43.4% 🟢)0.740s (+17.0% 🔺)7.346s (-25.0% 🟢)2.544s102.18x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.512s (+142.2% 🔺)2.005s (+99.2% 🔺)0.004s (-8.9% 🟢)2.024s (+97.9% 🔺)0.512s301.00x
💻 LocalNitro1.514s (+80.5% 🔺)2.011s (+98.7% 🔺)0.010s (+2.8%)2.022s (+81.2% 🔺)0.509s301.00x
💻 LocalExpress1.721s (+127.3% 🔺)2.011s (+95.5% 🔺)0.010s (+2.1%)2.202s (+111.7% 🔺)0.481s281.14x
🐘 PostgresNext.js (Turbopack)1.722s2.107s0.003s2.123s0.401s301.14x
💻 LocalNext.js (Turbopack)1.845s2.011s0.009s2.203s0.358s281.22x
🐘 PostgresExpress1.983s (+214.8% 🔺)2.330s (+131.5% 🔺)0.003s (-20.7% 🟢)2.395s (+134.1% 🔺)0.412s261.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.656s (-13.0% 🟢)7.281s (-9.1% 🟢)0.179s (-56.1% 🟢)8.330s (-5.7% 🟢)2.675s81.00x
▲ VercelNitro5.796s (-80.3% 🟢)7.455s (-75.8% 🟢)0.170s (+52.1% 🔺)8.071s (-74.6% 🟢)2.275s81.02x
▲ VercelNext.js (Turbopack)12.255s (-27.6% 🟢)13.600s (-25.4% 🟢)0.201s (-4.9%)15.301s (-19.2% 🟢)3.046s42.17x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.651s (-32.8% 🟢)1.033s (-17.2% 🟢)0.000s (-100.0% 🟢)1.042s (-17.2% 🟢)0.390s581.00x
🐘 PostgresExpress1.031s (+7.3% 🔺)1.390s (+8.7% 🔺)0.000s (+9.5% 🔺)1.437s (+10.0% 🔺)0.406s421.58x
🐘 PostgresNext.js (Turbopack)1.333s1.618s0.000s1.715s0.382s372.05x
💻 LocalNitro1.337s (+9.4% 🔺)2.015s (~)0.000s (+166.7% 🔺)2.017s (~)0.680s302.05x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-60.0% 🟢)2.017s (~)0.649s302.10x
💻 LocalNext.js (Turbopack)1.436s2.013s0.000s2.016s0.581s302.20x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.922s (+28.6% 🔺)5.598s (+27.4% 🔺)0.000s (-100.0% 🟢)6.027s (+25.3% 🔺)2.105s101.00x
▲ VercelExpress4.013s (+7.3% 🔺)5.879s (+15.2% 🔺)0.000s (-100.0% 🟢)6.416s (+16.0% 🔺)2.404s101.02x
▲ VercelNext.js (Turbopack)5.465s (-46.3% 🟢)6.707s (-41.8% 🟢)0.000s (+Infinity% 🔺)7.645s (-36.6% 🟢)2.181s81.39x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.327s (-25.9% 🟢)2.066s (-3.5%)0.000s (-100.0% 🟢)2.081s (-4.3%)0.754s291.00x
🐘 PostgresNext.js (Turbopack)1.844s2.402s0.000s2.460s0.616s251.39x
🐘 PostgresExpress2.377s (+34.2% 🔺)3.042s (+39.7% 🔺)0.000s (NaN%)3.091s (+40.6% 🔺)0.714s201.79x
💻 LocalNext.js (Turbopack)2.776s3.362s0.000s3.365s0.589s182.09x
💻 LocalNitro3.068s (-9.4% 🟢)3.838s (-4.8%)0.000s (-53.1% 🟢)3.843s (-4.8%)0.775s162.31x
💻 LocalExpress3.103s (-10.5% 🟢)3.775s (-6.4% 🟢)0.000s (-76.6% 🟢)3.781s (-6.3% 🟢)0.679s162.34x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.059s (+10.3% 🔺)6.788s (+12.7% 🔺)0.000s (+Infinity% 🔺)7.571s (+17.3% 🔺)2.512s81.00x
▲ VercelNitro5.089s (+24.3% 🔺)6.536s (+21.6% 🔺)0.000s (-18.5% 🟢)7.002s (+20.9% 🔺)1.913s91.01x
▲ VercelNext.js (Turbopack)8.675s (+54.5% 🔺)10.065s (+44.2% 🔺)0.000s (-100.0% 🟢)11.283s (+49.6% 🔺)2.608s61.71x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro17/21
🐘 PostgresNitro19/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other4380150588
Total4266010265292

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025

📋 View full workflow run

Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/vitest/src/index.ts
Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/core/src/workflow.ts Outdated
Comment threadpackages/builders/src/base-builder.ts
Comment threadpackages/core/src/runtime/get-world-lazy.ts

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Remaining issues:

  • getWorldLazy needs to be validated as a decision
  • packages/core/e2e/utils.ts skip for hasStepSourceMaps needs to be re-evaluated

@shtefcs

Copy link
Copy Markdown

Seems you had a lot of work here. It's not easy as it seems. Hopefully, this gonna solve some issues we are facing with Workflow.

Peace,

@TooTallNateTooTallNate 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.

Withdrawing my prior CHANGES_REQUESTED — the blocker I called out (the stale (err as any).meta?.retryAfter access in step-executor.ts) is fixed at line 169 with err.retryAfter ?? 1. As an aside, my fix recommendation in the prior review was based on a faulty type assumption (I confused TooEarlyError.retryAfter: number with RetryableError.retryAfter: Date). The author's err.retryAfter ?? 1 form is correct: it's a number of seconds with sensible fallback.

My other earlier concerns:

  • getStepNameFromEvent full-event-load: addressed. stepName is now plumbed through WorkflowInvokePayloadSchema so the handler reads it directly from the queue message payload instead of doing a paginated event load. ✓
  • step.error guard for max retries: known limitation acknowledged in-thread. The proposed long-term fix (server-side ne(status, 'running') on step_started) is separate work.
  • Background step → continuation enqueue safety: acknowledged as intentional.

So my specific items are resolved. But I'm not approving outright — there's still substantial unresolved review from @VaguelySerious touching real concerns I'd defer to them on:

  • Removal of the LegacyStreamWorld shim
  • Limiting queue concurrency to 1 in tests
  • Comments removed without explanation in suspension-handler.ts / runtime.ts
  • The builder-deferred.ts Next.js behavior prediction
  • Several "is there a systematic fix instead of conditionally allowing edge cases" questions

Plus the auto-flagged event cache duplication bug from Vercel's review bot is worth confirming was understood and dismissed (or fixed).

Approval-wise this should land when @VaguelySerious's concerns are resolved. From my side: no remaining blockers.

@TooTallNateTooTallNate 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.

Thorough re-review for Peter. Spent significant time tracing four risk surfaces: the Vercel bot's auto-flag, the concurrent-step-execution model, inline-step replay determinism, and the open review threads. Bottom line up front: no ship-blockers from my side. A few coverage gaps and one acknowledged limitation worth being explicit about, but nothing I'd hold this PR on.

Vercel bot's "event cache duplication" auto-flag is a false positive

"wait_completed events pushed locally without advancing eventsCursor cause duplicate events on next incremental fetch."

The author anticipated exactly this scenario. runtime.ts:585–594 has explicit dedupe-by-eventId logic:

constexistingIds=newSet(cachedEvents.map((e)=>e.eventId));for(consteofloaded.events){if(!existingIds.has(e.eventId)){cachedEvents.push(e);}}

Verified end-to-end: locally-pushed wait_completed (line 685) keeps its real server-returned eventId, the next iteration's incremental fetch returns it again, the dedupe filter drops it, the consumer never sees a duplicate. The comment in the code calls out the exact scenario the bot flagged. Safe to dismiss the auto-flag.

Concurrent-step-execution: a partial fix shipping with a known gap

Verified the L198 guard (step.attempt > maxRetries + 1 && step.error) traces correctly for the scenario it targets — concurrent first-attempts before any user-code error has been recorded. Without step.error, the guard correctly skips and the step proceeds normally even with inflated step.attempt.

But there's a symmetric gap in the catch-block path at L469:

if(currentAttempt>=maxRetries+1){/* step_failed */}

This check has no && step.error guard, so it fires on the first real catch-block failure even when currentAttempt has been inflated by concurrent first-attempts. End result: a workflow with maxRetries=3 and 4+ concurrent handlers can permanently fail after a single transient flake, with a misleading log saying "exceeded max retries (4 retries)" when the step body actually only ran once.

This is the only substantive open thread on the PR (step-executor.ts:198), and the author has acknowledged it in a separate resolved thread:

"Agreed on the concurrent-retry limitation. The server-side ne(status, 'running') WHERE clause is the right long-term fix — tracking as a follow-up."

So everyone's eyes-open. Worth being explicit in the changelog or PR description that under high queue back-pressure, default-maxRetries workflows can spuriously fail. Either of these would close the gap fully:

  1. Add the same && step.error guard at L469 (mirrors L198, easy to do, but doesn't fix the L198 case for catch-block).
  2. Server-side ne(status, 'running') on step_started (referenced in the resolved thread; also fixes the world-testing concurrency issue Peter resolved separately).

Not a ship-stopper, but I'd want it tracked as a known issue users can hit, not just an internal todo.

Inline-step replay: ordering subtlety in the dedupe path

The locally-pushed-then-incremental-fetch path can produce non-monotonic-by-eventId ordering in cachedEvents. Specifically: after wait_completed is pushed locally (L685) without advancing the cursor, the next incremental fetch can return concurrent events with eventIds between the local push and earlier events. After dedupe, cachedEvents becomes append-ordered but not eventId-ordered. The EventsConsumer walks by index, so it consumes them in append order, which is not the canonical replay order.

In practice this is unlikely to cause issues because:

  • Within one handler's writes, the same world process generates monotonic ULIDs.
  • Cross-handler events for the same run are gated by the ownership check (step_created ownership at L813).
  • The dangerous case requires concurrent handler activity for the same run + non-trivial cross-process clock skew.

But it's a footgun. The cleanest fix would be: after locally pushing an event, set eventsCursor to that event's eventId so the next fetch skips it. This also eliminates the dedupe pass entirely. Worth considering as a follow-up.

"VaguelySerious" review threads — context for Peter

For other reviewers who may stumble on this: most of the open @VaguelySerious threads are Peter's own AI-review notes posted via tooling. Of 28 such threads, 27 are either resolved or outdated (the diff has moved past them). The single unresolved + non-outdated thread is the concurrent-retry concern above (step-executor.ts:198), which Peter has acknowledged as a known limitation in a sibling resolved thread.

External-reviewer threads (mine, the Vercel bot's) are all resolved or addressed. So the apparent "long list of unresolved concerns" from my prior comment is misleading — it was conflating Peter's self-review notes with external review.

Test coverage gaps (non-blocking)

A few coverage gaps for novel V2 behavior worth tracking even if you ship:

  1. Dedupe path at runtime.ts:585–594 — zero test coverage. The behavior is novel to V2 and load-bearing for incremental-replay correctness. A unit test that:

    • First iteration fetches with cursor C0
    • Manually pushes a wait_completed to cachedEvents
    • Second iteration fetches and gets the same wait_completed plus an interleaved new event
    • Asserts: no duplicates AND consumer sees no orphans
      ...would lock in the contract.
  2. L198 guard's positive case — there's no test asserting that attempt > maxRetries + 1withoutstep.error does NOT fail the step. The guard exists specifically for this case and is currently un-asserted. The two step-handler.test.ts tests at lines 296–340 and 343–388 both set error on the fixture, so they exercise different paths.

  3. Concurrent-handler races — neither Scenario B (concurrent retry with prior error → premature L198 failure) nor Scenario D (catch-block premature failure with no && step.error guard) has a test. Adding these would catch the bug currently in code if a future fix lands.

  4. builder-deferred.ts Next.js manifest reading — Peter agreed in review that reading app-paths-manifest.json was the right approach over regex prediction. Verified that DID land (L1171–1175 reads the manifest with comment "This avoids predicting Next.js conventions with regexes and instead reads from Next.js's own output"). ✅

What looks good

  • The architectural shift is well-executed. The replay loop, ownership-gated inline dispatch, idempotency-key-based queue dedupe, builder unification, and __steps_registered rollup-tree-shaking guard are all clean.
  • The getStepNameFromEvent full-event-load concern from my prior review was addressed — stepName now flows through WorkflowInvokePayloadSchema. ✓
  • The TooEarlyError stale retryAfter access pattern is fixed at step-executor.ts:169. ✓
  • The CJS module.exports collision (ESM format for steps when bundleFinalOutput: true) is a subtle but correct fix.
  • The changelog doc docs/content/docs/changelog/eager-processing.mdx is one of the better architecture docs I've seen in a PR — covers every edge case, failure mode, and platform-specific quirk.

Verdict

No blockers from my side. Ship if Vade and storytime green-light. The concurrent-retry premature-failure case is real but acknowledged, the auto-flagged "event cache duplication" is a non-issue, and the test coverage gaps are real but not regressions (the V2 behaviors they cover are net-new).

If you want one piece of polish before merge, the L469 && step.error mirror would close the most user-visible failure mode at zero cost — but I won't gate on it.

@TooTallNateTooTallNate 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.

Pre-emptively :approved: but my agent really wants @VaguelySerious to resolve the open threads :lol: and there seems to be a few other things pointed out in the latest review comment.

VaguelySeriousand others added 7 commits May 3, 2026 11:38
Merges the V1 split flow/step routes (one queue message per step + one per
flow continuation, two separate function invocations per step) into a
single combined handler at /.well-known/workflow/v1/flow that executes
steps inline within the same invocation as the workflow replay.
A serial workflow with N steps that previously required ~2N+1 function
invocations now completes in 1.
Architecture
------------
- packages/core: workflowEntrypoint() handler with an inline replay loop.
Each loop iteration: load events incrementally, replay workflow,
inline-execute one owned step on suspension, loop. Background steps
(from Promise.all) are queued back to the same handler with stepId so
the in-flight handler advances the loop without queue round-trips
whenever possible.
- packages/builders: createCombinedBundle() emits a single ESM bundle
with workflowEntrypoint(workflowCode) plus a side-effect
__step_registrations bundle. ESM-by-default with createRequire banner
for CJS deps (matches main's #1562 pattern).
- packages/next, nestjs, sveltekit, astro, nitro, nuxt, hono, express,
fastify, vite: framework builders updated to use createCombinedBundle.
The step/ directory is no longer generated.
Correctness invariants (V2 polish)
----------------------------------
- Single inline executor per step. Atomic step_created per correlationId
(per-step in-process mutex in world-local; SQL-level guards in
postgres/vercel) means exactly one handler claims ownership of each
step. Inline execution gates on ownership; queue dispatch is
unconditional with idempotency keys for crash recovery (matches V1).
- onUnconsumedEvent reverts to its original PR #1055 contract: any
unconsumed event fatals as CORRUPTED_EVENT_LOG. Source-level fixes
removed every code path that produced orphaned step lifecycle events
during V2 work, so the skip branch became unnecessary.
- Lock-release polling interval lowered from 100ms to 10ms in
flushable-stream so the V2 step-executor's ops-settle race typically
resolves in ~5ms instead of ~50ms per writable-bearing step.
- Per-iteration runs.get round-trip eliminated: concurrent completion
is detected by scanning the loaded event log for terminal run events
instead.
- Worker-pool deadlock guard removed from Run#pollReturnValue. The
fibonacci/recursive parent→child polling case is now handled by
raising world-postgres default queueConcurrency from 10 to 50, with
the limitation documented at three call sites (fibonacciWorkflow's
JSDoc, queue.ts, eager-processing.mdx).
- getWorldLazy module-scope singleton breaks the step-bundle import
chain to world.ts so step bundles don't transitively pull in
world-vercel + cbor-x + the VQS dev-handler resolver.
World-local atomicity
---------------------
- Per-step in-process mutex serializes events.create for step
lifecycle events on a (runId, correlationId) key. step_started is
rejected only when the step is already terminal — duplicate
step_started on a non-terminal step is allowed (preserves retry
semantics after SIGKILL).
Runtime helpers
---------------
- loadWorkflowRunEvents(runId, afterCursor?) consolidates the
previous getAllWorkflowRunEvents{,WithCursor} + getNewWorkflowRunEvents
into one helper.
Documentation
-------------
- docs/content/docs/changelog/eager-processing.mdx: full record of
architecture, edge cases, and the fixes applied during the V2 work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Two CI failures from the earlier V2 cutover, now resolved:
1. @workflow/vitest unit tests (`packages/vitest/src/index.test.ts`)
were still mocking the V1 BaseBuilder methods (createWorkflowsBundle
+ createStepsBundle) and expecting two world handlers (workflow +
step). Updated the mocks to expose createCombinedBundle and
asserted a single combined handler at __wkf_workflow_, which is
what the V2 vitest builder actually emits.
2. E2E Local Dev Tests (nextjs-webpack) were asserting that step
error stacks contain `99_e2e.ts` / `helpers.ts`, which held in
pre-V2 webpack dev mode (which imported step sources directly).
V2 inlines the step bundle into the combined flow route, and
webpack's re-bundling collapses original step filenames out of the
dev-mode source maps. Extended the existing nextjs-webpack
carve-out in hasStepSourceMaps() to cover dev as well as prod, and
updated the source-map follow-up callout in eager-processing.mdx
to include nextjs-webpack in the matrix that needs source maps
wired up for V2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/e2e/e2e.test.ts` (PR #1879 vs. V2 hookDispose helper)
Drop the locally-defined `waitForHook(expectedRunId)` shadow in the
hookDisposeTestWorkflow test in favor of the new top-level
`waitForHook(token, { runId })` API merged from main. Keep V2's
event-driven `waitForHookDisposal()` helper, which is stricter than
main's `await sleep(3_000)` (it polls `getHookByToken` for the
HookNotFoundError signal rather than waiting a fixed interval).
* `packages/world-local/src/storage/events-storage.ts` (PR #1877 vs.
V2 per-step async mutex)
Both branches were closing race conditions in the local world's
step lifecycle. PR #1877 adds filesystem-level O_CREAT|O_EXCL locks
for `step_created` and `wait_created`; the V2 branch already wraps
step lifecycle events in an in-process `withStepLock` mutex. They
compose: the mutex serializes within a single Node process; the
filesystem lock additionally protects against cross-process races
(multiple pnpm workers, redelivered queue messages). Resolution
takes V2's mutex wrap as the base and patches in main's two
`writeExclusive` claims at the start of the `step_created` and
`wait_created` handlers, with comments noting the dual-layer
guarantee.
`packages/core/e2e/utils.ts` auto-merged: V2's nextjs-webpack +
all-Vercel source-map carve-outs preserved alongside main's
`tanstack-start` addition to `hasWorkflowSourceMaps`.
Verified locally: `@workflow/core` and `@workflow/world-local`
typecheck clean; all 343 world-local unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone tarballs project's `vercel.json` set
`"buildCommand": "pnpm --filter tarballs build"`, which only runs the
tarballs package's `build` script (`node scripts/pack.ts`) and does
NOT trigger turbo's `dependsOn: ["^build"]` chain. As a result, every
workspace package was packed with an empty `dist/` directory — only
`bin/`, `package.json`, `README.md`, and `docs/` made it into the
tarballs. Downstream installs failed with
`Cannot find module '<pkg>/dist/next.cjs'` (and similar for every
other entry point).
The previous docs-based pipeline didn't hit this because Vercel
detected `docs` as a Next.js framework, ran turbo for it, and pack.ts
was wired as a `prebuild` step that ran after deps were already
built.
Fix: switch the buildCommand to `pnpm turbo build --filter=tarballs`
so turbo evaluates the build task for `tarballs`, which fans out via
`^build` to compile every workspace dependency first, and then runs
`tarballs#build` (pack.ts) against the populated `dist/` directories.
Verified locally: a fresh build produces tarballs containing the full
`dist/` tree (including `dist/next.cjs`, the file flight-booking-app
was failing to resolve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/src/logger.ts` (PR #1849 vs. V2 webpack carve-out)
PR #1849 ("Friendlier workflow errors") reintroduced the `debug`
package as a static import and added a structured `Logger`
interface with `child()` / `forRun()` and `composeLogLine`. The V2
branch had previously removed the `debug` static import because it
pulls `debug/src/node` and a dynamic `require('tty')` into the
Next.js webpack flow route, breaking V2 builds with
`Dynamic require of "tty" is not supported`.
Resolution combines both: keep main's `Logger` interface,
`child()` / `forRun()`, and `composeLogLine` integration; replace
the `debug` invocations with the V2 lightweight
`matchesDebugNamespace` + `process.env.DEBUG` matcher so the flow
route still bundles cleanly under webpack. Comment in the file
documents why `debug` is intentionally absent.
* `packages/core/src/runtime.ts` (PR #1849 vs. V2 inline replay loop)
Multiple regions where main's structured-logging refactor and the
V2 inline-execution loop made overlapping changes. Took V2's
control flow as the base — its `if (!workflowRun) { ... }` setup
block, main replay `while (true)` loop with timeout-based
re-scheduling, terminal-event short-circuit, and per-iteration
incremental event load are all preserved as-is — and folded in
main's contributions: the scoped `runLogger = runtimeLogger.forRun(
runId, workflowName)` is created once and used for run-level info
/ error logging. Discarded main's V1 wait-completion loop and
`runWorkflow` user-code error block since V2 handles waits and
user-code errors inside the inline loop body. Updated
`buildWorkflowSuspensionMessage()` call to drop a stray `runId`
first arg that didn't match the helper's 3-param signature.
* `packages/core/e2e/e2e.test.ts` (PR #1849 unrelated to V2)
No conflict from main's friendly-errors PR, auto-merged. (No
hookDispose-style conflict this round.)
Verified locally: all 820 `@workflow/core` unit tests pass; all 343
`@workflow/world-local` unit tests pass; `pnpm turbo typecheck` is
green across all 40 typecheck tasks.
Co-Authored-By: Claude Opus 4.7 (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

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

[core] Combine flow+step bundle and process steps eagerly - #1338

Merged
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow
May 4, 2026
Merged

[core] Combine flow+step bundle and process steps eagerly#1338
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow

Conversation

@changeset-bot

changeset-botBot commented Mar 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 234fce4

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

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

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 Mar 11, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Mar 11, 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.033s (-24.6% 🟢)1.005s (~)0.972s101.00x
💻 LocalExpress0.034s (-23.3% 🟢)1.006s (~)0.972s101.05x
🐘 PostgresNext.js (Turbopack)0.049s1.013s0.964s101.50x
💻 LocalNext.js (Turbopack)0.049s1.005s0.956s101.51x
🐘 PostgresNitro0.051s (-46.8% 🟢)1.012s (-3.0%)0.961s101.56x
🌐 RedisNext.js (Turbopack)0.055s1.005s0.951s101.68x
🌐 MongoDBNext.js (Turbopack)0.109s1.007s0.899s103.35x
🐘 PostgresExpress0.177s (+205.2% 🔺)1.107s (+9.5% 🔺)0.930s105.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.236s (-42.4% 🟢)1.939s (-22.7% 🟢)1.703s101.00x
▲ VercelExpress0.241s (+2.2%)2.009s (-5.9% 🟢)1.768s101.02x
▲ VercelNext.js (Turbopack)0.550s (+118.6% 🔺)2.715s (+16.4% 🔺)2.165s102.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.069s (-5.0% 🟢)2.006s (~)0.937s101.00x
💻 LocalNitro1.073s (-5.1% 🟢)2.006s (~)0.933s101.00x
🐘 PostgresNitro1.084s (-4.9%)2.010s (~)0.927s101.01x
🌐 RedisNext.js (Turbopack)1.113s2.006s0.893s101.04x
🐘 PostgresNext.js (Turbopack)1.114s2.007s0.893s101.04x
💻 LocalNext.js (Turbopack)1.121s2.007s0.886s101.05x
🐘 PostgresExpress1.143s (~)2.021s (+0.5%)0.878s101.07x
🌐 MongoDBNext.js (Turbopack)1.162s2.009s0.847s101.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.498s (-20.1% 🟢)3.691s (-3.1%)2.193s101.00x
▲ VercelNitro1.524s (-60.9% 🟢)3.710s (-37.2% 🟢)2.186s101.02x
▲ VercelNext.js (Turbopack)1.942s (-4.6%)4.457s (+16.4% 🔺)2.516s101.30x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.409s (-4.2%)11.018s (~)0.609s31.00x
💻 LocalNitro10.413s (-4.9%)11.022s (~)0.608s31.00x
💻 LocalExpress10.436s (-4.5%)11.021s (~)0.586s31.00x
🐘 PostgresNext.js (Turbopack)10.568s11.013s0.445s31.02x
🌐 RedisNext.js (Turbopack)10.657s11.024s0.367s31.02x
💻 LocalNext.js (Turbopack)10.685s11.022s0.337s31.03x
🐘 PostgresExpress10.745s (-2.0%)11.419s (+3.6%)0.674s31.03x
🌐 MongoDBNext.js (Turbopack)10.789s11.018s0.229s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.059s (-40.8% 🟢)15.700s (-37.5% 🟢)1.641s21.00x
▲ VercelNext.js (Turbopack)14.845s (-14.3% 🟢)16.872s (-13.0% 🟢)2.027s21.06x
▲ VercelExpress15.996s (-5.8% 🟢)18.196s (-9.1% 🟢)2.200s21.14x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.431s (-10.8% 🟢)14.027s (-12.5% 🟢)0.596s51.00x
🐘 PostgresNitro13.434s (-8.0% 🟢)14.013s (-6.8% 🟢)0.579s51.00x
💻 LocalExpress13.490s (-9.9% 🟢)14.027s (-6.7% 🟢)0.537s51.00x
🐘 PostgresNext.js (Turbopack)13.820s14.018s0.198s51.03x
💻 LocalNext.js (Turbopack)14.076s15.030s0.954s41.05x
🌐 RedisNext.js (Turbopack)14.088s15.030s0.942s41.05x
🌐 MongoDBNext.js (Turbopack)14.283s15.019s0.736s41.06x
🐘 PostgresExpress14.537s (~)15.276s (+1.7%)0.739s41.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.821s (-58.6% 🟢)22.773s (-56.7% 🟢)1.952s31.00x
▲ VercelNitro21.829s (-66.1% 🟢)23.502s (-64.7% 🟢)1.673s31.05x
▲ VercelNext.js (Turbopack)23.199s (-55.9% 🟢)25.174s (-53.9% 🟢)1.975s31.11x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.846s (-29.4% 🟢)12.022s (-29.4% 🟢)0.176s81.00x
🐘 PostgresNitro11.923s (-14.6% 🟢)12.392s (-13.4% 🟢)0.469s81.01x
💻 LocalExpress11.997s (-27.7% 🟢)12.398s (-27.2% 🟢)0.401s81.01x
🌐 RedisNext.js (Turbopack)13.015s13.454s0.439s71.10x
💻 LocalNext.js (Turbopack)13.089s14.026s0.937s71.10x
🐘 PostgresNext.js (Turbopack)13.215s13.585s0.370s71.12x
🌐 MongoDBNext.js (Turbopack)13.301s14.021s0.720s71.12x
🐘 PostgresExpress15.027s (+7.3% 🔺)15.400s (+5.5% 🔺)0.373s61.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.095s (-74.3% 🟢)33.159s (-73.2% 🟢)2.064s31.00x
▲ VercelNitro31.508s (-92.5% 🟢)33.124s (-92.2% 🟢)1.615s31.01x
▲ VercelNext.js (Turbopack)31.987s (-91.9% 🟢)33.996s (-91.4% 🟢)2.009s31.03x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.140s (-10.6% 🟢)2.007s (~)0.868s151.00x
🐘 PostgresNext.js (Turbopack)1.168s2.007s0.838s151.03x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.187s (-20.3% 🟢)2.006s (~)0.820s151.04x
🐘 PostgresExpress1.237s (-1.9%)2.018s (~)0.781s151.09x
🌐 RedisNext.js (Turbopack)1.238s2.006s0.768s151.09x
💻 LocalNext.js (Turbopack)1.279s2.005s0.726s151.12x
🌐 MongoDBNext.js (Turbopack)2.048s2.917s0.869s111.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.551s (-10.8% 🟢)4.187s (-9.4% 🟢)1.636s81.00x
▲ VercelNitro2.674s (-5.1% 🟢)4.258s (-1.5%)1.584s81.05x
▲ VercelNext.js (Turbopack)4.193s (+23.4% 🔺)6.817s (+38.2% 🔺)2.624s51.64x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.225s (-47.9% 🟢)2.007s (-33.3% 🟢)0.782s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.05x
🐘 PostgresExpress1.483s (-37.2% 🟢)2.019s (-32.9% 🟢)0.536s151.21x
💻 LocalNitro1.726s (-45.1% 🟢)2.072s (-46.7% 🟢)0.346s151.41x
💻 LocalNext.js (Turbopack)1.831s2.149s0.318s141.50x
💻 LocalExpress1.905s (-35.5% 🟢)2.150s (-37.8% 🟢)0.244s141.56x
🌐 RedisNext.js (Turbopack)2.364s3.008s0.643s101.93x
🌐 MongoDBNext.js (Turbopack)3.568s4.008s0.440s82.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.844s (-5.1% 🟢)5.284s (-10.8% 🟢)1.440s61.00x
▲ VercelExpress3.884s (+7.3% 🔺)6.116s (+19.7% 🔺)2.232s51.01x
▲ VercelNext.js (Turbopack)5.098s (-28.2% 🟢)7.574s (-15.0% 🟢)2.476s41.33x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.419s (-59.2% 🟢)2.007s (-49.9% 🟢)0.588s151.00x
🐘 PostgresNext.js (Turbopack)1.493s2.007s0.514s151.05x
🐘 PostgresExpress2.153s (-38.2% 🟢)2.861s (-28.7% 🟢)0.708s111.52x
🌐 RedisNext.js (Turbopack)3.634s4.010s0.376s82.56x
💻 LocalNitro4.547s (-45.5% 🟢)5.013s (-44.4% 🟢)0.466s63.20x
💻 LocalExpress4.829s (-42.1% 🟢)5.512s (-38.9% 🟢)0.683s63.40x
💻 LocalNext.js (Turbopack)5.581s6.213s0.631s53.93x
🌐 MongoDBNext.js (Turbopack)6.289s7.012s0.723s54.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.147s (+21.4% 🔺)7.340s (+19.8% 🔺)2.193s51.00x
▲ VercelNitro5.410s (+53.5% 🔺)7.160s (+29.4% 🔺)1.750s51.05x
▲ VercelNext.js (Turbopack)7.157s (-19.7% 🟢)8.877s (-19.0% 🟢)1.721s41.39x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.170s (-6.9% 🟢)2.007s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.173s2.007s0.835s151.00x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.774s151.05x
💻 LocalNext.js (Turbopack)1.327s2.006s0.679s151.13x
💻 LocalNitro1.380s (-26.0% 🟢)2.006s (-14.3% 🟢)0.626s151.18x
💻 LocalExpress1.393s (-26.5% 🟢)2.006s (-15.1% 🟢)0.613s151.19x
🐘 PostgresExpress1.408s (+12.0% 🔺)2.062s (+2.7%)0.654s151.20x
🌐 MongoDBNext.js (Turbopack)2.036s2.916s0.880s111.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.271s (-7.6% 🟢)4.045s (-3.0%)1.774s81.00x
▲ VercelExpress2.336s (-9.5% 🟢)3.766s (-13.4% 🟢)1.430s81.03x
▲ VercelNext.js (Turbopack)4.170s (+42.2% 🔺)6.038s (+30.1% 🔺)1.869s51.84x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.251s (-46.5% 🟢)2.007s (-33.3% 🟢)0.757s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.03x
🐘 PostgresExpress1.393s (-40.5% 🟢)2.035s (-32.4% 🟢)0.642s151.11x
💻 LocalNitro1.957s (-36.2% 🟢)2.316s (-40.4% 🟢)0.359s131.56x
💻 LocalExpress2.022s (-35.4% 🟢)2.508s (-33.3% 🟢)0.485s121.62x
💻 LocalNext.js (Turbopack)2.130s2.826s0.696s111.70x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.88x
🌐 MongoDBNext.js (Turbopack)3.543s4.007s0.464s82.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.338s (+35.9% 🔺)6.011s (+25.4% 🔺)1.673s51.00x
▲ VercelNitro4.664s (+44.2% 🔺)6.658s (+31.1% 🔺)1.994s51.08x
▲ VercelNext.js (Turbopack)4.714s (+50.0% 🔺)6.542s (+44.7% 🔺)1.827s51.09x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.380s (-60.3% 🟢)2.007s (-49.9% 🟢)0.627s151.00x
🐘 PostgresNext.js (Turbopack)1.483s2.008s0.525s151.07x
🐘 PostgresExpress1.982s (-43.4% 🟢)2.737s (-31.8% 🟢)0.755s111.44x
🌐 RedisNext.js (Turbopack)3.571s4.010s0.439s82.59x
💻 LocalNitro4.894s (-46.5% 🟢)5.348s (-46.7% 🟢)0.454s63.55x
💻 LocalExpress5.939s (-32.5% 🟢)6.417s (-30.8% 🟢)0.478s54.30x
💻 LocalNext.js (Turbopack)6.115s6.416s0.301s54.43x
🌐 MongoDBNext.js (Turbopack)6.302s7.013s0.710s54.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.380s (+5.6% 🔺)7.062s (+3.6%)1.682s51.00x
▲ VercelNext.js (Turbopack)5.690s (-15.8% 🟢)7.416s (-13.2% 🟢)1.726s51.06x
▲ VercelExpress5.700s (-11.2% 🟢)7.539s (-7.8% 🟢)1.840s51.06x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.459s (-44.1% 🟢)1.022s (+1.6%)0.564s591.00x
💻 LocalNitro0.472s (-51.8% 🟢)1.021s (-6.7% 🟢)0.548s591.03x
💻 LocalExpress0.482s (-51.1% 🟢)1.004s (-6.7% 🟢)0.522s601.05x
🐘 PostgresNext.js (Turbopack)0.599s1.040s0.442s581.31x
🌐 RedisNext.js (Turbopack)0.619s1.004s0.386s601.35x
🐘 PostgresExpress0.644s (-23.2% 🟢)1.140s (+11.4% 🔺)0.496s531.40x
💻 LocalNext.js (Turbopack)0.719s1.004s0.285s601.57x
🌐 MongoDBNext.js (Turbopack)0.733s1.006s0.272s601.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.661s (-75.5% 🟢)6.339s (-70.3% 🟢)1.677s101.00x
▲ VercelNitro5.218s (-76.3% 🟢)7.258s (-69.8% 🟢)2.039s101.12x
▲ VercelNext.js (Turbopack)6.455s (-55.5% 🟢)7.922s (-50.7% 🟢)1.466s81.38x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.085s (-43.7% 🟢)1.705s (-18.8% 🟢)0.620s531.00x
🐘 PostgresExpress1.162s (-41.2% 🟢)1.588s (-29.7% 🟢)0.426s571.07x
💻 LocalExpress1.197s (-60.3% 🟢)2.006s (-44.1% 🟢)0.809s451.10x
💻 LocalNitro1.220s (-59.8% 🟢)2.005s (-46.6% 🟢)0.786s451.12x
🌐 RedisNext.js (Turbopack)1.476s2.006s0.530s451.36x
🐘 PostgresNext.js (Turbopack)1.707s2.387s0.679s381.57x
💻 LocalNext.js (Turbopack)1.753s2.005s0.253s451.61x
🌐 MongoDBNext.js (Turbopack)1.813s2.007s0.194s451.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.733s (-70.3% 🟢)13.729s (-66.8% 🟢)1.996s71.00x
▲ VercelExpress12.793s (-62.9% 🟢)14.645s (-60.2% 🟢)1.852s71.09x
▲ VercelNext.js (Turbopack)14.268s (-71.4% 🟢)16.580s (-67.9% 🟢)2.313s61.22x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.108s (-48.6% 🟢)2.696s (-41.4% 🟢)0.588s451.00x
🐘 PostgresExpress2.634s (-34.0% 🟢)3.074s (-29.6% 🟢)0.440s401.25x
🐘 PostgresNext.js (Turbopack)2.659s3.084s0.425s391.26x
💻 LocalNitro2.660s (-71.4% 🟢)3.007s (-70.0% 🟢)0.348s401.26x
💻 LocalExpress2.743s (-70.2% 🟢)3.033s (-69.7% 🟢)0.290s401.30x
🌐 RedisNext.js (Turbopack)2.992s3.191s0.200s381.42x
💻 LocalNext.js (Turbopack)3.748s4.007s0.259s301.78x
🌐 MongoDBNext.js (Turbopack)4.178s5.012s0.833s241.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express35.192s (-72.9% 🟢)37.772s (-71.4% 🟢)2.580s41.00x
▲ VercelNitro36.812s (-62.0% 🟢)39.053s (-60.3% 🟢)2.241s41.05x
▲ VercelNext.js (Turbopack)43.007s (-59.9% 🟢)46.755s (-57.1% 🟢)3.748s31.22x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-44.2% 🟢)1.007s (~)0.850s601.00x
🐘 PostgresNitro0.182s (-35.8% 🟢)1.006s (~)0.824s601.15x
🐘 PostgresNext.js (Turbopack)0.190s1.005s0.815s601.21x
🌐 RedisNext.js (Turbopack)0.316s1.021s0.705s592.00x
💻 LocalNitro0.412s (-31.9% 🟢)1.004s (-1.7%)0.592s602.61x
💻 LocalExpress0.452s (-19.3% 🟢)1.004s (~)0.552s602.87x
💻 LocalNext.js (Turbopack)0.603s1.039s0.436s583.83x
🌐 MongoDBNext.js (Turbopack)1.029s1.749s0.720s356.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.178s (+11.5% 🔺)4.588s (+26.2% 🔺)2.410s141.00x
▲ VercelNitro2.258s (+36.0% 🔺)4.485s (+33.9% 🔺)2.227s141.04x
▲ VercelNext.js (Turbopack)3.716s (+83.7% 🔺)5.458s (+43.9% 🔺)1.742s111.71x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.304s (-38.8% 🟢)1.006s (~)0.702s901.00x
🌐 RedisNext.js (Turbopack)0.423s1.004s0.581s901.39x
🐘 PostgresExpress0.520s (+2.0%)1.112s (+10.5% 🔺)0.592s821.71x
🐘 PostgresNext.js (Turbopack)0.705s1.318s0.613s692.32x
💻 LocalNitro2.170s (-14.5% 🟢)2.821s (-6.3% 🟢)0.651s327.14x
💻 LocalExpress2.186s (-13.0% 🟢)2.944s (-2.2%)0.758s317.19x
💻 LocalNext.js (Turbopack)2.424s3.043s0.619s307.97x
🌐 MongoDBNext.js (Turbopack)2.595s3.007s0.412s308.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.618s (+87.2% 🔺)8.356s (+60.9% 🔺)1.738s111.00x
▲ VercelNitro7.134s (+121.1% 🔺)9.355s (+94.0% 🔺)2.221s101.08x
▲ VercelExpress7.527s (+147.1% 🔺)9.552s (+98.7% 🔺)2.025s101.14x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.637s (-19.4% 🟢)1.006s (~)0.369s1201.00x
🐘 PostgresNext.js (Turbopack)0.730s1.013s0.284s1191.15x
🐘 PostgresExpress0.790s (-3.5%)1.192s (+17.1% 🔺)0.402s1011.24x
🌐 RedisNext.js (Turbopack)0.797s1.004s0.207s1201.25x
🌐 MongoDBNext.js (Turbopack)5.387s6.013s0.626s208.46x
💻 LocalNitro9.790s (-12.5% 🟢)10.276s (-11.9% 🟢)0.486s1215.37x
💻 LocalExpress10.085s (-9.9% 🟢)10.782s (-9.7% 🟢)0.697s1215.84x
💻 LocalNext.js (Turbopack)11.478s12.128s0.650s1018.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.821s (+117.8% 🔺)19.141s (+103.6% 🔺)2.319s71.00x
▲ VercelNext.js (Turbopack)18.466s (+78.8% 🔺)20.419s (+66.2% 🔺)1.953s71.10x
▲ VercelExpress20.247s (+172.9% 🔺)22.460s (+143.0% 🔺)2.213s61.20x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.133s (+430.1% 🔺)2.005s (+99.6% 🔺)0.010s (-19.2% 🟢)2.017s (+98.0% 🔺)0.884s101.00x
🐘 PostgresNitro1.134s (+453.4% 🔺)2.001s (+100.2% 🔺)0.001s (-26.7% 🟢)2.010s (+98.7% 🔺)0.875s101.00x
💻 LocalExpress1.136s (+470.5% 🔺)2.005s (+99.6% 🔺)0.012s (~)2.020s (+98.4% 🔺)0.884s101.00x
💻 LocalNext.js (Turbopack)1.172s2.003s0.012s2.019s0.847s101.03x
🐘 PostgresExpress1.232s (+500.5% 🔺)2.005s (+100.8% 🔺)0.001s (-37.5% 🟢)2.041s (+101.8% 🔺)0.810s101.09x
🐘 PostgresNext.js (Turbopack)2.024s2.601s0.220s2.874s0.850s101.79x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.200s (-42.6% 🟢)3.478s (-34.1% 🟢)1.562s (+110.5% 🔺)5.432s (-16.2% 🟢)3.232s101.00x
▲ VercelExpress2.336s (-6.8% 🟢)3.935s (-3.8%)1.087s (+13.1% 🔺)5.425s (-3.0%)3.089s101.06x
▲ VercelNext.js (Turbopack)4.802s (-29.9% 🟢)4.900s (-43.4% 🟢)0.740s (+17.0% 🔺)7.346s (-25.0% 🟢)2.544s102.18x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.512s (+142.2% 🔺)2.005s (+99.2% 🔺)0.004s (-8.9% 🟢)2.024s (+97.9% 🔺)0.512s301.00x
💻 LocalNitro1.514s (+80.5% 🔺)2.011s (+98.7% 🔺)0.010s (+2.8%)2.022s (+81.2% 🔺)0.509s301.00x
💻 LocalExpress1.721s (+127.3% 🔺)2.011s (+95.5% 🔺)0.010s (+2.1%)2.202s (+111.7% 🔺)0.481s281.14x
🐘 PostgresNext.js (Turbopack)1.722s2.107s0.003s2.123s0.401s301.14x
💻 LocalNext.js (Turbopack)1.845s2.011s0.009s2.203s0.358s281.22x
🐘 PostgresExpress1.983s (+214.8% 🔺)2.330s (+131.5% 🔺)0.003s (-20.7% 🟢)2.395s (+134.1% 🔺)0.412s261.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.656s (-13.0% 🟢)7.281s (-9.1% 🟢)0.179s (-56.1% 🟢)8.330s (-5.7% 🟢)2.675s81.00x
▲ VercelNitro5.796s (-80.3% 🟢)7.455s (-75.8% 🟢)0.170s (+52.1% 🔺)8.071s (-74.6% 🟢)2.275s81.02x
▲ VercelNext.js (Turbopack)12.255s (-27.6% 🟢)13.600s (-25.4% 🟢)0.201s (-4.9%)15.301s (-19.2% 🟢)3.046s42.17x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.651s (-32.8% 🟢)1.033s (-17.2% 🟢)0.000s (-100.0% 🟢)1.042s (-17.2% 🟢)0.390s581.00x
🐘 PostgresExpress1.031s (+7.3% 🔺)1.390s (+8.7% 🔺)0.000s (+9.5% 🔺)1.437s (+10.0% 🔺)0.406s421.58x
🐘 PostgresNext.js (Turbopack)1.333s1.618s0.000s1.715s0.382s372.05x
💻 LocalNitro1.337s (+9.4% 🔺)2.015s (~)0.000s (+166.7% 🔺)2.017s (~)0.680s302.05x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-60.0% 🟢)2.017s (~)0.649s302.10x
💻 LocalNext.js (Turbopack)1.436s2.013s0.000s2.016s0.581s302.20x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.922s (+28.6% 🔺)5.598s (+27.4% 🔺)0.000s (-100.0% 🟢)6.027s (+25.3% 🔺)2.105s101.00x
▲ VercelExpress4.013s (+7.3% 🔺)5.879s (+15.2% 🔺)0.000s (-100.0% 🟢)6.416s (+16.0% 🔺)2.404s101.02x
▲ VercelNext.js (Turbopack)5.465s (-46.3% 🟢)6.707s (-41.8% 🟢)0.000s (+Infinity% 🔺)7.645s (-36.6% 🟢)2.181s81.39x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.327s (-25.9% 🟢)2.066s (-3.5%)0.000s (-100.0% 🟢)2.081s (-4.3%)0.754s291.00x
🐘 PostgresNext.js (Turbopack)1.844s2.402s0.000s2.460s0.616s251.39x
🐘 PostgresExpress2.377s (+34.2% 🔺)3.042s (+39.7% 🔺)0.000s (NaN%)3.091s (+40.6% 🔺)0.714s201.79x
💻 LocalNext.js (Turbopack)2.776s3.362s0.000s3.365s0.589s182.09x
💻 LocalNitro3.068s (-9.4% 🟢)3.838s (-4.8%)0.000s (-53.1% 🟢)3.843s (-4.8%)0.775s162.31x
💻 LocalExpress3.103s (-10.5% 🟢)3.775s (-6.4% 🟢)0.000s (-76.6% 🟢)3.781s (-6.3% 🟢)0.679s162.34x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.059s (+10.3% 🔺)6.788s (+12.7% 🔺)0.000s (+Infinity% 🔺)7.571s (+17.3% 🔺)2.512s81.00x
▲ VercelNitro5.089s (+24.3% 🔺)6.536s (+21.6% 🔺)0.000s (-18.5% 🟢)7.002s (+20.9% 🔺)1.913s91.01x
▲ VercelNext.js (Turbopack)8.675s (+54.5% 🔺)10.065s (+44.2% 🔺)0.000s (-100.0% 🟢)11.283s (+49.6% 🔺)2.608s61.71x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro17/21
🐘 PostgresNitro19/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other4380150588
Total4266010265292

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025

📋 View full workflow run

Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/vitest/src/index.ts
Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/core/src/workflow.ts Outdated
Comment threadpackages/builders/src/base-builder.ts
Comment threadpackages/core/src/runtime/get-world-lazy.ts

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Remaining issues:

  • getWorldLazy needs to be validated as a decision
  • packages/core/e2e/utils.ts skip for hasStepSourceMaps needs to be re-evaluated

@shtefcs

Copy link
Copy Markdown

Seems you had a lot of work here. It's not easy as it seems. Hopefully, this gonna solve some issues we are facing with Workflow.

Peace,

@TooTallNateTooTallNate 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.

Withdrawing my prior CHANGES_REQUESTED — the blocker I called out (the stale (err as any).meta?.retryAfter access in step-executor.ts) is fixed at line 169 with err.retryAfter ?? 1. As an aside, my fix recommendation in the prior review was based on a faulty type assumption (I confused TooEarlyError.retryAfter: number with RetryableError.retryAfter: Date). The author's err.retryAfter ?? 1 form is correct: it's a number of seconds with sensible fallback.

My other earlier concerns:

  • getStepNameFromEvent full-event-load: addressed. stepName is now plumbed through WorkflowInvokePayloadSchema so the handler reads it directly from the queue message payload instead of doing a paginated event load. ✓
  • step.error guard for max retries: known limitation acknowledged in-thread. The proposed long-term fix (server-side ne(status, 'running') on step_started) is separate work.
  • Background step → continuation enqueue safety: acknowledged as intentional.

So my specific items are resolved. But I'm not approving outright — there's still substantial unresolved review from @VaguelySerious touching real concerns I'd defer to them on:

  • Removal of the LegacyStreamWorld shim
  • Limiting queue concurrency to 1 in tests
  • Comments removed without explanation in suspension-handler.ts / runtime.ts
  • The builder-deferred.ts Next.js behavior prediction
  • Several "is there a systematic fix instead of conditionally allowing edge cases" questions

Plus the auto-flagged event cache duplication bug from Vercel's review bot is worth confirming was understood and dismissed (or fixed).

Approval-wise this should land when @VaguelySerious's concerns are resolved. From my side: no remaining blockers.

@TooTallNateTooTallNate 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.

Thorough re-review for Peter. Spent significant time tracing four risk surfaces: the Vercel bot's auto-flag, the concurrent-step-execution model, inline-step replay determinism, and the open review threads. Bottom line up front: no ship-blockers from my side. A few coverage gaps and one acknowledged limitation worth being explicit about, but nothing I'd hold this PR on.

Vercel bot's "event cache duplication" auto-flag is a false positive

"wait_completed events pushed locally without advancing eventsCursor cause duplicate events on next incremental fetch."

The author anticipated exactly this scenario. runtime.ts:585–594 has explicit dedupe-by-eventId logic:

constexistingIds=newSet(cachedEvents.map((e)=>e.eventId));for(consteofloaded.events){if(!existingIds.has(e.eventId)){cachedEvents.push(e);}}

Verified end-to-end: locally-pushed wait_completed (line 685) keeps its real server-returned eventId, the next iteration's incremental fetch returns it again, the dedupe filter drops it, the consumer never sees a duplicate. The comment in the code calls out the exact scenario the bot flagged. Safe to dismiss the auto-flag.

Concurrent-step-execution: a partial fix shipping with a known gap

Verified the L198 guard (step.attempt > maxRetries + 1 && step.error) traces correctly for the scenario it targets — concurrent first-attempts before any user-code error has been recorded. Without step.error, the guard correctly skips and the step proceeds normally even with inflated step.attempt.

But there's a symmetric gap in the catch-block path at L469:

if(currentAttempt>=maxRetries+1){/* step_failed */}

This check has no && step.error guard, so it fires on the first real catch-block failure even when currentAttempt has been inflated by concurrent first-attempts. End result: a workflow with maxRetries=3 and 4+ concurrent handlers can permanently fail after a single transient flake, with a misleading log saying "exceeded max retries (4 retries)" when the step body actually only ran once.

This is the only substantive open thread on the PR (step-executor.ts:198), and the author has acknowledged it in a separate resolved thread:

"Agreed on the concurrent-retry limitation. The server-side ne(status, 'running') WHERE clause is the right long-term fix — tracking as a follow-up."

So everyone's eyes-open. Worth being explicit in the changelog or PR description that under high queue back-pressure, default-maxRetries workflows can spuriously fail. Either of these would close the gap fully:

  1. Add the same && step.error guard at L469 (mirrors L198, easy to do, but doesn't fix the L198 case for catch-block).
  2. Server-side ne(status, 'running') on step_started (referenced in the resolved thread; also fixes the world-testing concurrency issue Peter resolved separately).

Not a ship-stopper, but I'd want it tracked as a known issue users can hit, not just an internal todo.

Inline-step replay: ordering subtlety in the dedupe path

The locally-pushed-then-incremental-fetch path can produce non-monotonic-by-eventId ordering in cachedEvents. Specifically: after wait_completed is pushed locally (L685) without advancing the cursor, the next incremental fetch can return concurrent events with eventIds between the local push and earlier events. After dedupe, cachedEvents becomes append-ordered but not eventId-ordered. The EventsConsumer walks by index, so it consumes them in append order, which is not the canonical replay order.

In practice this is unlikely to cause issues because:

  • Within one handler's writes, the same world process generates monotonic ULIDs.
  • Cross-handler events for the same run are gated by the ownership check (step_created ownership at L813).
  • The dangerous case requires concurrent handler activity for the same run + non-trivial cross-process clock skew.

But it's a footgun. The cleanest fix would be: after locally pushing an event, set eventsCursor to that event's eventId so the next fetch skips it. This also eliminates the dedupe pass entirely. Worth considering as a follow-up.

"VaguelySerious" review threads — context for Peter

For other reviewers who may stumble on this: most of the open @VaguelySerious threads are Peter's own AI-review notes posted via tooling. Of 28 such threads, 27 are either resolved or outdated (the diff has moved past them). The single unresolved + non-outdated thread is the concurrent-retry concern above (step-executor.ts:198), which Peter has acknowledged as a known limitation in a sibling resolved thread.

External-reviewer threads (mine, the Vercel bot's) are all resolved or addressed. So the apparent "long list of unresolved concerns" from my prior comment is misleading — it was conflating Peter's self-review notes with external review.

Test coverage gaps (non-blocking)

A few coverage gaps for novel V2 behavior worth tracking even if you ship:

  1. Dedupe path at runtime.ts:585–594 — zero test coverage. The behavior is novel to V2 and load-bearing for incremental-replay correctness. A unit test that:

    • First iteration fetches with cursor C0
    • Manually pushes a wait_completed to cachedEvents
    • Second iteration fetches and gets the same wait_completed plus an interleaved new event
    • Asserts: no duplicates AND consumer sees no orphans
      ...would lock in the contract.
  2. L198 guard's positive case — there's no test asserting that attempt > maxRetries + 1withoutstep.error does NOT fail the step. The guard exists specifically for this case and is currently un-asserted. The two step-handler.test.ts tests at lines 296–340 and 343–388 both set error on the fixture, so they exercise different paths.

  3. Concurrent-handler races — neither Scenario B (concurrent retry with prior error → premature L198 failure) nor Scenario D (catch-block premature failure with no && step.error guard) has a test. Adding these would catch the bug currently in code if a future fix lands.

  4. builder-deferred.ts Next.js manifest reading — Peter agreed in review that reading app-paths-manifest.json was the right approach over regex prediction. Verified that DID land (L1171–1175 reads the manifest with comment "This avoids predicting Next.js conventions with regexes and instead reads from Next.js's own output"). ✅

What looks good

  • The architectural shift is well-executed. The replay loop, ownership-gated inline dispatch, idempotency-key-based queue dedupe, builder unification, and __steps_registered rollup-tree-shaking guard are all clean.
  • The getStepNameFromEvent full-event-load concern from my prior review was addressed — stepName now flows through WorkflowInvokePayloadSchema. ✓
  • The TooEarlyError stale retryAfter access pattern is fixed at step-executor.ts:169. ✓
  • The CJS module.exports collision (ESM format for steps when bundleFinalOutput: true) is a subtle but correct fix.
  • The changelog doc docs/content/docs/changelog/eager-processing.mdx is one of the better architecture docs I've seen in a PR — covers every edge case, failure mode, and platform-specific quirk.

Verdict

No blockers from my side. Ship if Vade and storytime green-light. The concurrent-retry premature-failure case is real but acknowledged, the auto-flagged "event cache duplication" is a non-issue, and the test coverage gaps are real but not regressions (the V2 behaviors they cover are net-new).

If you want one piece of polish before merge, the L469 && step.error mirror would close the most user-visible failure mode at zero cost — but I won't gate on it.

@TooTallNateTooTallNate 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.

Pre-emptively :approved: but my agent really wants @VaguelySerious to resolve the open threads :lol: and there seems to be a few other things pointed out in the latest review comment.

VaguelySeriousand others added 7 commits May 3, 2026 11:38
Merges the V1 split flow/step routes (one queue message per step + one per
flow continuation, two separate function invocations per step) into a
single combined handler at /.well-known/workflow/v1/flow that executes
steps inline within the same invocation as the workflow replay.
A serial workflow with N steps that previously required ~2N+1 function
invocations now completes in 1.
Architecture
------------
- packages/core: workflowEntrypoint() handler with an inline replay loop.
Each loop iteration: load events incrementally, replay workflow,
inline-execute one owned step on suspension, loop. Background steps
(from Promise.all) are queued back to the same handler with stepId so
the in-flight handler advances the loop without queue round-trips
whenever possible.
- packages/builders: createCombinedBundle() emits a single ESM bundle
with workflowEntrypoint(workflowCode) plus a side-effect
__step_registrations bundle. ESM-by-default with createRequire banner
for CJS deps (matches main's #1562 pattern).
- packages/next, nestjs, sveltekit, astro, nitro, nuxt, hono, express,
fastify, vite: framework builders updated to use createCombinedBundle.
The step/ directory is no longer generated.
Correctness invariants (V2 polish)
----------------------------------
- Single inline executor per step. Atomic step_created per correlationId
(per-step in-process mutex in world-local; SQL-level guards in
postgres/vercel) means exactly one handler claims ownership of each
step. Inline execution gates on ownership; queue dispatch is
unconditional with idempotency keys for crash recovery (matches V1).
- onUnconsumedEvent reverts to its original PR #1055 contract: any
unconsumed event fatals as CORRUPTED_EVENT_LOG. Source-level fixes
removed every code path that produced orphaned step lifecycle events
during V2 work, so the skip branch became unnecessary.
- Lock-release polling interval lowered from 100ms to 10ms in
flushable-stream so the V2 step-executor's ops-settle race typically
resolves in ~5ms instead of ~50ms per writable-bearing step.
- Per-iteration runs.get round-trip eliminated: concurrent completion
is detected by scanning the loaded event log for terminal run events
instead.
- Worker-pool deadlock guard removed from Run#pollReturnValue. The
fibonacci/recursive parent→child polling case is now handled by
raising world-postgres default queueConcurrency from 10 to 50, with
the limitation documented at three call sites (fibonacciWorkflow's
JSDoc, queue.ts, eager-processing.mdx).
- getWorldLazy module-scope singleton breaks the step-bundle import
chain to world.ts so step bundles don't transitively pull in
world-vercel + cbor-x + the VQS dev-handler resolver.
World-local atomicity
---------------------
- Per-step in-process mutex serializes events.create for step
lifecycle events on a (runId, correlationId) key. step_started is
rejected only when the step is already terminal — duplicate
step_started on a non-terminal step is allowed (preserves retry
semantics after SIGKILL).
Runtime helpers
---------------
- loadWorkflowRunEvents(runId, afterCursor?) consolidates the
previous getAllWorkflowRunEvents{,WithCursor} + getNewWorkflowRunEvents
into one helper.
Documentation
-------------
- docs/content/docs/changelog/eager-processing.mdx: full record of
architecture, edge cases, and the fixes applied during the V2 work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Two CI failures from the earlier V2 cutover, now resolved:
1. @workflow/vitest unit tests (`packages/vitest/src/index.test.ts`)
were still mocking the V1 BaseBuilder methods (createWorkflowsBundle
+ createStepsBundle) and expecting two world handlers (workflow +
step). Updated the mocks to expose createCombinedBundle and
asserted a single combined handler at __wkf_workflow_, which is
what the V2 vitest builder actually emits.
2. E2E Local Dev Tests (nextjs-webpack) were asserting that step
error stacks contain `99_e2e.ts` / `helpers.ts`, which held in
pre-V2 webpack dev mode (which imported step sources directly).
V2 inlines the step bundle into the combined flow route, and
webpack's re-bundling collapses original step filenames out of the
dev-mode source maps. Extended the existing nextjs-webpack
carve-out in hasStepSourceMaps() to cover dev as well as prod, and
updated the source-map follow-up callout in eager-processing.mdx
to include nextjs-webpack in the matrix that needs source maps
wired up for V2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/e2e/e2e.test.ts` (PR #1879 vs. V2 hookDispose helper)
Drop the locally-defined `waitForHook(expectedRunId)` shadow in the
hookDisposeTestWorkflow test in favor of the new top-level
`waitForHook(token, { runId })` API merged from main. Keep V2's
event-driven `waitForHookDisposal()` helper, which is stricter than
main's `await sleep(3_000)` (it polls `getHookByToken` for the
HookNotFoundError signal rather than waiting a fixed interval).
* `packages/world-local/src/storage/events-storage.ts` (PR #1877 vs.
V2 per-step async mutex)
Both branches were closing race conditions in the local world's
step lifecycle. PR #1877 adds filesystem-level O_CREAT|O_EXCL locks
for `step_created` and `wait_created`; the V2 branch already wraps
step lifecycle events in an in-process `withStepLock` mutex. They
compose: the mutex serializes within a single Node process; the
filesystem lock additionally protects against cross-process races
(multiple pnpm workers, redelivered queue messages). Resolution
takes V2's mutex wrap as the base and patches in main's two
`writeExclusive` claims at the start of the `step_created` and
`wait_created` handlers, with comments noting the dual-layer
guarantee.
`packages/core/e2e/utils.ts` auto-merged: V2's nextjs-webpack +
all-Vercel source-map carve-outs preserved alongside main's
`tanstack-start` addition to `hasWorkflowSourceMaps`.
Verified locally: `@workflow/core` and `@workflow/world-local`
typecheck clean; all 343 world-local unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone tarballs project's `vercel.json` set
`"buildCommand": "pnpm --filter tarballs build"`, which only runs the
tarballs package's `build` script (`node scripts/pack.ts`) and does
NOT trigger turbo's `dependsOn: ["^build"]` chain. As a result, every
workspace package was packed with an empty `dist/` directory — only
`bin/`, `package.json`, `README.md`, and `docs/` made it into the
tarballs. Downstream installs failed with
`Cannot find module '<pkg>/dist/next.cjs'` (and similar for every
other entry point).
The previous docs-based pipeline didn't hit this because Vercel
detected `docs` as a Next.js framework, ran turbo for it, and pack.ts
was wired as a `prebuild` step that ran after deps were already
built.
Fix: switch the buildCommand to `pnpm turbo build --filter=tarballs`
so turbo evaluates the build task for `tarballs`, which fans out via
`^build` to compile every workspace dependency first, and then runs
`tarballs#build` (pack.ts) against the populated `dist/` directories.
Verified locally: a fresh build produces tarballs containing the full
`dist/` tree (including `dist/next.cjs`, the file flight-booking-app
was failing to resolve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/src/logger.ts` (PR #1849 vs. V2 webpack carve-out)
PR #1849 ("Friendlier workflow errors") reintroduced the `debug`
package as a static import and added a structured `Logger`
interface with `child()` / `forRun()` and `composeLogLine`. The V2
branch had previously removed the `debug` static import because it
pulls `debug/src/node` and a dynamic `require('tty')` into the
Next.js webpack flow route, breaking V2 builds with
`Dynamic require of "tty" is not supported`.
Resolution combines both: keep main's `Logger` interface,
`child()` / `forRun()`, and `composeLogLine` integration; replace
the `debug` invocations with the V2 lightweight
`matchesDebugNamespace` + `process.env.DEBUG` matcher so the flow
route still bundles cleanly under webpack. Comment in the file
documents why `debug` is intentionally absent.
* `packages/core/src/runtime.ts` (PR #1849 vs. V2 inline replay loop)
Multiple regions where main's structured-logging refactor and the
V2 inline-execution loop made overlapping changes. Took V2's
control flow as the base — its `if (!workflowRun) { ... }` setup
block, main replay `while (true)` loop with timeout-based
re-scheduling, terminal-event short-circuit, and per-iteration
incremental event load are all preserved as-is — and folded in
main's contributions: the scoped `runLogger = runtimeLogger.forRun(
runId, workflowName)` is created once and used for run-level info
/ error logging. Discarded main's V1 wait-completion loop and
`runWorkflow` user-code error block since V2 handles waits and
user-code errors inside the inline loop body. Updated
`buildWorkflowSuspensionMessage()` call to drop a stray `runId`
first arg that didn't match the helper's 3-param signature.
* `packages/core/e2e/e2e.test.ts` (PR #1849 unrelated to V2)
No conflict from main's friendly-errors PR, auto-merged. (No
hookDispose-style conflict this round.)
Verified locally: all 820 `@workflow/core` unit tests pass; all 343
`@workflow/world-local` unit tests pass; `pnpm turbo typecheck` is
green across all 40 typecheck tasks.
Co-Authored-By: Claude Opus 4.7 (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

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

[core] Combine flow+step bundle and process steps eagerly - #1338

Merged
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow
May 4, 2026
Merged

[core] Combine flow+step bundle and process steps eagerly#1338
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow

Conversation

@changeset-bot

changeset-botBot commented Mar 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 234fce4

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

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

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 Mar 11, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Mar 11, 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.033s (-24.6% 🟢)1.005s (~)0.972s101.00x
💻 LocalExpress0.034s (-23.3% 🟢)1.006s (~)0.972s101.05x
🐘 PostgresNext.js (Turbopack)0.049s1.013s0.964s101.50x
💻 LocalNext.js (Turbopack)0.049s1.005s0.956s101.51x
🐘 PostgresNitro0.051s (-46.8% 🟢)1.012s (-3.0%)0.961s101.56x
🌐 RedisNext.js (Turbopack)0.055s1.005s0.951s101.68x
🌐 MongoDBNext.js (Turbopack)0.109s1.007s0.899s103.35x
🐘 PostgresExpress0.177s (+205.2% 🔺)1.107s (+9.5% 🔺)0.930s105.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.236s (-42.4% 🟢)1.939s (-22.7% 🟢)1.703s101.00x
▲ VercelExpress0.241s (+2.2%)2.009s (-5.9% 🟢)1.768s101.02x
▲ VercelNext.js (Turbopack)0.550s (+118.6% 🔺)2.715s (+16.4% 🔺)2.165s102.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.069s (-5.0% 🟢)2.006s (~)0.937s101.00x
💻 LocalNitro1.073s (-5.1% 🟢)2.006s (~)0.933s101.00x
🐘 PostgresNitro1.084s (-4.9%)2.010s (~)0.927s101.01x
🌐 RedisNext.js (Turbopack)1.113s2.006s0.893s101.04x
🐘 PostgresNext.js (Turbopack)1.114s2.007s0.893s101.04x
💻 LocalNext.js (Turbopack)1.121s2.007s0.886s101.05x
🐘 PostgresExpress1.143s (~)2.021s (+0.5%)0.878s101.07x
🌐 MongoDBNext.js (Turbopack)1.162s2.009s0.847s101.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.498s (-20.1% 🟢)3.691s (-3.1%)2.193s101.00x
▲ VercelNitro1.524s (-60.9% 🟢)3.710s (-37.2% 🟢)2.186s101.02x
▲ VercelNext.js (Turbopack)1.942s (-4.6%)4.457s (+16.4% 🔺)2.516s101.30x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.409s (-4.2%)11.018s (~)0.609s31.00x
💻 LocalNitro10.413s (-4.9%)11.022s (~)0.608s31.00x
💻 LocalExpress10.436s (-4.5%)11.021s (~)0.586s31.00x
🐘 PostgresNext.js (Turbopack)10.568s11.013s0.445s31.02x
🌐 RedisNext.js (Turbopack)10.657s11.024s0.367s31.02x
💻 LocalNext.js (Turbopack)10.685s11.022s0.337s31.03x
🐘 PostgresExpress10.745s (-2.0%)11.419s (+3.6%)0.674s31.03x
🌐 MongoDBNext.js (Turbopack)10.789s11.018s0.229s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.059s (-40.8% 🟢)15.700s (-37.5% 🟢)1.641s21.00x
▲ VercelNext.js (Turbopack)14.845s (-14.3% 🟢)16.872s (-13.0% 🟢)2.027s21.06x
▲ VercelExpress15.996s (-5.8% 🟢)18.196s (-9.1% 🟢)2.200s21.14x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.431s (-10.8% 🟢)14.027s (-12.5% 🟢)0.596s51.00x
🐘 PostgresNitro13.434s (-8.0% 🟢)14.013s (-6.8% 🟢)0.579s51.00x
💻 LocalExpress13.490s (-9.9% 🟢)14.027s (-6.7% 🟢)0.537s51.00x
🐘 PostgresNext.js (Turbopack)13.820s14.018s0.198s51.03x
💻 LocalNext.js (Turbopack)14.076s15.030s0.954s41.05x
🌐 RedisNext.js (Turbopack)14.088s15.030s0.942s41.05x
🌐 MongoDBNext.js (Turbopack)14.283s15.019s0.736s41.06x
🐘 PostgresExpress14.537s (~)15.276s (+1.7%)0.739s41.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.821s (-58.6% 🟢)22.773s (-56.7% 🟢)1.952s31.00x
▲ VercelNitro21.829s (-66.1% 🟢)23.502s (-64.7% 🟢)1.673s31.05x
▲ VercelNext.js (Turbopack)23.199s (-55.9% 🟢)25.174s (-53.9% 🟢)1.975s31.11x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.846s (-29.4% 🟢)12.022s (-29.4% 🟢)0.176s81.00x
🐘 PostgresNitro11.923s (-14.6% 🟢)12.392s (-13.4% 🟢)0.469s81.01x
💻 LocalExpress11.997s (-27.7% 🟢)12.398s (-27.2% 🟢)0.401s81.01x
🌐 RedisNext.js (Turbopack)13.015s13.454s0.439s71.10x
💻 LocalNext.js (Turbopack)13.089s14.026s0.937s71.10x
🐘 PostgresNext.js (Turbopack)13.215s13.585s0.370s71.12x
🌐 MongoDBNext.js (Turbopack)13.301s14.021s0.720s71.12x
🐘 PostgresExpress15.027s (+7.3% 🔺)15.400s (+5.5% 🔺)0.373s61.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.095s (-74.3% 🟢)33.159s (-73.2% 🟢)2.064s31.00x
▲ VercelNitro31.508s (-92.5% 🟢)33.124s (-92.2% 🟢)1.615s31.01x
▲ VercelNext.js (Turbopack)31.987s (-91.9% 🟢)33.996s (-91.4% 🟢)2.009s31.03x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.140s (-10.6% 🟢)2.007s (~)0.868s151.00x
🐘 PostgresNext.js (Turbopack)1.168s2.007s0.838s151.03x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.187s (-20.3% 🟢)2.006s (~)0.820s151.04x
🐘 PostgresExpress1.237s (-1.9%)2.018s (~)0.781s151.09x
🌐 RedisNext.js (Turbopack)1.238s2.006s0.768s151.09x
💻 LocalNext.js (Turbopack)1.279s2.005s0.726s151.12x
🌐 MongoDBNext.js (Turbopack)2.048s2.917s0.869s111.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.551s (-10.8% 🟢)4.187s (-9.4% 🟢)1.636s81.00x
▲ VercelNitro2.674s (-5.1% 🟢)4.258s (-1.5%)1.584s81.05x
▲ VercelNext.js (Turbopack)4.193s (+23.4% 🔺)6.817s (+38.2% 🔺)2.624s51.64x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.225s (-47.9% 🟢)2.007s (-33.3% 🟢)0.782s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.05x
🐘 PostgresExpress1.483s (-37.2% 🟢)2.019s (-32.9% 🟢)0.536s151.21x
💻 LocalNitro1.726s (-45.1% 🟢)2.072s (-46.7% 🟢)0.346s151.41x
💻 LocalNext.js (Turbopack)1.831s2.149s0.318s141.50x
💻 LocalExpress1.905s (-35.5% 🟢)2.150s (-37.8% 🟢)0.244s141.56x
🌐 RedisNext.js (Turbopack)2.364s3.008s0.643s101.93x
🌐 MongoDBNext.js (Turbopack)3.568s4.008s0.440s82.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.844s (-5.1% 🟢)5.284s (-10.8% 🟢)1.440s61.00x
▲ VercelExpress3.884s (+7.3% 🔺)6.116s (+19.7% 🔺)2.232s51.01x
▲ VercelNext.js (Turbopack)5.098s (-28.2% 🟢)7.574s (-15.0% 🟢)2.476s41.33x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.419s (-59.2% 🟢)2.007s (-49.9% 🟢)0.588s151.00x
🐘 PostgresNext.js (Turbopack)1.493s2.007s0.514s151.05x
🐘 PostgresExpress2.153s (-38.2% 🟢)2.861s (-28.7% 🟢)0.708s111.52x
🌐 RedisNext.js (Turbopack)3.634s4.010s0.376s82.56x
💻 LocalNitro4.547s (-45.5% 🟢)5.013s (-44.4% 🟢)0.466s63.20x
💻 LocalExpress4.829s (-42.1% 🟢)5.512s (-38.9% 🟢)0.683s63.40x
💻 LocalNext.js (Turbopack)5.581s6.213s0.631s53.93x
🌐 MongoDBNext.js (Turbopack)6.289s7.012s0.723s54.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.147s (+21.4% 🔺)7.340s (+19.8% 🔺)2.193s51.00x
▲ VercelNitro5.410s (+53.5% 🔺)7.160s (+29.4% 🔺)1.750s51.05x
▲ VercelNext.js (Turbopack)7.157s (-19.7% 🟢)8.877s (-19.0% 🟢)1.721s41.39x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.170s (-6.9% 🟢)2.007s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.173s2.007s0.835s151.00x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.774s151.05x
💻 LocalNext.js (Turbopack)1.327s2.006s0.679s151.13x
💻 LocalNitro1.380s (-26.0% 🟢)2.006s (-14.3% 🟢)0.626s151.18x
💻 LocalExpress1.393s (-26.5% 🟢)2.006s (-15.1% 🟢)0.613s151.19x
🐘 PostgresExpress1.408s (+12.0% 🔺)2.062s (+2.7%)0.654s151.20x
🌐 MongoDBNext.js (Turbopack)2.036s2.916s0.880s111.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.271s (-7.6% 🟢)4.045s (-3.0%)1.774s81.00x
▲ VercelExpress2.336s (-9.5% 🟢)3.766s (-13.4% 🟢)1.430s81.03x
▲ VercelNext.js (Turbopack)4.170s (+42.2% 🔺)6.038s (+30.1% 🔺)1.869s51.84x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.251s (-46.5% 🟢)2.007s (-33.3% 🟢)0.757s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.03x
🐘 PostgresExpress1.393s (-40.5% 🟢)2.035s (-32.4% 🟢)0.642s151.11x
💻 LocalNitro1.957s (-36.2% 🟢)2.316s (-40.4% 🟢)0.359s131.56x
💻 LocalExpress2.022s (-35.4% 🟢)2.508s (-33.3% 🟢)0.485s121.62x
💻 LocalNext.js (Turbopack)2.130s2.826s0.696s111.70x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.88x
🌐 MongoDBNext.js (Turbopack)3.543s4.007s0.464s82.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.338s (+35.9% 🔺)6.011s (+25.4% 🔺)1.673s51.00x
▲ VercelNitro4.664s (+44.2% 🔺)6.658s (+31.1% 🔺)1.994s51.08x
▲ VercelNext.js (Turbopack)4.714s (+50.0% 🔺)6.542s (+44.7% 🔺)1.827s51.09x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.380s (-60.3% 🟢)2.007s (-49.9% 🟢)0.627s151.00x
🐘 PostgresNext.js (Turbopack)1.483s2.008s0.525s151.07x
🐘 PostgresExpress1.982s (-43.4% 🟢)2.737s (-31.8% 🟢)0.755s111.44x
🌐 RedisNext.js (Turbopack)3.571s4.010s0.439s82.59x
💻 LocalNitro4.894s (-46.5% 🟢)5.348s (-46.7% 🟢)0.454s63.55x
💻 LocalExpress5.939s (-32.5% 🟢)6.417s (-30.8% 🟢)0.478s54.30x
💻 LocalNext.js (Turbopack)6.115s6.416s0.301s54.43x
🌐 MongoDBNext.js (Turbopack)6.302s7.013s0.710s54.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.380s (+5.6% 🔺)7.062s (+3.6%)1.682s51.00x
▲ VercelNext.js (Turbopack)5.690s (-15.8% 🟢)7.416s (-13.2% 🟢)1.726s51.06x
▲ VercelExpress5.700s (-11.2% 🟢)7.539s (-7.8% 🟢)1.840s51.06x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.459s (-44.1% 🟢)1.022s (+1.6%)0.564s591.00x
💻 LocalNitro0.472s (-51.8% 🟢)1.021s (-6.7% 🟢)0.548s591.03x
💻 LocalExpress0.482s (-51.1% 🟢)1.004s (-6.7% 🟢)0.522s601.05x
🐘 PostgresNext.js (Turbopack)0.599s1.040s0.442s581.31x
🌐 RedisNext.js (Turbopack)0.619s1.004s0.386s601.35x
🐘 PostgresExpress0.644s (-23.2% 🟢)1.140s (+11.4% 🔺)0.496s531.40x
💻 LocalNext.js (Turbopack)0.719s1.004s0.285s601.57x
🌐 MongoDBNext.js (Turbopack)0.733s1.006s0.272s601.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.661s (-75.5% 🟢)6.339s (-70.3% 🟢)1.677s101.00x
▲ VercelNitro5.218s (-76.3% 🟢)7.258s (-69.8% 🟢)2.039s101.12x
▲ VercelNext.js (Turbopack)6.455s (-55.5% 🟢)7.922s (-50.7% 🟢)1.466s81.38x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.085s (-43.7% 🟢)1.705s (-18.8% 🟢)0.620s531.00x
🐘 PostgresExpress1.162s (-41.2% 🟢)1.588s (-29.7% 🟢)0.426s571.07x
💻 LocalExpress1.197s (-60.3% 🟢)2.006s (-44.1% 🟢)0.809s451.10x
💻 LocalNitro1.220s (-59.8% 🟢)2.005s (-46.6% 🟢)0.786s451.12x
🌐 RedisNext.js (Turbopack)1.476s2.006s0.530s451.36x
🐘 PostgresNext.js (Turbopack)1.707s2.387s0.679s381.57x
💻 LocalNext.js (Turbopack)1.753s2.005s0.253s451.61x
🌐 MongoDBNext.js (Turbopack)1.813s2.007s0.194s451.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.733s (-70.3% 🟢)13.729s (-66.8% 🟢)1.996s71.00x
▲ VercelExpress12.793s (-62.9% 🟢)14.645s (-60.2% 🟢)1.852s71.09x
▲ VercelNext.js (Turbopack)14.268s (-71.4% 🟢)16.580s (-67.9% 🟢)2.313s61.22x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.108s (-48.6% 🟢)2.696s (-41.4% 🟢)0.588s451.00x
🐘 PostgresExpress2.634s (-34.0% 🟢)3.074s (-29.6% 🟢)0.440s401.25x
🐘 PostgresNext.js (Turbopack)2.659s3.084s0.425s391.26x
💻 LocalNitro2.660s (-71.4% 🟢)3.007s (-70.0% 🟢)0.348s401.26x
💻 LocalExpress2.743s (-70.2% 🟢)3.033s (-69.7% 🟢)0.290s401.30x
🌐 RedisNext.js (Turbopack)2.992s3.191s0.200s381.42x
💻 LocalNext.js (Turbopack)3.748s4.007s0.259s301.78x
🌐 MongoDBNext.js (Turbopack)4.178s5.012s0.833s241.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express35.192s (-72.9% 🟢)37.772s (-71.4% 🟢)2.580s41.00x
▲ VercelNitro36.812s (-62.0% 🟢)39.053s (-60.3% 🟢)2.241s41.05x
▲ VercelNext.js (Turbopack)43.007s (-59.9% 🟢)46.755s (-57.1% 🟢)3.748s31.22x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-44.2% 🟢)1.007s (~)0.850s601.00x
🐘 PostgresNitro0.182s (-35.8% 🟢)1.006s (~)0.824s601.15x
🐘 PostgresNext.js (Turbopack)0.190s1.005s0.815s601.21x
🌐 RedisNext.js (Turbopack)0.316s1.021s0.705s592.00x
💻 LocalNitro0.412s (-31.9% 🟢)1.004s (-1.7%)0.592s602.61x
💻 LocalExpress0.452s (-19.3% 🟢)1.004s (~)0.552s602.87x
💻 LocalNext.js (Turbopack)0.603s1.039s0.436s583.83x
🌐 MongoDBNext.js (Turbopack)1.029s1.749s0.720s356.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.178s (+11.5% 🔺)4.588s (+26.2% 🔺)2.410s141.00x
▲ VercelNitro2.258s (+36.0% 🔺)4.485s (+33.9% 🔺)2.227s141.04x
▲ VercelNext.js (Turbopack)3.716s (+83.7% 🔺)5.458s (+43.9% 🔺)1.742s111.71x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.304s (-38.8% 🟢)1.006s (~)0.702s901.00x
🌐 RedisNext.js (Turbopack)0.423s1.004s0.581s901.39x
🐘 PostgresExpress0.520s (+2.0%)1.112s (+10.5% 🔺)0.592s821.71x
🐘 PostgresNext.js (Turbopack)0.705s1.318s0.613s692.32x
💻 LocalNitro2.170s (-14.5% 🟢)2.821s (-6.3% 🟢)0.651s327.14x
💻 LocalExpress2.186s (-13.0% 🟢)2.944s (-2.2%)0.758s317.19x
💻 LocalNext.js (Turbopack)2.424s3.043s0.619s307.97x
🌐 MongoDBNext.js (Turbopack)2.595s3.007s0.412s308.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.618s (+87.2% 🔺)8.356s (+60.9% 🔺)1.738s111.00x
▲ VercelNitro7.134s (+121.1% 🔺)9.355s (+94.0% 🔺)2.221s101.08x
▲ VercelExpress7.527s (+147.1% 🔺)9.552s (+98.7% 🔺)2.025s101.14x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.637s (-19.4% 🟢)1.006s (~)0.369s1201.00x
🐘 PostgresNext.js (Turbopack)0.730s1.013s0.284s1191.15x
🐘 PostgresExpress0.790s (-3.5%)1.192s (+17.1% 🔺)0.402s1011.24x
🌐 RedisNext.js (Turbopack)0.797s1.004s0.207s1201.25x
🌐 MongoDBNext.js (Turbopack)5.387s6.013s0.626s208.46x
💻 LocalNitro9.790s (-12.5% 🟢)10.276s (-11.9% 🟢)0.486s1215.37x
💻 LocalExpress10.085s (-9.9% 🟢)10.782s (-9.7% 🟢)0.697s1215.84x
💻 LocalNext.js (Turbopack)11.478s12.128s0.650s1018.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.821s (+117.8% 🔺)19.141s (+103.6% 🔺)2.319s71.00x
▲ VercelNext.js (Turbopack)18.466s (+78.8% 🔺)20.419s (+66.2% 🔺)1.953s71.10x
▲ VercelExpress20.247s (+172.9% 🔺)22.460s (+143.0% 🔺)2.213s61.20x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.133s (+430.1% 🔺)2.005s (+99.6% 🔺)0.010s (-19.2% 🟢)2.017s (+98.0% 🔺)0.884s101.00x
🐘 PostgresNitro1.134s (+453.4% 🔺)2.001s (+100.2% 🔺)0.001s (-26.7% 🟢)2.010s (+98.7% 🔺)0.875s101.00x
💻 LocalExpress1.136s (+470.5% 🔺)2.005s (+99.6% 🔺)0.012s (~)2.020s (+98.4% 🔺)0.884s101.00x
💻 LocalNext.js (Turbopack)1.172s2.003s0.012s2.019s0.847s101.03x
🐘 PostgresExpress1.232s (+500.5% 🔺)2.005s (+100.8% 🔺)0.001s (-37.5% 🟢)2.041s (+101.8% 🔺)0.810s101.09x
🐘 PostgresNext.js (Turbopack)2.024s2.601s0.220s2.874s0.850s101.79x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.200s (-42.6% 🟢)3.478s (-34.1% 🟢)1.562s (+110.5% 🔺)5.432s (-16.2% 🟢)3.232s101.00x
▲ VercelExpress2.336s (-6.8% 🟢)3.935s (-3.8%)1.087s (+13.1% 🔺)5.425s (-3.0%)3.089s101.06x
▲ VercelNext.js (Turbopack)4.802s (-29.9% 🟢)4.900s (-43.4% 🟢)0.740s (+17.0% 🔺)7.346s (-25.0% 🟢)2.544s102.18x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.512s (+142.2% 🔺)2.005s (+99.2% 🔺)0.004s (-8.9% 🟢)2.024s (+97.9% 🔺)0.512s301.00x
💻 LocalNitro1.514s (+80.5% 🔺)2.011s (+98.7% 🔺)0.010s (+2.8%)2.022s (+81.2% 🔺)0.509s301.00x
💻 LocalExpress1.721s (+127.3% 🔺)2.011s (+95.5% 🔺)0.010s (+2.1%)2.202s (+111.7% 🔺)0.481s281.14x
🐘 PostgresNext.js (Turbopack)1.722s2.107s0.003s2.123s0.401s301.14x
💻 LocalNext.js (Turbopack)1.845s2.011s0.009s2.203s0.358s281.22x
🐘 PostgresExpress1.983s (+214.8% 🔺)2.330s (+131.5% 🔺)0.003s (-20.7% 🟢)2.395s (+134.1% 🔺)0.412s261.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.656s (-13.0% 🟢)7.281s (-9.1% 🟢)0.179s (-56.1% 🟢)8.330s (-5.7% 🟢)2.675s81.00x
▲ VercelNitro5.796s (-80.3% 🟢)7.455s (-75.8% 🟢)0.170s (+52.1% 🔺)8.071s (-74.6% 🟢)2.275s81.02x
▲ VercelNext.js (Turbopack)12.255s (-27.6% 🟢)13.600s (-25.4% 🟢)0.201s (-4.9%)15.301s (-19.2% 🟢)3.046s42.17x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.651s (-32.8% 🟢)1.033s (-17.2% 🟢)0.000s (-100.0% 🟢)1.042s (-17.2% 🟢)0.390s581.00x
🐘 PostgresExpress1.031s (+7.3% 🔺)1.390s (+8.7% 🔺)0.000s (+9.5% 🔺)1.437s (+10.0% 🔺)0.406s421.58x
🐘 PostgresNext.js (Turbopack)1.333s1.618s0.000s1.715s0.382s372.05x
💻 LocalNitro1.337s (+9.4% 🔺)2.015s (~)0.000s (+166.7% 🔺)2.017s (~)0.680s302.05x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-60.0% 🟢)2.017s (~)0.649s302.10x
💻 LocalNext.js (Turbopack)1.436s2.013s0.000s2.016s0.581s302.20x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.922s (+28.6% 🔺)5.598s (+27.4% 🔺)0.000s (-100.0% 🟢)6.027s (+25.3% 🔺)2.105s101.00x
▲ VercelExpress4.013s (+7.3% 🔺)5.879s (+15.2% 🔺)0.000s (-100.0% 🟢)6.416s (+16.0% 🔺)2.404s101.02x
▲ VercelNext.js (Turbopack)5.465s (-46.3% 🟢)6.707s (-41.8% 🟢)0.000s (+Infinity% 🔺)7.645s (-36.6% 🟢)2.181s81.39x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.327s (-25.9% 🟢)2.066s (-3.5%)0.000s (-100.0% 🟢)2.081s (-4.3%)0.754s291.00x
🐘 PostgresNext.js (Turbopack)1.844s2.402s0.000s2.460s0.616s251.39x
🐘 PostgresExpress2.377s (+34.2% 🔺)3.042s (+39.7% 🔺)0.000s (NaN%)3.091s (+40.6% 🔺)0.714s201.79x
💻 LocalNext.js (Turbopack)2.776s3.362s0.000s3.365s0.589s182.09x
💻 LocalNitro3.068s (-9.4% 🟢)3.838s (-4.8%)0.000s (-53.1% 🟢)3.843s (-4.8%)0.775s162.31x
💻 LocalExpress3.103s (-10.5% 🟢)3.775s (-6.4% 🟢)0.000s (-76.6% 🟢)3.781s (-6.3% 🟢)0.679s162.34x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.059s (+10.3% 🔺)6.788s (+12.7% 🔺)0.000s (+Infinity% 🔺)7.571s (+17.3% 🔺)2.512s81.00x
▲ VercelNitro5.089s (+24.3% 🔺)6.536s (+21.6% 🔺)0.000s (-18.5% 🟢)7.002s (+20.9% 🔺)1.913s91.01x
▲ VercelNext.js (Turbopack)8.675s (+54.5% 🔺)10.065s (+44.2% 🔺)0.000s (-100.0% 🟢)11.283s (+49.6% 🔺)2.608s61.71x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro17/21
🐘 PostgresNitro19/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other4380150588
Total4266010265292

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025

📋 View full workflow run

Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/vitest/src/index.ts
Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/core/src/workflow.ts Outdated
Comment threadpackages/builders/src/base-builder.ts
Comment threadpackages/core/src/runtime/get-world-lazy.ts

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Remaining issues:

  • getWorldLazy needs to be validated as a decision
  • packages/core/e2e/utils.ts skip for hasStepSourceMaps needs to be re-evaluated

@shtefcs

Copy link
Copy Markdown

Seems you had a lot of work here. It's not easy as it seems. Hopefully, this gonna solve some issues we are facing with Workflow.

Peace,

@TooTallNateTooTallNate 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.

Withdrawing my prior CHANGES_REQUESTED — the blocker I called out (the stale (err as any).meta?.retryAfter access in step-executor.ts) is fixed at line 169 with err.retryAfter ?? 1. As an aside, my fix recommendation in the prior review was based on a faulty type assumption (I confused TooEarlyError.retryAfter: number with RetryableError.retryAfter: Date). The author's err.retryAfter ?? 1 form is correct: it's a number of seconds with sensible fallback.

My other earlier concerns:

  • getStepNameFromEvent full-event-load: addressed. stepName is now plumbed through WorkflowInvokePayloadSchema so the handler reads it directly from the queue message payload instead of doing a paginated event load. ✓
  • step.error guard for max retries: known limitation acknowledged in-thread. The proposed long-term fix (server-side ne(status, 'running') on step_started) is separate work.
  • Background step → continuation enqueue safety: acknowledged as intentional.

So my specific items are resolved. But I'm not approving outright — there's still substantial unresolved review from @VaguelySerious touching real concerns I'd defer to them on:

  • Removal of the LegacyStreamWorld shim
  • Limiting queue concurrency to 1 in tests
  • Comments removed without explanation in suspension-handler.ts / runtime.ts
  • The builder-deferred.ts Next.js behavior prediction
  • Several "is there a systematic fix instead of conditionally allowing edge cases" questions

Plus the auto-flagged event cache duplication bug from Vercel's review bot is worth confirming was understood and dismissed (or fixed).

Approval-wise this should land when @VaguelySerious's concerns are resolved. From my side: no remaining blockers.

@TooTallNateTooTallNate 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.

Thorough re-review for Peter. Spent significant time tracing four risk surfaces: the Vercel bot's auto-flag, the concurrent-step-execution model, inline-step replay determinism, and the open review threads. Bottom line up front: no ship-blockers from my side. A few coverage gaps and one acknowledged limitation worth being explicit about, but nothing I'd hold this PR on.

Vercel bot's "event cache duplication" auto-flag is a false positive

"wait_completed events pushed locally without advancing eventsCursor cause duplicate events on next incremental fetch."

The author anticipated exactly this scenario. runtime.ts:585–594 has explicit dedupe-by-eventId logic:

constexistingIds=newSet(cachedEvents.map((e)=>e.eventId));for(consteofloaded.events){if(!existingIds.has(e.eventId)){cachedEvents.push(e);}}

Verified end-to-end: locally-pushed wait_completed (line 685) keeps its real server-returned eventId, the next iteration's incremental fetch returns it again, the dedupe filter drops it, the consumer never sees a duplicate. The comment in the code calls out the exact scenario the bot flagged. Safe to dismiss the auto-flag.

Concurrent-step-execution: a partial fix shipping with a known gap

Verified the L198 guard (step.attempt > maxRetries + 1 && step.error) traces correctly for the scenario it targets — concurrent first-attempts before any user-code error has been recorded. Without step.error, the guard correctly skips and the step proceeds normally even with inflated step.attempt.

But there's a symmetric gap in the catch-block path at L469:

if(currentAttempt>=maxRetries+1){/* step_failed */}

This check has no && step.error guard, so it fires on the first real catch-block failure even when currentAttempt has been inflated by concurrent first-attempts. End result: a workflow with maxRetries=3 and 4+ concurrent handlers can permanently fail after a single transient flake, with a misleading log saying "exceeded max retries (4 retries)" when the step body actually only ran once.

This is the only substantive open thread on the PR (step-executor.ts:198), and the author has acknowledged it in a separate resolved thread:

"Agreed on the concurrent-retry limitation. The server-side ne(status, 'running') WHERE clause is the right long-term fix — tracking as a follow-up."

So everyone's eyes-open. Worth being explicit in the changelog or PR description that under high queue back-pressure, default-maxRetries workflows can spuriously fail. Either of these would close the gap fully:

  1. Add the same && step.error guard at L469 (mirrors L198, easy to do, but doesn't fix the L198 case for catch-block).
  2. Server-side ne(status, 'running') on step_started (referenced in the resolved thread; also fixes the world-testing concurrency issue Peter resolved separately).

Not a ship-stopper, but I'd want it tracked as a known issue users can hit, not just an internal todo.

Inline-step replay: ordering subtlety in the dedupe path

The locally-pushed-then-incremental-fetch path can produce non-monotonic-by-eventId ordering in cachedEvents. Specifically: after wait_completed is pushed locally (L685) without advancing the cursor, the next incremental fetch can return concurrent events with eventIds between the local push and earlier events. After dedupe, cachedEvents becomes append-ordered but not eventId-ordered. The EventsConsumer walks by index, so it consumes them in append order, which is not the canonical replay order.

In practice this is unlikely to cause issues because:

  • Within one handler's writes, the same world process generates monotonic ULIDs.
  • Cross-handler events for the same run are gated by the ownership check (step_created ownership at L813).
  • The dangerous case requires concurrent handler activity for the same run + non-trivial cross-process clock skew.

But it's a footgun. The cleanest fix would be: after locally pushing an event, set eventsCursor to that event's eventId so the next fetch skips it. This also eliminates the dedupe pass entirely. Worth considering as a follow-up.

"VaguelySerious" review threads — context for Peter

For other reviewers who may stumble on this: most of the open @VaguelySerious threads are Peter's own AI-review notes posted via tooling. Of 28 such threads, 27 are either resolved or outdated (the diff has moved past them). The single unresolved + non-outdated thread is the concurrent-retry concern above (step-executor.ts:198), which Peter has acknowledged as a known limitation in a sibling resolved thread.

External-reviewer threads (mine, the Vercel bot's) are all resolved or addressed. So the apparent "long list of unresolved concerns" from my prior comment is misleading — it was conflating Peter's self-review notes with external review.

Test coverage gaps (non-blocking)

A few coverage gaps for novel V2 behavior worth tracking even if you ship:

  1. Dedupe path at runtime.ts:585–594 — zero test coverage. The behavior is novel to V2 and load-bearing for incremental-replay correctness. A unit test that:

    • First iteration fetches with cursor C0
    • Manually pushes a wait_completed to cachedEvents
    • Second iteration fetches and gets the same wait_completed plus an interleaved new event
    • Asserts: no duplicates AND consumer sees no orphans
      ...would lock in the contract.
  2. L198 guard's positive case — there's no test asserting that attempt > maxRetries + 1withoutstep.error does NOT fail the step. The guard exists specifically for this case and is currently un-asserted. The two step-handler.test.ts tests at lines 296–340 and 343–388 both set error on the fixture, so they exercise different paths.

  3. Concurrent-handler races — neither Scenario B (concurrent retry with prior error → premature L198 failure) nor Scenario D (catch-block premature failure with no && step.error guard) has a test. Adding these would catch the bug currently in code if a future fix lands.

  4. builder-deferred.ts Next.js manifest reading — Peter agreed in review that reading app-paths-manifest.json was the right approach over regex prediction. Verified that DID land (L1171–1175 reads the manifest with comment "This avoids predicting Next.js conventions with regexes and instead reads from Next.js's own output"). ✅

What looks good

  • The architectural shift is well-executed. The replay loop, ownership-gated inline dispatch, idempotency-key-based queue dedupe, builder unification, and __steps_registered rollup-tree-shaking guard are all clean.
  • The getStepNameFromEvent full-event-load concern from my prior review was addressed — stepName now flows through WorkflowInvokePayloadSchema. ✓
  • The TooEarlyError stale retryAfter access pattern is fixed at step-executor.ts:169. ✓
  • The CJS module.exports collision (ESM format for steps when bundleFinalOutput: true) is a subtle but correct fix.
  • The changelog doc docs/content/docs/changelog/eager-processing.mdx is one of the better architecture docs I've seen in a PR — covers every edge case, failure mode, and platform-specific quirk.

Verdict

No blockers from my side. Ship if Vade and storytime green-light. The concurrent-retry premature-failure case is real but acknowledged, the auto-flagged "event cache duplication" is a non-issue, and the test coverage gaps are real but not regressions (the V2 behaviors they cover are net-new).

If you want one piece of polish before merge, the L469 && step.error mirror would close the most user-visible failure mode at zero cost — but I won't gate on it.

@TooTallNateTooTallNate 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.

Pre-emptively :approved: but my agent really wants @VaguelySerious to resolve the open threads :lol: and there seems to be a few other things pointed out in the latest review comment.

VaguelySeriousand others added 7 commits May 3, 2026 11:38
Merges the V1 split flow/step routes (one queue message per step + one per
flow continuation, two separate function invocations per step) into a
single combined handler at /.well-known/workflow/v1/flow that executes
steps inline within the same invocation as the workflow replay.
A serial workflow with N steps that previously required ~2N+1 function
invocations now completes in 1.
Architecture
------------
- packages/core: workflowEntrypoint() handler with an inline replay loop.
Each loop iteration: load events incrementally, replay workflow,
inline-execute one owned step on suspension, loop. Background steps
(from Promise.all) are queued back to the same handler with stepId so
the in-flight handler advances the loop without queue round-trips
whenever possible.
- packages/builders: createCombinedBundle() emits a single ESM bundle
with workflowEntrypoint(workflowCode) plus a side-effect
__step_registrations bundle. ESM-by-default with createRequire banner
for CJS deps (matches main's #1562 pattern).
- packages/next, nestjs, sveltekit, astro, nitro, nuxt, hono, express,
fastify, vite: framework builders updated to use createCombinedBundle.
The step/ directory is no longer generated.
Correctness invariants (V2 polish)
----------------------------------
- Single inline executor per step. Atomic step_created per correlationId
(per-step in-process mutex in world-local; SQL-level guards in
postgres/vercel) means exactly one handler claims ownership of each
step. Inline execution gates on ownership; queue dispatch is
unconditional with idempotency keys for crash recovery (matches V1).
- onUnconsumedEvent reverts to its original PR #1055 contract: any
unconsumed event fatals as CORRUPTED_EVENT_LOG. Source-level fixes
removed every code path that produced orphaned step lifecycle events
during V2 work, so the skip branch became unnecessary.
- Lock-release polling interval lowered from 100ms to 10ms in
flushable-stream so the V2 step-executor's ops-settle race typically
resolves in ~5ms instead of ~50ms per writable-bearing step.
- Per-iteration runs.get round-trip eliminated: concurrent completion
is detected by scanning the loaded event log for terminal run events
instead.
- Worker-pool deadlock guard removed from Run#pollReturnValue. The
fibonacci/recursive parent→child polling case is now handled by
raising world-postgres default queueConcurrency from 10 to 50, with
the limitation documented at three call sites (fibonacciWorkflow's
JSDoc, queue.ts, eager-processing.mdx).
- getWorldLazy module-scope singleton breaks the step-bundle import
chain to world.ts so step bundles don't transitively pull in
world-vercel + cbor-x + the VQS dev-handler resolver.
World-local atomicity
---------------------
- Per-step in-process mutex serializes events.create for step
lifecycle events on a (runId, correlationId) key. step_started is
rejected only when the step is already terminal — duplicate
step_started on a non-terminal step is allowed (preserves retry
semantics after SIGKILL).
Runtime helpers
---------------
- loadWorkflowRunEvents(runId, afterCursor?) consolidates the
previous getAllWorkflowRunEvents{,WithCursor} + getNewWorkflowRunEvents
into one helper.
Documentation
-------------
- docs/content/docs/changelog/eager-processing.mdx: full record of
architecture, edge cases, and the fixes applied during the V2 work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Two CI failures from the earlier V2 cutover, now resolved:
1. @workflow/vitest unit tests (`packages/vitest/src/index.test.ts`)
were still mocking the V1 BaseBuilder methods (createWorkflowsBundle
+ createStepsBundle) and expecting two world handlers (workflow +
step). Updated the mocks to expose createCombinedBundle and
asserted a single combined handler at __wkf_workflow_, which is
what the V2 vitest builder actually emits.
2. E2E Local Dev Tests (nextjs-webpack) were asserting that step
error stacks contain `99_e2e.ts` / `helpers.ts`, which held in
pre-V2 webpack dev mode (which imported step sources directly).
V2 inlines the step bundle into the combined flow route, and
webpack's re-bundling collapses original step filenames out of the
dev-mode source maps. Extended the existing nextjs-webpack
carve-out in hasStepSourceMaps() to cover dev as well as prod, and
updated the source-map follow-up callout in eager-processing.mdx
to include nextjs-webpack in the matrix that needs source maps
wired up for V2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/e2e/e2e.test.ts` (PR #1879 vs. V2 hookDispose helper)
Drop the locally-defined `waitForHook(expectedRunId)` shadow in the
hookDisposeTestWorkflow test in favor of the new top-level
`waitForHook(token, { runId })` API merged from main. Keep V2's
event-driven `waitForHookDisposal()` helper, which is stricter than
main's `await sleep(3_000)` (it polls `getHookByToken` for the
HookNotFoundError signal rather than waiting a fixed interval).
* `packages/world-local/src/storage/events-storage.ts` (PR #1877 vs.
V2 per-step async mutex)
Both branches were closing race conditions in the local world's
step lifecycle. PR #1877 adds filesystem-level O_CREAT|O_EXCL locks
for `step_created` and `wait_created`; the V2 branch already wraps
step lifecycle events in an in-process `withStepLock` mutex. They
compose: the mutex serializes within a single Node process; the
filesystem lock additionally protects against cross-process races
(multiple pnpm workers, redelivered queue messages). Resolution
takes V2's mutex wrap as the base and patches in main's two
`writeExclusive` claims at the start of the `step_created` and
`wait_created` handlers, with comments noting the dual-layer
guarantee.
`packages/core/e2e/utils.ts` auto-merged: V2's nextjs-webpack +
all-Vercel source-map carve-outs preserved alongside main's
`tanstack-start` addition to `hasWorkflowSourceMaps`.
Verified locally: `@workflow/core` and `@workflow/world-local`
typecheck clean; all 343 world-local unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone tarballs project's `vercel.json` set
`"buildCommand": "pnpm --filter tarballs build"`, which only runs the
tarballs package's `build` script (`node scripts/pack.ts`) and does
NOT trigger turbo's `dependsOn: ["^build"]` chain. As a result, every
workspace package was packed with an empty `dist/` directory — only
`bin/`, `package.json`, `README.md`, and `docs/` made it into the
tarballs. Downstream installs failed with
`Cannot find module '<pkg>/dist/next.cjs'` (and similar for every
other entry point).
The previous docs-based pipeline didn't hit this because Vercel
detected `docs` as a Next.js framework, ran turbo for it, and pack.ts
was wired as a `prebuild` step that ran after deps were already
built.
Fix: switch the buildCommand to `pnpm turbo build --filter=tarballs`
so turbo evaluates the build task for `tarballs`, which fans out via
`^build` to compile every workspace dependency first, and then runs
`tarballs#build` (pack.ts) against the populated `dist/` directories.
Verified locally: a fresh build produces tarballs containing the full
`dist/` tree (including `dist/next.cjs`, the file flight-booking-app
was failing to resolve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/src/logger.ts` (PR #1849 vs. V2 webpack carve-out)
PR #1849 ("Friendlier workflow errors") reintroduced the `debug`
package as a static import and added a structured `Logger`
interface with `child()` / `forRun()` and `composeLogLine`. The V2
branch had previously removed the `debug` static import because it
pulls `debug/src/node` and a dynamic `require('tty')` into the
Next.js webpack flow route, breaking V2 builds with
`Dynamic require of "tty" is not supported`.
Resolution combines both: keep main's `Logger` interface,
`child()` / `forRun()`, and `composeLogLine` integration; replace
the `debug` invocations with the V2 lightweight
`matchesDebugNamespace` + `process.env.DEBUG` matcher so the flow
route still bundles cleanly under webpack. Comment in the file
documents why `debug` is intentionally absent.
* `packages/core/src/runtime.ts` (PR #1849 vs. V2 inline replay loop)
Multiple regions where main's structured-logging refactor and the
V2 inline-execution loop made overlapping changes. Took V2's
control flow as the base — its `if (!workflowRun) { ... }` setup
block, main replay `while (true)` loop with timeout-based
re-scheduling, terminal-event short-circuit, and per-iteration
incremental event load are all preserved as-is — and folded in
main's contributions: the scoped `runLogger = runtimeLogger.forRun(
runId, workflowName)` is created once and used for run-level info
/ error logging. Discarded main's V1 wait-completion loop and
`runWorkflow` user-code error block since V2 handles waits and
user-code errors inside the inline loop body. Updated
`buildWorkflowSuspensionMessage()` call to drop a stray `runId`
first arg that didn't match the helper's 3-param signature.
* `packages/core/e2e/e2e.test.ts` (PR #1849 unrelated to V2)
No conflict from main's friendly-errors PR, auto-merged. (No
hookDispose-style conflict this round.)
Verified locally: all 820 `@workflow/core` unit tests pass; all 343
`@workflow/world-local` unit tests pass; `pnpm turbo typecheck` is
green across all 40 typecheck tasks.
Co-Authored-By: Claude Opus 4.7 (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

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

[core] Combine flow+step bundle and process steps eagerly - #1338

Merged
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow
May 4, 2026
Merged

[core] Combine flow+step bundle and process steps eagerly#1338
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow

Conversation

@changeset-bot

changeset-botBot commented Mar 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 234fce4

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

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

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 Mar 11, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Mar 11, 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.033s (-24.6% 🟢)1.005s (~)0.972s101.00x
💻 LocalExpress0.034s (-23.3% 🟢)1.006s (~)0.972s101.05x
🐘 PostgresNext.js (Turbopack)0.049s1.013s0.964s101.50x
💻 LocalNext.js (Turbopack)0.049s1.005s0.956s101.51x
🐘 PostgresNitro0.051s (-46.8% 🟢)1.012s (-3.0%)0.961s101.56x
🌐 RedisNext.js (Turbopack)0.055s1.005s0.951s101.68x
🌐 MongoDBNext.js (Turbopack)0.109s1.007s0.899s103.35x
🐘 PostgresExpress0.177s (+205.2% 🔺)1.107s (+9.5% 🔺)0.930s105.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.236s (-42.4% 🟢)1.939s (-22.7% 🟢)1.703s101.00x
▲ VercelExpress0.241s (+2.2%)2.009s (-5.9% 🟢)1.768s101.02x
▲ VercelNext.js (Turbopack)0.550s (+118.6% 🔺)2.715s (+16.4% 🔺)2.165s102.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.069s (-5.0% 🟢)2.006s (~)0.937s101.00x
💻 LocalNitro1.073s (-5.1% 🟢)2.006s (~)0.933s101.00x
🐘 PostgresNitro1.084s (-4.9%)2.010s (~)0.927s101.01x
🌐 RedisNext.js (Turbopack)1.113s2.006s0.893s101.04x
🐘 PostgresNext.js (Turbopack)1.114s2.007s0.893s101.04x
💻 LocalNext.js (Turbopack)1.121s2.007s0.886s101.05x
🐘 PostgresExpress1.143s (~)2.021s (+0.5%)0.878s101.07x
🌐 MongoDBNext.js (Turbopack)1.162s2.009s0.847s101.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.498s (-20.1% 🟢)3.691s (-3.1%)2.193s101.00x
▲ VercelNitro1.524s (-60.9% 🟢)3.710s (-37.2% 🟢)2.186s101.02x
▲ VercelNext.js (Turbopack)1.942s (-4.6%)4.457s (+16.4% 🔺)2.516s101.30x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.409s (-4.2%)11.018s (~)0.609s31.00x
💻 LocalNitro10.413s (-4.9%)11.022s (~)0.608s31.00x
💻 LocalExpress10.436s (-4.5%)11.021s (~)0.586s31.00x
🐘 PostgresNext.js (Turbopack)10.568s11.013s0.445s31.02x
🌐 RedisNext.js (Turbopack)10.657s11.024s0.367s31.02x
💻 LocalNext.js (Turbopack)10.685s11.022s0.337s31.03x
🐘 PostgresExpress10.745s (-2.0%)11.419s (+3.6%)0.674s31.03x
🌐 MongoDBNext.js (Turbopack)10.789s11.018s0.229s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.059s (-40.8% 🟢)15.700s (-37.5% 🟢)1.641s21.00x
▲ VercelNext.js (Turbopack)14.845s (-14.3% 🟢)16.872s (-13.0% 🟢)2.027s21.06x
▲ VercelExpress15.996s (-5.8% 🟢)18.196s (-9.1% 🟢)2.200s21.14x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.431s (-10.8% 🟢)14.027s (-12.5% 🟢)0.596s51.00x
🐘 PostgresNitro13.434s (-8.0% 🟢)14.013s (-6.8% 🟢)0.579s51.00x
💻 LocalExpress13.490s (-9.9% 🟢)14.027s (-6.7% 🟢)0.537s51.00x
🐘 PostgresNext.js (Turbopack)13.820s14.018s0.198s51.03x
💻 LocalNext.js (Turbopack)14.076s15.030s0.954s41.05x
🌐 RedisNext.js (Turbopack)14.088s15.030s0.942s41.05x
🌐 MongoDBNext.js (Turbopack)14.283s15.019s0.736s41.06x
🐘 PostgresExpress14.537s (~)15.276s (+1.7%)0.739s41.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.821s (-58.6% 🟢)22.773s (-56.7% 🟢)1.952s31.00x
▲ VercelNitro21.829s (-66.1% 🟢)23.502s (-64.7% 🟢)1.673s31.05x
▲ VercelNext.js (Turbopack)23.199s (-55.9% 🟢)25.174s (-53.9% 🟢)1.975s31.11x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.846s (-29.4% 🟢)12.022s (-29.4% 🟢)0.176s81.00x
🐘 PostgresNitro11.923s (-14.6% 🟢)12.392s (-13.4% 🟢)0.469s81.01x
💻 LocalExpress11.997s (-27.7% 🟢)12.398s (-27.2% 🟢)0.401s81.01x
🌐 RedisNext.js (Turbopack)13.015s13.454s0.439s71.10x
💻 LocalNext.js (Turbopack)13.089s14.026s0.937s71.10x
🐘 PostgresNext.js (Turbopack)13.215s13.585s0.370s71.12x
🌐 MongoDBNext.js (Turbopack)13.301s14.021s0.720s71.12x
🐘 PostgresExpress15.027s (+7.3% 🔺)15.400s (+5.5% 🔺)0.373s61.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.095s (-74.3% 🟢)33.159s (-73.2% 🟢)2.064s31.00x
▲ VercelNitro31.508s (-92.5% 🟢)33.124s (-92.2% 🟢)1.615s31.01x
▲ VercelNext.js (Turbopack)31.987s (-91.9% 🟢)33.996s (-91.4% 🟢)2.009s31.03x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.140s (-10.6% 🟢)2.007s (~)0.868s151.00x
🐘 PostgresNext.js (Turbopack)1.168s2.007s0.838s151.03x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.187s (-20.3% 🟢)2.006s (~)0.820s151.04x
🐘 PostgresExpress1.237s (-1.9%)2.018s (~)0.781s151.09x
🌐 RedisNext.js (Turbopack)1.238s2.006s0.768s151.09x
💻 LocalNext.js (Turbopack)1.279s2.005s0.726s151.12x
🌐 MongoDBNext.js (Turbopack)2.048s2.917s0.869s111.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.551s (-10.8% 🟢)4.187s (-9.4% 🟢)1.636s81.00x
▲ VercelNitro2.674s (-5.1% 🟢)4.258s (-1.5%)1.584s81.05x
▲ VercelNext.js (Turbopack)4.193s (+23.4% 🔺)6.817s (+38.2% 🔺)2.624s51.64x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.225s (-47.9% 🟢)2.007s (-33.3% 🟢)0.782s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.05x
🐘 PostgresExpress1.483s (-37.2% 🟢)2.019s (-32.9% 🟢)0.536s151.21x
💻 LocalNitro1.726s (-45.1% 🟢)2.072s (-46.7% 🟢)0.346s151.41x
💻 LocalNext.js (Turbopack)1.831s2.149s0.318s141.50x
💻 LocalExpress1.905s (-35.5% 🟢)2.150s (-37.8% 🟢)0.244s141.56x
🌐 RedisNext.js (Turbopack)2.364s3.008s0.643s101.93x
🌐 MongoDBNext.js (Turbopack)3.568s4.008s0.440s82.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.844s (-5.1% 🟢)5.284s (-10.8% 🟢)1.440s61.00x
▲ VercelExpress3.884s (+7.3% 🔺)6.116s (+19.7% 🔺)2.232s51.01x
▲ VercelNext.js (Turbopack)5.098s (-28.2% 🟢)7.574s (-15.0% 🟢)2.476s41.33x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.419s (-59.2% 🟢)2.007s (-49.9% 🟢)0.588s151.00x
🐘 PostgresNext.js (Turbopack)1.493s2.007s0.514s151.05x
🐘 PostgresExpress2.153s (-38.2% 🟢)2.861s (-28.7% 🟢)0.708s111.52x
🌐 RedisNext.js (Turbopack)3.634s4.010s0.376s82.56x
💻 LocalNitro4.547s (-45.5% 🟢)5.013s (-44.4% 🟢)0.466s63.20x
💻 LocalExpress4.829s (-42.1% 🟢)5.512s (-38.9% 🟢)0.683s63.40x
💻 LocalNext.js (Turbopack)5.581s6.213s0.631s53.93x
🌐 MongoDBNext.js (Turbopack)6.289s7.012s0.723s54.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.147s (+21.4% 🔺)7.340s (+19.8% 🔺)2.193s51.00x
▲ VercelNitro5.410s (+53.5% 🔺)7.160s (+29.4% 🔺)1.750s51.05x
▲ VercelNext.js (Turbopack)7.157s (-19.7% 🟢)8.877s (-19.0% 🟢)1.721s41.39x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.170s (-6.9% 🟢)2.007s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.173s2.007s0.835s151.00x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.774s151.05x
💻 LocalNext.js (Turbopack)1.327s2.006s0.679s151.13x
💻 LocalNitro1.380s (-26.0% 🟢)2.006s (-14.3% 🟢)0.626s151.18x
💻 LocalExpress1.393s (-26.5% 🟢)2.006s (-15.1% 🟢)0.613s151.19x
🐘 PostgresExpress1.408s (+12.0% 🔺)2.062s (+2.7%)0.654s151.20x
🌐 MongoDBNext.js (Turbopack)2.036s2.916s0.880s111.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.271s (-7.6% 🟢)4.045s (-3.0%)1.774s81.00x
▲ VercelExpress2.336s (-9.5% 🟢)3.766s (-13.4% 🟢)1.430s81.03x
▲ VercelNext.js (Turbopack)4.170s (+42.2% 🔺)6.038s (+30.1% 🔺)1.869s51.84x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.251s (-46.5% 🟢)2.007s (-33.3% 🟢)0.757s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.03x
🐘 PostgresExpress1.393s (-40.5% 🟢)2.035s (-32.4% 🟢)0.642s151.11x
💻 LocalNitro1.957s (-36.2% 🟢)2.316s (-40.4% 🟢)0.359s131.56x
💻 LocalExpress2.022s (-35.4% 🟢)2.508s (-33.3% 🟢)0.485s121.62x
💻 LocalNext.js (Turbopack)2.130s2.826s0.696s111.70x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.88x
🌐 MongoDBNext.js (Turbopack)3.543s4.007s0.464s82.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.338s (+35.9% 🔺)6.011s (+25.4% 🔺)1.673s51.00x
▲ VercelNitro4.664s (+44.2% 🔺)6.658s (+31.1% 🔺)1.994s51.08x
▲ VercelNext.js (Turbopack)4.714s (+50.0% 🔺)6.542s (+44.7% 🔺)1.827s51.09x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.380s (-60.3% 🟢)2.007s (-49.9% 🟢)0.627s151.00x
🐘 PostgresNext.js (Turbopack)1.483s2.008s0.525s151.07x
🐘 PostgresExpress1.982s (-43.4% 🟢)2.737s (-31.8% 🟢)0.755s111.44x
🌐 RedisNext.js (Turbopack)3.571s4.010s0.439s82.59x
💻 LocalNitro4.894s (-46.5% 🟢)5.348s (-46.7% 🟢)0.454s63.55x
💻 LocalExpress5.939s (-32.5% 🟢)6.417s (-30.8% 🟢)0.478s54.30x
💻 LocalNext.js (Turbopack)6.115s6.416s0.301s54.43x
🌐 MongoDBNext.js (Turbopack)6.302s7.013s0.710s54.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.380s (+5.6% 🔺)7.062s (+3.6%)1.682s51.00x
▲ VercelNext.js (Turbopack)5.690s (-15.8% 🟢)7.416s (-13.2% 🟢)1.726s51.06x
▲ VercelExpress5.700s (-11.2% 🟢)7.539s (-7.8% 🟢)1.840s51.06x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.459s (-44.1% 🟢)1.022s (+1.6%)0.564s591.00x
💻 LocalNitro0.472s (-51.8% 🟢)1.021s (-6.7% 🟢)0.548s591.03x
💻 LocalExpress0.482s (-51.1% 🟢)1.004s (-6.7% 🟢)0.522s601.05x
🐘 PostgresNext.js (Turbopack)0.599s1.040s0.442s581.31x
🌐 RedisNext.js (Turbopack)0.619s1.004s0.386s601.35x
🐘 PostgresExpress0.644s (-23.2% 🟢)1.140s (+11.4% 🔺)0.496s531.40x
💻 LocalNext.js (Turbopack)0.719s1.004s0.285s601.57x
🌐 MongoDBNext.js (Turbopack)0.733s1.006s0.272s601.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.661s (-75.5% 🟢)6.339s (-70.3% 🟢)1.677s101.00x
▲ VercelNitro5.218s (-76.3% 🟢)7.258s (-69.8% 🟢)2.039s101.12x
▲ VercelNext.js (Turbopack)6.455s (-55.5% 🟢)7.922s (-50.7% 🟢)1.466s81.38x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.085s (-43.7% 🟢)1.705s (-18.8% 🟢)0.620s531.00x
🐘 PostgresExpress1.162s (-41.2% 🟢)1.588s (-29.7% 🟢)0.426s571.07x
💻 LocalExpress1.197s (-60.3% 🟢)2.006s (-44.1% 🟢)0.809s451.10x
💻 LocalNitro1.220s (-59.8% 🟢)2.005s (-46.6% 🟢)0.786s451.12x
🌐 RedisNext.js (Turbopack)1.476s2.006s0.530s451.36x
🐘 PostgresNext.js (Turbopack)1.707s2.387s0.679s381.57x
💻 LocalNext.js (Turbopack)1.753s2.005s0.253s451.61x
🌐 MongoDBNext.js (Turbopack)1.813s2.007s0.194s451.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.733s (-70.3% 🟢)13.729s (-66.8% 🟢)1.996s71.00x
▲ VercelExpress12.793s (-62.9% 🟢)14.645s (-60.2% 🟢)1.852s71.09x
▲ VercelNext.js (Turbopack)14.268s (-71.4% 🟢)16.580s (-67.9% 🟢)2.313s61.22x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.108s (-48.6% 🟢)2.696s (-41.4% 🟢)0.588s451.00x
🐘 PostgresExpress2.634s (-34.0% 🟢)3.074s (-29.6% 🟢)0.440s401.25x
🐘 PostgresNext.js (Turbopack)2.659s3.084s0.425s391.26x
💻 LocalNitro2.660s (-71.4% 🟢)3.007s (-70.0% 🟢)0.348s401.26x
💻 LocalExpress2.743s (-70.2% 🟢)3.033s (-69.7% 🟢)0.290s401.30x
🌐 RedisNext.js (Turbopack)2.992s3.191s0.200s381.42x
💻 LocalNext.js (Turbopack)3.748s4.007s0.259s301.78x
🌐 MongoDBNext.js (Turbopack)4.178s5.012s0.833s241.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express35.192s (-72.9% 🟢)37.772s (-71.4% 🟢)2.580s41.00x
▲ VercelNitro36.812s (-62.0% 🟢)39.053s (-60.3% 🟢)2.241s41.05x
▲ VercelNext.js (Turbopack)43.007s (-59.9% 🟢)46.755s (-57.1% 🟢)3.748s31.22x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-44.2% 🟢)1.007s (~)0.850s601.00x
🐘 PostgresNitro0.182s (-35.8% 🟢)1.006s (~)0.824s601.15x
🐘 PostgresNext.js (Turbopack)0.190s1.005s0.815s601.21x
🌐 RedisNext.js (Turbopack)0.316s1.021s0.705s592.00x
💻 LocalNitro0.412s (-31.9% 🟢)1.004s (-1.7%)0.592s602.61x
💻 LocalExpress0.452s (-19.3% 🟢)1.004s (~)0.552s602.87x
💻 LocalNext.js (Turbopack)0.603s1.039s0.436s583.83x
🌐 MongoDBNext.js (Turbopack)1.029s1.749s0.720s356.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.178s (+11.5% 🔺)4.588s (+26.2% 🔺)2.410s141.00x
▲ VercelNitro2.258s (+36.0% 🔺)4.485s (+33.9% 🔺)2.227s141.04x
▲ VercelNext.js (Turbopack)3.716s (+83.7% 🔺)5.458s (+43.9% 🔺)1.742s111.71x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.304s (-38.8% 🟢)1.006s (~)0.702s901.00x
🌐 RedisNext.js (Turbopack)0.423s1.004s0.581s901.39x
🐘 PostgresExpress0.520s (+2.0%)1.112s (+10.5% 🔺)0.592s821.71x
🐘 PostgresNext.js (Turbopack)0.705s1.318s0.613s692.32x
💻 LocalNitro2.170s (-14.5% 🟢)2.821s (-6.3% 🟢)0.651s327.14x
💻 LocalExpress2.186s (-13.0% 🟢)2.944s (-2.2%)0.758s317.19x
💻 LocalNext.js (Turbopack)2.424s3.043s0.619s307.97x
🌐 MongoDBNext.js (Turbopack)2.595s3.007s0.412s308.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.618s (+87.2% 🔺)8.356s (+60.9% 🔺)1.738s111.00x
▲ VercelNitro7.134s (+121.1% 🔺)9.355s (+94.0% 🔺)2.221s101.08x
▲ VercelExpress7.527s (+147.1% 🔺)9.552s (+98.7% 🔺)2.025s101.14x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.637s (-19.4% 🟢)1.006s (~)0.369s1201.00x
🐘 PostgresNext.js (Turbopack)0.730s1.013s0.284s1191.15x
🐘 PostgresExpress0.790s (-3.5%)1.192s (+17.1% 🔺)0.402s1011.24x
🌐 RedisNext.js (Turbopack)0.797s1.004s0.207s1201.25x
🌐 MongoDBNext.js (Turbopack)5.387s6.013s0.626s208.46x
💻 LocalNitro9.790s (-12.5% 🟢)10.276s (-11.9% 🟢)0.486s1215.37x
💻 LocalExpress10.085s (-9.9% 🟢)10.782s (-9.7% 🟢)0.697s1215.84x
💻 LocalNext.js (Turbopack)11.478s12.128s0.650s1018.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.821s (+117.8% 🔺)19.141s (+103.6% 🔺)2.319s71.00x
▲ VercelNext.js (Turbopack)18.466s (+78.8% 🔺)20.419s (+66.2% 🔺)1.953s71.10x
▲ VercelExpress20.247s (+172.9% 🔺)22.460s (+143.0% 🔺)2.213s61.20x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.133s (+430.1% 🔺)2.005s (+99.6% 🔺)0.010s (-19.2% 🟢)2.017s (+98.0% 🔺)0.884s101.00x
🐘 PostgresNitro1.134s (+453.4% 🔺)2.001s (+100.2% 🔺)0.001s (-26.7% 🟢)2.010s (+98.7% 🔺)0.875s101.00x
💻 LocalExpress1.136s (+470.5% 🔺)2.005s (+99.6% 🔺)0.012s (~)2.020s (+98.4% 🔺)0.884s101.00x
💻 LocalNext.js (Turbopack)1.172s2.003s0.012s2.019s0.847s101.03x
🐘 PostgresExpress1.232s (+500.5% 🔺)2.005s (+100.8% 🔺)0.001s (-37.5% 🟢)2.041s (+101.8% 🔺)0.810s101.09x
🐘 PostgresNext.js (Turbopack)2.024s2.601s0.220s2.874s0.850s101.79x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.200s (-42.6% 🟢)3.478s (-34.1% 🟢)1.562s (+110.5% 🔺)5.432s (-16.2% 🟢)3.232s101.00x
▲ VercelExpress2.336s (-6.8% 🟢)3.935s (-3.8%)1.087s (+13.1% 🔺)5.425s (-3.0%)3.089s101.06x
▲ VercelNext.js (Turbopack)4.802s (-29.9% 🟢)4.900s (-43.4% 🟢)0.740s (+17.0% 🔺)7.346s (-25.0% 🟢)2.544s102.18x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.512s (+142.2% 🔺)2.005s (+99.2% 🔺)0.004s (-8.9% 🟢)2.024s (+97.9% 🔺)0.512s301.00x
💻 LocalNitro1.514s (+80.5% 🔺)2.011s (+98.7% 🔺)0.010s (+2.8%)2.022s (+81.2% 🔺)0.509s301.00x
💻 LocalExpress1.721s (+127.3% 🔺)2.011s (+95.5% 🔺)0.010s (+2.1%)2.202s (+111.7% 🔺)0.481s281.14x
🐘 PostgresNext.js (Turbopack)1.722s2.107s0.003s2.123s0.401s301.14x
💻 LocalNext.js (Turbopack)1.845s2.011s0.009s2.203s0.358s281.22x
🐘 PostgresExpress1.983s (+214.8% 🔺)2.330s (+131.5% 🔺)0.003s (-20.7% 🟢)2.395s (+134.1% 🔺)0.412s261.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.656s (-13.0% 🟢)7.281s (-9.1% 🟢)0.179s (-56.1% 🟢)8.330s (-5.7% 🟢)2.675s81.00x
▲ VercelNitro5.796s (-80.3% 🟢)7.455s (-75.8% 🟢)0.170s (+52.1% 🔺)8.071s (-74.6% 🟢)2.275s81.02x
▲ VercelNext.js (Turbopack)12.255s (-27.6% 🟢)13.600s (-25.4% 🟢)0.201s (-4.9%)15.301s (-19.2% 🟢)3.046s42.17x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.651s (-32.8% 🟢)1.033s (-17.2% 🟢)0.000s (-100.0% 🟢)1.042s (-17.2% 🟢)0.390s581.00x
🐘 PostgresExpress1.031s (+7.3% 🔺)1.390s (+8.7% 🔺)0.000s (+9.5% 🔺)1.437s (+10.0% 🔺)0.406s421.58x
🐘 PostgresNext.js (Turbopack)1.333s1.618s0.000s1.715s0.382s372.05x
💻 LocalNitro1.337s (+9.4% 🔺)2.015s (~)0.000s (+166.7% 🔺)2.017s (~)0.680s302.05x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-60.0% 🟢)2.017s (~)0.649s302.10x
💻 LocalNext.js (Turbopack)1.436s2.013s0.000s2.016s0.581s302.20x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.922s (+28.6% 🔺)5.598s (+27.4% 🔺)0.000s (-100.0% 🟢)6.027s (+25.3% 🔺)2.105s101.00x
▲ VercelExpress4.013s (+7.3% 🔺)5.879s (+15.2% 🔺)0.000s (-100.0% 🟢)6.416s (+16.0% 🔺)2.404s101.02x
▲ VercelNext.js (Turbopack)5.465s (-46.3% 🟢)6.707s (-41.8% 🟢)0.000s (+Infinity% 🔺)7.645s (-36.6% 🟢)2.181s81.39x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.327s (-25.9% 🟢)2.066s (-3.5%)0.000s (-100.0% 🟢)2.081s (-4.3%)0.754s291.00x
🐘 PostgresNext.js (Turbopack)1.844s2.402s0.000s2.460s0.616s251.39x
🐘 PostgresExpress2.377s (+34.2% 🔺)3.042s (+39.7% 🔺)0.000s (NaN%)3.091s (+40.6% 🔺)0.714s201.79x
💻 LocalNext.js (Turbopack)2.776s3.362s0.000s3.365s0.589s182.09x
💻 LocalNitro3.068s (-9.4% 🟢)3.838s (-4.8%)0.000s (-53.1% 🟢)3.843s (-4.8%)0.775s162.31x
💻 LocalExpress3.103s (-10.5% 🟢)3.775s (-6.4% 🟢)0.000s (-76.6% 🟢)3.781s (-6.3% 🟢)0.679s162.34x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.059s (+10.3% 🔺)6.788s (+12.7% 🔺)0.000s (+Infinity% 🔺)7.571s (+17.3% 🔺)2.512s81.00x
▲ VercelNitro5.089s (+24.3% 🔺)6.536s (+21.6% 🔺)0.000s (-18.5% 🟢)7.002s (+20.9% 🔺)1.913s91.01x
▲ VercelNext.js (Turbopack)8.675s (+54.5% 🔺)10.065s (+44.2% 🔺)0.000s (-100.0% 🟢)11.283s (+49.6% 🔺)2.608s61.71x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro17/21
🐘 PostgresNitro19/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other4380150588
Total4266010265292

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025

📋 View full workflow run

Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/vitest/src/index.ts
Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/core/src/workflow.ts Outdated
Comment threadpackages/builders/src/base-builder.ts
Comment threadpackages/core/src/runtime/get-world-lazy.ts

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Remaining issues:

  • getWorldLazy needs to be validated as a decision
  • packages/core/e2e/utils.ts skip for hasStepSourceMaps needs to be re-evaluated

@shtefcs

Copy link
Copy Markdown

Seems you had a lot of work here. It's not easy as it seems. Hopefully, this gonna solve some issues we are facing with Workflow.

Peace,

@TooTallNateTooTallNate 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.

Withdrawing my prior CHANGES_REQUESTED — the blocker I called out (the stale (err as any).meta?.retryAfter access in step-executor.ts) is fixed at line 169 with err.retryAfter ?? 1. As an aside, my fix recommendation in the prior review was based on a faulty type assumption (I confused TooEarlyError.retryAfter: number with RetryableError.retryAfter: Date). The author's err.retryAfter ?? 1 form is correct: it's a number of seconds with sensible fallback.

My other earlier concerns:

  • getStepNameFromEvent full-event-load: addressed. stepName is now plumbed through WorkflowInvokePayloadSchema so the handler reads it directly from the queue message payload instead of doing a paginated event load. ✓
  • step.error guard for max retries: known limitation acknowledged in-thread. The proposed long-term fix (server-side ne(status, 'running') on step_started) is separate work.
  • Background step → continuation enqueue safety: acknowledged as intentional.

So my specific items are resolved. But I'm not approving outright — there's still substantial unresolved review from @VaguelySerious touching real concerns I'd defer to them on:

  • Removal of the LegacyStreamWorld shim
  • Limiting queue concurrency to 1 in tests
  • Comments removed without explanation in suspension-handler.ts / runtime.ts
  • The builder-deferred.ts Next.js behavior prediction
  • Several "is there a systematic fix instead of conditionally allowing edge cases" questions

Plus the auto-flagged event cache duplication bug from Vercel's review bot is worth confirming was understood and dismissed (or fixed).

Approval-wise this should land when @VaguelySerious's concerns are resolved. From my side: no remaining blockers.

@TooTallNateTooTallNate 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.

Thorough re-review for Peter. Spent significant time tracing four risk surfaces: the Vercel bot's auto-flag, the concurrent-step-execution model, inline-step replay determinism, and the open review threads. Bottom line up front: no ship-blockers from my side. A few coverage gaps and one acknowledged limitation worth being explicit about, but nothing I'd hold this PR on.

Vercel bot's "event cache duplication" auto-flag is a false positive

"wait_completed events pushed locally without advancing eventsCursor cause duplicate events on next incremental fetch."

The author anticipated exactly this scenario. runtime.ts:585–594 has explicit dedupe-by-eventId logic:

constexistingIds=newSet(cachedEvents.map((e)=>e.eventId));for(consteofloaded.events){if(!existingIds.has(e.eventId)){cachedEvents.push(e);}}

Verified end-to-end: locally-pushed wait_completed (line 685) keeps its real server-returned eventId, the next iteration's incremental fetch returns it again, the dedupe filter drops it, the consumer never sees a duplicate. The comment in the code calls out the exact scenario the bot flagged. Safe to dismiss the auto-flag.

Concurrent-step-execution: a partial fix shipping with a known gap

Verified the L198 guard (step.attempt > maxRetries + 1 && step.error) traces correctly for the scenario it targets — concurrent first-attempts before any user-code error has been recorded. Without step.error, the guard correctly skips and the step proceeds normally even with inflated step.attempt.

But there's a symmetric gap in the catch-block path at L469:

if(currentAttempt>=maxRetries+1){/* step_failed */}

This check has no && step.error guard, so it fires on the first real catch-block failure even when currentAttempt has been inflated by concurrent first-attempts. End result: a workflow with maxRetries=3 and 4+ concurrent handlers can permanently fail after a single transient flake, with a misleading log saying "exceeded max retries (4 retries)" when the step body actually only ran once.

This is the only substantive open thread on the PR (step-executor.ts:198), and the author has acknowledged it in a separate resolved thread:

"Agreed on the concurrent-retry limitation. The server-side ne(status, 'running') WHERE clause is the right long-term fix — tracking as a follow-up."

So everyone's eyes-open. Worth being explicit in the changelog or PR description that under high queue back-pressure, default-maxRetries workflows can spuriously fail. Either of these would close the gap fully:

  1. Add the same && step.error guard at L469 (mirrors L198, easy to do, but doesn't fix the L198 case for catch-block).
  2. Server-side ne(status, 'running') on step_started (referenced in the resolved thread; also fixes the world-testing concurrency issue Peter resolved separately).

Not a ship-stopper, but I'd want it tracked as a known issue users can hit, not just an internal todo.

Inline-step replay: ordering subtlety in the dedupe path

The locally-pushed-then-incremental-fetch path can produce non-monotonic-by-eventId ordering in cachedEvents. Specifically: after wait_completed is pushed locally (L685) without advancing the cursor, the next incremental fetch can return concurrent events with eventIds between the local push and earlier events. After dedupe, cachedEvents becomes append-ordered but not eventId-ordered. The EventsConsumer walks by index, so it consumes them in append order, which is not the canonical replay order.

In practice this is unlikely to cause issues because:

  • Within one handler's writes, the same world process generates monotonic ULIDs.
  • Cross-handler events for the same run are gated by the ownership check (step_created ownership at L813).
  • The dangerous case requires concurrent handler activity for the same run + non-trivial cross-process clock skew.

But it's a footgun. The cleanest fix would be: after locally pushing an event, set eventsCursor to that event's eventId so the next fetch skips it. This also eliminates the dedupe pass entirely. Worth considering as a follow-up.

"VaguelySerious" review threads — context for Peter

For other reviewers who may stumble on this: most of the open @VaguelySerious threads are Peter's own AI-review notes posted via tooling. Of 28 such threads, 27 are either resolved or outdated (the diff has moved past them). The single unresolved + non-outdated thread is the concurrent-retry concern above (step-executor.ts:198), which Peter has acknowledged as a known limitation in a sibling resolved thread.

External-reviewer threads (mine, the Vercel bot's) are all resolved or addressed. So the apparent "long list of unresolved concerns" from my prior comment is misleading — it was conflating Peter's self-review notes with external review.

Test coverage gaps (non-blocking)

A few coverage gaps for novel V2 behavior worth tracking even if you ship:

  1. Dedupe path at runtime.ts:585–594 — zero test coverage. The behavior is novel to V2 and load-bearing for incremental-replay correctness. A unit test that:

    • First iteration fetches with cursor C0
    • Manually pushes a wait_completed to cachedEvents
    • Second iteration fetches and gets the same wait_completed plus an interleaved new event
    • Asserts: no duplicates AND consumer sees no orphans
      ...would lock in the contract.
  2. L198 guard's positive case — there's no test asserting that attempt > maxRetries + 1withoutstep.error does NOT fail the step. The guard exists specifically for this case and is currently un-asserted. The two step-handler.test.ts tests at lines 296–340 and 343–388 both set error on the fixture, so they exercise different paths.

  3. Concurrent-handler races — neither Scenario B (concurrent retry with prior error → premature L198 failure) nor Scenario D (catch-block premature failure with no && step.error guard) has a test. Adding these would catch the bug currently in code if a future fix lands.

  4. builder-deferred.ts Next.js manifest reading — Peter agreed in review that reading app-paths-manifest.json was the right approach over regex prediction. Verified that DID land (L1171–1175 reads the manifest with comment "This avoids predicting Next.js conventions with regexes and instead reads from Next.js's own output"). ✅

What looks good

  • The architectural shift is well-executed. The replay loop, ownership-gated inline dispatch, idempotency-key-based queue dedupe, builder unification, and __steps_registered rollup-tree-shaking guard are all clean.
  • The getStepNameFromEvent full-event-load concern from my prior review was addressed — stepName now flows through WorkflowInvokePayloadSchema. ✓
  • The TooEarlyError stale retryAfter access pattern is fixed at step-executor.ts:169. ✓
  • The CJS module.exports collision (ESM format for steps when bundleFinalOutput: true) is a subtle but correct fix.
  • The changelog doc docs/content/docs/changelog/eager-processing.mdx is one of the better architecture docs I've seen in a PR — covers every edge case, failure mode, and platform-specific quirk.

Verdict

No blockers from my side. Ship if Vade and storytime green-light. The concurrent-retry premature-failure case is real but acknowledged, the auto-flagged "event cache duplication" is a non-issue, and the test coverage gaps are real but not regressions (the V2 behaviors they cover are net-new).

If you want one piece of polish before merge, the L469 && step.error mirror would close the most user-visible failure mode at zero cost — but I won't gate on it.

@TooTallNateTooTallNate 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.

Pre-emptively :approved: but my agent really wants @VaguelySerious to resolve the open threads :lol: and there seems to be a few other things pointed out in the latest review comment.

VaguelySeriousand others added 7 commits May 3, 2026 11:38
Merges the V1 split flow/step routes (one queue message per step + one per
flow continuation, two separate function invocations per step) into a
single combined handler at /.well-known/workflow/v1/flow that executes
steps inline within the same invocation as the workflow replay.
A serial workflow with N steps that previously required ~2N+1 function
invocations now completes in 1.
Architecture
------------
- packages/core: workflowEntrypoint() handler with an inline replay loop.
Each loop iteration: load events incrementally, replay workflow,
inline-execute one owned step on suspension, loop. Background steps
(from Promise.all) are queued back to the same handler with stepId so
the in-flight handler advances the loop without queue round-trips
whenever possible.
- packages/builders: createCombinedBundle() emits a single ESM bundle
with workflowEntrypoint(workflowCode) plus a side-effect
__step_registrations bundle. ESM-by-default with createRequire banner
for CJS deps (matches main's #1562 pattern).
- packages/next, nestjs, sveltekit, astro, nitro, nuxt, hono, express,
fastify, vite: framework builders updated to use createCombinedBundle.
The step/ directory is no longer generated.
Correctness invariants (V2 polish)
----------------------------------
- Single inline executor per step. Atomic step_created per correlationId
(per-step in-process mutex in world-local; SQL-level guards in
postgres/vercel) means exactly one handler claims ownership of each
step. Inline execution gates on ownership; queue dispatch is
unconditional with idempotency keys for crash recovery (matches V1).
- onUnconsumedEvent reverts to its original PR #1055 contract: any
unconsumed event fatals as CORRUPTED_EVENT_LOG. Source-level fixes
removed every code path that produced orphaned step lifecycle events
during V2 work, so the skip branch became unnecessary.
- Lock-release polling interval lowered from 100ms to 10ms in
flushable-stream so the V2 step-executor's ops-settle race typically
resolves in ~5ms instead of ~50ms per writable-bearing step.
- Per-iteration runs.get round-trip eliminated: concurrent completion
is detected by scanning the loaded event log for terminal run events
instead.
- Worker-pool deadlock guard removed from Run#pollReturnValue. The
fibonacci/recursive parent→child polling case is now handled by
raising world-postgres default queueConcurrency from 10 to 50, with
the limitation documented at three call sites (fibonacciWorkflow's
JSDoc, queue.ts, eager-processing.mdx).
- getWorldLazy module-scope singleton breaks the step-bundle import
chain to world.ts so step bundles don't transitively pull in
world-vercel + cbor-x + the VQS dev-handler resolver.
World-local atomicity
---------------------
- Per-step in-process mutex serializes events.create for step
lifecycle events on a (runId, correlationId) key. step_started is
rejected only when the step is already terminal — duplicate
step_started on a non-terminal step is allowed (preserves retry
semantics after SIGKILL).
Runtime helpers
---------------
- loadWorkflowRunEvents(runId, afterCursor?) consolidates the
previous getAllWorkflowRunEvents{,WithCursor} + getNewWorkflowRunEvents
into one helper.
Documentation
-------------
- docs/content/docs/changelog/eager-processing.mdx: full record of
architecture, edge cases, and the fixes applied during the V2 work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Two CI failures from the earlier V2 cutover, now resolved:
1. @workflow/vitest unit tests (`packages/vitest/src/index.test.ts`)
were still mocking the V1 BaseBuilder methods (createWorkflowsBundle
+ createStepsBundle) and expecting two world handlers (workflow +
step). Updated the mocks to expose createCombinedBundle and
asserted a single combined handler at __wkf_workflow_, which is
what the V2 vitest builder actually emits.
2. E2E Local Dev Tests (nextjs-webpack) were asserting that step
error stacks contain `99_e2e.ts` / `helpers.ts`, which held in
pre-V2 webpack dev mode (which imported step sources directly).
V2 inlines the step bundle into the combined flow route, and
webpack's re-bundling collapses original step filenames out of the
dev-mode source maps. Extended the existing nextjs-webpack
carve-out in hasStepSourceMaps() to cover dev as well as prod, and
updated the source-map follow-up callout in eager-processing.mdx
to include nextjs-webpack in the matrix that needs source maps
wired up for V2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/e2e/e2e.test.ts` (PR #1879 vs. V2 hookDispose helper)
Drop the locally-defined `waitForHook(expectedRunId)` shadow in the
hookDisposeTestWorkflow test in favor of the new top-level
`waitForHook(token, { runId })` API merged from main. Keep V2's
event-driven `waitForHookDisposal()` helper, which is stricter than
main's `await sleep(3_000)` (it polls `getHookByToken` for the
HookNotFoundError signal rather than waiting a fixed interval).
* `packages/world-local/src/storage/events-storage.ts` (PR #1877 vs.
V2 per-step async mutex)
Both branches were closing race conditions in the local world's
step lifecycle. PR #1877 adds filesystem-level O_CREAT|O_EXCL locks
for `step_created` and `wait_created`; the V2 branch already wraps
step lifecycle events in an in-process `withStepLock` mutex. They
compose: the mutex serializes within a single Node process; the
filesystem lock additionally protects against cross-process races
(multiple pnpm workers, redelivered queue messages). Resolution
takes V2's mutex wrap as the base and patches in main's two
`writeExclusive` claims at the start of the `step_created` and
`wait_created` handlers, with comments noting the dual-layer
guarantee.
`packages/core/e2e/utils.ts` auto-merged: V2's nextjs-webpack +
all-Vercel source-map carve-outs preserved alongside main's
`tanstack-start` addition to `hasWorkflowSourceMaps`.
Verified locally: `@workflow/core` and `@workflow/world-local`
typecheck clean; all 343 world-local unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone tarballs project's `vercel.json` set
`"buildCommand": "pnpm --filter tarballs build"`, which only runs the
tarballs package's `build` script (`node scripts/pack.ts`) and does
NOT trigger turbo's `dependsOn: ["^build"]` chain. As a result, every
workspace package was packed with an empty `dist/` directory — only
`bin/`, `package.json`, `README.md`, and `docs/` made it into the
tarballs. Downstream installs failed with
`Cannot find module '<pkg>/dist/next.cjs'` (and similar for every
other entry point).
The previous docs-based pipeline didn't hit this because Vercel
detected `docs` as a Next.js framework, ran turbo for it, and pack.ts
was wired as a `prebuild` step that ran after deps were already
built.
Fix: switch the buildCommand to `pnpm turbo build --filter=tarballs`
so turbo evaluates the build task for `tarballs`, which fans out via
`^build` to compile every workspace dependency first, and then runs
`tarballs#build` (pack.ts) against the populated `dist/` directories.
Verified locally: a fresh build produces tarballs containing the full
`dist/` tree (including `dist/next.cjs`, the file flight-booking-app
was failing to resolve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/src/logger.ts` (PR #1849 vs. V2 webpack carve-out)
PR #1849 ("Friendlier workflow errors") reintroduced the `debug`
package as a static import and added a structured `Logger`
interface with `child()` / `forRun()` and `composeLogLine`. The V2
branch had previously removed the `debug` static import because it
pulls `debug/src/node` and a dynamic `require('tty')` into the
Next.js webpack flow route, breaking V2 builds with
`Dynamic require of "tty" is not supported`.
Resolution combines both: keep main's `Logger` interface,
`child()` / `forRun()`, and `composeLogLine` integration; replace
the `debug` invocations with the V2 lightweight
`matchesDebugNamespace` + `process.env.DEBUG` matcher so the flow
route still bundles cleanly under webpack. Comment in the file
documents why `debug` is intentionally absent.
* `packages/core/src/runtime.ts` (PR #1849 vs. V2 inline replay loop)
Multiple regions where main's structured-logging refactor and the
V2 inline-execution loop made overlapping changes. Took V2's
control flow as the base — its `if (!workflowRun) { ... }` setup
block, main replay `while (true)` loop with timeout-based
re-scheduling, terminal-event short-circuit, and per-iteration
incremental event load are all preserved as-is — and folded in
main's contributions: the scoped `runLogger = runtimeLogger.forRun(
runId, workflowName)` is created once and used for run-level info
/ error logging. Discarded main's V1 wait-completion loop and
`runWorkflow` user-code error block since V2 handles waits and
user-code errors inside the inline loop body. Updated
`buildWorkflowSuspensionMessage()` call to drop a stray `runId`
first arg that didn't match the helper's 3-param signature.
* `packages/core/e2e/e2e.test.ts` (PR #1849 unrelated to V2)
No conflict from main's friendly-errors PR, auto-merged. (No
hookDispose-style conflict this round.)
Verified locally: all 820 `@workflow/core` unit tests pass; all 343
`@workflow/world-local` unit tests pass; `pnpm turbo typecheck` is
green across all 40 typecheck tasks.
Co-Authored-By: Claude Opus 4.7 (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

@VaguelySerious@shtefcs@TooTallNate
, '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('^' + ".*" + ' [core] Combine flow+step bundle and process steps eagerly by VaguelySerious · Pull Request #1338 · vercel/workflow · GitHub
Skip to content

[core] Combine flow+step bundle and process steps eagerly - #1338

Merged
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow
May 4, 2026
Merged

[core] Combine flow+step bundle and process steps eagerly#1338
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow

Conversation

@changeset-bot

changeset-botBot commented Mar 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 234fce4

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

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

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 Mar 11, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Mar 11, 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.033s (-24.6% 🟢)1.005s (~)0.972s101.00x
💻 LocalExpress0.034s (-23.3% 🟢)1.006s (~)0.972s101.05x
🐘 PostgresNext.js (Turbopack)0.049s1.013s0.964s101.50x
💻 LocalNext.js (Turbopack)0.049s1.005s0.956s101.51x
🐘 PostgresNitro0.051s (-46.8% 🟢)1.012s (-3.0%)0.961s101.56x
🌐 RedisNext.js (Turbopack)0.055s1.005s0.951s101.68x
🌐 MongoDBNext.js (Turbopack)0.109s1.007s0.899s103.35x
🐘 PostgresExpress0.177s (+205.2% 🔺)1.107s (+9.5% 🔺)0.930s105.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.236s (-42.4% 🟢)1.939s (-22.7% 🟢)1.703s101.00x
▲ VercelExpress0.241s (+2.2%)2.009s (-5.9% 🟢)1.768s101.02x
▲ VercelNext.js (Turbopack)0.550s (+118.6% 🔺)2.715s (+16.4% 🔺)2.165s102.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.069s (-5.0% 🟢)2.006s (~)0.937s101.00x
💻 LocalNitro1.073s (-5.1% 🟢)2.006s (~)0.933s101.00x
🐘 PostgresNitro1.084s (-4.9%)2.010s (~)0.927s101.01x
🌐 RedisNext.js (Turbopack)1.113s2.006s0.893s101.04x
🐘 PostgresNext.js (Turbopack)1.114s2.007s0.893s101.04x
💻 LocalNext.js (Turbopack)1.121s2.007s0.886s101.05x
🐘 PostgresExpress1.143s (~)2.021s (+0.5%)0.878s101.07x
🌐 MongoDBNext.js (Turbopack)1.162s2.009s0.847s101.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.498s (-20.1% 🟢)3.691s (-3.1%)2.193s101.00x
▲ VercelNitro1.524s (-60.9% 🟢)3.710s (-37.2% 🟢)2.186s101.02x
▲ VercelNext.js (Turbopack)1.942s (-4.6%)4.457s (+16.4% 🔺)2.516s101.30x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.409s (-4.2%)11.018s (~)0.609s31.00x
💻 LocalNitro10.413s (-4.9%)11.022s (~)0.608s31.00x
💻 LocalExpress10.436s (-4.5%)11.021s (~)0.586s31.00x
🐘 PostgresNext.js (Turbopack)10.568s11.013s0.445s31.02x
🌐 RedisNext.js (Turbopack)10.657s11.024s0.367s31.02x
💻 LocalNext.js (Turbopack)10.685s11.022s0.337s31.03x
🐘 PostgresExpress10.745s (-2.0%)11.419s (+3.6%)0.674s31.03x
🌐 MongoDBNext.js (Turbopack)10.789s11.018s0.229s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.059s (-40.8% 🟢)15.700s (-37.5% 🟢)1.641s21.00x
▲ VercelNext.js (Turbopack)14.845s (-14.3% 🟢)16.872s (-13.0% 🟢)2.027s21.06x
▲ VercelExpress15.996s (-5.8% 🟢)18.196s (-9.1% 🟢)2.200s21.14x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.431s (-10.8% 🟢)14.027s (-12.5% 🟢)0.596s51.00x
🐘 PostgresNitro13.434s (-8.0% 🟢)14.013s (-6.8% 🟢)0.579s51.00x
💻 LocalExpress13.490s (-9.9% 🟢)14.027s (-6.7% 🟢)0.537s51.00x
🐘 PostgresNext.js (Turbopack)13.820s14.018s0.198s51.03x
💻 LocalNext.js (Turbopack)14.076s15.030s0.954s41.05x
🌐 RedisNext.js (Turbopack)14.088s15.030s0.942s41.05x
🌐 MongoDBNext.js (Turbopack)14.283s15.019s0.736s41.06x
🐘 PostgresExpress14.537s (~)15.276s (+1.7%)0.739s41.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.821s (-58.6% 🟢)22.773s (-56.7% 🟢)1.952s31.00x
▲ VercelNitro21.829s (-66.1% 🟢)23.502s (-64.7% 🟢)1.673s31.05x
▲ VercelNext.js (Turbopack)23.199s (-55.9% 🟢)25.174s (-53.9% 🟢)1.975s31.11x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.846s (-29.4% 🟢)12.022s (-29.4% 🟢)0.176s81.00x
🐘 PostgresNitro11.923s (-14.6% 🟢)12.392s (-13.4% 🟢)0.469s81.01x
💻 LocalExpress11.997s (-27.7% 🟢)12.398s (-27.2% 🟢)0.401s81.01x
🌐 RedisNext.js (Turbopack)13.015s13.454s0.439s71.10x
💻 LocalNext.js (Turbopack)13.089s14.026s0.937s71.10x
🐘 PostgresNext.js (Turbopack)13.215s13.585s0.370s71.12x
🌐 MongoDBNext.js (Turbopack)13.301s14.021s0.720s71.12x
🐘 PostgresExpress15.027s (+7.3% 🔺)15.400s (+5.5% 🔺)0.373s61.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.095s (-74.3% 🟢)33.159s (-73.2% 🟢)2.064s31.00x
▲ VercelNitro31.508s (-92.5% 🟢)33.124s (-92.2% 🟢)1.615s31.01x
▲ VercelNext.js (Turbopack)31.987s (-91.9% 🟢)33.996s (-91.4% 🟢)2.009s31.03x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.140s (-10.6% 🟢)2.007s (~)0.868s151.00x
🐘 PostgresNext.js (Turbopack)1.168s2.007s0.838s151.03x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.187s (-20.3% 🟢)2.006s (~)0.820s151.04x
🐘 PostgresExpress1.237s (-1.9%)2.018s (~)0.781s151.09x
🌐 RedisNext.js (Turbopack)1.238s2.006s0.768s151.09x
💻 LocalNext.js (Turbopack)1.279s2.005s0.726s151.12x
🌐 MongoDBNext.js (Turbopack)2.048s2.917s0.869s111.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.551s (-10.8% 🟢)4.187s (-9.4% 🟢)1.636s81.00x
▲ VercelNitro2.674s (-5.1% 🟢)4.258s (-1.5%)1.584s81.05x
▲ VercelNext.js (Turbopack)4.193s (+23.4% 🔺)6.817s (+38.2% 🔺)2.624s51.64x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.225s (-47.9% 🟢)2.007s (-33.3% 🟢)0.782s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.05x
🐘 PostgresExpress1.483s (-37.2% 🟢)2.019s (-32.9% 🟢)0.536s151.21x
💻 LocalNitro1.726s (-45.1% 🟢)2.072s (-46.7% 🟢)0.346s151.41x
💻 LocalNext.js (Turbopack)1.831s2.149s0.318s141.50x
💻 LocalExpress1.905s (-35.5% 🟢)2.150s (-37.8% 🟢)0.244s141.56x
🌐 RedisNext.js (Turbopack)2.364s3.008s0.643s101.93x
🌐 MongoDBNext.js (Turbopack)3.568s4.008s0.440s82.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.844s (-5.1% 🟢)5.284s (-10.8% 🟢)1.440s61.00x
▲ VercelExpress3.884s (+7.3% 🔺)6.116s (+19.7% 🔺)2.232s51.01x
▲ VercelNext.js (Turbopack)5.098s (-28.2% 🟢)7.574s (-15.0% 🟢)2.476s41.33x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.419s (-59.2% 🟢)2.007s (-49.9% 🟢)0.588s151.00x
🐘 PostgresNext.js (Turbopack)1.493s2.007s0.514s151.05x
🐘 PostgresExpress2.153s (-38.2% 🟢)2.861s (-28.7% 🟢)0.708s111.52x
🌐 RedisNext.js (Turbopack)3.634s4.010s0.376s82.56x
💻 LocalNitro4.547s (-45.5% 🟢)5.013s (-44.4% 🟢)0.466s63.20x
💻 LocalExpress4.829s (-42.1% 🟢)5.512s (-38.9% 🟢)0.683s63.40x
💻 LocalNext.js (Turbopack)5.581s6.213s0.631s53.93x
🌐 MongoDBNext.js (Turbopack)6.289s7.012s0.723s54.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.147s (+21.4% 🔺)7.340s (+19.8% 🔺)2.193s51.00x
▲ VercelNitro5.410s (+53.5% 🔺)7.160s (+29.4% 🔺)1.750s51.05x
▲ VercelNext.js (Turbopack)7.157s (-19.7% 🟢)8.877s (-19.0% 🟢)1.721s41.39x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.170s (-6.9% 🟢)2.007s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.173s2.007s0.835s151.00x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.774s151.05x
💻 LocalNext.js (Turbopack)1.327s2.006s0.679s151.13x
💻 LocalNitro1.380s (-26.0% 🟢)2.006s (-14.3% 🟢)0.626s151.18x
💻 LocalExpress1.393s (-26.5% 🟢)2.006s (-15.1% 🟢)0.613s151.19x
🐘 PostgresExpress1.408s (+12.0% 🔺)2.062s (+2.7%)0.654s151.20x
🌐 MongoDBNext.js (Turbopack)2.036s2.916s0.880s111.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.271s (-7.6% 🟢)4.045s (-3.0%)1.774s81.00x
▲ VercelExpress2.336s (-9.5% 🟢)3.766s (-13.4% 🟢)1.430s81.03x
▲ VercelNext.js (Turbopack)4.170s (+42.2% 🔺)6.038s (+30.1% 🔺)1.869s51.84x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.251s (-46.5% 🟢)2.007s (-33.3% 🟢)0.757s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.03x
🐘 PostgresExpress1.393s (-40.5% 🟢)2.035s (-32.4% 🟢)0.642s151.11x
💻 LocalNitro1.957s (-36.2% 🟢)2.316s (-40.4% 🟢)0.359s131.56x
💻 LocalExpress2.022s (-35.4% 🟢)2.508s (-33.3% 🟢)0.485s121.62x
💻 LocalNext.js (Turbopack)2.130s2.826s0.696s111.70x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.88x
🌐 MongoDBNext.js (Turbopack)3.543s4.007s0.464s82.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.338s (+35.9% 🔺)6.011s (+25.4% 🔺)1.673s51.00x
▲ VercelNitro4.664s (+44.2% 🔺)6.658s (+31.1% 🔺)1.994s51.08x
▲ VercelNext.js (Turbopack)4.714s (+50.0% 🔺)6.542s (+44.7% 🔺)1.827s51.09x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.380s (-60.3% 🟢)2.007s (-49.9% 🟢)0.627s151.00x
🐘 PostgresNext.js (Turbopack)1.483s2.008s0.525s151.07x
🐘 PostgresExpress1.982s (-43.4% 🟢)2.737s (-31.8% 🟢)0.755s111.44x
🌐 RedisNext.js (Turbopack)3.571s4.010s0.439s82.59x
💻 LocalNitro4.894s (-46.5% 🟢)5.348s (-46.7% 🟢)0.454s63.55x
💻 LocalExpress5.939s (-32.5% 🟢)6.417s (-30.8% 🟢)0.478s54.30x
💻 LocalNext.js (Turbopack)6.115s6.416s0.301s54.43x
🌐 MongoDBNext.js (Turbopack)6.302s7.013s0.710s54.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.380s (+5.6% 🔺)7.062s (+3.6%)1.682s51.00x
▲ VercelNext.js (Turbopack)5.690s (-15.8% 🟢)7.416s (-13.2% 🟢)1.726s51.06x
▲ VercelExpress5.700s (-11.2% 🟢)7.539s (-7.8% 🟢)1.840s51.06x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.459s (-44.1% 🟢)1.022s (+1.6%)0.564s591.00x
💻 LocalNitro0.472s (-51.8% 🟢)1.021s (-6.7% 🟢)0.548s591.03x
💻 LocalExpress0.482s (-51.1% 🟢)1.004s (-6.7% 🟢)0.522s601.05x
🐘 PostgresNext.js (Turbopack)0.599s1.040s0.442s581.31x
🌐 RedisNext.js (Turbopack)0.619s1.004s0.386s601.35x
🐘 PostgresExpress0.644s (-23.2% 🟢)1.140s (+11.4% 🔺)0.496s531.40x
💻 LocalNext.js (Turbopack)0.719s1.004s0.285s601.57x
🌐 MongoDBNext.js (Turbopack)0.733s1.006s0.272s601.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.661s (-75.5% 🟢)6.339s (-70.3% 🟢)1.677s101.00x
▲ VercelNitro5.218s (-76.3% 🟢)7.258s (-69.8% 🟢)2.039s101.12x
▲ VercelNext.js (Turbopack)6.455s (-55.5% 🟢)7.922s (-50.7% 🟢)1.466s81.38x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.085s (-43.7% 🟢)1.705s (-18.8% 🟢)0.620s531.00x
🐘 PostgresExpress1.162s (-41.2% 🟢)1.588s (-29.7% 🟢)0.426s571.07x
💻 LocalExpress1.197s (-60.3% 🟢)2.006s (-44.1% 🟢)0.809s451.10x
💻 LocalNitro1.220s (-59.8% 🟢)2.005s (-46.6% 🟢)0.786s451.12x
🌐 RedisNext.js (Turbopack)1.476s2.006s0.530s451.36x
🐘 PostgresNext.js (Turbopack)1.707s2.387s0.679s381.57x
💻 LocalNext.js (Turbopack)1.753s2.005s0.253s451.61x
🌐 MongoDBNext.js (Turbopack)1.813s2.007s0.194s451.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.733s (-70.3% 🟢)13.729s (-66.8% 🟢)1.996s71.00x
▲ VercelExpress12.793s (-62.9% 🟢)14.645s (-60.2% 🟢)1.852s71.09x
▲ VercelNext.js (Turbopack)14.268s (-71.4% 🟢)16.580s (-67.9% 🟢)2.313s61.22x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.108s (-48.6% 🟢)2.696s (-41.4% 🟢)0.588s451.00x
🐘 PostgresExpress2.634s (-34.0% 🟢)3.074s (-29.6% 🟢)0.440s401.25x
🐘 PostgresNext.js (Turbopack)2.659s3.084s0.425s391.26x
💻 LocalNitro2.660s (-71.4% 🟢)3.007s (-70.0% 🟢)0.348s401.26x
💻 LocalExpress2.743s (-70.2% 🟢)3.033s (-69.7% 🟢)0.290s401.30x
🌐 RedisNext.js (Turbopack)2.992s3.191s0.200s381.42x
💻 LocalNext.js (Turbopack)3.748s4.007s0.259s301.78x
🌐 MongoDBNext.js (Turbopack)4.178s5.012s0.833s241.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express35.192s (-72.9% 🟢)37.772s (-71.4% 🟢)2.580s41.00x
▲ VercelNitro36.812s (-62.0% 🟢)39.053s (-60.3% 🟢)2.241s41.05x
▲ VercelNext.js (Turbopack)43.007s (-59.9% 🟢)46.755s (-57.1% 🟢)3.748s31.22x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-44.2% 🟢)1.007s (~)0.850s601.00x
🐘 PostgresNitro0.182s (-35.8% 🟢)1.006s (~)0.824s601.15x
🐘 PostgresNext.js (Turbopack)0.190s1.005s0.815s601.21x
🌐 RedisNext.js (Turbopack)0.316s1.021s0.705s592.00x
💻 LocalNitro0.412s (-31.9% 🟢)1.004s (-1.7%)0.592s602.61x
💻 LocalExpress0.452s (-19.3% 🟢)1.004s (~)0.552s602.87x
💻 LocalNext.js (Turbopack)0.603s1.039s0.436s583.83x
🌐 MongoDBNext.js (Turbopack)1.029s1.749s0.720s356.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.178s (+11.5% 🔺)4.588s (+26.2% 🔺)2.410s141.00x
▲ VercelNitro2.258s (+36.0% 🔺)4.485s (+33.9% 🔺)2.227s141.04x
▲ VercelNext.js (Turbopack)3.716s (+83.7% 🔺)5.458s (+43.9% 🔺)1.742s111.71x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.304s (-38.8% 🟢)1.006s (~)0.702s901.00x
🌐 RedisNext.js (Turbopack)0.423s1.004s0.581s901.39x
🐘 PostgresExpress0.520s (+2.0%)1.112s (+10.5% 🔺)0.592s821.71x
🐘 PostgresNext.js (Turbopack)0.705s1.318s0.613s692.32x
💻 LocalNitro2.170s (-14.5% 🟢)2.821s (-6.3% 🟢)0.651s327.14x
💻 LocalExpress2.186s (-13.0% 🟢)2.944s (-2.2%)0.758s317.19x
💻 LocalNext.js (Turbopack)2.424s3.043s0.619s307.97x
🌐 MongoDBNext.js (Turbopack)2.595s3.007s0.412s308.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.618s (+87.2% 🔺)8.356s (+60.9% 🔺)1.738s111.00x
▲ VercelNitro7.134s (+121.1% 🔺)9.355s (+94.0% 🔺)2.221s101.08x
▲ VercelExpress7.527s (+147.1% 🔺)9.552s (+98.7% 🔺)2.025s101.14x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.637s (-19.4% 🟢)1.006s (~)0.369s1201.00x
🐘 PostgresNext.js (Turbopack)0.730s1.013s0.284s1191.15x
🐘 PostgresExpress0.790s (-3.5%)1.192s (+17.1% 🔺)0.402s1011.24x
🌐 RedisNext.js (Turbopack)0.797s1.004s0.207s1201.25x
🌐 MongoDBNext.js (Turbopack)5.387s6.013s0.626s208.46x
💻 LocalNitro9.790s (-12.5% 🟢)10.276s (-11.9% 🟢)0.486s1215.37x
💻 LocalExpress10.085s (-9.9% 🟢)10.782s (-9.7% 🟢)0.697s1215.84x
💻 LocalNext.js (Turbopack)11.478s12.128s0.650s1018.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.821s (+117.8% 🔺)19.141s (+103.6% 🔺)2.319s71.00x
▲ VercelNext.js (Turbopack)18.466s (+78.8% 🔺)20.419s (+66.2% 🔺)1.953s71.10x
▲ VercelExpress20.247s (+172.9% 🔺)22.460s (+143.0% 🔺)2.213s61.20x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.133s (+430.1% 🔺)2.005s (+99.6% 🔺)0.010s (-19.2% 🟢)2.017s (+98.0% 🔺)0.884s101.00x
🐘 PostgresNitro1.134s (+453.4% 🔺)2.001s (+100.2% 🔺)0.001s (-26.7% 🟢)2.010s (+98.7% 🔺)0.875s101.00x
💻 LocalExpress1.136s (+470.5% 🔺)2.005s (+99.6% 🔺)0.012s (~)2.020s (+98.4% 🔺)0.884s101.00x
💻 LocalNext.js (Turbopack)1.172s2.003s0.012s2.019s0.847s101.03x
🐘 PostgresExpress1.232s (+500.5% 🔺)2.005s (+100.8% 🔺)0.001s (-37.5% 🟢)2.041s (+101.8% 🔺)0.810s101.09x
🐘 PostgresNext.js (Turbopack)2.024s2.601s0.220s2.874s0.850s101.79x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.200s (-42.6% 🟢)3.478s (-34.1% 🟢)1.562s (+110.5% 🔺)5.432s (-16.2% 🟢)3.232s101.00x
▲ VercelExpress2.336s (-6.8% 🟢)3.935s (-3.8%)1.087s (+13.1% 🔺)5.425s (-3.0%)3.089s101.06x
▲ VercelNext.js (Turbopack)4.802s (-29.9% 🟢)4.900s (-43.4% 🟢)0.740s (+17.0% 🔺)7.346s (-25.0% 🟢)2.544s102.18x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.512s (+142.2% 🔺)2.005s (+99.2% 🔺)0.004s (-8.9% 🟢)2.024s (+97.9% 🔺)0.512s301.00x
💻 LocalNitro1.514s (+80.5% 🔺)2.011s (+98.7% 🔺)0.010s (+2.8%)2.022s (+81.2% 🔺)0.509s301.00x
💻 LocalExpress1.721s (+127.3% 🔺)2.011s (+95.5% 🔺)0.010s (+2.1%)2.202s (+111.7% 🔺)0.481s281.14x
🐘 PostgresNext.js (Turbopack)1.722s2.107s0.003s2.123s0.401s301.14x
💻 LocalNext.js (Turbopack)1.845s2.011s0.009s2.203s0.358s281.22x
🐘 PostgresExpress1.983s (+214.8% 🔺)2.330s (+131.5% 🔺)0.003s (-20.7% 🟢)2.395s (+134.1% 🔺)0.412s261.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.656s (-13.0% 🟢)7.281s (-9.1% 🟢)0.179s (-56.1% 🟢)8.330s (-5.7% 🟢)2.675s81.00x
▲ VercelNitro5.796s (-80.3% 🟢)7.455s (-75.8% 🟢)0.170s (+52.1% 🔺)8.071s (-74.6% 🟢)2.275s81.02x
▲ VercelNext.js (Turbopack)12.255s (-27.6% 🟢)13.600s (-25.4% 🟢)0.201s (-4.9%)15.301s (-19.2% 🟢)3.046s42.17x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.651s (-32.8% 🟢)1.033s (-17.2% 🟢)0.000s (-100.0% 🟢)1.042s (-17.2% 🟢)0.390s581.00x
🐘 PostgresExpress1.031s (+7.3% 🔺)1.390s (+8.7% 🔺)0.000s (+9.5% 🔺)1.437s (+10.0% 🔺)0.406s421.58x
🐘 PostgresNext.js (Turbopack)1.333s1.618s0.000s1.715s0.382s372.05x
💻 LocalNitro1.337s (+9.4% 🔺)2.015s (~)0.000s (+166.7% 🔺)2.017s (~)0.680s302.05x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-60.0% 🟢)2.017s (~)0.649s302.10x
💻 LocalNext.js (Turbopack)1.436s2.013s0.000s2.016s0.581s302.20x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.922s (+28.6% 🔺)5.598s (+27.4% 🔺)0.000s (-100.0% 🟢)6.027s (+25.3% 🔺)2.105s101.00x
▲ VercelExpress4.013s (+7.3% 🔺)5.879s (+15.2% 🔺)0.000s (-100.0% 🟢)6.416s (+16.0% 🔺)2.404s101.02x
▲ VercelNext.js (Turbopack)5.465s (-46.3% 🟢)6.707s (-41.8% 🟢)0.000s (+Infinity% 🔺)7.645s (-36.6% 🟢)2.181s81.39x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.327s (-25.9% 🟢)2.066s (-3.5%)0.000s (-100.0% 🟢)2.081s (-4.3%)0.754s291.00x
🐘 PostgresNext.js (Turbopack)1.844s2.402s0.000s2.460s0.616s251.39x
🐘 PostgresExpress2.377s (+34.2% 🔺)3.042s (+39.7% 🔺)0.000s (NaN%)3.091s (+40.6% 🔺)0.714s201.79x
💻 LocalNext.js (Turbopack)2.776s3.362s0.000s3.365s0.589s182.09x
💻 LocalNitro3.068s (-9.4% 🟢)3.838s (-4.8%)0.000s (-53.1% 🟢)3.843s (-4.8%)0.775s162.31x
💻 LocalExpress3.103s (-10.5% 🟢)3.775s (-6.4% 🟢)0.000s (-76.6% 🟢)3.781s (-6.3% 🟢)0.679s162.34x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.059s (+10.3% 🔺)6.788s (+12.7% 🔺)0.000s (+Infinity% 🔺)7.571s (+17.3% 🔺)2.512s81.00x
▲ VercelNitro5.089s (+24.3% 🔺)6.536s (+21.6% 🔺)0.000s (-18.5% 🟢)7.002s (+20.9% 🔺)1.913s91.01x
▲ VercelNext.js (Turbopack)8.675s (+54.5% 🔺)10.065s (+44.2% 🔺)0.000s (-100.0% 🟢)11.283s (+49.6% 🔺)2.608s61.71x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro17/21
🐘 PostgresNitro19/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other4380150588
Total4266010265292

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025

📋 View full workflow run

Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/vitest/src/index.ts
Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/core/src/workflow.ts Outdated
Comment threadpackages/builders/src/base-builder.ts
Comment threadpackages/core/src/runtime/get-world-lazy.ts

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Remaining issues:

  • getWorldLazy needs to be validated as a decision
  • packages/core/e2e/utils.ts skip for hasStepSourceMaps needs to be re-evaluated

@shtefcs

Copy link
Copy Markdown

Seems you had a lot of work here. It's not easy as it seems. Hopefully, this gonna solve some issues we are facing with Workflow.

Peace,

@TooTallNateTooTallNate 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.

Withdrawing my prior CHANGES_REQUESTED — the blocker I called out (the stale (err as any).meta?.retryAfter access in step-executor.ts) is fixed at line 169 with err.retryAfter ?? 1. As an aside, my fix recommendation in the prior review was based on a faulty type assumption (I confused TooEarlyError.retryAfter: number with RetryableError.retryAfter: Date). The author's err.retryAfter ?? 1 form is correct: it's a number of seconds with sensible fallback.

My other earlier concerns:

  • getStepNameFromEvent full-event-load: addressed. stepName is now plumbed through WorkflowInvokePayloadSchema so the handler reads it directly from the queue message payload instead of doing a paginated event load. ✓
  • step.error guard for max retries: known limitation acknowledged in-thread. The proposed long-term fix (server-side ne(status, 'running') on step_started) is separate work.
  • Background step → continuation enqueue safety: acknowledged as intentional.

So my specific items are resolved. But I'm not approving outright — there's still substantial unresolved review from @VaguelySerious touching real concerns I'd defer to them on:

  • Removal of the LegacyStreamWorld shim
  • Limiting queue concurrency to 1 in tests
  • Comments removed without explanation in suspension-handler.ts / runtime.ts
  • The builder-deferred.ts Next.js behavior prediction
  • Several "is there a systematic fix instead of conditionally allowing edge cases" questions

Plus the auto-flagged event cache duplication bug from Vercel's review bot is worth confirming was understood and dismissed (or fixed).

Approval-wise this should land when @VaguelySerious's concerns are resolved. From my side: no remaining blockers.

@TooTallNateTooTallNate 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.

Thorough re-review for Peter. Spent significant time tracing four risk surfaces: the Vercel bot's auto-flag, the concurrent-step-execution model, inline-step replay determinism, and the open review threads. Bottom line up front: no ship-blockers from my side. A few coverage gaps and one acknowledged limitation worth being explicit about, but nothing I'd hold this PR on.

Vercel bot's "event cache duplication" auto-flag is a false positive

"wait_completed events pushed locally without advancing eventsCursor cause duplicate events on next incremental fetch."

The author anticipated exactly this scenario. runtime.ts:585–594 has explicit dedupe-by-eventId logic:

constexistingIds=newSet(cachedEvents.map((e)=>e.eventId));for(consteofloaded.events){if(!existingIds.has(e.eventId)){cachedEvents.push(e);}}

Verified end-to-end: locally-pushed wait_completed (line 685) keeps its real server-returned eventId, the next iteration's incremental fetch returns it again, the dedupe filter drops it, the consumer never sees a duplicate. The comment in the code calls out the exact scenario the bot flagged. Safe to dismiss the auto-flag.

Concurrent-step-execution: a partial fix shipping with a known gap

Verified the L198 guard (step.attempt > maxRetries + 1 && step.error) traces correctly for the scenario it targets — concurrent first-attempts before any user-code error has been recorded. Without step.error, the guard correctly skips and the step proceeds normally even with inflated step.attempt.

But there's a symmetric gap in the catch-block path at L469:

if(currentAttempt>=maxRetries+1){/* step_failed */}

This check has no && step.error guard, so it fires on the first real catch-block failure even when currentAttempt has been inflated by concurrent first-attempts. End result: a workflow with maxRetries=3 and 4+ concurrent handlers can permanently fail after a single transient flake, with a misleading log saying "exceeded max retries (4 retries)" when the step body actually only ran once.

This is the only substantive open thread on the PR (step-executor.ts:198), and the author has acknowledged it in a separate resolved thread:

"Agreed on the concurrent-retry limitation. The server-side ne(status, 'running') WHERE clause is the right long-term fix — tracking as a follow-up."

So everyone's eyes-open. Worth being explicit in the changelog or PR description that under high queue back-pressure, default-maxRetries workflows can spuriously fail. Either of these would close the gap fully:

  1. Add the same && step.error guard at L469 (mirrors L198, easy to do, but doesn't fix the L198 case for catch-block).
  2. Server-side ne(status, 'running') on step_started (referenced in the resolved thread; also fixes the world-testing concurrency issue Peter resolved separately).

Not a ship-stopper, but I'd want it tracked as a known issue users can hit, not just an internal todo.

Inline-step replay: ordering subtlety in the dedupe path

The locally-pushed-then-incremental-fetch path can produce non-monotonic-by-eventId ordering in cachedEvents. Specifically: after wait_completed is pushed locally (L685) without advancing the cursor, the next incremental fetch can return concurrent events with eventIds between the local push and earlier events. After dedupe, cachedEvents becomes append-ordered but not eventId-ordered. The EventsConsumer walks by index, so it consumes them in append order, which is not the canonical replay order.

In practice this is unlikely to cause issues because:

  • Within one handler's writes, the same world process generates monotonic ULIDs.
  • Cross-handler events for the same run are gated by the ownership check (step_created ownership at L813).
  • The dangerous case requires concurrent handler activity for the same run + non-trivial cross-process clock skew.

But it's a footgun. The cleanest fix would be: after locally pushing an event, set eventsCursor to that event's eventId so the next fetch skips it. This also eliminates the dedupe pass entirely. Worth considering as a follow-up.

"VaguelySerious" review threads — context for Peter

For other reviewers who may stumble on this: most of the open @VaguelySerious threads are Peter's own AI-review notes posted via tooling. Of 28 such threads, 27 are either resolved or outdated (the diff has moved past them). The single unresolved + non-outdated thread is the concurrent-retry concern above (step-executor.ts:198), which Peter has acknowledged as a known limitation in a sibling resolved thread.

External-reviewer threads (mine, the Vercel bot's) are all resolved or addressed. So the apparent "long list of unresolved concerns" from my prior comment is misleading — it was conflating Peter's self-review notes with external review.

Test coverage gaps (non-blocking)

A few coverage gaps for novel V2 behavior worth tracking even if you ship:

  1. Dedupe path at runtime.ts:585–594 — zero test coverage. The behavior is novel to V2 and load-bearing for incremental-replay correctness. A unit test that:

    • First iteration fetches with cursor C0
    • Manually pushes a wait_completed to cachedEvents
    • Second iteration fetches and gets the same wait_completed plus an interleaved new event
    • Asserts: no duplicates AND consumer sees no orphans
      ...would lock in the contract.
  2. L198 guard's positive case — there's no test asserting that attempt > maxRetries + 1withoutstep.error does NOT fail the step. The guard exists specifically for this case and is currently un-asserted. The two step-handler.test.ts tests at lines 296–340 and 343–388 both set error on the fixture, so they exercise different paths.

  3. Concurrent-handler races — neither Scenario B (concurrent retry with prior error → premature L198 failure) nor Scenario D (catch-block premature failure with no && step.error guard) has a test. Adding these would catch the bug currently in code if a future fix lands.

  4. builder-deferred.ts Next.js manifest reading — Peter agreed in review that reading app-paths-manifest.json was the right approach over regex prediction. Verified that DID land (L1171–1175 reads the manifest with comment "This avoids predicting Next.js conventions with regexes and instead reads from Next.js's own output"). ✅

What looks good

  • The architectural shift is well-executed. The replay loop, ownership-gated inline dispatch, idempotency-key-based queue dedupe, builder unification, and __steps_registered rollup-tree-shaking guard are all clean.
  • The getStepNameFromEvent full-event-load concern from my prior review was addressed — stepName now flows through WorkflowInvokePayloadSchema. ✓
  • The TooEarlyError stale retryAfter access pattern is fixed at step-executor.ts:169. ✓
  • The CJS module.exports collision (ESM format for steps when bundleFinalOutput: true) is a subtle but correct fix.
  • The changelog doc docs/content/docs/changelog/eager-processing.mdx is one of the better architecture docs I've seen in a PR — covers every edge case, failure mode, and platform-specific quirk.

Verdict

No blockers from my side. Ship if Vade and storytime green-light. The concurrent-retry premature-failure case is real but acknowledged, the auto-flagged "event cache duplication" is a non-issue, and the test coverage gaps are real but not regressions (the V2 behaviors they cover are net-new).

If you want one piece of polish before merge, the L469 && step.error mirror would close the most user-visible failure mode at zero cost — but I won't gate on it.

@TooTallNateTooTallNate 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.

Pre-emptively :approved: but my agent really wants @VaguelySerious to resolve the open threads :lol: and there seems to be a few other things pointed out in the latest review comment.

VaguelySeriousand others added 7 commits May 3, 2026 11:38
Merges the V1 split flow/step routes (one queue message per step + one per
flow continuation, two separate function invocations per step) into a
single combined handler at /.well-known/workflow/v1/flow that executes
steps inline within the same invocation as the workflow replay.
A serial workflow with N steps that previously required ~2N+1 function
invocations now completes in 1.
Architecture
------------
- packages/core: workflowEntrypoint() handler with an inline replay loop.
Each loop iteration: load events incrementally, replay workflow,
inline-execute one owned step on suspension, loop. Background steps
(from Promise.all) are queued back to the same handler with stepId so
the in-flight handler advances the loop without queue round-trips
whenever possible.
- packages/builders: createCombinedBundle() emits a single ESM bundle
with workflowEntrypoint(workflowCode) plus a side-effect
__step_registrations bundle. ESM-by-default with createRequire banner
for CJS deps (matches main's #1562 pattern).
- packages/next, nestjs, sveltekit, astro, nitro, nuxt, hono, express,
fastify, vite: framework builders updated to use createCombinedBundle.
The step/ directory is no longer generated.
Correctness invariants (V2 polish)
----------------------------------
- Single inline executor per step. Atomic step_created per correlationId
(per-step in-process mutex in world-local; SQL-level guards in
postgres/vercel) means exactly one handler claims ownership of each
step. Inline execution gates on ownership; queue dispatch is
unconditional with idempotency keys for crash recovery (matches V1).
- onUnconsumedEvent reverts to its original PR #1055 contract: any
unconsumed event fatals as CORRUPTED_EVENT_LOG. Source-level fixes
removed every code path that produced orphaned step lifecycle events
during V2 work, so the skip branch became unnecessary.
- Lock-release polling interval lowered from 100ms to 10ms in
flushable-stream so the V2 step-executor's ops-settle race typically
resolves in ~5ms instead of ~50ms per writable-bearing step.
- Per-iteration runs.get round-trip eliminated: concurrent completion
is detected by scanning the loaded event log for terminal run events
instead.
- Worker-pool deadlock guard removed from Run#pollReturnValue. The
fibonacci/recursive parent→child polling case is now handled by
raising world-postgres default queueConcurrency from 10 to 50, with
the limitation documented at three call sites (fibonacciWorkflow's
JSDoc, queue.ts, eager-processing.mdx).
- getWorldLazy module-scope singleton breaks the step-bundle import
chain to world.ts so step bundles don't transitively pull in
world-vercel + cbor-x + the VQS dev-handler resolver.
World-local atomicity
---------------------
- Per-step in-process mutex serializes events.create for step
lifecycle events on a (runId, correlationId) key. step_started is
rejected only when the step is already terminal — duplicate
step_started on a non-terminal step is allowed (preserves retry
semantics after SIGKILL).
Runtime helpers
---------------
- loadWorkflowRunEvents(runId, afterCursor?) consolidates the
previous getAllWorkflowRunEvents{,WithCursor} + getNewWorkflowRunEvents
into one helper.
Documentation
-------------
- docs/content/docs/changelog/eager-processing.mdx: full record of
architecture, edge cases, and the fixes applied during the V2 work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Two CI failures from the earlier V2 cutover, now resolved:
1. @workflow/vitest unit tests (`packages/vitest/src/index.test.ts`)
were still mocking the V1 BaseBuilder methods (createWorkflowsBundle
+ createStepsBundle) and expecting two world handlers (workflow +
step). Updated the mocks to expose createCombinedBundle and
asserted a single combined handler at __wkf_workflow_, which is
what the V2 vitest builder actually emits.
2. E2E Local Dev Tests (nextjs-webpack) were asserting that step
error stacks contain `99_e2e.ts` / `helpers.ts`, which held in
pre-V2 webpack dev mode (which imported step sources directly).
V2 inlines the step bundle into the combined flow route, and
webpack's re-bundling collapses original step filenames out of the
dev-mode source maps. Extended the existing nextjs-webpack
carve-out in hasStepSourceMaps() to cover dev as well as prod, and
updated the source-map follow-up callout in eager-processing.mdx
to include nextjs-webpack in the matrix that needs source maps
wired up for V2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/e2e/e2e.test.ts` (PR #1879 vs. V2 hookDispose helper)
Drop the locally-defined `waitForHook(expectedRunId)` shadow in the
hookDisposeTestWorkflow test in favor of the new top-level
`waitForHook(token, { runId })` API merged from main. Keep V2's
event-driven `waitForHookDisposal()` helper, which is stricter than
main's `await sleep(3_000)` (it polls `getHookByToken` for the
HookNotFoundError signal rather than waiting a fixed interval).
* `packages/world-local/src/storage/events-storage.ts` (PR #1877 vs.
V2 per-step async mutex)
Both branches were closing race conditions in the local world's
step lifecycle. PR #1877 adds filesystem-level O_CREAT|O_EXCL locks
for `step_created` and `wait_created`; the V2 branch already wraps
step lifecycle events in an in-process `withStepLock` mutex. They
compose: the mutex serializes within a single Node process; the
filesystem lock additionally protects against cross-process races
(multiple pnpm workers, redelivered queue messages). Resolution
takes V2's mutex wrap as the base and patches in main's two
`writeExclusive` claims at the start of the `step_created` and
`wait_created` handlers, with comments noting the dual-layer
guarantee.
`packages/core/e2e/utils.ts` auto-merged: V2's nextjs-webpack +
all-Vercel source-map carve-outs preserved alongside main's
`tanstack-start` addition to `hasWorkflowSourceMaps`.
Verified locally: `@workflow/core` and `@workflow/world-local`
typecheck clean; all 343 world-local unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone tarballs project's `vercel.json` set
`"buildCommand": "pnpm --filter tarballs build"`, which only runs the
tarballs package's `build` script (`node scripts/pack.ts`) and does
NOT trigger turbo's `dependsOn: ["^build"]` chain. As a result, every
workspace package was packed with an empty `dist/` directory — only
`bin/`, `package.json`, `README.md`, and `docs/` made it into the
tarballs. Downstream installs failed with
`Cannot find module '<pkg>/dist/next.cjs'` (and similar for every
other entry point).
The previous docs-based pipeline didn't hit this because Vercel
detected `docs` as a Next.js framework, ran turbo for it, and pack.ts
was wired as a `prebuild` step that ran after deps were already
built.
Fix: switch the buildCommand to `pnpm turbo build --filter=tarballs`
so turbo evaluates the build task for `tarballs`, which fans out via
`^build` to compile every workspace dependency first, and then runs
`tarballs#build` (pack.ts) against the populated `dist/` directories.
Verified locally: a fresh build produces tarballs containing the full
`dist/` tree (including `dist/next.cjs`, the file flight-booking-app
was failing to resolve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/src/logger.ts` (PR #1849 vs. V2 webpack carve-out)
PR #1849 ("Friendlier workflow errors") reintroduced the `debug`
package as a static import and added a structured `Logger`
interface with `child()` / `forRun()` and `composeLogLine`. The V2
branch had previously removed the `debug` static import because it
pulls `debug/src/node` and a dynamic `require('tty')` into the
Next.js webpack flow route, breaking V2 builds with
`Dynamic require of "tty" is not supported`.
Resolution combines both: keep main's `Logger` interface,
`child()` / `forRun()`, and `composeLogLine` integration; replace
the `debug` invocations with the V2 lightweight
`matchesDebugNamespace` + `process.env.DEBUG` matcher so the flow
route still bundles cleanly under webpack. Comment in the file
documents why `debug` is intentionally absent.
* `packages/core/src/runtime.ts` (PR #1849 vs. V2 inline replay loop)
Multiple regions where main's structured-logging refactor and the
V2 inline-execution loop made overlapping changes. Took V2's
control flow as the base — its `if (!workflowRun) { ... }` setup
block, main replay `while (true)` loop with timeout-based
re-scheduling, terminal-event short-circuit, and per-iteration
incremental event load are all preserved as-is — and folded in
main's contributions: the scoped `runLogger = runtimeLogger.forRun(
runId, workflowName)` is created once and used for run-level info
/ error logging. Discarded main's V1 wait-completion loop and
`runWorkflow` user-code error block since V2 handles waits and
user-code errors inside the inline loop body. Updated
`buildWorkflowSuspensionMessage()` call to drop a stray `runId`
first arg that didn't match the helper's 3-param signature.
* `packages/core/e2e/e2e.test.ts` (PR #1849 unrelated to V2)
No conflict from main's friendly-errors PR, auto-merged. (No
hookDispose-style conflict this round.)
Verified locally: all 820 `@workflow/core` unit tests pass; all 343
`@workflow/world-local` unit tests pass; `pnpm turbo typecheck` is
green across all 40 typecheck tasks.
Co-Authored-By: Claude Opus 4.7 (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

@VaguelySerious@shtefcs@TooTallNate
, '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); } })(); })(); [core] Combine flow+step bundle and process steps eagerly by VaguelySerious · Pull Request #1338 · vercel/workflow · GitHub
Skip to content

[core] Combine flow+step bundle and process steps eagerly - #1338

Merged
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow
May 4, 2026
Merged

[core] Combine flow+step bundle and process steps eagerly#1338
VaguelySerious merged 7 commits into
mainfrom
peter/v2-flow

Conversation

@changeset-bot

changeset-botBot commented Mar 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 234fce4

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

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

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 Mar 11, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Mar 11, 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.033s (-24.6% 🟢)1.005s (~)0.972s101.00x
💻 LocalExpress0.034s (-23.3% 🟢)1.006s (~)0.972s101.05x
🐘 PostgresNext.js (Turbopack)0.049s1.013s0.964s101.50x
💻 LocalNext.js (Turbopack)0.049s1.005s0.956s101.51x
🐘 PostgresNitro0.051s (-46.8% 🟢)1.012s (-3.0%)0.961s101.56x
🌐 RedisNext.js (Turbopack)0.055s1.005s0.951s101.68x
🌐 MongoDBNext.js (Turbopack)0.109s1.007s0.899s103.35x
🐘 PostgresExpress0.177s (+205.2% 🔺)1.107s (+9.5% 🔺)0.930s105.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.236s (-42.4% 🟢)1.939s (-22.7% 🟢)1.703s101.00x
▲ VercelExpress0.241s (+2.2%)2.009s (-5.9% 🟢)1.768s101.02x
▲ VercelNext.js (Turbopack)0.550s (+118.6% 🔺)2.715s (+16.4% 🔺)2.165s102.33x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.069s (-5.0% 🟢)2.006s (~)0.937s101.00x
💻 LocalNitro1.073s (-5.1% 🟢)2.006s (~)0.933s101.00x
🐘 PostgresNitro1.084s (-4.9%)2.010s (~)0.927s101.01x
🌐 RedisNext.js (Turbopack)1.113s2.006s0.893s101.04x
🐘 PostgresNext.js (Turbopack)1.114s2.007s0.893s101.04x
💻 LocalNext.js (Turbopack)1.121s2.007s0.886s101.05x
🐘 PostgresExpress1.143s (~)2.021s (+0.5%)0.878s101.07x
🌐 MongoDBNext.js (Turbopack)1.162s2.009s0.847s101.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.498s (-20.1% 🟢)3.691s (-3.1%)2.193s101.00x
▲ VercelNitro1.524s (-60.9% 🟢)3.710s (-37.2% 🟢)2.186s101.02x
▲ VercelNext.js (Turbopack)1.942s (-4.6%)4.457s (+16.4% 🔺)2.516s101.30x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro10.409s (-4.2%)11.018s (~)0.609s31.00x
💻 LocalNitro10.413s (-4.9%)11.022s (~)0.608s31.00x
💻 LocalExpress10.436s (-4.5%)11.021s (~)0.586s31.00x
🐘 PostgresNext.js (Turbopack)10.568s11.013s0.445s31.02x
🌐 RedisNext.js (Turbopack)10.657s11.024s0.367s31.02x
💻 LocalNext.js (Turbopack)10.685s11.022s0.337s31.03x
🐘 PostgresExpress10.745s (-2.0%)11.419s (+3.6%)0.674s31.03x
🌐 MongoDBNext.js (Turbopack)10.789s11.018s0.229s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.059s (-40.8% 🟢)15.700s (-37.5% 🟢)1.641s21.00x
▲ VercelNext.js (Turbopack)14.845s (-14.3% 🟢)16.872s (-13.0% 🟢)2.027s21.06x
▲ VercelExpress15.996s (-5.8% 🟢)18.196s (-9.1% 🟢)2.200s21.14x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.431s (-10.8% 🟢)14.027s (-12.5% 🟢)0.596s51.00x
🐘 PostgresNitro13.434s (-8.0% 🟢)14.013s (-6.8% 🟢)0.579s51.00x
💻 LocalExpress13.490s (-9.9% 🟢)14.027s (-6.7% 🟢)0.537s51.00x
🐘 PostgresNext.js (Turbopack)13.820s14.018s0.198s51.03x
💻 LocalNext.js (Turbopack)14.076s15.030s0.954s41.05x
🌐 RedisNext.js (Turbopack)14.088s15.030s0.942s41.05x
🌐 MongoDBNext.js (Turbopack)14.283s15.019s0.736s41.06x
🐘 PostgresExpress14.537s (~)15.276s (+1.7%)0.739s41.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express20.821s (-58.6% 🟢)22.773s (-56.7% 🟢)1.952s31.00x
▲ VercelNitro21.829s (-66.1% 🟢)23.502s (-64.7% 🟢)1.673s31.05x
▲ VercelNext.js (Turbopack)23.199s (-55.9% 🟢)25.174s (-53.9% 🟢)1.975s31.11x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.846s (-29.4% 🟢)12.022s (-29.4% 🟢)0.176s81.00x
🐘 PostgresNitro11.923s (-14.6% 🟢)12.392s (-13.4% 🟢)0.469s81.01x
💻 LocalExpress11.997s (-27.7% 🟢)12.398s (-27.2% 🟢)0.401s81.01x
🌐 RedisNext.js (Turbopack)13.015s13.454s0.439s71.10x
💻 LocalNext.js (Turbopack)13.089s14.026s0.937s71.10x
🐘 PostgresNext.js (Turbopack)13.215s13.585s0.370s71.12x
🌐 MongoDBNext.js (Turbopack)13.301s14.021s0.720s71.12x
🐘 PostgresExpress15.027s (+7.3% 🔺)15.400s (+5.5% 🔺)0.373s61.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express31.095s (-74.3% 🟢)33.159s (-73.2% 🟢)2.064s31.00x
▲ VercelNitro31.508s (-92.5% 🟢)33.124s (-92.2% 🟢)1.615s31.01x
▲ VercelNext.js (Turbopack)31.987s (-91.9% 🟢)33.996s (-91.4% 🟢)2.009s31.03x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.140s (-10.6% 🟢)2.007s (~)0.868s151.00x
🐘 PostgresNext.js (Turbopack)1.168s2.007s0.838s151.03x
💻 LocalNitro1.172s (-28.2% 🟢)2.006s (-3.3%)0.834s151.03x
💻 LocalExpress1.187s (-20.3% 🟢)2.006s (~)0.820s151.04x
🐘 PostgresExpress1.237s (-1.9%)2.018s (~)0.781s151.09x
🌐 RedisNext.js (Turbopack)1.238s2.006s0.768s151.09x
💻 LocalNext.js (Turbopack)1.279s2.005s0.726s151.12x
🌐 MongoDBNext.js (Turbopack)2.048s2.917s0.869s111.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.551s (-10.8% 🟢)4.187s (-9.4% 🟢)1.636s81.00x
▲ VercelNitro2.674s (-5.1% 🟢)4.258s (-1.5%)1.584s81.05x
▲ VercelNext.js (Turbopack)4.193s (+23.4% 🔺)6.817s (+38.2% 🔺)2.624s51.64x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.225s (-47.9% 🟢)2.007s (-33.3% 🟢)0.782s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.05x
🐘 PostgresExpress1.483s (-37.2% 🟢)2.019s (-32.9% 🟢)0.536s151.21x
💻 LocalNitro1.726s (-45.1% 🟢)2.072s (-46.7% 🟢)0.346s151.41x
💻 LocalNext.js (Turbopack)1.831s2.149s0.318s141.50x
💻 LocalExpress1.905s (-35.5% 🟢)2.150s (-37.8% 🟢)0.244s141.56x
🌐 RedisNext.js (Turbopack)2.364s3.008s0.643s101.93x
🌐 MongoDBNext.js (Turbopack)3.568s4.008s0.440s82.91x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.844s (-5.1% 🟢)5.284s (-10.8% 🟢)1.440s61.00x
▲ VercelExpress3.884s (+7.3% 🔺)6.116s (+19.7% 🔺)2.232s51.01x
▲ VercelNext.js (Turbopack)5.098s (-28.2% 🟢)7.574s (-15.0% 🟢)2.476s41.33x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.419s (-59.2% 🟢)2.007s (-49.9% 🟢)0.588s151.00x
🐘 PostgresNext.js (Turbopack)1.493s2.007s0.514s151.05x
🐘 PostgresExpress2.153s (-38.2% 🟢)2.861s (-28.7% 🟢)0.708s111.52x
🌐 RedisNext.js (Turbopack)3.634s4.010s0.376s82.56x
💻 LocalNitro4.547s (-45.5% 🟢)5.013s (-44.4% 🟢)0.466s63.20x
💻 LocalExpress4.829s (-42.1% 🟢)5.512s (-38.9% 🟢)0.683s63.40x
💻 LocalNext.js (Turbopack)5.581s6.213s0.631s53.93x
🌐 MongoDBNext.js (Turbopack)6.289s7.012s0.723s54.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.147s (+21.4% 🔺)7.340s (+19.8% 🔺)2.193s51.00x
▲ VercelNitro5.410s (+53.5% 🔺)7.160s (+29.4% 🔺)1.750s51.05x
▲ VercelNext.js (Turbopack)7.157s (-19.7% 🟢)8.877s (-19.0% 🟢)1.721s41.39x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.170s (-6.9% 🟢)2.007s (~)0.837s151.00x
🐘 PostgresNext.js (Turbopack)1.173s2.007s0.835s151.00x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.774s151.05x
💻 LocalNext.js (Turbopack)1.327s2.006s0.679s151.13x
💻 LocalNitro1.380s (-26.0% 🟢)2.006s (-14.3% 🟢)0.626s151.18x
💻 LocalExpress1.393s (-26.5% 🟢)2.006s (-15.1% 🟢)0.613s151.19x
🐘 PostgresExpress1.408s (+12.0% 🔺)2.062s (+2.7%)0.654s151.20x
🌐 MongoDBNext.js (Turbopack)2.036s2.916s0.880s111.74x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.271s (-7.6% 🟢)4.045s (-3.0%)1.774s81.00x
▲ VercelExpress2.336s (-9.5% 🟢)3.766s (-13.4% 🟢)1.430s81.03x
▲ VercelNext.js (Turbopack)4.170s (+42.2% 🔺)6.038s (+30.1% 🔺)1.869s51.84x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.251s (-46.5% 🟢)2.007s (-33.3% 🟢)0.757s151.00x
🐘 PostgresNext.js (Turbopack)1.282s2.006s0.724s151.03x
🐘 PostgresExpress1.393s (-40.5% 🟢)2.035s (-32.4% 🟢)0.642s151.11x
💻 LocalNitro1.957s (-36.2% 🟢)2.316s (-40.4% 🟢)0.359s131.56x
💻 LocalExpress2.022s (-35.4% 🟢)2.508s (-33.3% 🟢)0.485s121.62x
💻 LocalNext.js (Turbopack)2.130s2.826s0.696s111.70x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.88x
🌐 MongoDBNext.js (Turbopack)3.543s4.007s0.464s82.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.338s (+35.9% 🔺)6.011s (+25.4% 🔺)1.673s51.00x
▲ VercelNitro4.664s (+44.2% 🔺)6.658s (+31.1% 🔺)1.994s51.08x
▲ VercelNext.js (Turbopack)4.714s (+50.0% 🔺)6.542s (+44.7% 🔺)1.827s51.09x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.380s (-60.3% 🟢)2.007s (-49.9% 🟢)0.627s151.00x
🐘 PostgresNext.js (Turbopack)1.483s2.008s0.525s151.07x
🐘 PostgresExpress1.982s (-43.4% 🟢)2.737s (-31.8% 🟢)0.755s111.44x
🌐 RedisNext.js (Turbopack)3.571s4.010s0.439s82.59x
💻 LocalNitro4.894s (-46.5% 🟢)5.348s (-46.7% 🟢)0.454s63.55x
💻 LocalExpress5.939s (-32.5% 🟢)6.417s (-30.8% 🟢)0.478s54.30x
💻 LocalNext.js (Turbopack)6.115s6.416s0.301s54.43x
🌐 MongoDBNext.js (Turbopack)6.302s7.013s0.710s54.57x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.380s (+5.6% 🔺)7.062s (+3.6%)1.682s51.00x
▲ VercelNext.js (Turbopack)5.690s (-15.8% 🟢)7.416s (-13.2% 🟢)1.726s51.06x
▲ VercelExpress5.700s (-11.2% 🟢)7.539s (-7.8% 🟢)1.840s51.06x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.459s (-44.1% 🟢)1.022s (+1.6%)0.564s591.00x
💻 LocalNitro0.472s (-51.8% 🟢)1.021s (-6.7% 🟢)0.548s591.03x
💻 LocalExpress0.482s (-51.1% 🟢)1.004s (-6.7% 🟢)0.522s601.05x
🐘 PostgresNext.js (Turbopack)0.599s1.040s0.442s581.31x
🌐 RedisNext.js (Turbopack)0.619s1.004s0.386s601.35x
🐘 PostgresExpress0.644s (-23.2% 🟢)1.140s (+11.4% 🔺)0.496s531.40x
💻 LocalNext.js (Turbopack)0.719s1.004s0.285s601.57x
🌐 MongoDBNext.js (Turbopack)0.733s1.006s0.272s601.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.661s (-75.5% 🟢)6.339s (-70.3% 🟢)1.677s101.00x
▲ VercelNitro5.218s (-76.3% 🟢)7.258s (-69.8% 🟢)2.039s101.12x
▲ VercelNext.js (Turbopack)6.455s (-55.5% 🟢)7.922s (-50.7% 🟢)1.466s81.38x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.085s (-43.7% 🟢)1.705s (-18.8% 🟢)0.620s531.00x
🐘 PostgresExpress1.162s (-41.2% 🟢)1.588s (-29.7% 🟢)0.426s571.07x
💻 LocalExpress1.197s (-60.3% 🟢)2.006s (-44.1% 🟢)0.809s451.10x
💻 LocalNitro1.220s (-59.8% 🟢)2.005s (-46.6% 🟢)0.786s451.12x
🌐 RedisNext.js (Turbopack)1.476s2.006s0.530s451.36x
🐘 PostgresNext.js (Turbopack)1.707s2.387s0.679s381.57x
💻 LocalNext.js (Turbopack)1.753s2.005s0.253s451.61x
🌐 MongoDBNext.js (Turbopack)1.813s2.007s0.194s451.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro11.733s (-70.3% 🟢)13.729s (-66.8% 🟢)1.996s71.00x
▲ VercelExpress12.793s (-62.9% 🟢)14.645s (-60.2% 🟢)1.852s71.09x
▲ VercelNext.js (Turbopack)14.268s (-71.4% 🟢)16.580s (-67.9% 🟢)2.313s61.22x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.108s (-48.6% 🟢)2.696s (-41.4% 🟢)0.588s451.00x
🐘 PostgresExpress2.634s (-34.0% 🟢)3.074s (-29.6% 🟢)0.440s401.25x
🐘 PostgresNext.js (Turbopack)2.659s3.084s0.425s391.26x
💻 LocalNitro2.660s (-71.4% 🟢)3.007s (-70.0% 🟢)0.348s401.26x
💻 LocalExpress2.743s (-70.2% 🟢)3.033s (-69.7% 🟢)0.290s401.30x
🌐 RedisNext.js (Turbopack)2.992s3.191s0.200s381.42x
💻 LocalNext.js (Turbopack)3.748s4.007s0.259s301.78x
🌐 MongoDBNext.js (Turbopack)4.178s5.012s0.833s241.98x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express35.192s (-72.9% 🟢)37.772s (-71.4% 🟢)2.580s41.00x
▲ VercelNitro36.812s (-62.0% 🟢)39.053s (-60.3% 🟢)2.241s41.05x
▲ VercelNext.js (Turbopack)43.007s (-59.9% 🟢)46.755s (-57.1% 🟢)3.748s31.22x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.158s (-44.2% 🟢)1.007s (~)0.850s601.00x
🐘 PostgresNitro0.182s (-35.8% 🟢)1.006s (~)0.824s601.15x
🐘 PostgresNext.js (Turbopack)0.190s1.005s0.815s601.21x
🌐 RedisNext.js (Turbopack)0.316s1.021s0.705s592.00x
💻 LocalNitro0.412s (-31.9% 🟢)1.004s (-1.7%)0.592s602.61x
💻 LocalExpress0.452s (-19.3% 🟢)1.004s (~)0.552s602.87x
💻 LocalNext.js (Turbopack)0.603s1.039s0.436s583.83x
🌐 MongoDBNext.js (Turbopack)1.029s1.749s0.720s356.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.178s (+11.5% 🔺)4.588s (+26.2% 🔺)2.410s141.00x
▲ VercelNitro2.258s (+36.0% 🔺)4.485s (+33.9% 🔺)2.227s141.04x
▲ VercelNext.js (Turbopack)3.716s (+83.7% 🔺)5.458s (+43.9% 🔺)1.742s111.71x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.304s (-38.8% 🟢)1.006s (~)0.702s901.00x
🌐 RedisNext.js (Turbopack)0.423s1.004s0.581s901.39x
🐘 PostgresExpress0.520s (+2.0%)1.112s (+10.5% 🔺)0.592s821.71x
🐘 PostgresNext.js (Turbopack)0.705s1.318s0.613s692.32x
💻 LocalNitro2.170s (-14.5% 🟢)2.821s (-6.3% 🟢)0.651s327.14x
💻 LocalExpress2.186s (-13.0% 🟢)2.944s (-2.2%)0.758s317.19x
💻 LocalNext.js (Turbopack)2.424s3.043s0.619s307.97x
🌐 MongoDBNext.js (Turbopack)2.595s3.007s0.412s308.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.618s (+87.2% 🔺)8.356s (+60.9% 🔺)1.738s111.00x
▲ VercelNitro7.134s (+121.1% 🔺)9.355s (+94.0% 🔺)2.221s101.08x
▲ VercelExpress7.527s (+147.1% 🔺)9.552s (+98.7% 🔺)2.025s101.14x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.637s (-19.4% 🟢)1.006s (~)0.369s1201.00x
🐘 PostgresNext.js (Turbopack)0.730s1.013s0.284s1191.15x
🐘 PostgresExpress0.790s (-3.5%)1.192s (+17.1% 🔺)0.402s1011.24x
🌐 RedisNext.js (Turbopack)0.797s1.004s0.207s1201.25x
🌐 MongoDBNext.js (Turbopack)5.387s6.013s0.626s208.46x
💻 LocalNitro9.790s (-12.5% 🟢)10.276s (-11.9% 🟢)0.486s1215.37x
💻 LocalExpress10.085s (-9.9% 🟢)10.782s (-9.7% 🟢)0.697s1215.84x
💻 LocalNext.js (Turbopack)11.478s12.128s0.650s1018.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro16.821s (+117.8% 🔺)19.141s (+103.6% 🔺)2.319s71.00x
▲ VercelNext.js (Turbopack)18.466s (+78.8% 🔺)20.419s (+66.2% 🔺)1.953s71.10x
▲ VercelExpress20.247s (+172.9% 🔺)22.460s (+143.0% 🔺)2.213s61.20x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.133s (+430.1% 🔺)2.005s (+99.6% 🔺)0.010s (-19.2% 🟢)2.017s (+98.0% 🔺)0.884s101.00x
🐘 PostgresNitro1.134s (+453.4% 🔺)2.001s (+100.2% 🔺)0.001s (-26.7% 🟢)2.010s (+98.7% 🔺)0.875s101.00x
💻 LocalExpress1.136s (+470.5% 🔺)2.005s (+99.6% 🔺)0.012s (~)2.020s (+98.4% 🔺)0.884s101.00x
💻 LocalNext.js (Turbopack)1.172s2.003s0.012s2.019s0.847s101.03x
🐘 PostgresExpress1.232s (+500.5% 🔺)2.005s (+100.8% 🔺)0.001s (-37.5% 🟢)2.041s (+101.8% 🔺)0.810s101.09x
🐘 PostgresNext.js (Turbopack)2.024s2.601s0.220s2.874s0.850s101.79x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.200s (-42.6% 🟢)3.478s (-34.1% 🟢)1.562s (+110.5% 🔺)5.432s (-16.2% 🟢)3.232s101.00x
▲ VercelExpress2.336s (-6.8% 🟢)3.935s (-3.8%)1.087s (+13.1% 🔺)5.425s (-3.0%)3.089s101.06x
▲ VercelNext.js (Turbopack)4.802s (-29.9% 🟢)4.900s (-43.4% 🟢)0.740s (+17.0% 🔺)7.346s (-25.0% 🟢)2.544s102.18x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.512s (+142.2% 🔺)2.005s (+99.2% 🔺)0.004s (-8.9% 🟢)2.024s (+97.9% 🔺)0.512s301.00x
💻 LocalNitro1.514s (+80.5% 🔺)2.011s (+98.7% 🔺)0.010s (+2.8%)2.022s (+81.2% 🔺)0.509s301.00x
💻 LocalExpress1.721s (+127.3% 🔺)2.011s (+95.5% 🔺)0.010s (+2.1%)2.202s (+111.7% 🔺)0.481s281.14x
🐘 PostgresNext.js (Turbopack)1.722s2.107s0.003s2.123s0.401s301.14x
💻 LocalNext.js (Turbopack)1.845s2.011s0.009s2.203s0.358s281.22x
🐘 PostgresExpress1.983s (+214.8% 🔺)2.330s (+131.5% 🔺)0.003s (-20.7% 🟢)2.395s (+134.1% 🔺)0.412s261.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.656s (-13.0% 🟢)7.281s (-9.1% 🟢)0.179s (-56.1% 🟢)8.330s (-5.7% 🟢)2.675s81.00x
▲ VercelNitro5.796s (-80.3% 🟢)7.455s (-75.8% 🟢)0.170s (+52.1% 🔺)8.071s (-74.6% 🟢)2.275s81.02x
▲ VercelNext.js (Turbopack)12.255s (-27.6% 🟢)13.600s (-25.4% 🟢)0.201s (-4.9%)15.301s (-19.2% 🟢)3.046s42.17x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.651s (-32.8% 🟢)1.033s (-17.2% 🟢)0.000s (-100.0% 🟢)1.042s (-17.2% 🟢)0.390s581.00x
🐘 PostgresExpress1.031s (+7.3% 🔺)1.390s (+8.7% 🔺)0.000s (+9.5% 🔺)1.437s (+10.0% 🔺)0.406s421.58x
🐘 PostgresNext.js (Turbopack)1.333s1.618s0.000s1.715s0.382s372.05x
💻 LocalNitro1.337s (+9.4% 🔺)2.015s (~)0.000s (+166.7% 🔺)2.017s (~)0.680s302.05x
💻 LocalExpress1.368s (+11.7% 🔺)2.015s (~)0.000s (-60.0% 🟢)2.017s (~)0.649s302.10x
💻 LocalNext.js (Turbopack)1.436s2.013s0.000s2.016s0.581s302.20x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.922s (+28.6% 🔺)5.598s (+27.4% 🔺)0.000s (-100.0% 🟢)6.027s (+25.3% 🔺)2.105s101.00x
▲ VercelExpress4.013s (+7.3% 🔺)5.879s (+15.2% 🔺)0.000s (-100.0% 🟢)6.416s (+16.0% 🔺)2.404s101.02x
▲ VercelNext.js (Turbopack)5.465s (-46.3% 🟢)6.707s (-41.8% 🟢)0.000s (+Infinity% 🔺)7.645s (-36.6% 🟢)2.181s81.39x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.327s (-25.9% 🟢)2.066s (-3.5%)0.000s (-100.0% 🟢)2.081s (-4.3%)0.754s291.00x
🐘 PostgresNext.js (Turbopack)1.844s2.402s0.000s2.460s0.616s251.39x
🐘 PostgresExpress2.377s (+34.2% 🔺)3.042s (+39.7% 🔺)0.000s (NaN%)3.091s (+40.6% 🔺)0.714s201.79x
💻 LocalNext.js (Turbopack)2.776s3.362s0.000s3.365s0.589s182.09x
💻 LocalNitro3.068s (-9.4% 🟢)3.838s (-4.8%)0.000s (-53.1% 🟢)3.843s (-4.8%)0.775s162.31x
💻 LocalExpress3.103s (-10.5% 🟢)3.775s (-6.4% 🟢)0.000s (-76.6% 🟢)3.781s (-6.3% 🟢)0.679s162.34x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.059s (+10.3% 🔺)6.788s (+12.7% 🔺)0.000s (+Infinity% 🔺)7.571s (+17.3% 🔺)2.512s81.00x
▲ VercelNitro5.089s (+24.3% 🔺)6.536s (+21.6% 🔺)0.000s (-18.5% 🟢)7.002s (+20.9% 🔺)1.913s91.01x
▲ VercelNext.js (Turbopack)8.675s (+54.5% 🔺)10.065s (+44.2% 🔺)0.000s (-100.0% 🟢)11.283s (+49.6% 🔺)2.608s61.71x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro17/21
🐘 PostgresNitro19/21
▲ VercelExpress11/21
Fastest World by Framework

Winner determined by most benchmark wins

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

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other4380150588
Total4266010265292

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025

📋 View full workflow run

Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/vitest/src/index.ts
Comment threadpackages/core/src/runtime/run.ts Outdated
Comment threadpackages/core/src/workflow.ts Outdated
Comment threadpackages/builders/src/base-builder.ts
Comment threadpackages/core/src/runtime/get-world-lazy.ts

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Remaining issues:

  • getWorldLazy needs to be validated as a decision
  • packages/core/e2e/utils.ts skip for hasStepSourceMaps needs to be re-evaluated

@shtefcs

Copy link
Copy Markdown

Seems you had a lot of work here. It's not easy as it seems. Hopefully, this gonna solve some issues we are facing with Workflow.

Peace,

@TooTallNateTooTallNate 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.

Withdrawing my prior CHANGES_REQUESTED — the blocker I called out (the stale (err as any).meta?.retryAfter access in step-executor.ts) is fixed at line 169 with err.retryAfter ?? 1. As an aside, my fix recommendation in the prior review was based on a faulty type assumption (I confused TooEarlyError.retryAfter: number with RetryableError.retryAfter: Date). The author's err.retryAfter ?? 1 form is correct: it's a number of seconds with sensible fallback.

My other earlier concerns:

  • getStepNameFromEvent full-event-load: addressed. stepName is now plumbed through WorkflowInvokePayloadSchema so the handler reads it directly from the queue message payload instead of doing a paginated event load. ✓
  • step.error guard for max retries: known limitation acknowledged in-thread. The proposed long-term fix (server-side ne(status, 'running') on step_started) is separate work.
  • Background step → continuation enqueue safety: acknowledged as intentional.

So my specific items are resolved. But I'm not approving outright — there's still substantial unresolved review from @VaguelySerious touching real concerns I'd defer to them on:

  • Removal of the LegacyStreamWorld shim
  • Limiting queue concurrency to 1 in tests
  • Comments removed without explanation in suspension-handler.ts / runtime.ts
  • The builder-deferred.ts Next.js behavior prediction
  • Several "is there a systematic fix instead of conditionally allowing edge cases" questions

Plus the auto-flagged event cache duplication bug from Vercel's review bot is worth confirming was understood and dismissed (or fixed).

Approval-wise this should land when @VaguelySerious's concerns are resolved. From my side: no remaining blockers.

@TooTallNateTooTallNate 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.

Thorough re-review for Peter. Spent significant time tracing four risk surfaces: the Vercel bot's auto-flag, the concurrent-step-execution model, inline-step replay determinism, and the open review threads. Bottom line up front: no ship-blockers from my side. A few coverage gaps and one acknowledged limitation worth being explicit about, but nothing I'd hold this PR on.

Vercel bot's "event cache duplication" auto-flag is a false positive

"wait_completed events pushed locally without advancing eventsCursor cause duplicate events on next incremental fetch."

The author anticipated exactly this scenario. runtime.ts:585–594 has explicit dedupe-by-eventId logic:

constexistingIds=newSet(cachedEvents.map((e)=>e.eventId));for(consteofloaded.events){if(!existingIds.has(e.eventId)){cachedEvents.push(e);}}

Verified end-to-end: locally-pushed wait_completed (line 685) keeps its real server-returned eventId, the next iteration's incremental fetch returns it again, the dedupe filter drops it, the consumer never sees a duplicate. The comment in the code calls out the exact scenario the bot flagged. Safe to dismiss the auto-flag.

Concurrent-step-execution: a partial fix shipping with a known gap

Verified the L198 guard (step.attempt > maxRetries + 1 && step.error) traces correctly for the scenario it targets — concurrent first-attempts before any user-code error has been recorded. Without step.error, the guard correctly skips and the step proceeds normally even with inflated step.attempt.

But there's a symmetric gap in the catch-block path at L469:

if(currentAttempt>=maxRetries+1){/* step_failed */}

This check has no && step.error guard, so it fires on the first real catch-block failure even when currentAttempt has been inflated by concurrent first-attempts. End result: a workflow with maxRetries=3 and 4+ concurrent handlers can permanently fail after a single transient flake, with a misleading log saying "exceeded max retries (4 retries)" when the step body actually only ran once.

This is the only substantive open thread on the PR (step-executor.ts:198), and the author has acknowledged it in a separate resolved thread:

"Agreed on the concurrent-retry limitation. The server-side ne(status, 'running') WHERE clause is the right long-term fix — tracking as a follow-up."

So everyone's eyes-open. Worth being explicit in the changelog or PR description that under high queue back-pressure, default-maxRetries workflows can spuriously fail. Either of these would close the gap fully:

  1. Add the same && step.error guard at L469 (mirrors L198, easy to do, but doesn't fix the L198 case for catch-block).
  2. Server-side ne(status, 'running') on step_started (referenced in the resolved thread; also fixes the world-testing concurrency issue Peter resolved separately).

Not a ship-stopper, but I'd want it tracked as a known issue users can hit, not just an internal todo.

Inline-step replay: ordering subtlety in the dedupe path

The locally-pushed-then-incremental-fetch path can produce non-monotonic-by-eventId ordering in cachedEvents. Specifically: after wait_completed is pushed locally (L685) without advancing the cursor, the next incremental fetch can return concurrent events with eventIds between the local push and earlier events. After dedupe, cachedEvents becomes append-ordered but not eventId-ordered. The EventsConsumer walks by index, so it consumes them in append order, which is not the canonical replay order.

In practice this is unlikely to cause issues because:

  • Within one handler's writes, the same world process generates monotonic ULIDs.
  • Cross-handler events for the same run are gated by the ownership check (step_created ownership at L813).
  • The dangerous case requires concurrent handler activity for the same run + non-trivial cross-process clock skew.

But it's a footgun. The cleanest fix would be: after locally pushing an event, set eventsCursor to that event's eventId so the next fetch skips it. This also eliminates the dedupe pass entirely. Worth considering as a follow-up.

"VaguelySerious" review threads — context for Peter

For other reviewers who may stumble on this: most of the open @VaguelySerious threads are Peter's own AI-review notes posted via tooling. Of 28 such threads, 27 are either resolved or outdated (the diff has moved past them). The single unresolved + non-outdated thread is the concurrent-retry concern above (step-executor.ts:198), which Peter has acknowledged as a known limitation in a sibling resolved thread.

External-reviewer threads (mine, the Vercel bot's) are all resolved or addressed. So the apparent "long list of unresolved concerns" from my prior comment is misleading — it was conflating Peter's self-review notes with external review.

Test coverage gaps (non-blocking)

A few coverage gaps for novel V2 behavior worth tracking even if you ship:

  1. Dedupe path at runtime.ts:585–594 — zero test coverage. The behavior is novel to V2 and load-bearing for incremental-replay correctness. A unit test that:

    • First iteration fetches with cursor C0
    • Manually pushes a wait_completed to cachedEvents
    • Second iteration fetches and gets the same wait_completed plus an interleaved new event
    • Asserts: no duplicates AND consumer sees no orphans
      ...would lock in the contract.
  2. L198 guard's positive case — there's no test asserting that attempt > maxRetries + 1withoutstep.error does NOT fail the step. The guard exists specifically for this case and is currently un-asserted. The two step-handler.test.ts tests at lines 296–340 and 343–388 both set error on the fixture, so they exercise different paths.

  3. Concurrent-handler races — neither Scenario B (concurrent retry with prior error → premature L198 failure) nor Scenario D (catch-block premature failure with no && step.error guard) has a test. Adding these would catch the bug currently in code if a future fix lands.

  4. builder-deferred.ts Next.js manifest reading — Peter agreed in review that reading app-paths-manifest.json was the right approach over regex prediction. Verified that DID land (L1171–1175 reads the manifest with comment "This avoids predicting Next.js conventions with regexes and instead reads from Next.js's own output"). ✅

What looks good

  • The architectural shift is well-executed. The replay loop, ownership-gated inline dispatch, idempotency-key-based queue dedupe, builder unification, and __steps_registered rollup-tree-shaking guard are all clean.
  • The getStepNameFromEvent full-event-load concern from my prior review was addressed — stepName now flows through WorkflowInvokePayloadSchema. ✓
  • The TooEarlyError stale retryAfter access pattern is fixed at step-executor.ts:169. ✓
  • The CJS module.exports collision (ESM format for steps when bundleFinalOutput: true) is a subtle but correct fix.
  • The changelog doc docs/content/docs/changelog/eager-processing.mdx is one of the better architecture docs I've seen in a PR — covers every edge case, failure mode, and platform-specific quirk.

Verdict

No blockers from my side. Ship if Vade and storytime green-light. The concurrent-retry premature-failure case is real but acknowledged, the auto-flagged "event cache duplication" is a non-issue, and the test coverage gaps are real but not regressions (the V2 behaviors they cover are net-new).

If you want one piece of polish before merge, the L469 && step.error mirror would close the most user-visible failure mode at zero cost — but I won't gate on it.

@TooTallNateTooTallNate 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.

Pre-emptively :approved: but my agent really wants @VaguelySerious to resolve the open threads :lol: and there seems to be a few other things pointed out in the latest review comment.

VaguelySeriousand others added 7 commits May 3, 2026 11:38
Merges the V1 split flow/step routes (one queue message per step + one per
flow continuation, two separate function invocations per step) into a
single combined handler at /.well-known/workflow/v1/flow that executes
steps inline within the same invocation as the workflow replay.
A serial workflow with N steps that previously required ~2N+1 function
invocations now completes in 1.
Architecture
------------
- packages/core: workflowEntrypoint() handler with an inline replay loop.
Each loop iteration: load events incrementally, replay workflow,
inline-execute one owned step on suspension, loop. Background steps
(from Promise.all) are queued back to the same handler with stepId so
the in-flight handler advances the loop without queue round-trips
whenever possible.
- packages/builders: createCombinedBundle() emits a single ESM bundle
with workflowEntrypoint(workflowCode) plus a side-effect
__step_registrations bundle. ESM-by-default with createRequire banner
for CJS deps (matches main's #1562 pattern).
- packages/next, nestjs, sveltekit, astro, nitro, nuxt, hono, express,
fastify, vite: framework builders updated to use createCombinedBundle.
The step/ directory is no longer generated.
Correctness invariants (V2 polish)
----------------------------------
- Single inline executor per step. Atomic step_created per correlationId
(per-step in-process mutex in world-local; SQL-level guards in
postgres/vercel) means exactly one handler claims ownership of each
step. Inline execution gates on ownership; queue dispatch is
unconditional with idempotency keys for crash recovery (matches V1).
- onUnconsumedEvent reverts to its original PR #1055 contract: any
unconsumed event fatals as CORRUPTED_EVENT_LOG. Source-level fixes
removed every code path that produced orphaned step lifecycle events
during V2 work, so the skip branch became unnecessary.
- Lock-release polling interval lowered from 100ms to 10ms in
flushable-stream so the V2 step-executor's ops-settle race typically
resolves in ~5ms instead of ~50ms per writable-bearing step.
- Per-iteration runs.get round-trip eliminated: concurrent completion
is detected by scanning the loaded event log for terminal run events
instead.
- Worker-pool deadlock guard removed from Run#pollReturnValue. The
fibonacci/recursive parent→child polling case is now handled by
raising world-postgres default queueConcurrency from 10 to 50, with
the limitation documented at three call sites (fibonacciWorkflow's
JSDoc, queue.ts, eager-processing.mdx).
- getWorldLazy module-scope singleton breaks the step-bundle import
chain to world.ts so step bundles don't transitively pull in
world-vercel + cbor-x + the VQS dev-handler resolver.
World-local atomicity
---------------------
- Per-step in-process mutex serializes events.create for step
lifecycle events on a (runId, correlationId) key. step_started is
rejected only when the step is already terminal — duplicate
step_started on a non-terminal step is allowed (preserves retry
semantics after SIGKILL).
Runtime helpers
---------------
- loadWorkflowRunEvents(runId, afterCursor?) consolidates the
previous getAllWorkflowRunEvents{,WithCursor} + getNewWorkflowRunEvents
into one helper.
Documentation
-------------
- docs/content/docs/changelog/eager-processing.mdx: full record of
architecture, edge cases, and the fixes applied during the V2 work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Two CI failures from the earlier V2 cutover, now resolved:
1. @workflow/vitest unit tests (`packages/vitest/src/index.test.ts`)
were still mocking the V1 BaseBuilder methods (createWorkflowsBundle
+ createStepsBundle) and expecting two world handlers (workflow +
step). Updated the mocks to expose createCombinedBundle and
asserted a single combined handler at __wkf_workflow_, which is
what the V2 vitest builder actually emits.
2. E2E Local Dev Tests (nextjs-webpack) were asserting that step
error stacks contain `99_e2e.ts` / `helpers.ts`, which held in
pre-V2 webpack dev mode (which imported step sources directly).
V2 inlines the step bundle into the combined flow route, and
webpack's re-bundling collapses original step filenames out of the
dev-mode source maps. Extended the existing nextjs-webpack
carve-out in hasStepSourceMaps() to cover dev as well as prod, and
updated the source-map follow-up callout in eager-processing.mdx
to include nextjs-webpack in the matrix that needs source maps
wired up for V2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/e2e/e2e.test.ts` (PR #1879 vs. V2 hookDispose helper)
Drop the locally-defined `waitForHook(expectedRunId)` shadow in the
hookDisposeTestWorkflow test in favor of the new top-level
`waitForHook(token, { runId })` API merged from main. Keep V2's
event-driven `waitForHookDisposal()` helper, which is stricter than
main's `await sleep(3_000)` (it polls `getHookByToken` for the
HookNotFoundError signal rather than waiting a fixed interval).
* `packages/world-local/src/storage/events-storage.ts` (PR #1877 vs.
V2 per-step async mutex)
Both branches were closing race conditions in the local world's
step lifecycle. PR #1877 adds filesystem-level O_CREAT|O_EXCL locks
for `step_created` and `wait_created`; the V2 branch already wraps
step lifecycle events in an in-process `withStepLock` mutex. They
compose: the mutex serializes within a single Node process; the
filesystem lock additionally protects against cross-process races
(multiple pnpm workers, redelivered queue messages). Resolution
takes V2's mutex wrap as the base and patches in main's two
`writeExclusive` claims at the start of the `step_created` and
`wait_created` handlers, with comments noting the dual-layer
guarantee.
`packages/core/e2e/utils.ts` auto-merged: V2's nextjs-webpack +
all-Vercel source-map carve-outs preserved alongside main's
`tanstack-start` addition to `hasWorkflowSourceMaps`.
Verified locally: `@workflow/core` and `@workflow/world-local`
typecheck clean; all 343 world-local unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The standalone tarballs project's `vercel.json` set
`"buildCommand": "pnpm --filter tarballs build"`, which only runs the
tarballs package's `build` script (`node scripts/pack.ts`) and does
NOT trigger turbo's `dependsOn: ["^build"]` chain. As a result, every
workspace package was packed with an empty `dist/` directory — only
`bin/`, `package.json`, `README.md`, and `docs/` made it into the
tarballs. Downstream installs failed with
`Cannot find module '<pkg>/dist/next.cjs'` (and similar for every
other entry point).
The previous docs-based pipeline didn't hit this because Vercel
detected `docs` as a Next.js framework, ran turbo for it, and pack.ts
was wired as a `prebuild` step that ran after deps were already
built.
Fix: switch the buildCommand to `pnpm turbo build --filter=tarballs`
so turbo evaluates the build task for `tarballs`, which fans out via
`^build` to compile every workspace dependency first, and then runs
`tarballs#build` (pack.ts) against the populated `dist/` directories.
Verified locally: a fresh build produces tarballs containing the full
`dist/` tree (including `dist/next.cjs`, the file flight-booking-app
was failing to resolve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Conflicts resolved:
* `packages/core/src/logger.ts` (PR #1849 vs. V2 webpack carve-out)
PR #1849 ("Friendlier workflow errors") reintroduced the `debug`
package as a static import and added a structured `Logger`
interface with `child()` / `forRun()` and `composeLogLine`. The V2
branch had previously removed the `debug` static import because it
pulls `debug/src/node` and a dynamic `require('tty')` into the
Next.js webpack flow route, breaking V2 builds with
`Dynamic require of "tty" is not supported`.
Resolution combines both: keep main's `Logger` interface,
`child()` / `forRun()`, and `composeLogLine` integration; replace
the `debug` invocations with the V2 lightweight
`matchesDebugNamespace` + `process.env.DEBUG` matcher so the flow
route still bundles cleanly under webpack. Comment in the file
documents why `debug` is intentionally absent.
* `packages/core/src/runtime.ts` (PR #1849 vs. V2 inline replay loop)
Multiple regions where main's structured-logging refactor and the
V2 inline-execution loop made overlapping changes. Took V2's
control flow as the base — its `if (!workflowRun) { ... }` setup
block, main replay `while (true)` loop with timeout-based
re-scheduling, terminal-event short-circuit, and per-iteration
incremental event load are all preserved as-is — and folded in
main's contributions: the scoped `runLogger = runtimeLogger.forRun(
runId, workflowName)` is created once and used for run-level info
/ error logging. Discarded main's V1 wait-completion loop and
`runWorkflow` user-code error block since V2 handles waits and
user-code errors inside the inline loop body. Updated
`buildWorkflowSuspensionMessage()` call to drop a stray `runId`
first arg that didn't match the helper's 3-param signature.
* `packages/core/e2e/e2e.test.ts` (PR #1849 unrelated to V2)
No conflict from main's friendly-errors PR, auto-merged. (No
hookDispose-style conflict this round.)
Verified locally: all 820 `@workflow/core` unit tests pass; all 343
`@workflow/world-local` unit tests pass; `pnpm turbo typecheck` is
green across all 40 typecheck tasks.
Co-Authored-By: Claude Opus 4.7 (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

@VaguelySerious@shtefcs@TooTallNate