') + ')', '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('^' + ".*" + ', '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" + ', '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('^' + ".*" + ', '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); } })(); })(); perf(core): decouple workflow VM seed/clock from startedAt by TooTallNate · Pull Request #2525 · vercel/workflow · GitHub
Skip to content

perf(core): decouple workflow VM seed/clock from startedAt - #2525

Merged
pranaygp merged 1 commit into
mainfrom
perf/early-vm-init
Jun 18, 2026
Merged

perf(core): decouple workflow VM seed/clock from startedAt#2525
pranaygp merged 1 commit into
mainfrom
perf/early-vm-init

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

  • Derive the workflow VM's deterministic RNG seed from runId:workflowName:deploymentId (dropping the startedAt timestamp component) and its initial fixed clock from the ULID timestamp embedded in runId.
  • All of these inputs are available the instant a queue message arrives, so VM seed/clock setup no longer depends on startedAt (which is only known after the run_started round-trip).
  • This is the prerequisite for a follow-up that physically starts VM initialization earlier on the critical path (before run_started).

Details

  • packages/core/src/runtime/run-id-time.ts (new): runIdCreatedAt(runId) decodes the creation time from a wrun_<ulid> run ID, returning undefined for non-ULID ids (e.g. test fixtures) so callers fall back to the run snapshot's createdAt.
  • packages/core/src/workflow.ts: seed is now runId:workflowName:deploymentId; initial fixedTimestamp comes from runIdCreatedAt(runId) ?? +workflowRun.createdAt. startedAt is still used for generateUlid time and the workflowStartedAt metadata exposed to user code.

The initial fixedTimestamp only governs Date.now()/new Date() in the window before the first event is consumed; updateTimestamp then advances the VM clock to each consumed event's createdAt, starting with run_created.

Behavior change / migration note

Changing the seed formula changes every seed-derived value (step/hook correlation ID ULIDs, nanoids, crypto.getRandomValues, randomUUID) for a given run. Runs started before this change must not be replayed across the upgrade — they would diverge. Acceptable on the beta channel.

Testing

  • Regenerated the affected deterministic test fixtures by computing the exact old→new correlation IDs from the seeded ULID factory, and switched seed-deriving tests to the new runId:workflowName:deploymentId formula.
  • New unit tests for runIdCreatedAt.
  • All @workflow/coresrc/ tests pass (1251). The only failing suites are the pre-existing environment-dependent e2e/ tests (fail identically on main).

Changeset

  • @workflow/core: minor (includes the in-flight-replay migration note)

Follow-up

The structural change — splitting runWorkflow into prepare/execute and kicking off context creation before run_started — is intentionally left as a focused follow-up so it can be benchmarked in isolation. This PR is the foundation that unblocks it.

Derive the deterministic RNG seed from `runId:workflowName:deploymentId`
and the VM's initial fixed clock from the ULID timestamp embedded in
`runId` (via the new `runIdCreatedAt` helper). All of these inputs are
available the instant a queue message arrives, so the VM seed and clock
no longer depend on `startedAt` (set only after the `run_started`
round-trip). This is the prerequisite for starting VM initialization
earlier on the critical path.
This changes the seed-derived value sequence for a given run, so the
affected deterministic test fixtures are regenerated accordingly.
@TooTallNate
TooTallNate requested a review from a team as a code ownerJune 18, 2026 21:36
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e9725ac

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

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

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

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

@vercel

vercelBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jun 18, 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.038s (-5.7% 🟢)1.005s (~)0.967s101.00x
💻 LocalExpress0.042s (-3.5%)1.006s (~)0.964s101.09x
💻 LocalNext.js (Turbopack)0.060s (-9.4% 🟢)1.006s (~)0.946s101.56x
🐘 PostgresNitro0.063s (~)1.013s (~)0.950s101.64x
🐘 PostgresExpress0.065s (-10.7% 🟢)1.013s (~)0.948s101.69x
🐘 PostgresNext.js (Turbopack)0.068s (-4.0%)1.012s (~)0.944s101.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)0.456s (+56.7% 🔺)2.663s (+40.8% 🔺)2.207s101.00x
▲ VercelExpress0.761s (+101.7% 🔺)3.750s (+63.4% 🔺)2.989s101.67x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.087s (-1.2%)2.007s (~)0.920s101.00x
💻 LocalNitro1.095s (+0.9%)2.006s (~)0.912s101.01x
🐘 PostgresNitro1.111s (-0.8%)2.011s (~)0.900s101.02x
🐘 PostgresExpress1.115s (+0.7%)2.010s (~)0.895s101.03x
💻 LocalNext.js (Turbopack)1.127s (-1.8%)2.007s (~)0.880s101.04x
🐘 PostgresNext.js (Turbopack)1.153s (+1.1%)2.009s (~)0.856s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.919s (-0.9%)4.197s (+14.2% 🔺)2.279s101.00x
▲ VercelNext.js (Turbopack)2.118s (+20.0% 🔺)3.863s (+4.1%)1.745s101.10x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.485s (-0.5%)11.022s (~)0.536s31.00x
💻 LocalExpress10.520s (~)11.023s (~)0.503s31.00x
🐘 PostgresNitro10.536s (~)11.016s (~)0.480s31.00x
🐘 PostgresExpress10.544s (~)11.016s (~)0.472s31.01x
💻 LocalNext.js (Turbopack)10.760s (~)11.022s (~)0.262s31.03x
🐘 PostgresNext.js (Turbopack)10.836s (~)11.020s (~)0.185s31.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.390s (-25.9% 🟢)15.582s (-22.0% 🟢)2.192s21.00x
▲ VercelNext.js (Turbopack)13.993s (-2.2%)15.819s (-2.9%)1.826s21.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.634s (~)14.027s (~)0.393s51.00x
💻 LocalExpress13.743s (~)14.027s (~)0.284s51.01x
🐘 PostgresExpress13.798s (~)14.016s (~)0.218s51.01x
🐘 PostgresNitro13.858s (~)14.022s (~)0.164s51.02x
💻 LocalNext.js (Turbopack)14.300s (~)15.029s (~)0.729s41.05x
🐘 PostgresNext.js (Turbopack)14.467s (~)15.015s (~)0.548s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.020s (-9.1% 🟢)24.101s (-8.0% 🟢)2.081s31.00x
▲ VercelNext.js (Turbopack)23.886s (-32.6% 🟢)25.811s (-29.9% 🟢)1.925s31.08x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.244s (+1.4%)13.026s (+1.0%)0.782s71.00x
💻 LocalExpress12.393s (-1.2%)13.024s (~)0.631s71.01x
🐘 PostgresNitro12.491s (~)13.018s (~)0.527s71.02x
🐘 PostgresExpress12.523s (+0.6%)13.019s (~)0.496s71.02x
💻 LocalNext.js (Turbopack)13.630s (~)14.027s (~)0.397s71.11x
🐘 PostgresNext.js (Turbopack)13.811s (~)14.449s (+2.0%)0.637s71.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express30.538s (-12.9% 🟢)32.802s (-11.1% 🟢)2.264s31.00x
▲ VercelNext.js (Turbopack)31.777s (-9.7% 🟢)33.590s (-9.0% 🟢)1.813s31.04x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.206s (-0.6%)2.007s (~)0.801s151.00x
🐘 PostgresNitro1.213s (+1.2%)2.007s (~)0.795s151.01x
💻 LocalNitro1.223s (+4.6%)2.006s (~)0.782s151.01x
🐘 PostgresNext.js (Turbopack)1.255s (-2.5%)2.008s (~)0.753s151.04x
💻 LocalExpress1.260s (+8.1% 🔺)2.006s (~)0.746s151.04x
💻 LocalNext.js (Turbopack)1.464s (+13.0% 🔺)2.007s (~)0.542s151.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.952s (+48.2% 🔺)5.972s (+45.7% 🔺)2.020s61.00x
▲ VercelExpress4.034s (+81.8% 🔺)6.621s (+70.4% 🔺)2.587s51.02x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.306s (-8.3% 🟢)2.007s (-16.1% 🟢)0.702s151.00x
🐘 PostgresNitro1.311s (-3.9%)2.008s (-19.9% 🟢)0.697s151.00x
🐘 PostgresNext.js (Turbopack)1.476s (-9.9% 🟢)2.152s (-10.1% 🟢)0.676s141.13x
💻 LocalExpress2.071s (+27.3% 🔺)2.827s (+40.9% 🔺)0.755s111.59x
💻 LocalNitro2.094s (+34.9% 🔺)2.592s (+28.9% 🔺)0.498s121.60x
💻 LocalNext.js (Turbopack)2.352s (+23.1% 🔺)3.009s (+31.2% 🔺)0.656s101.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.357s (+10.0% 🔺)6.312s (+11.3% 🔺)1.955s51.00x
▲ VercelNext.js (Turbopack)4.473s (~)5.810s (-10.3% 🟢)1.337s61.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.456s (-17.4% 🟢)3.454s (-11.1% 🟢)1.997s91.00x
🐘 PostgresNitro1.520s (-4.7%)3.343s (-16.6% 🟢)1.822s91.04x
🐘 PostgresNext.js (Turbopack)2.729s (-13.6% 🟢)3.680s (-14.5% 🟢)0.951s91.87x
💻 LocalNitro4.535s (+32.5% 🔺)5.013s (+25.0% 🔺)0.479s73.11x
💻 LocalExpress5.935s (+35.8% 🔺)6.616s (+35.9% 🔺)0.681s54.08x
💻 LocalNext.js (Turbopack)6.604s (+14.4% 🔺)7.015s (+12.8% 🔺)0.411s54.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.324s (-3.3%)7.299s (-7.9% 🟢)1.975s51.00x
▲ VercelNext.js (Turbopack)6.085s (+8.2% 🔺)7.769s (+0.7%)1.685s41.14x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.203s (-1.2%)2.074s (+3.3%)0.871s151.00x
🐘 PostgresNitro1.206s (~)2.008s (~)0.802s151.00x
💻 LocalNitro1.216s (+1.3%)2.006s (~)0.790s151.01x
💻 LocalExpress1.245s (-21.0% 🟢)2.006s (~)0.761s151.04x
🐘 PostgresNext.js (Turbopack)1.259s (-1.7%)2.007s (~)0.748s151.05x
💻 LocalNext.js (Turbopack)1.524s (+9.0% 🔺)2.007s (~)0.483s151.27x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.148s (+35.5% 🔺)5.077s (+33.4% 🔺)1.928s61.00x
▲ VercelNext.js (Turbopack)3.150s (-15.3% 🟢)4.768s (-8.8% 🟢)1.617s71.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.275s (-6.6% 🟢)2.007s (-13.3% 🟢)0.732s151.00x
🐘 PostgresNitro1.291s (-10.6% 🟢)2.074s (-13.4% 🟢)0.783s151.01x
🐘 PostgresNext.js (Turbopack)1.551s (~)2.151s (-3.2%)0.600s141.22x
💻 LocalNitro1.948s (+15.0% 🔺)2.293s (+14.2% 🔺)0.345s141.53x
💻 LocalExpress2.041s (+7.5% 🔺)2.676s (+24.5% 🔺)0.636s121.60x
💻 LocalNext.js (Turbopack)2.417s (+11.4% 🔺)3.009s (~)0.592s101.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.561s (+67.3% 🔺)6.432s (+40.7% 🔺)1.871s51.00x
▲ VercelNext.js (Turbopack)5.187s (+16.2% 🔺)7.027s (+14.2% 🔺)1.839s51.14x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.454s (-19.4% 🟢)3.760s (-12.5% 🟢)2.306s81.00x
🐘 PostgresExpress1.462s (-18.8% 🟢)3.885s (-3.1%)2.423s81.01x
🐘 PostgresNext.js (Turbopack)2.624s (-35.5% 🟢)3.344s (-24.8% 🟢)0.719s91.80x
💻 LocalExpress5.480s (+19.4% 🔺)6.216s (+24.0% 🔺)0.736s53.77x
💻 LocalNitro5.769s (+28.4% 🔺)6.215s (+24.0% 🔺)0.447s53.97x
💻 LocalNext.js (Turbopack)6.917s (+19.1% 🔺)7.517s (+17.2% 🔺)0.601s44.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.946s (-41.3% 🟢)6.929s (-32.3% 🟢)1.983s51.00x
▲ VercelNext.js (Turbopack)6.033s (+59.5% 🔺)7.768s (+48.5% 🔺)1.735s51.22x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.545s (-4.5%)1.023s (+1.7%)0.478s591.00x
🐘 PostgresNitro0.549s (-7.6% 🟢)1.006s (-3.3%)0.457s601.01x
💻 LocalExpress0.567s (-9.2% 🟢)1.005s (-1.6%)0.438s601.04x
💻 LocalNitro0.581s (+16.1% 🔺)1.005s (~)0.424s601.07x
🐘 PostgresNext.js (Turbopack)0.797s (-4.6%)1.023s (~)0.226s591.46x
💻 LocalNext.js (Turbopack)0.844s (-4.9%)1.022s (-1.6%)0.178s591.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.181s (-17.9% 🟢)6.847s (-15.9% 🟢)1.666s91.00x
▲ VercelExpress5.596s (+23.0% 🔺)7.519s (+16.1% 🔺)1.923s81.08x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.282s (-5.5% 🟢)2.007s (-1.1%)0.725s451.00x
🐘 PostgresNitro1.361s (-2.0%)2.029s (~)0.668s451.06x
💻 LocalNitro1.399s (+17.8% 🔺)2.006s (~)0.607s451.09x
💻 LocalExpress1.447s (-2.8%)2.006s (~)0.559s451.13x
🐘 PostgresNext.js (Turbopack)1.866s (-3.9%)2.031s (-2.2%)0.165s451.46x
💻 LocalNext.js (Turbopack)2.050s (-2.4%)2.883s (-4.1%)0.833s321.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.305s (-46.8% 🟢)11.475s (-40.1% 🟢)2.170s91.00x
▲ VercelNext.js (Turbopack)9.742s (-28.3% 🟢)11.293s (-26.3% 🟢)1.550s81.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.649s (-2.3%)3.086s (-0.8%)0.436s391.00x
🐘 PostgresNitro2.694s (-3.7%)3.086s (-2.5%)0.391s391.02x
💻 LocalNitro3.092s (+13.2% 🔺)3.566s (+10.7% 🔺)0.475s341.17x
💻 LocalExpress3.167s (-1.0%)3.913s (-2.4%)0.745s311.20x
🐘 PostgresNext.js (Turbopack)3.792s (-2.0%)4.011s (-1.6%)0.219s301.43x
💻 LocalNext.js (Turbopack)4.236s (-2.5%)5.010s (~)0.774s241.60x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.144s (-18.9% 🟢)23.426s (-16.4% 🟢)2.281s61.00x
▲ VercelNext.js (Turbopack)24.875s (~)26.573s (-0.8%)1.697s51.18x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.236s (+2.0%)1.007s (~)0.771s601.00x
🐘 PostgresNitro0.237s (~)1.006s (~)0.770s601.00x
🐘 PostgresNext.js (Turbopack)0.262s (-10.8% 🟢)1.007s (~)0.745s601.11x
💻 LocalNitro0.412s (+10.5% 🔺)1.004s (~)0.592s601.75x
💻 LocalExpress0.427s (-2.9%)1.005s (~)0.577s601.81x
💻 LocalNext.js (Turbopack)0.654s (+12.3% 🔺)1.039s (+3.4%)0.386s582.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.144s (+38.3% 🔺)4.098s (+42.8% 🔺)1.954s151.00x
▲ VercelNext.js (Turbopack)2.274s (+21.5% 🔺)3.674s (-5.5% 🟢)1.400s171.06x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.345s (-3.1%)1.018s (-2.2%)0.673s891.00x
🐘 PostgresNitro0.350s (+2.3%)1.029s (+1.2%)0.679s881.02x
🐘 PostgresNext.js (Turbopack)0.454s (-15.7% 🟢)1.017s (-9.0% 🟢)0.563s891.32x
💻 LocalNitro2.095s (+37.4% 🔺)2.715s (+26.1% 🔺)0.620s346.08x
💻 LocalExpress2.276s (+8.9% 🔺)2.854s (+7.4% 🔺)0.578s326.61x
💻 LocalNext.js (Turbopack)2.582s (+10.8% 🔺)3.343s (+8.7% 🔺)0.761s277.49x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.131s (+7.0% 🔺)5.113s (+6.6% 🔺)1.983s181.00x
▲ VercelNext.js (Turbopack)3.197s (+5.1% 🔺)4.723s (-1.4%)1.526s201.02x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.547s (-3.7%)1.087s (-11.7% 🟢)0.540s1111.00x
🐘 PostgresExpress0.591s (+4.0%)1.161s (-4.6%)0.570s1041.08x
🐘 PostgresNext.js (Turbopack)1.787s (-31.7% 🟢)2.532s (-27.1% 🟢)0.745s483.27x
💻 LocalNitro9.781s (+55.0% 🔺)10.445s (+54.9% 🔺)0.663s1217.87x
💻 LocalExpress10.159s (+22.3% 🔺)10.941s (+23.2% 🔺)0.783s1118.56x
💻 LocalNext.js (Turbopack)10.274s (-4.5%)11.486s (-0.8%)1.212s1118.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.086s (-25.2% 🟢)6.223s (-12.2% 🟢)2.137s201.00x
▲ VercelNext.js (Turbopack)5.917s (+22.2% 🔺)7.736s (+18.4% 🔺)1.819s161.45x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.159s (-1.7%)2.000s (~)0.002s (+54.5% 🔺)2.012s (~)0.853s101.00x
💻 LocalExpress1.165s (+1.3%)2.005s (~)0.012s (+21.8% 🔺)2.019s (~)0.854s101.01x
💻 LocalNitro1.166s (+4.3%)2.005s (~)0.010s (-62.9% 🟢)2.018s (-1.0%)0.852s101.01x
🐘 PostgresNitro1.167s (~)1.998s (~)0.001s (+30.0% 🔺)2.011s (~)0.845s101.01x
💻 LocalNext.js (Turbopack)1.210s (~)2.004s (~)0.012s (-1.6%)2.020s (~)0.810s101.04x
🐘 PostgresNext.js (Turbopack)1.230s (~)2.002s (~)0.001s (-7.7% 🟢)2.011s (~)0.781s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.672s (+11.3% 🔺)4.173s (+13.4% 🔺)4.430s (+497.8% 🔺)9.106s (+86.8% 🔺)6.433s101.00x
▲ VercelNext.js (Turbopack)2.995s (+18.7% 🔺)3.899s (+2.1%)5.047s (+525.1% 🔺)9.480s (+86.5% 🔺)6.485s101.12x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.553s (+8.0% 🔺)2.010s (~)0.012s (-31.3% 🟢)2.024s (~)0.471s301.00x
🐘 PostgresExpress1.567s (-1.1%)2.004s (~)0.005s (+8.6% 🔺)2.025s (~)0.457s301.01x
💻 LocalExpress1.571s (~)2.010s (~)0.012s (-7.2% 🟢)2.024s (~)0.453s301.01x
🐘 PostgresNitro1.589s (+0.7%)2.005s (~)0.005s (-7.4% 🟢)2.026s (~)0.437s301.02x
💻 LocalNext.js (Turbopack)1.713s (-2.1%)2.008s (~)0.012s (-1.9%)2.023s (~)0.311s301.10x
🐘 PostgresNext.js (Turbopack)1.757s (-1.6%)2.010s (~)0.005s (-0.6%)2.025s (~)0.268s301.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)7.293s (-14.1% 🟢)8.445s (-14.0% 🟢)0.723s (+174.7% 🔺)9.665s (-8.6% 🟢)2.373s71.00x
▲ VercelExpress7.631s (+19.5% 🔺)9.547s (+22.3% 🔺)0.389s (+117.5% 🔺)10.428s (+23.3% 🔺)2.796s61.05x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.765s (~)1.065s (+3.9%)0.000s (-74.6% 🟢)1.078s (+2.2%)0.313s561.00x
🐘 PostgresNitro0.774s (~)1.106s (+5.5% 🔺)0.000s (+3.8%)1.134s (+3.7%)0.360s531.01x
🐘 PostgresNext.js (Turbopack)0.965s (-5.4% 🟢)1.304s (-11.0% 🟢)0.000s (-55.4% 🟢)1.312s (-10.9% 🟢)0.348s461.26x
💻 LocalNitro1.472s (+36.9% 🔺)2.014s (+9.6% 🔺)0.000s (-48.9% 🟢)2.017s (+9.6% 🔺)0.544s301.93x
💻 LocalExpress1.498s (+10.0% 🔺)2.013s (~)0.000s (+66.7% 🔺)2.016s (~)0.517s301.96x
💻 LocalNext.js (Turbopack)1.870s (+23.6% 🔺)2.157s (+7.1% 🔺)0.000s (-4.8%)2.160s (+7.1% 🔺)0.290s282.45x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.858s (+40.9% 🔺)5.981s (+25.4% 🔺)0.000s (-100.0% 🟢)6.400s (+21.6% 🔺)1.543s101.00x
▲ VercelExpress5.381s (+77.5% 🔺)6.904s (+51.2% 🔺)0.000s (NaN%)7.407s (+47.0% 🔺)2.027s91.11x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.661s (+2.9%)2.171s (+5.2% 🔺)0.000s (+Infinity% 🔺)2.205s (+5.6% 🔺)0.544s281.00x
🐘 PostgresNitro1.824s (+17.5% 🔺)2.341s (+9.7% 🔺)0.000s (+123.1% 🔺)2.354s (+9.7% 🔺)0.530s261.10x
🐘 PostgresNext.js (Turbopack)2.025s (-4.1%)2.545s (-1.6%)0.000s (~)2.551s (-1.7%)0.526s241.22x
💻 LocalExpress3.759s (+28.3% 🔺)4.316s (+17.6% 🔺)0.000s (-2.9%)4.322s (+17.7% 🔺)0.563s142.26x
💻 LocalNitro3.922s (+77.8% 🔺)4.319s (+57.5% 🔺)0.002s (+94.6% 🔺)4.325s (+57.1% 🔺)0.403s142.36x
💻 LocalNext.js (Turbopack)4.799s (+65.5% 🔺)5.444s (+62.1% 🔺)0.001s (~)5.452s (+62.1% 🔺)0.653s122.89x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.450s (+13.3% 🔺)7.826s (+8.3% 🔺)0.000s (NaN%)8.295s (+7.9% 🔺)1.845s81.00x
▲ VercelNext.js (Turbopack)7.186s (-0.9%)8.655s (+3.1%)0.000s (-100.0% 🟢)9.013s (+1.9%)1.827s71.11x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro15/21
🐘 PostgresExpress15/21
▲ VercelExpress16/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202301672
✅ 💻 Local Development190902192128
✅ 📦 Local Production190902192128
✅ 🐘 Local Postgres189502332128
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total8192010809272

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15200
✅ nextjs-turbopack-stable-lazy-discovery-enabled15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable-lazy-discovery-disabled15200
✅ nextjs-webpack-stable-lazy-discovery-enabled15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable-lazy-discovery-disabled15200
✅ nextjs-turbopack-stable-lazy-discovery-enabled15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable-lazy-discovery-disabled15200
✅ nextjs-webpack-stable-lazy-discovery-enabled15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable-lazy-discovery-disabled15101
✅ nextjs-turbopack-stable-lazy-discovery-enabled15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable-lazy-discovery-disabled15101
✅ nextjs-webpack-stable-lazy-discovery-enabled15101
✅ nitro-stable126026
✅ nuxt-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run

} = createContext({
seed: `${workflowRun.runId}:${workflowRun.workflowName}:${+startedAt}`,
fixedTimestamp: +startedAt,
seed: `${workflowRun.runId}:${workflowRun.workflowName}:${workflowRun.deploymentId}`,

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.

would this stringify as undefined in non-vercel environments?

@pranaygppranaygp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the seed/clock decoupling closely — the core change is correct and replay-safe:

  • Seed stability:seedrandom(seed) in vm/index.ts is the only RNG input (fixedTimestamp only drives Date), so changing the seed source doesn't perturb the clock and vice-versa. The new seed runId:workflowName:deploymentId is replay-stable: runId is the ULID (fixed), and deploymentId is read from the persisted run snapshot (a required z.string()), not process.env.VERCEL_DEPLOYMENT_ID — so it's the creation-time pinned deployment and is identical on every replay, even if execution somehow lands on a newer deployment. runId alone already guarantees uniqueness, as the comment notes.
  • Clock stability:fixedTimestamp = runIdCreatedAt(runId) ?? +createdAt. createdAt is a required z.coerce.date(), so the fallback can't be NaN. Good call using ?? rather than || — a decoded epoch of 0 won't wrongly trigger the fallback. The branch taken depends only on runId, so it's deterministic per run. Net effect is also a small improvement: the pre-first-event clock now starts at ~createdAt and advances monotonically to run_started, instead of jumping backward from startedAt.
  • The seed formula is reconstructed nowhere else in non-test code (no server/step-executor copy to drift), and the regenerated fixtures keep the same 01HK153X00 time prefix with only the random tail changing — exactly consistent with timestamp-from-startedAt / randomness-from-seed. CI Unit Tests pass on ubuntu + windows, and the dynamic self-validating test (workflow.test.ts:5188) derives IDs from the new formula.

LGTM. One optional reuse nit inline.

* legacy/non-ULID id, or a test fixture like `wrun_test`); callers fall back to
* an authoritative timestamp from the run snapshot (`createdAt`) in that case.
*/
export function runIdCreatedAt(runId: string): number | undefined {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional (non-blocking): this re-implements "strip a prefix, then decode the ULID time" that already lives in @workflow/world as ulidToDate() (and validateUlidTimestamp() does the exact prefix-strip + decode). @workflow/core already depends on @workflow/world and imports values from it (e.g. SPEC_VERSION_SUPPORTS_COMPRESSION in workflow.ts), and ulidToDate is exported from the package root, so this could fold into:

import{ulidToDate}from'@workflow/world';exportfunctionrunIdCreatedAt(runId: string): number|undefined{constulidPart=runId.startsWith(RUN_ID_PREFIX)
? runId.slice(RUN_ID_PREFIX.length)
: runId;returnulidToDate(ulidPart)?.getTime();}

Bonus: ulidToDate validates via z.string().ulid() (stricter than a bare decodeTime in try/catch) and returns null for non-ULIDs, which ?.getTime() turns into undefined — same contract you have now. Reasonable to keep it self-contained in core if you'd rather not couple the early-init path to the world util, but worth a look.

@pranaygp
pranaygp enabled auto-merge (squash) June 18, 2026 21:56
@pranaygp
pranaygp merged commit 7aee0d4 into mainJun 18, 2026
192 of 195 checks passed
@pranaygp
pranaygp deleted the perf/early-vm-init branch June 18, 2026 22:21
@github-actionsgithub-actionsBot mentioned this pull request Jun 18, 2026
github-actionsBot added a commit that referenced this pull request Jun 18, 2026
Derive the deterministic RNG seed from `runId:workflowName:deploymentId`
and the VM's initial fixed clock from the ULID timestamp embedded in
`runId` (via the new `runIdCreatedAt` helper). All of these inputs are
available the instant a queue message arrives, so the VM seed and clock
no longer depend on `startedAt` (set only after the `run_started`
round-trip). This is the prerequisite for starting VM initialization
earlier on the critical path.
This changes the seed-derived value sequence for a given run, so the
affected deterministic test fixtures are regenerated accordingly.
Signed-off-by: Nathan Rajlich <n@n8.io>
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2530. Merge conflicts were resolved by AI — please review carefully. (backport job run)

pranaygp added a commit that referenced this pull request Jun 18, 2026
…testing
* origin/main:
perf(core): decouple workflow VM seed/clock from startedAt (#2525)
[world-local] [core] Cache local dev server port per process (#2522)
Show pending runs as gray animated stripes in trace viewer (#2520)
[world-vercel] Route v4 event requests through global fetch (#2514)
[core] Send workflowName with step events (#2511)
Stamp run IDs on world spans (#2508)
Reject empty-string hook tokens in createHook() (#2490)
perf(core): cache compiled workflow-bundle vm.Script across replays (#2471)
perf(core): drain consumable replay events synchronously (#2473)
perf(core): lazy inline step start (save one world round-trip per step) (#2478)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@pranaygp@VaguelySerious