Trace /flow route initialization - #2592

Merged
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading
Jun 23, 2026
Merged

Trace /flow route initialization#2592
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

This instruments the Workflow SDK /flow route path so we can explain delay before the route makes its first workflow-server run_started POST.

Instrumentation contract

workflow.route.flow

New server span around each generated /flow route request.

Attributes:

  • workflow.route.type: currently flow
  • workflow.route.handler_cached: whether this invocation reused the in-memory route handler
  • workflow.route.invocation_count: per-process invocation count for this route entrypoint
  • workflow.route.entrypoint_age_ms: milliseconds since workflowEntrypoint() constructed this route entrypoint
  • workflow.route.module_body_init_ms: process-level milliseconds from the generated route module-body timestamp to workflowEntrypoint() construction, when generated routes provide the timestamp
  • http.request.method: request method
  • http.route: /.well-known/workflow/v1/flow
  • http.response.status_code: response status when the handler returns a Response

workflow.route.init

New child span under workflow.route.flow, emitted when the route creates the cached queue handler.

Use this to separate first-hit handler construction from normal warm-route request handling. Under concurrent cold requests, more than one request can observe an empty cache and emit workflow.route.init; that is pre-existing harmless last-write-wins behavior.

workflow.route.get_world_handlers

New child span under workflow.route.init around getWorldHandlers().

Use this to identify latency from loading/creating the world handler layer during route setup.

workflow.route.get_world

New child span under workflow.route.flow, emitted for each workflow invocation before workflow.execute.

Use this to identify latency from resolving the per-invocation world before SDK runtime work posts run_started.

workflow.execute <workflowName>

Existing workflow execution span. This PR adds:

Attributes:

  • workflow.turbo: whether this invocation is using the turbo first-delivery path

Span events:

  • workflow.run_started.create.start: emitted immediately before world.events.create(... run_started ...), i.e. right before the first workflow-server run_started POST begins

Event attributes:

  • workflow.run_started.skip_preload: whether the run_started create call is using the turbo skipPreload path

Reading the signal

  • Gap from workflow.route.flow start to workflow.run_started.create.start: SDK/runtime work before the first workflow-server run_started POST begins.
  • Large workflow.route.module_body_init_ms: generated route module body work after static imports, before the SDK route handler is created. This value is computed once per route entrypoint and emitted on every request from that in-memory entrypoint.
  • Near-zero workflow.route.entrypoint_age_ms: the route entrypoint was just created for this request, so this request hit a cold SDK route instance.
  • workflow.route.handler_cached=false plus workflow.route.init: the request paid route handler creation. Sequentially this happens once per warm instance; concurrent cold requests may briefly produce more than one init span.
  • workflow.route.handler_cached=true: route handler was already created; delay is not from handler construction.
  • workflow.turbo=true: this invocation is the turbo first-delivery path. workflow.run_started.skip_preload=true is the finer-grained marker for the associated run_started create call.

Static ESM import evaluation still happens before generated route body code can run, so this does not directly measure the entire import graph. It gives us the earliest SDK-controlled marker in the generated /flow module, plus the exact pre-run_started marker inside the SDK handler.

Testing

  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec vitest run packages/core/src/runtime-trace-mode.test.ts packages/builders/src/constants.test.ts
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/core typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/builders typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec biome check .changeset/flow-route-init-traces.md packages/builders/src/base-builder.ts packages/builders/src/constants.ts packages/builders/src/constants.test.ts packages/core/src/runtime.ts packages/core/src/runtime-trace-mode.test.ts packages/core/src/telemetry/semantic-conventions.ts (exit 0; reports existing complexity/unused-suppression warnings)
  • Previous full @workflow/core suite under Node 24: all 1322 tests passed, but Vitest exited 1 due an existing pending-fetch shutdown error; packages/core/src/abort-controller-step.test.ts passed by itself.

@vercel

vercelBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9b83f3

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

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

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

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

@github-actions

github-actionsBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production144112301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7281110788360

❌ Failed Tests

▲ Vercel Production (1 failed)

nuxt (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
❌ nuxt124127
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ 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


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jun 23, 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.044s (-5.7% 🟢)1.006s (~)0.962s101.00x
💻 LocalExpress0.046s (-2.1%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.049s (-6.3% 🟢)1.008s (~)0.959s101.11x
🐘 PostgresNext.js (Turbopack)0.056s (+1.1%)1.011s (~)0.955s101.26x
🐘 PostgresNitro0.061s (-6.0% 🟢)1.013s (~)0.952s101.39x
🐘 PostgresExpress0.072s (+3.9%)1.013s (~)0.940s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.292s (+26.2% 🔺)2.213s (+5.2% 🔺)1.921s101.00x
▲ VercelNext.js (Turbopack)0.314s (+16.1% 🔺)2.353s (+15.1% 🔺)2.039s101.08x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.079s (-0.6%)2.006s (~)0.928s101.00x
💻 LocalExpress1.083s (~)2.007s (~)0.923s101.00x
🐘 PostgresNext.js (Turbopack)1.086s (~)2.011s (~)0.925s101.01x
💻 LocalNext.js (Turbopack)1.086s (~)2.006s (~)0.920s101.01x
🐘 PostgresNitro1.101s (~)2.011s (~)0.911s101.02x
🐘 PostgresExpress1.103s (+1.0%)2.011s (~)0.908s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.507s (+8.9% 🔺)3.416s (+20.7% 🔺)1.909s101.00x
▲ VercelNext.js (Turbopack)1.588s (+10.7% 🔺)3.280s (-2.2%)1.692s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.443s (~)11.022s (~)0.580s31.00x
💻 LocalNitro10.446s (~)11.022s (~)0.576s31.00x
💻 LocalExpress10.463s (~)11.022s (~)0.560s31.00x
🐘 PostgresNext.js (Turbopack)10.471s (~)11.017s (~)0.547s31.00x
🐘 PostgresExpress10.484s (~)11.021s (~)0.537s31.00x
🐘 PostgresNitro10.498s (~)11.021s (~)0.523s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.720s (+3.3%)14.402s (+3.8%)1.682s31.00x
▲ VercelNext.js (Turbopack)12.862s (+9.0% 🔺)14.699s (+5.4% 🔺)1.837s31.01x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.569s (~)14.028s (~)0.459s51.00x
💻 LocalExpress13.602s (~)14.028s (~)0.426s51.00x
🐘 PostgresNitro13.641s (-0.6%)14.020s (~)0.379s51.01x
💻 LocalNext.js (Turbopack)13.681s (+0.7%)14.028s (~)0.347s51.01x
🐘 PostgresExpress13.701s (+0.9%)14.022s (~)0.320s51.01x
🐘 PostgresNext.js (Turbopack)13.721s (~)14.021s (~)0.299s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)18.660s (+7.6% 🔺)20.341s (+6.2% 🔺)1.681s31.00x
▲ VercelExpress19.542s (+18.9% 🔺)21.199s (+17.8% 🔺)1.657s31.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.055s (-1.0%)13.025s (~)0.970s71.00x
💻 LocalExpress12.145s (-1.6%)13.027s (~)0.882s71.01x
🐘 PostgresNitro12.162s (-0.9%)13.020s (~)0.858s71.01x
💻 LocalNext.js (Turbopack)12.189s (-0.5%)13.025s (~)0.836s71.01x
🐘 PostgresNext.js (Turbopack)12.218s (+1.0%)13.023s (~)0.805s71.01x
🐘 PostgresExpress12.276s (+1.4%)13.018s (~)0.742s71.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.857s (+35.3% 🔺)26.850s (+33.2% 🔺)1.994s41.00x
▲ VercelNext.js (Turbopack)25.505s (+31.8% 🔺)27.773s (+30.8% 🔺)2.268s41.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.165s (-1.0%)2.008s (~)0.843s151.00x
🐘 PostgresNitro1.194s (+1.0%)2.007s (~)0.813s151.02x
🐘 PostgresExpress1.213s (+3.8%)2.008s (~)0.795s151.04x
💻 LocalNext.js (Turbopack)1.380s (-7.8% 🟢)2.006s (~)0.626s151.18x
💻 LocalExpress1.385s (~)2.006s (~)0.621s151.19x
💻 LocalNitro1.396s (-2.9%)2.006s (~)0.611s151.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.793s (+29.9% 🔺)4.385s (+15.7% 🔺)1.592s71.00x
▲ VercelExpress3.109s (+53.2% 🔺)5.068s (+44.8% 🔺)1.959s61.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.301s (-0.9%)3.009s (+3.1%)1.708s101.00x
🐘 PostgresExpress1.343s (-3.6%)2.509s (-6.2% 🟢)1.166s121.03x
🐘 PostgresNitro1.348s (+0.6%)2.510s (~)1.162s121.04x
💻 LocalNitro2.313s (-3.9%)2.736s (-6.2% 🟢)0.423s111.78x
💻 LocalNext.js (Turbopack)2.391s (-12.6% 🟢)3.008s (-3.2%)0.618s101.84x
💻 LocalExpress2.529s (+7.3% 🔺)2.918s (~)0.389s111.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.579s (+7.1% 🔺)5.199s (+3.0%)1.620s61.00x
▲ VercelExpress3.756s (+3.4%)5.354s (+3.7%)1.598s61.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.591s (-3.0%)4.010s (-5.9% 🟢)2.419s81.00x
🐘 PostgresExpress1.637s (+3.2%)4.136s (-3.9%)2.499s81.03x
🐘 PostgresNext.js (Turbopack)2.944s (+4.6%)5.349s (-3.0%)2.406s61.85x
💻 LocalNext.js (Turbopack)4.978s (-9.0% 🟢)5.349s (-11.1% 🟢)0.371s63.13x
💻 LocalNitro5.957s (-7.5% 🟢)6.616s (-8.3% 🟢)0.658s53.74x
💻 LocalExpress6.009s (-5.0%)6.619s (-5.7% 🟢)0.611s53.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.831s (+38.8% 🔺)6.612s (+27.1% 🔺)1.781s51.00x
▲ VercelExpress4.852s (+58.3% 🔺)6.870s (+36.2% 🔺)2.018s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.170s (~)2.007s (~)0.837s151.00x
🐘 PostgresNitro1.178s (-2.4%)2.007s (~)0.829s151.01x
🐘 PostgresExpress1.182s (~)2.008s (~)0.826s151.01x
💻 LocalNitro1.357s (-4.5%)2.006s (~)0.649s151.16x
💻 LocalNext.js (Turbopack)1.399s (~)2.006s (~)0.607s151.20x
💻 LocalExpress1.452s (+1.0%)2.007s (~)0.555s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.794s (-51.7% 🟢)4.333s (-41.8% 🟢)1.539s81.00x
▲ VercelExpress2.871s (+39.1% 🔺)4.642s (+32.7% 🔺)1.772s71.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.294s (-5.7% 🟢)2.508s (+1.5%)1.214s121.00x
🐘 PostgresNext.js (Turbopack)1.306s (+1.3%)2.918s (~)1.611s111.01x
🐘 PostgresExpress1.328s (-1.0%)2.222s (-10.1% 🟢)0.894s141.03x
💻 LocalNitro2.318s (-10.2% 🟢)3.008s (-3.2%)0.691s101.79x
💻 LocalNext.js (Turbopack)2.607s (-1.2%)3.108s (+3.3%)0.502s102.01x
💻 LocalExpress2.613s (+7.9% 🔺)3.009s (-3.2%)0.396s102.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.700s (-22.0% 🟢)5.332s (-23.3% 🟢)1.631s61.00x
▲ VercelExpress4.302s (-10.2% 🟢)5.906s (-11.0% 🟢)1.604s61.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.610s (+1.5%)4.011s (-3.0%)2.401s81.00x
🐘 PostgresNitro1.620s (-2.4%)4.137s (+3.1%)2.517s81.01x
🐘 PostgresNext.js (Turbopack)3.012s (+5.4% 🔺)5.848s (~)2.837s61.87x
💻 LocalNext.js (Turbopack)4.998s (-12.6% 🟢)5.683s (-5.5% 🟢)0.685s63.11x
💻 LocalExpress6.217s (-7.2% 🟢)7.016s (-2.8%)0.799s53.86x
💻 LocalNitro6.501s (-4.1%)7.020s (-2.8%)0.518s54.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.961s (-15.5% 🟢)5.681s (-9.8% 🟢)1.720s61.00x
▲ VercelNext.js (Turbopack)4.577s (+46.5% 🔺)6.497s (+25.9% 🔺)1.920s51.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.507s (-0.9%)1.006s (~)0.500s601.00x
🐘 PostgresNitro0.538s (-9.6% 🟢)1.006s (-3.4%)0.468s601.06x
🐘 PostgresExpress0.561s (+5.1% 🔺)1.023s (~)0.462s591.11x
💻 LocalNext.js (Turbopack)0.565s (-6.5% 🟢)1.005s (-1.7%)0.440s601.11x
💻 LocalNitro0.580s (-1.0%)1.022s (+1.6%)0.442s591.15x
💻 LocalExpress0.584s (+1.9%)1.005s (~)0.421s601.15x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.239s (+39.5% 🔺)6.064s (+21.4% 🔺)1.825s111.00x
▲ VercelExpress4.460s (+59.4% 🔺)6.406s (+42.8% 🔺)1.946s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.221s (-1.9%)2.007s (-1.1%)0.786s451.00x
🐘 PostgresNitro1.294s (-8.5% 🟢)2.007s (-2.3%)0.713s451.06x
🐘 PostgresExpress1.379s (+10.3% 🔺)2.030s (+1.1%)0.651s451.13x
💻 LocalNext.js (Turbopack)1.406s (-4.2%)2.005s (~)0.599s451.15x
💻 LocalExpress1.436s (-1.3%)2.006s (~)0.570s451.18x
💻 LocalNitro1.478s (~)2.028s (+1.1%)0.550s451.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.677s (+80.6% 🔺)13.672s (+70.1% 🔺)1.995s71.00x
▲ VercelNext.js (Turbopack)12.418s (+73.8% 🔺)14.184s (+59.5% 🔺)1.766s71.06x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)2.476s (~)3.034s (-0.8%)0.558s401.00x
🐘 PostgresNitro2.595s (-4.4%)3.085s (+0.9%)0.490s391.05x
🐘 PostgresExpress2.846s (+9.7% 🔺)3.194s (+3.5%)0.347s381.15x
💻 LocalNitro3.052s (-5.2% 🟢)3.675s (-8.3% 🟢)0.623s331.23x
💻 LocalNext.js (Turbopack)3.099s (-5.0%)3.675s (-9.1% 🟢)0.577s331.25x
💻 LocalExpress3.179s (-2.1%)3.913s (-1.6%)0.734s311.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.472s (+63.0% 🔺)24.735s (+60.0% 🔺)2.262s51.00x
▲ VercelNext.js (Turbopack)26.936s (+64.1% 🔺)28.462s (+54.0% 🔺)1.527s51.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.166s (-0.8%)1.006s (~)0.840s601.00x
🐘 PostgresNitro0.209s (-7.8% 🟢)1.006s (~)0.797s601.26x
🐘 PostgresExpress0.237s (+12.5% 🔺)1.007s (~)0.770s601.42x
💻 LocalNitro0.431s (-7.9% 🟢)1.004s (~)0.573s602.59x
💻 LocalExpress0.455s (+5.5% 🔺)1.005s (~)0.550s602.73x
💻 LocalNext.js (Turbopack)0.564s (-3.5%)1.022s (+1.7%)0.457s593.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.803s (-0.8%)3.477s (-2.0%)1.674s181.00x
▲ VercelExpress1.994s (-3.8%)3.433s (-10.6% 🟢)1.439s181.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.253s (-5.5% 🟢)1.006s (~)0.752s901.00x
🐘 PostgresNitro0.326s (-3.3%)1.006s (~)0.680s901.29x
🐘 PostgresExpress0.356s (+10.2% 🔺)1.018s (~)0.662s891.41x
💻 LocalNitro2.085s (-2.3%)2.686s (~)0.602s348.23x
💻 LocalExpress2.121s (-1.8%)2.766s (+1.1%)0.645s338.37x
💻 LocalNext.js (Turbopack)2.599s (-6.8% 🟢)3.113s (-4.5%)0.514s2910.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.670s (+8.4% 🔺)4.342s (+7.1% 🔺)1.672s211.00x
▲ VercelNext.js (Turbopack)3.196s (+47.3% 🔺)5.002s (+27.3% 🔺)1.806s191.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.490s (+3.8%)3.343s (+11.1% 🔺)2.853s361.00x
🐘 PostgresNitro0.519s (-1.7%)1.059s (+2.6%)0.540s1141.06x
🐘 PostgresExpress0.574s (+11.3% 🔺)1.118s (+4.7%)0.544s1081.17x
💻 LocalNext.js (Turbopack)9.280s (-5.1% 🟢)10.195s (-5.4% 🟢)0.916s1218.95x
💻 LocalExpress9.668s (-4.2%)10.528s (-5.4% 🟢)0.860s1219.74x
💻 LocalNitro9.701s (-2.2%)10.779s (-2.3%)1.077s1219.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.551s (+48.6% 🔺)5.489s (+35.4% 🔺)1.938s221.00x
▲ VercelNext.js (Turbopack)5.084s (+8.5% 🔺)7.510s (+14.7% 🔺)2.426s161.43x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.138s (-2.7%)1.968s (~)0.010s (-16.1% 🟢)2.018s (~)0.880s101.00x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.000s (~)0.001s (~)2.010s (~)0.863s101.01x
💻 LocalNitro1.155s (~)2.004s (~)0.010s (-14.0% 🟢)2.017s (~)0.862s101.01x
💻 LocalExpress1.159s (+0.6%)2.005s (~)0.012s (+2.5%)2.020s (~)0.860s101.02x
🐘 PostgresExpress1.169s (+1.7%)1.997s (~)0.001s (+10.0% 🔺)2.012s (~)0.842s101.03x
🐘 PostgresNitro1.193s (+2.3%)1.995s (~)0.001s (+9.1% 🔺)2.011s (~)0.817s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.409s (+17.7% 🔺)3.498s (+14.0% 🔺)3.398s (+94.0% 🔺)7.469s (+38.2% 🔺)5.061s101.00x
▲ VercelNext.js (Turbopack)2.673s (+29.4% 🔺)3.825s (+10.9% 🔺)3.340s (+137.0% 🔺)7.691s (+42.3% 🔺)5.018s101.11x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.535s (-4.3%)2.002s (~)0.005s (-1.3%)2.024s (~)0.489s301.00x
💻 LocalExpress1.544s (~)2.010s (~)0.012s (-4.9%)2.026s (~)0.482s301.01x
🐘 PostgresNext.js (Turbopack)1.547s (~)2.009s (~)0.005s (-0.6%)2.026s (~)0.479s301.01x
💻 LocalNext.js (Turbopack)1.555s (-1.4%)1.972s (~)0.013s (+1.0%)2.027s (~)0.472s301.01x
💻 LocalNitro1.566s (+0.8%)2.010s (~)0.013s (+5.0%)2.026s (~)0.460s301.02x
🐘 PostgresExpress1.602s (+3.5%)2.002s (~)0.005s (-7.2% 🟢)2.026s (~)0.424s301.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.722s (+13.1% 🔺)8.052s (+9.2% 🔺)0.240s (-24.5% 🟢)8.776s (+6.3% 🔺)2.054s71.00x
▲ VercelExpress7.606s (+29.2% 🔺)9.161s (+27.1% 🔺)0.500s (+131.5% 🔺)10.151s (+28.8% 🔺)2.545s61.13x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.764s (-7.0% 🟢)1.065s (-3.0%)0.000s (-100.0% 🟢)1.079s (-3.6%)0.314s561.00x
🐘 PostgresExpress0.830s (+7.2% 🔺)1.104s (+5.3% 🔺)0.000s (+Infinity% 🔺)1.119s (+5.2% 🔺)0.289s541.09x
🐘 PostgresNext.js (Turbopack)0.965s (+5.3% 🔺)1.429s (+11.9% 🔺)0.000s (+11.9% 🔺)1.436s (+11.8% 🔺)0.471s421.26x
💻 LocalExpress1.212s (-4.4%)1.981s (-1.6%)0.000s (+45.2% 🔺)1.983s (-1.6%)0.771s311.59x
💻 LocalNitro1.256s (-4.8%)1.982s (~)0.000s (-33.3% 🟢)1.984s (~)0.729s311.64x
💻 LocalNext.js (Turbopack)1.323s (-0.9%)1.981s (~)0.000s (+66.7% 🔺)2.017s (~)0.694s301.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.029s (+3.1%)5.021s (~)0.000s (~)5.476s (-1.1%)1.447s111.00x
▲ VercelNext.js (Turbopack)4.175s (+26.8% 🔺)5.055s (+7.5% 🔺)0.000s (-100.0% 🟢)5.543s (+5.5% 🔺)1.367s111.04x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.783s (+5.2% 🔺)2.435s (+8.0% 🔺)0.000s (+8.0% 🔺)2.451s (+8.0% 🔺)0.668s251.00x
🐘 PostgresNitro1.937s (+1.0%)2.410s (-3.5%)0.000s (-100.0% 🟢)2.450s (-2.9%)0.512s261.09x
🐘 PostgresNext.js (Turbopack)2.470s (+21.6% 🔺)2.907s (+11.3% 🔺)0.000s (-100.0% 🟢)2.914s (+10.3% 🔺)0.444s211.39x
💻 LocalExpress3.357s (-7.6% 🟢)3.904s (-7.7% 🟢)0.001s (+40.6% 🔺)3.909s (-7.6% 🟢)0.551s161.88x
💻 LocalNitro3.367s (-5.6% 🟢)3.965s (-1.5%)0.001s (-22.8% 🟢)3.971s (-1.5%)0.604s161.89x
💻 LocalNext.js (Turbopack)3.441s (-6.1% 🟢)3.868s (-7.7% 🟢)0.001s (-47.3% 🟢)3.907s (-7.8% 🟢)0.466s161.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.023s (+27.2% 🔺)7.515s (+30.8% 🔺)0.000s (-100.0% 🟢)8.032s (+28.4% 🔺)2.009s81.00x
▲ VercelNext.js (Turbopack)6.120s (+32.1% 🔺)7.138s (+18.6% 🔺)0.000s (+25.0% 🔺)7.761s (+18.0% 🔺)1.641s81.02x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNext.js (Turbopack)13/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres16/21
Nitro🐘 Postgres15/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)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@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 diff in depth — runtime span wiring, the builders codegen across all three generated route templates (workflow-only bundleFinal, combined, and watch-mode combined) plus the sveltekit/astro regex post-processing, the new semantic conventions, and the tests.

The instrumentation is clean, well-scoped, and well-tested. The span-hierarchy change (workflow.execute now nests under workflow.route.flow) is correctly reflected in the linked-mode tests and doesn't regress continuous mode (it still parents to the run-origin via withTraceContext). All three codegen templates declare workflowRouteModuleBodyStartedAt and reference it via the new options, and the sveltekit/astro regex ([^)]* capture) still matches the variable-reference options. CI E2E is fully green.

Main suggestion (re: the turbo-mode question): there is no dedicated turbo span attribute — turbo is only inferable from the workflow.run_started.skip_preloadevent. Recommend adding a workflow.turbo tag on workflow.execute so runs/invocations are trivially filterable by turbo mode (details inline). Everything else is minor/optional; nothing blocking.

skipPreload: boolean
) => {
span?.addEvent('workflow.run_started.create.start', {
'workflow.run_started.skip_preload': skipPreload,

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.

Turbo mode is only observable indirectly here. This workflow.run_started.skip_preload event attribute is currently the only turbo signal on the trace, and it's an awkward proxy for "is this run in turbo mode?":

  • It's a span event attribute, not a span tag, so it's harder to facet/filter on in most backends (Datadog, etc.).
  • The name skip_preload doesn't say "turbo" — someone asking the turbo question wouldn't know to look here.
  • The event is only emitted when run_started.create runs (first delivery). On redeliveries/replays (attempt > 1) there's no event at all, so those invocations carry no turbo signal.

turbo is already computed once per invocation at line 535 (and since turbo requires runInput, this skipPreload=true branch is exactly turbo). Consider also setting a dedicated tag on the workflow.execute span — e.g. right after the turbo constant is computed, where span is in scope:

span?.setAttributes(Attribute.WorkflowTurbo(turbo));

backed by a new export const WorkflowTurbo = SemanticConvention<boolean>('workflow.turbo'). That puts a boolean turbo tag on every workflow.execute span (true on the turbo first delivery, false on replays — which is the precise per-invocation distinction we want), and keeps the existing skip_preload event as the finer-grained marker.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a dedicated workflow.turbo semantic convention and set it on every workflow.execute span after the per-invocation turbo decision is computed. I kept the existing workflow.run_started.skip_preload event attribute as the lower-level marker for the run_started create call.

),
...(routeModuleBodyInitMs === undefined
? {}
: Attribute.WorkflowRouteModuleBodyInitMs(routeModuleBodyInitMs)),

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.

routeModuleBodyInitMs is a process-level constant (computed once at entrypoint construction) but emitted on everyworkflow.route.flow span, so it's identical across all requests from a given instance. Fine for queryability — just noting it isn't per-request.

Also worth setting expectations on what it measures: the marker is captured at the top of the generated module body, i.e. afterimport { workflowEntrypoint } from 'workflow/runtime' has fully evaluated. So it won't capture the @workflow/core import/eval cost (the likely dominant cold-start contributor) and will typically read ~0 in the workflow-only template, where the only body work between the marker and the workflowEntrypoint() call is a string-literal assignment. The PR description already acknowledges this; flagging only so the attribute's usefulness isn't over-read.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the PR description to make this explicit: workflow.route.module_body_init_ms is computed once per route entrypoint, emitted on each request from that in-memory entrypoint, and starts after static ESM imports have already evaluated. No code change here since the behavior is intentional.

return cachedHandler(req);
invocationCount += 1;
const handlerCached = cachedHandler !== undefined;
const spanKind = await getSpanKind('SERVER');

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.

Micro-nit (optional, fine to leave): getSpanKind('SERVER') plus the attributes object are now built on every flow request — including the warm path and when no OTEL SDK is registered — whereas previously the warm path was just return cachedHandler(req). The cost is negligible (awaiting an already-memoized once() promise; trace() awaits the same lazy values anyway), so no action needed; just calling it out since this route is perf-sensitive.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Acknowledged. I left this as-is: the added warm-path work remains a memoized span-kind lookup plus attribute construction, and trace() already hits the same memoized OTel handles. Keeping the route-level span always available seems worth that small overhead for this incident path.

},
async (span) => {
if (!cachedHandler) {
cachedHandler = await trace('workflow.route.init', async () => {

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.

Pre-existing behavior (not introduced by this PR), but now more visible in the spans: two concurrent requests on a cold instance can both observe cachedHandler === undefined and each run getWorldHandlers() + handler(), producing two workflow.route.init spans for a single instance (and handler_cached=false on both). Harmless — handlers are stateless and it's effectively last-write-wins — and the new test covers the sequential case. Not a blocker; just noting that under concurrency the span counts can briefly exceed the "once per warm instance" framing in the PR description.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good call. I updated the PR description to stop implying a strict once-per-instance invariant: it now says workflow.route.init is emitted when the cached handler is created, and that concurrent cold requests can briefly produce more than one init span because the pre-existing cache assignment is last-write-wins.


if (options?.routeModuleBodyStartedAt) {
fields.push(
`routeModuleBodyStartedAt: ${options.routeModuleBodyStartedAt}`

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.

routeModuleBodyStartedAt is interpolated raw here (unlike namespace, which is JSON.stringify'd) because it must emit a code identifier reference, not a string literal — correct by design, and the sole caller passes a hardcoded 'workflowRouteModuleBodyStartedAt'. Since this makes the param a code-injection surface if it's ever called with untrusted input, consider a one-line doc comment on the option noting the value must be a code identifier, not data.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a doc comment on routeModuleBodyStartedAt noting that it is emitted as a raw code identifier/expression in generated route files, not serialized data.

);

/** Route pattern for the request */
export const HttpRoute = SemanticConvention<string>('http.route');

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.

Tiny organizational nit: http.route is defined here in the "Route attributes" block, while the other standard OTEL Http* conventions (HttpRequestMethod, HttpResponseStatusCode, UrlFull, …) are grouped together around lines 286–309. Consider moving it next to them for discoverability.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: moved HttpRoute down next to the other standard HTTP semantic convention helpers and left the workflow-specific route attributes grouped separately.

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

Approving. The workflow.turbo attribute (the main ask from the review) plus the smaller nits are all addressed in c9b83f3 — I'd independently prepared the same set of changes, so rather than push a duplicate commit (which would have collided on the identical WorkflowTurbo/HttpRoute additions) I verified your version instead.

What's now in:

  • workflow.turbo boolean tag on the workflow.execute span (Attribute.WorkflowTurbo(turbo)), so turbo invocations are trivially filterable — true only on the turbo first delivery, false on replays/redeliveries. Complements the finer-grained workflow.run_started.skip_preload event.
  • http.route convention regrouped with the other standard OTEL Http* conventions.
  • Doc comment on createWorkflowEntrypointOptionsCode's routeModuleBodyStartedAt noting it's emitted raw as a code identifier (not data).
  • workflow.turbo assertion added to the linked-mode test.

Verified locally on c9b83f3 (built deps from source): @workflow/core + @workflow/builders typecheck clean, runtime-trace-mode.test.ts (11) and constants.test.ts (7) green. CI E2E was already fully green.

The earlier observations I left (module_body_init_ms being a per-process constant emitted on every span; tiny warm-path overhead; pre-existing concurrent cold-start init) are all non-blocking — fine as-is.

@pranaygp
pranaygp merged commit 2bf5257 into mainJun 23, 2026
108 of 110 checks passed
@pranaygp
pranaygp deleted the kk/otel-flow-module-loading branch June 23, 2026 21:28
@github-actionsgithub-actionsBot mentioned this pull request Jun 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2bf5257 (AI decision).

The core instrumentation in this PR builds directly on main-only runtime architecture: stable's workflowEntrypoint creates the queue handler eagerly and synchronously at module load (no lazy cachedHandler/getWorldHandlers() caching, no getSpanKind), so the new workflow.route.flow/workflow.route.init/workflow.route.get_world_handlers spans have nothing to wrap there. The workflow.turbo attribute and workflow.run_started.skip_preload event instrument the turbo first-delivery path, which does not exist on stable, and the modified runtime-trace-mode.test.ts is also absent on stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2bf5257f97fc4fea036717a7882dfd39bf2b3804

pranaygp added a commit that referenced this pull request Jun 23, 2026
…testing
* origin/main:
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
[web-shared] reskin json viewer (no duplicates, better colours and navigation) (#2434)
Send occurredAt with workflow events (#2580)
docs: use actual eve logo and tidy OSS nav dropdown (#2586)
Display occurredAt in trace details (#2581)
fix(next): discover root entrypoints (#2564)
[core] Turbo: skip the unused run_started event-log preload (#2569)
fix(next): prewarm SWC plugin cache (#2538)
[world-vercel] Use v3 stream endpoint (supports transparent reconnect on timeout) (#2424)
[core] Retry stream reopen against the reconnect budget (#2334)
Add Platformatic World to worlds-manifest.json (#1450)
docs: derive section landing-page cards from the page tree + lint drift (#2567)
pranaygp added a commit that referenced this pull request Jun 26, 2026
* origin/main: (46 commits)
[web] Fix HTTP/2 in bundled server build so observability reads work (#2632)
[core] Fix turbo-mode step-body writes racing run_started (#2629)
fix(world-postgres): rename setup command (#2644)
chore: ignore workflow swc caches (#2640)
fix(ai): ignore intentional stream aborts (#2635)
fix(nitro): reload steps during Vite HMR (#2572)
[world] Make @workflow/world own its Zod dependency (#2622)
feat(web-shared): extend cn with custom tailwind-merge class groups (#2619)
[world-vercel] Enable HTTP/2 for the events API and stream writes (#2573)
Remove shadow from off-screen marker indicator button (#2614)
Fix discovery of dotted JS workflow imports (#2594)
Version Packages (beta) (#2597)
Thread occurredAt into web-shared entities (#2608)
Propagate trace context to vercel-workflow.com in workbench instrumentation (#2601)
Reduce workflow build log noise (#2565)
Version Packages (beta) (#2596)
Use text-gray-900 for trace shortcut helper text (#2595)
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
...
# Conflicts:
#	workbench/nextjs-turbopack/instrumentation.ts
#	workbench/nextjs-webpack/instrumentation.ts
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.

2 participants

@karthikscale3@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Trace /flow route initialization - #2592

Merged
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading
Jun 23, 2026
Merged

Trace /flow route initialization#2592
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

This instruments the Workflow SDK /flow route path so we can explain delay before the route makes its first workflow-server run_started POST.

Instrumentation contract

workflow.route.flow

New server span around each generated /flow route request.

Attributes:

  • workflow.route.type: currently flow
  • workflow.route.handler_cached: whether this invocation reused the in-memory route handler
  • workflow.route.invocation_count: per-process invocation count for this route entrypoint
  • workflow.route.entrypoint_age_ms: milliseconds since workflowEntrypoint() constructed this route entrypoint
  • workflow.route.module_body_init_ms: process-level milliseconds from the generated route module-body timestamp to workflowEntrypoint() construction, when generated routes provide the timestamp
  • http.request.method: request method
  • http.route: /.well-known/workflow/v1/flow
  • http.response.status_code: response status when the handler returns a Response

workflow.route.init

New child span under workflow.route.flow, emitted when the route creates the cached queue handler.

Use this to separate first-hit handler construction from normal warm-route request handling. Under concurrent cold requests, more than one request can observe an empty cache and emit workflow.route.init; that is pre-existing harmless last-write-wins behavior.

workflow.route.get_world_handlers

New child span under workflow.route.init around getWorldHandlers().

Use this to identify latency from loading/creating the world handler layer during route setup.

workflow.route.get_world

New child span under workflow.route.flow, emitted for each workflow invocation before workflow.execute.

Use this to identify latency from resolving the per-invocation world before SDK runtime work posts run_started.

workflow.execute <workflowName>

Existing workflow execution span. This PR adds:

Attributes:

  • workflow.turbo: whether this invocation is using the turbo first-delivery path

Span events:

  • workflow.run_started.create.start: emitted immediately before world.events.create(... run_started ...), i.e. right before the first workflow-server run_started POST begins

Event attributes:

  • workflow.run_started.skip_preload: whether the run_started create call is using the turbo skipPreload path

Reading the signal

  • Gap from workflow.route.flow start to workflow.run_started.create.start: SDK/runtime work before the first workflow-server run_started POST begins.
  • Large workflow.route.module_body_init_ms: generated route module body work after static imports, before the SDK route handler is created. This value is computed once per route entrypoint and emitted on every request from that in-memory entrypoint.
  • Near-zero workflow.route.entrypoint_age_ms: the route entrypoint was just created for this request, so this request hit a cold SDK route instance.
  • workflow.route.handler_cached=false plus workflow.route.init: the request paid route handler creation. Sequentially this happens once per warm instance; concurrent cold requests may briefly produce more than one init span.
  • workflow.route.handler_cached=true: route handler was already created; delay is not from handler construction.
  • workflow.turbo=true: this invocation is the turbo first-delivery path. workflow.run_started.skip_preload=true is the finer-grained marker for the associated run_started create call.

Static ESM import evaluation still happens before generated route body code can run, so this does not directly measure the entire import graph. It gives us the earliest SDK-controlled marker in the generated /flow module, plus the exact pre-run_started marker inside the SDK handler.

Testing

  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec vitest run packages/core/src/runtime-trace-mode.test.ts packages/builders/src/constants.test.ts
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/core typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/builders typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec biome check .changeset/flow-route-init-traces.md packages/builders/src/base-builder.ts packages/builders/src/constants.ts packages/builders/src/constants.test.ts packages/core/src/runtime.ts packages/core/src/runtime-trace-mode.test.ts packages/core/src/telemetry/semantic-conventions.ts (exit 0; reports existing complexity/unused-suppression warnings)
  • Previous full @workflow/core suite under Node 24: all 1322 tests passed, but Vitest exited 1 due an existing pending-fetch shutdown error; packages/core/src/abort-controller-step.test.ts passed by itself.

@vercel

vercelBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9b83f3

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

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

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

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

@github-actions

github-actionsBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production144112301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7281110788360

❌ Failed Tests

▲ Vercel Production (1 failed)

nuxt (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
❌ nuxt124127
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ 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


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jun 23, 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.044s (-5.7% 🟢)1.006s (~)0.962s101.00x
💻 LocalExpress0.046s (-2.1%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.049s (-6.3% 🟢)1.008s (~)0.959s101.11x
🐘 PostgresNext.js (Turbopack)0.056s (+1.1%)1.011s (~)0.955s101.26x
🐘 PostgresNitro0.061s (-6.0% 🟢)1.013s (~)0.952s101.39x
🐘 PostgresExpress0.072s (+3.9%)1.013s (~)0.940s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.292s (+26.2% 🔺)2.213s (+5.2% 🔺)1.921s101.00x
▲ VercelNext.js (Turbopack)0.314s (+16.1% 🔺)2.353s (+15.1% 🔺)2.039s101.08x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.079s (-0.6%)2.006s (~)0.928s101.00x
💻 LocalExpress1.083s (~)2.007s (~)0.923s101.00x
🐘 PostgresNext.js (Turbopack)1.086s (~)2.011s (~)0.925s101.01x
💻 LocalNext.js (Turbopack)1.086s (~)2.006s (~)0.920s101.01x
🐘 PostgresNitro1.101s (~)2.011s (~)0.911s101.02x
🐘 PostgresExpress1.103s (+1.0%)2.011s (~)0.908s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.507s (+8.9% 🔺)3.416s (+20.7% 🔺)1.909s101.00x
▲ VercelNext.js (Turbopack)1.588s (+10.7% 🔺)3.280s (-2.2%)1.692s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.443s (~)11.022s (~)0.580s31.00x
💻 LocalNitro10.446s (~)11.022s (~)0.576s31.00x
💻 LocalExpress10.463s (~)11.022s (~)0.560s31.00x
🐘 PostgresNext.js (Turbopack)10.471s (~)11.017s (~)0.547s31.00x
🐘 PostgresExpress10.484s (~)11.021s (~)0.537s31.00x
🐘 PostgresNitro10.498s (~)11.021s (~)0.523s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.720s (+3.3%)14.402s (+3.8%)1.682s31.00x
▲ VercelNext.js (Turbopack)12.862s (+9.0% 🔺)14.699s (+5.4% 🔺)1.837s31.01x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.569s (~)14.028s (~)0.459s51.00x
💻 LocalExpress13.602s (~)14.028s (~)0.426s51.00x
🐘 PostgresNitro13.641s (-0.6%)14.020s (~)0.379s51.01x
💻 LocalNext.js (Turbopack)13.681s (+0.7%)14.028s (~)0.347s51.01x
🐘 PostgresExpress13.701s (+0.9%)14.022s (~)0.320s51.01x
🐘 PostgresNext.js (Turbopack)13.721s (~)14.021s (~)0.299s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)18.660s (+7.6% 🔺)20.341s (+6.2% 🔺)1.681s31.00x
▲ VercelExpress19.542s (+18.9% 🔺)21.199s (+17.8% 🔺)1.657s31.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.055s (-1.0%)13.025s (~)0.970s71.00x
💻 LocalExpress12.145s (-1.6%)13.027s (~)0.882s71.01x
🐘 PostgresNitro12.162s (-0.9%)13.020s (~)0.858s71.01x
💻 LocalNext.js (Turbopack)12.189s (-0.5%)13.025s (~)0.836s71.01x
🐘 PostgresNext.js (Turbopack)12.218s (+1.0%)13.023s (~)0.805s71.01x
🐘 PostgresExpress12.276s (+1.4%)13.018s (~)0.742s71.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.857s (+35.3% 🔺)26.850s (+33.2% 🔺)1.994s41.00x
▲ VercelNext.js (Turbopack)25.505s (+31.8% 🔺)27.773s (+30.8% 🔺)2.268s41.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.165s (-1.0%)2.008s (~)0.843s151.00x
🐘 PostgresNitro1.194s (+1.0%)2.007s (~)0.813s151.02x
🐘 PostgresExpress1.213s (+3.8%)2.008s (~)0.795s151.04x
💻 LocalNext.js (Turbopack)1.380s (-7.8% 🟢)2.006s (~)0.626s151.18x
💻 LocalExpress1.385s (~)2.006s (~)0.621s151.19x
💻 LocalNitro1.396s (-2.9%)2.006s (~)0.611s151.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.793s (+29.9% 🔺)4.385s (+15.7% 🔺)1.592s71.00x
▲ VercelExpress3.109s (+53.2% 🔺)5.068s (+44.8% 🔺)1.959s61.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.301s (-0.9%)3.009s (+3.1%)1.708s101.00x
🐘 PostgresExpress1.343s (-3.6%)2.509s (-6.2% 🟢)1.166s121.03x
🐘 PostgresNitro1.348s (+0.6%)2.510s (~)1.162s121.04x
💻 LocalNitro2.313s (-3.9%)2.736s (-6.2% 🟢)0.423s111.78x
💻 LocalNext.js (Turbopack)2.391s (-12.6% 🟢)3.008s (-3.2%)0.618s101.84x
💻 LocalExpress2.529s (+7.3% 🔺)2.918s (~)0.389s111.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.579s (+7.1% 🔺)5.199s (+3.0%)1.620s61.00x
▲ VercelExpress3.756s (+3.4%)5.354s (+3.7%)1.598s61.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.591s (-3.0%)4.010s (-5.9% 🟢)2.419s81.00x
🐘 PostgresExpress1.637s (+3.2%)4.136s (-3.9%)2.499s81.03x
🐘 PostgresNext.js (Turbopack)2.944s (+4.6%)5.349s (-3.0%)2.406s61.85x
💻 LocalNext.js (Turbopack)4.978s (-9.0% 🟢)5.349s (-11.1% 🟢)0.371s63.13x
💻 LocalNitro5.957s (-7.5% 🟢)6.616s (-8.3% 🟢)0.658s53.74x
💻 LocalExpress6.009s (-5.0%)6.619s (-5.7% 🟢)0.611s53.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.831s (+38.8% 🔺)6.612s (+27.1% 🔺)1.781s51.00x
▲ VercelExpress4.852s (+58.3% 🔺)6.870s (+36.2% 🔺)2.018s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.170s (~)2.007s (~)0.837s151.00x
🐘 PostgresNitro1.178s (-2.4%)2.007s (~)0.829s151.01x
🐘 PostgresExpress1.182s (~)2.008s (~)0.826s151.01x
💻 LocalNitro1.357s (-4.5%)2.006s (~)0.649s151.16x
💻 LocalNext.js (Turbopack)1.399s (~)2.006s (~)0.607s151.20x
💻 LocalExpress1.452s (+1.0%)2.007s (~)0.555s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.794s (-51.7% 🟢)4.333s (-41.8% 🟢)1.539s81.00x
▲ VercelExpress2.871s (+39.1% 🔺)4.642s (+32.7% 🔺)1.772s71.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.294s (-5.7% 🟢)2.508s (+1.5%)1.214s121.00x
🐘 PostgresNext.js (Turbopack)1.306s (+1.3%)2.918s (~)1.611s111.01x
🐘 PostgresExpress1.328s (-1.0%)2.222s (-10.1% 🟢)0.894s141.03x
💻 LocalNitro2.318s (-10.2% 🟢)3.008s (-3.2%)0.691s101.79x
💻 LocalNext.js (Turbopack)2.607s (-1.2%)3.108s (+3.3%)0.502s102.01x
💻 LocalExpress2.613s (+7.9% 🔺)3.009s (-3.2%)0.396s102.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.700s (-22.0% 🟢)5.332s (-23.3% 🟢)1.631s61.00x
▲ VercelExpress4.302s (-10.2% 🟢)5.906s (-11.0% 🟢)1.604s61.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.610s (+1.5%)4.011s (-3.0%)2.401s81.00x
🐘 PostgresNitro1.620s (-2.4%)4.137s (+3.1%)2.517s81.01x
🐘 PostgresNext.js (Turbopack)3.012s (+5.4% 🔺)5.848s (~)2.837s61.87x
💻 LocalNext.js (Turbopack)4.998s (-12.6% 🟢)5.683s (-5.5% 🟢)0.685s63.11x
💻 LocalExpress6.217s (-7.2% 🟢)7.016s (-2.8%)0.799s53.86x
💻 LocalNitro6.501s (-4.1%)7.020s (-2.8%)0.518s54.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.961s (-15.5% 🟢)5.681s (-9.8% 🟢)1.720s61.00x
▲ VercelNext.js (Turbopack)4.577s (+46.5% 🔺)6.497s (+25.9% 🔺)1.920s51.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.507s (-0.9%)1.006s (~)0.500s601.00x
🐘 PostgresNitro0.538s (-9.6% 🟢)1.006s (-3.4%)0.468s601.06x
🐘 PostgresExpress0.561s (+5.1% 🔺)1.023s (~)0.462s591.11x
💻 LocalNext.js (Turbopack)0.565s (-6.5% 🟢)1.005s (-1.7%)0.440s601.11x
💻 LocalNitro0.580s (-1.0%)1.022s (+1.6%)0.442s591.15x
💻 LocalExpress0.584s (+1.9%)1.005s (~)0.421s601.15x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.239s (+39.5% 🔺)6.064s (+21.4% 🔺)1.825s111.00x
▲ VercelExpress4.460s (+59.4% 🔺)6.406s (+42.8% 🔺)1.946s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.221s (-1.9%)2.007s (-1.1%)0.786s451.00x
🐘 PostgresNitro1.294s (-8.5% 🟢)2.007s (-2.3%)0.713s451.06x
🐘 PostgresExpress1.379s (+10.3% 🔺)2.030s (+1.1%)0.651s451.13x
💻 LocalNext.js (Turbopack)1.406s (-4.2%)2.005s (~)0.599s451.15x
💻 LocalExpress1.436s (-1.3%)2.006s (~)0.570s451.18x
💻 LocalNitro1.478s (~)2.028s (+1.1%)0.550s451.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.677s (+80.6% 🔺)13.672s (+70.1% 🔺)1.995s71.00x
▲ VercelNext.js (Turbopack)12.418s (+73.8% 🔺)14.184s (+59.5% 🔺)1.766s71.06x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)2.476s (~)3.034s (-0.8%)0.558s401.00x
🐘 PostgresNitro2.595s (-4.4%)3.085s (+0.9%)0.490s391.05x
🐘 PostgresExpress2.846s (+9.7% 🔺)3.194s (+3.5%)0.347s381.15x
💻 LocalNitro3.052s (-5.2% 🟢)3.675s (-8.3% 🟢)0.623s331.23x
💻 LocalNext.js (Turbopack)3.099s (-5.0%)3.675s (-9.1% 🟢)0.577s331.25x
💻 LocalExpress3.179s (-2.1%)3.913s (-1.6%)0.734s311.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.472s (+63.0% 🔺)24.735s (+60.0% 🔺)2.262s51.00x
▲ VercelNext.js (Turbopack)26.936s (+64.1% 🔺)28.462s (+54.0% 🔺)1.527s51.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.166s (-0.8%)1.006s (~)0.840s601.00x
🐘 PostgresNitro0.209s (-7.8% 🟢)1.006s (~)0.797s601.26x
🐘 PostgresExpress0.237s (+12.5% 🔺)1.007s (~)0.770s601.42x
💻 LocalNitro0.431s (-7.9% 🟢)1.004s (~)0.573s602.59x
💻 LocalExpress0.455s (+5.5% 🔺)1.005s (~)0.550s602.73x
💻 LocalNext.js (Turbopack)0.564s (-3.5%)1.022s (+1.7%)0.457s593.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.803s (-0.8%)3.477s (-2.0%)1.674s181.00x
▲ VercelExpress1.994s (-3.8%)3.433s (-10.6% 🟢)1.439s181.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.253s (-5.5% 🟢)1.006s (~)0.752s901.00x
🐘 PostgresNitro0.326s (-3.3%)1.006s (~)0.680s901.29x
🐘 PostgresExpress0.356s (+10.2% 🔺)1.018s (~)0.662s891.41x
💻 LocalNitro2.085s (-2.3%)2.686s (~)0.602s348.23x
💻 LocalExpress2.121s (-1.8%)2.766s (+1.1%)0.645s338.37x
💻 LocalNext.js (Turbopack)2.599s (-6.8% 🟢)3.113s (-4.5%)0.514s2910.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.670s (+8.4% 🔺)4.342s (+7.1% 🔺)1.672s211.00x
▲ VercelNext.js (Turbopack)3.196s (+47.3% 🔺)5.002s (+27.3% 🔺)1.806s191.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.490s (+3.8%)3.343s (+11.1% 🔺)2.853s361.00x
🐘 PostgresNitro0.519s (-1.7%)1.059s (+2.6%)0.540s1141.06x
🐘 PostgresExpress0.574s (+11.3% 🔺)1.118s (+4.7%)0.544s1081.17x
💻 LocalNext.js (Turbopack)9.280s (-5.1% 🟢)10.195s (-5.4% 🟢)0.916s1218.95x
💻 LocalExpress9.668s (-4.2%)10.528s (-5.4% 🟢)0.860s1219.74x
💻 LocalNitro9.701s (-2.2%)10.779s (-2.3%)1.077s1219.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.551s (+48.6% 🔺)5.489s (+35.4% 🔺)1.938s221.00x
▲ VercelNext.js (Turbopack)5.084s (+8.5% 🔺)7.510s (+14.7% 🔺)2.426s161.43x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.138s (-2.7%)1.968s (~)0.010s (-16.1% 🟢)2.018s (~)0.880s101.00x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.000s (~)0.001s (~)2.010s (~)0.863s101.01x
💻 LocalNitro1.155s (~)2.004s (~)0.010s (-14.0% 🟢)2.017s (~)0.862s101.01x
💻 LocalExpress1.159s (+0.6%)2.005s (~)0.012s (+2.5%)2.020s (~)0.860s101.02x
🐘 PostgresExpress1.169s (+1.7%)1.997s (~)0.001s (+10.0% 🔺)2.012s (~)0.842s101.03x
🐘 PostgresNitro1.193s (+2.3%)1.995s (~)0.001s (+9.1% 🔺)2.011s (~)0.817s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.409s (+17.7% 🔺)3.498s (+14.0% 🔺)3.398s (+94.0% 🔺)7.469s (+38.2% 🔺)5.061s101.00x
▲ VercelNext.js (Turbopack)2.673s (+29.4% 🔺)3.825s (+10.9% 🔺)3.340s (+137.0% 🔺)7.691s (+42.3% 🔺)5.018s101.11x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.535s (-4.3%)2.002s (~)0.005s (-1.3%)2.024s (~)0.489s301.00x
💻 LocalExpress1.544s (~)2.010s (~)0.012s (-4.9%)2.026s (~)0.482s301.01x
🐘 PostgresNext.js (Turbopack)1.547s (~)2.009s (~)0.005s (-0.6%)2.026s (~)0.479s301.01x
💻 LocalNext.js (Turbopack)1.555s (-1.4%)1.972s (~)0.013s (+1.0%)2.027s (~)0.472s301.01x
💻 LocalNitro1.566s (+0.8%)2.010s (~)0.013s (+5.0%)2.026s (~)0.460s301.02x
🐘 PostgresExpress1.602s (+3.5%)2.002s (~)0.005s (-7.2% 🟢)2.026s (~)0.424s301.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.722s (+13.1% 🔺)8.052s (+9.2% 🔺)0.240s (-24.5% 🟢)8.776s (+6.3% 🔺)2.054s71.00x
▲ VercelExpress7.606s (+29.2% 🔺)9.161s (+27.1% 🔺)0.500s (+131.5% 🔺)10.151s (+28.8% 🔺)2.545s61.13x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.764s (-7.0% 🟢)1.065s (-3.0%)0.000s (-100.0% 🟢)1.079s (-3.6%)0.314s561.00x
🐘 PostgresExpress0.830s (+7.2% 🔺)1.104s (+5.3% 🔺)0.000s (+Infinity% 🔺)1.119s (+5.2% 🔺)0.289s541.09x
🐘 PostgresNext.js (Turbopack)0.965s (+5.3% 🔺)1.429s (+11.9% 🔺)0.000s (+11.9% 🔺)1.436s (+11.8% 🔺)0.471s421.26x
💻 LocalExpress1.212s (-4.4%)1.981s (-1.6%)0.000s (+45.2% 🔺)1.983s (-1.6%)0.771s311.59x
💻 LocalNitro1.256s (-4.8%)1.982s (~)0.000s (-33.3% 🟢)1.984s (~)0.729s311.64x
💻 LocalNext.js (Turbopack)1.323s (-0.9%)1.981s (~)0.000s (+66.7% 🔺)2.017s (~)0.694s301.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.029s (+3.1%)5.021s (~)0.000s (~)5.476s (-1.1%)1.447s111.00x
▲ VercelNext.js (Turbopack)4.175s (+26.8% 🔺)5.055s (+7.5% 🔺)0.000s (-100.0% 🟢)5.543s (+5.5% 🔺)1.367s111.04x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.783s (+5.2% 🔺)2.435s (+8.0% 🔺)0.000s (+8.0% 🔺)2.451s (+8.0% 🔺)0.668s251.00x
🐘 PostgresNitro1.937s (+1.0%)2.410s (-3.5%)0.000s (-100.0% 🟢)2.450s (-2.9%)0.512s261.09x
🐘 PostgresNext.js (Turbopack)2.470s (+21.6% 🔺)2.907s (+11.3% 🔺)0.000s (-100.0% 🟢)2.914s (+10.3% 🔺)0.444s211.39x
💻 LocalExpress3.357s (-7.6% 🟢)3.904s (-7.7% 🟢)0.001s (+40.6% 🔺)3.909s (-7.6% 🟢)0.551s161.88x
💻 LocalNitro3.367s (-5.6% 🟢)3.965s (-1.5%)0.001s (-22.8% 🟢)3.971s (-1.5%)0.604s161.89x
💻 LocalNext.js (Turbopack)3.441s (-6.1% 🟢)3.868s (-7.7% 🟢)0.001s (-47.3% 🟢)3.907s (-7.8% 🟢)0.466s161.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.023s (+27.2% 🔺)7.515s (+30.8% 🔺)0.000s (-100.0% 🟢)8.032s (+28.4% 🔺)2.009s81.00x
▲ VercelNext.js (Turbopack)6.120s (+32.1% 🔺)7.138s (+18.6% 🔺)0.000s (+25.0% 🔺)7.761s (+18.0% 🔺)1.641s81.02x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNext.js (Turbopack)13/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres16/21
Nitro🐘 Postgres15/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)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@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 diff in depth — runtime span wiring, the builders codegen across all three generated route templates (workflow-only bundleFinal, combined, and watch-mode combined) plus the sveltekit/astro regex post-processing, the new semantic conventions, and the tests.

The instrumentation is clean, well-scoped, and well-tested. The span-hierarchy change (workflow.execute now nests under workflow.route.flow) is correctly reflected in the linked-mode tests and doesn't regress continuous mode (it still parents to the run-origin via withTraceContext). All three codegen templates declare workflowRouteModuleBodyStartedAt and reference it via the new options, and the sveltekit/astro regex ([^)]* capture) still matches the variable-reference options. CI E2E is fully green.

Main suggestion (re: the turbo-mode question): there is no dedicated turbo span attribute — turbo is only inferable from the workflow.run_started.skip_preloadevent. Recommend adding a workflow.turbo tag on workflow.execute so runs/invocations are trivially filterable by turbo mode (details inline). Everything else is minor/optional; nothing blocking.

skipPreload: boolean
) => {
span?.addEvent('workflow.run_started.create.start', {
'workflow.run_started.skip_preload': skipPreload,

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.

Turbo mode is only observable indirectly here. This workflow.run_started.skip_preload event attribute is currently the only turbo signal on the trace, and it's an awkward proxy for "is this run in turbo mode?":

  • It's a span event attribute, not a span tag, so it's harder to facet/filter on in most backends (Datadog, etc.).
  • The name skip_preload doesn't say "turbo" — someone asking the turbo question wouldn't know to look here.
  • The event is only emitted when run_started.create runs (first delivery). On redeliveries/replays (attempt > 1) there's no event at all, so those invocations carry no turbo signal.

turbo is already computed once per invocation at line 535 (and since turbo requires runInput, this skipPreload=true branch is exactly turbo). Consider also setting a dedicated tag on the workflow.execute span — e.g. right after the turbo constant is computed, where span is in scope:

span?.setAttributes(Attribute.WorkflowTurbo(turbo));

backed by a new export const WorkflowTurbo = SemanticConvention<boolean>('workflow.turbo'). That puts a boolean turbo tag on every workflow.execute span (true on the turbo first delivery, false on replays — which is the precise per-invocation distinction we want), and keeps the existing skip_preload event as the finer-grained marker.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a dedicated workflow.turbo semantic convention and set it on every workflow.execute span after the per-invocation turbo decision is computed. I kept the existing workflow.run_started.skip_preload event attribute as the lower-level marker for the run_started create call.

),
...(routeModuleBodyInitMs === undefined
? {}
: Attribute.WorkflowRouteModuleBodyInitMs(routeModuleBodyInitMs)),

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.

routeModuleBodyInitMs is a process-level constant (computed once at entrypoint construction) but emitted on everyworkflow.route.flow span, so it's identical across all requests from a given instance. Fine for queryability — just noting it isn't per-request.

Also worth setting expectations on what it measures: the marker is captured at the top of the generated module body, i.e. afterimport { workflowEntrypoint } from 'workflow/runtime' has fully evaluated. So it won't capture the @workflow/core import/eval cost (the likely dominant cold-start contributor) and will typically read ~0 in the workflow-only template, where the only body work between the marker and the workflowEntrypoint() call is a string-literal assignment. The PR description already acknowledges this; flagging only so the attribute's usefulness isn't over-read.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the PR description to make this explicit: workflow.route.module_body_init_ms is computed once per route entrypoint, emitted on each request from that in-memory entrypoint, and starts after static ESM imports have already evaluated. No code change here since the behavior is intentional.

return cachedHandler(req);
invocationCount += 1;
const handlerCached = cachedHandler !== undefined;
const spanKind = await getSpanKind('SERVER');

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.

Micro-nit (optional, fine to leave): getSpanKind('SERVER') plus the attributes object are now built on every flow request — including the warm path and when no OTEL SDK is registered — whereas previously the warm path was just return cachedHandler(req). The cost is negligible (awaiting an already-memoized once() promise; trace() awaits the same lazy values anyway), so no action needed; just calling it out since this route is perf-sensitive.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Acknowledged. I left this as-is: the added warm-path work remains a memoized span-kind lookup plus attribute construction, and trace() already hits the same memoized OTel handles. Keeping the route-level span always available seems worth that small overhead for this incident path.

},
async (span) => {
if (!cachedHandler) {
cachedHandler = await trace('workflow.route.init', async () => {

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.

Pre-existing behavior (not introduced by this PR), but now more visible in the spans: two concurrent requests on a cold instance can both observe cachedHandler === undefined and each run getWorldHandlers() + handler(), producing two workflow.route.init spans for a single instance (and handler_cached=false on both). Harmless — handlers are stateless and it's effectively last-write-wins — and the new test covers the sequential case. Not a blocker; just noting that under concurrency the span counts can briefly exceed the "once per warm instance" framing in the PR description.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good call. I updated the PR description to stop implying a strict once-per-instance invariant: it now says workflow.route.init is emitted when the cached handler is created, and that concurrent cold requests can briefly produce more than one init span because the pre-existing cache assignment is last-write-wins.


if (options?.routeModuleBodyStartedAt) {
fields.push(
`routeModuleBodyStartedAt: ${options.routeModuleBodyStartedAt}`

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.

routeModuleBodyStartedAt is interpolated raw here (unlike namespace, which is JSON.stringify'd) because it must emit a code identifier reference, not a string literal — correct by design, and the sole caller passes a hardcoded 'workflowRouteModuleBodyStartedAt'. Since this makes the param a code-injection surface if it's ever called with untrusted input, consider a one-line doc comment on the option noting the value must be a code identifier, not data.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a doc comment on routeModuleBodyStartedAt noting that it is emitted as a raw code identifier/expression in generated route files, not serialized data.

);

/** Route pattern for the request */
export const HttpRoute = SemanticConvention<string>('http.route');

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.

Tiny organizational nit: http.route is defined here in the "Route attributes" block, while the other standard OTEL Http* conventions (HttpRequestMethod, HttpResponseStatusCode, UrlFull, …) are grouped together around lines 286–309. Consider moving it next to them for discoverability.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: moved HttpRoute down next to the other standard HTTP semantic convention helpers and left the workflow-specific route attributes grouped separately.

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

Approving. The workflow.turbo attribute (the main ask from the review) plus the smaller nits are all addressed in c9b83f3 — I'd independently prepared the same set of changes, so rather than push a duplicate commit (which would have collided on the identical WorkflowTurbo/HttpRoute additions) I verified your version instead.

What's now in:

  • workflow.turbo boolean tag on the workflow.execute span (Attribute.WorkflowTurbo(turbo)), so turbo invocations are trivially filterable — true only on the turbo first delivery, false on replays/redeliveries. Complements the finer-grained workflow.run_started.skip_preload event.
  • http.route convention regrouped with the other standard OTEL Http* conventions.
  • Doc comment on createWorkflowEntrypointOptionsCode's routeModuleBodyStartedAt noting it's emitted raw as a code identifier (not data).
  • workflow.turbo assertion added to the linked-mode test.

Verified locally on c9b83f3 (built deps from source): @workflow/core + @workflow/builders typecheck clean, runtime-trace-mode.test.ts (11) and constants.test.ts (7) green. CI E2E was already fully green.

The earlier observations I left (module_body_init_ms being a per-process constant emitted on every span; tiny warm-path overhead; pre-existing concurrent cold-start init) are all non-blocking — fine as-is.

@pranaygp
pranaygp merged commit 2bf5257 into mainJun 23, 2026
108 of 110 checks passed
@pranaygp
pranaygp deleted the kk/otel-flow-module-loading branch June 23, 2026 21:28
@github-actionsgithub-actionsBot mentioned this pull request Jun 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2bf5257 (AI decision).

The core instrumentation in this PR builds directly on main-only runtime architecture: stable's workflowEntrypoint creates the queue handler eagerly and synchronously at module load (no lazy cachedHandler/getWorldHandlers() caching, no getSpanKind), so the new workflow.route.flow/workflow.route.init/workflow.route.get_world_handlers spans have nothing to wrap there. The workflow.turbo attribute and workflow.run_started.skip_preload event instrument the turbo first-delivery path, which does not exist on stable, and the modified runtime-trace-mode.test.ts is also absent on stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2bf5257f97fc4fea036717a7882dfd39bf2b3804

pranaygp added a commit that referenced this pull request Jun 23, 2026
…testing
* origin/main:
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
[web-shared] reskin json viewer (no duplicates, better colours and navigation) (#2434)
Send occurredAt with workflow events (#2580)
docs: use actual eve logo and tidy OSS nav dropdown (#2586)
Display occurredAt in trace details (#2581)
fix(next): discover root entrypoints (#2564)
[core] Turbo: skip the unused run_started event-log preload (#2569)
fix(next): prewarm SWC plugin cache (#2538)
[world-vercel] Use v3 stream endpoint (supports transparent reconnect on timeout) (#2424)
[core] Retry stream reopen against the reconnect budget (#2334)
Add Platformatic World to worlds-manifest.json (#1450)
docs: derive section landing-page cards from the page tree + lint drift (#2567)
pranaygp added a commit that referenced this pull request Jun 26, 2026
* origin/main: (46 commits)
[web] Fix HTTP/2 in bundled server build so observability reads work (#2632)
[core] Fix turbo-mode step-body writes racing run_started (#2629)
fix(world-postgres): rename setup command (#2644)
chore: ignore workflow swc caches (#2640)
fix(ai): ignore intentional stream aborts (#2635)
fix(nitro): reload steps during Vite HMR (#2572)
[world] Make @workflow/world own its Zod dependency (#2622)
feat(web-shared): extend cn with custom tailwind-merge class groups (#2619)
[world-vercel] Enable HTTP/2 for the events API and stream writes (#2573)
Remove shadow from off-screen marker indicator button (#2614)
Fix discovery of dotted JS workflow imports (#2594)
Version Packages (beta) (#2597)
Thread occurredAt into web-shared entities (#2608)
Propagate trace context to vercel-workflow.com in workbench instrumentation (#2601)
Reduce workflow build log noise (#2565)
Version Packages (beta) (#2596)
Use text-gray-900 for trace shortcut helper text (#2595)
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
...
# Conflicts:
#	workbench/nextjs-turbopack/instrumentation.ts
#	workbench/nextjs-webpack/instrumentation.ts
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.

2 participants

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

Trace /flow route initialization - #2592

Merged
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading
Jun 23, 2026
Merged

Trace /flow route initialization#2592
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

This instruments the Workflow SDK /flow route path so we can explain delay before the route makes its first workflow-server run_started POST.

Instrumentation contract

workflow.route.flow

New server span around each generated /flow route request.

Attributes:

  • workflow.route.type: currently flow
  • workflow.route.handler_cached: whether this invocation reused the in-memory route handler
  • workflow.route.invocation_count: per-process invocation count for this route entrypoint
  • workflow.route.entrypoint_age_ms: milliseconds since workflowEntrypoint() constructed this route entrypoint
  • workflow.route.module_body_init_ms: process-level milliseconds from the generated route module-body timestamp to workflowEntrypoint() construction, when generated routes provide the timestamp
  • http.request.method: request method
  • http.route: /.well-known/workflow/v1/flow
  • http.response.status_code: response status when the handler returns a Response

workflow.route.init

New child span under workflow.route.flow, emitted when the route creates the cached queue handler.

Use this to separate first-hit handler construction from normal warm-route request handling. Under concurrent cold requests, more than one request can observe an empty cache and emit workflow.route.init; that is pre-existing harmless last-write-wins behavior.

workflow.route.get_world_handlers

New child span under workflow.route.init around getWorldHandlers().

Use this to identify latency from loading/creating the world handler layer during route setup.

workflow.route.get_world

New child span under workflow.route.flow, emitted for each workflow invocation before workflow.execute.

Use this to identify latency from resolving the per-invocation world before SDK runtime work posts run_started.

workflow.execute <workflowName>

Existing workflow execution span. This PR adds:

Attributes:

  • workflow.turbo: whether this invocation is using the turbo first-delivery path

Span events:

  • workflow.run_started.create.start: emitted immediately before world.events.create(... run_started ...), i.e. right before the first workflow-server run_started POST begins

Event attributes:

  • workflow.run_started.skip_preload: whether the run_started create call is using the turbo skipPreload path

Reading the signal

  • Gap from workflow.route.flow start to workflow.run_started.create.start: SDK/runtime work before the first workflow-server run_started POST begins.
  • Large workflow.route.module_body_init_ms: generated route module body work after static imports, before the SDK route handler is created. This value is computed once per route entrypoint and emitted on every request from that in-memory entrypoint.
  • Near-zero workflow.route.entrypoint_age_ms: the route entrypoint was just created for this request, so this request hit a cold SDK route instance.
  • workflow.route.handler_cached=false plus workflow.route.init: the request paid route handler creation. Sequentially this happens once per warm instance; concurrent cold requests may briefly produce more than one init span.
  • workflow.route.handler_cached=true: route handler was already created; delay is not from handler construction.
  • workflow.turbo=true: this invocation is the turbo first-delivery path. workflow.run_started.skip_preload=true is the finer-grained marker for the associated run_started create call.

Static ESM import evaluation still happens before generated route body code can run, so this does not directly measure the entire import graph. It gives us the earliest SDK-controlled marker in the generated /flow module, plus the exact pre-run_started marker inside the SDK handler.

Testing

  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec vitest run packages/core/src/runtime-trace-mode.test.ts packages/builders/src/constants.test.ts
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/core typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/builders typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec biome check .changeset/flow-route-init-traces.md packages/builders/src/base-builder.ts packages/builders/src/constants.ts packages/builders/src/constants.test.ts packages/core/src/runtime.ts packages/core/src/runtime-trace-mode.test.ts packages/core/src/telemetry/semantic-conventions.ts (exit 0; reports existing complexity/unused-suppression warnings)
  • Previous full @workflow/core suite under Node 24: all 1322 tests passed, but Vitest exited 1 due an existing pending-fetch shutdown error; packages/core/src/abort-controller-step.test.ts passed by itself.

@vercel

vercelBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9b83f3

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

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

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

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

@github-actions

github-actionsBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production144112301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7281110788360

❌ Failed Tests

▲ Vercel Production (1 failed)

nuxt (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
❌ nuxt124127
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ 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


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jun 23, 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.044s (-5.7% 🟢)1.006s (~)0.962s101.00x
💻 LocalExpress0.046s (-2.1%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.049s (-6.3% 🟢)1.008s (~)0.959s101.11x
🐘 PostgresNext.js (Turbopack)0.056s (+1.1%)1.011s (~)0.955s101.26x
🐘 PostgresNitro0.061s (-6.0% 🟢)1.013s (~)0.952s101.39x
🐘 PostgresExpress0.072s (+3.9%)1.013s (~)0.940s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.292s (+26.2% 🔺)2.213s (+5.2% 🔺)1.921s101.00x
▲ VercelNext.js (Turbopack)0.314s (+16.1% 🔺)2.353s (+15.1% 🔺)2.039s101.08x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.079s (-0.6%)2.006s (~)0.928s101.00x
💻 LocalExpress1.083s (~)2.007s (~)0.923s101.00x
🐘 PostgresNext.js (Turbopack)1.086s (~)2.011s (~)0.925s101.01x
💻 LocalNext.js (Turbopack)1.086s (~)2.006s (~)0.920s101.01x
🐘 PostgresNitro1.101s (~)2.011s (~)0.911s101.02x
🐘 PostgresExpress1.103s (+1.0%)2.011s (~)0.908s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.507s (+8.9% 🔺)3.416s (+20.7% 🔺)1.909s101.00x
▲ VercelNext.js (Turbopack)1.588s (+10.7% 🔺)3.280s (-2.2%)1.692s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.443s (~)11.022s (~)0.580s31.00x
💻 LocalNitro10.446s (~)11.022s (~)0.576s31.00x
💻 LocalExpress10.463s (~)11.022s (~)0.560s31.00x
🐘 PostgresNext.js (Turbopack)10.471s (~)11.017s (~)0.547s31.00x
🐘 PostgresExpress10.484s (~)11.021s (~)0.537s31.00x
🐘 PostgresNitro10.498s (~)11.021s (~)0.523s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.720s (+3.3%)14.402s (+3.8%)1.682s31.00x
▲ VercelNext.js (Turbopack)12.862s (+9.0% 🔺)14.699s (+5.4% 🔺)1.837s31.01x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.569s (~)14.028s (~)0.459s51.00x
💻 LocalExpress13.602s (~)14.028s (~)0.426s51.00x
🐘 PostgresNitro13.641s (-0.6%)14.020s (~)0.379s51.01x
💻 LocalNext.js (Turbopack)13.681s (+0.7%)14.028s (~)0.347s51.01x
🐘 PostgresExpress13.701s (+0.9%)14.022s (~)0.320s51.01x
🐘 PostgresNext.js (Turbopack)13.721s (~)14.021s (~)0.299s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)18.660s (+7.6% 🔺)20.341s (+6.2% 🔺)1.681s31.00x
▲ VercelExpress19.542s (+18.9% 🔺)21.199s (+17.8% 🔺)1.657s31.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.055s (-1.0%)13.025s (~)0.970s71.00x
💻 LocalExpress12.145s (-1.6%)13.027s (~)0.882s71.01x
🐘 PostgresNitro12.162s (-0.9%)13.020s (~)0.858s71.01x
💻 LocalNext.js (Turbopack)12.189s (-0.5%)13.025s (~)0.836s71.01x
🐘 PostgresNext.js (Turbopack)12.218s (+1.0%)13.023s (~)0.805s71.01x
🐘 PostgresExpress12.276s (+1.4%)13.018s (~)0.742s71.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.857s (+35.3% 🔺)26.850s (+33.2% 🔺)1.994s41.00x
▲ VercelNext.js (Turbopack)25.505s (+31.8% 🔺)27.773s (+30.8% 🔺)2.268s41.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.165s (-1.0%)2.008s (~)0.843s151.00x
🐘 PostgresNitro1.194s (+1.0%)2.007s (~)0.813s151.02x
🐘 PostgresExpress1.213s (+3.8%)2.008s (~)0.795s151.04x
💻 LocalNext.js (Turbopack)1.380s (-7.8% 🟢)2.006s (~)0.626s151.18x
💻 LocalExpress1.385s (~)2.006s (~)0.621s151.19x
💻 LocalNitro1.396s (-2.9%)2.006s (~)0.611s151.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.793s (+29.9% 🔺)4.385s (+15.7% 🔺)1.592s71.00x
▲ VercelExpress3.109s (+53.2% 🔺)5.068s (+44.8% 🔺)1.959s61.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.301s (-0.9%)3.009s (+3.1%)1.708s101.00x
🐘 PostgresExpress1.343s (-3.6%)2.509s (-6.2% 🟢)1.166s121.03x
🐘 PostgresNitro1.348s (+0.6%)2.510s (~)1.162s121.04x
💻 LocalNitro2.313s (-3.9%)2.736s (-6.2% 🟢)0.423s111.78x
💻 LocalNext.js (Turbopack)2.391s (-12.6% 🟢)3.008s (-3.2%)0.618s101.84x
💻 LocalExpress2.529s (+7.3% 🔺)2.918s (~)0.389s111.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.579s (+7.1% 🔺)5.199s (+3.0%)1.620s61.00x
▲ VercelExpress3.756s (+3.4%)5.354s (+3.7%)1.598s61.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.591s (-3.0%)4.010s (-5.9% 🟢)2.419s81.00x
🐘 PostgresExpress1.637s (+3.2%)4.136s (-3.9%)2.499s81.03x
🐘 PostgresNext.js (Turbopack)2.944s (+4.6%)5.349s (-3.0%)2.406s61.85x
💻 LocalNext.js (Turbopack)4.978s (-9.0% 🟢)5.349s (-11.1% 🟢)0.371s63.13x
💻 LocalNitro5.957s (-7.5% 🟢)6.616s (-8.3% 🟢)0.658s53.74x
💻 LocalExpress6.009s (-5.0%)6.619s (-5.7% 🟢)0.611s53.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.831s (+38.8% 🔺)6.612s (+27.1% 🔺)1.781s51.00x
▲ VercelExpress4.852s (+58.3% 🔺)6.870s (+36.2% 🔺)2.018s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.170s (~)2.007s (~)0.837s151.00x
🐘 PostgresNitro1.178s (-2.4%)2.007s (~)0.829s151.01x
🐘 PostgresExpress1.182s (~)2.008s (~)0.826s151.01x
💻 LocalNitro1.357s (-4.5%)2.006s (~)0.649s151.16x
💻 LocalNext.js (Turbopack)1.399s (~)2.006s (~)0.607s151.20x
💻 LocalExpress1.452s (+1.0%)2.007s (~)0.555s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.794s (-51.7% 🟢)4.333s (-41.8% 🟢)1.539s81.00x
▲ VercelExpress2.871s (+39.1% 🔺)4.642s (+32.7% 🔺)1.772s71.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.294s (-5.7% 🟢)2.508s (+1.5%)1.214s121.00x
🐘 PostgresNext.js (Turbopack)1.306s (+1.3%)2.918s (~)1.611s111.01x
🐘 PostgresExpress1.328s (-1.0%)2.222s (-10.1% 🟢)0.894s141.03x
💻 LocalNitro2.318s (-10.2% 🟢)3.008s (-3.2%)0.691s101.79x
💻 LocalNext.js (Turbopack)2.607s (-1.2%)3.108s (+3.3%)0.502s102.01x
💻 LocalExpress2.613s (+7.9% 🔺)3.009s (-3.2%)0.396s102.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.700s (-22.0% 🟢)5.332s (-23.3% 🟢)1.631s61.00x
▲ VercelExpress4.302s (-10.2% 🟢)5.906s (-11.0% 🟢)1.604s61.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.610s (+1.5%)4.011s (-3.0%)2.401s81.00x
🐘 PostgresNitro1.620s (-2.4%)4.137s (+3.1%)2.517s81.01x
🐘 PostgresNext.js (Turbopack)3.012s (+5.4% 🔺)5.848s (~)2.837s61.87x
💻 LocalNext.js (Turbopack)4.998s (-12.6% 🟢)5.683s (-5.5% 🟢)0.685s63.11x
💻 LocalExpress6.217s (-7.2% 🟢)7.016s (-2.8%)0.799s53.86x
💻 LocalNitro6.501s (-4.1%)7.020s (-2.8%)0.518s54.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.961s (-15.5% 🟢)5.681s (-9.8% 🟢)1.720s61.00x
▲ VercelNext.js (Turbopack)4.577s (+46.5% 🔺)6.497s (+25.9% 🔺)1.920s51.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.507s (-0.9%)1.006s (~)0.500s601.00x
🐘 PostgresNitro0.538s (-9.6% 🟢)1.006s (-3.4%)0.468s601.06x
🐘 PostgresExpress0.561s (+5.1% 🔺)1.023s (~)0.462s591.11x
💻 LocalNext.js (Turbopack)0.565s (-6.5% 🟢)1.005s (-1.7%)0.440s601.11x
💻 LocalNitro0.580s (-1.0%)1.022s (+1.6%)0.442s591.15x
💻 LocalExpress0.584s (+1.9%)1.005s (~)0.421s601.15x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.239s (+39.5% 🔺)6.064s (+21.4% 🔺)1.825s111.00x
▲ VercelExpress4.460s (+59.4% 🔺)6.406s (+42.8% 🔺)1.946s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.221s (-1.9%)2.007s (-1.1%)0.786s451.00x
🐘 PostgresNitro1.294s (-8.5% 🟢)2.007s (-2.3%)0.713s451.06x
🐘 PostgresExpress1.379s (+10.3% 🔺)2.030s (+1.1%)0.651s451.13x
💻 LocalNext.js (Turbopack)1.406s (-4.2%)2.005s (~)0.599s451.15x
💻 LocalExpress1.436s (-1.3%)2.006s (~)0.570s451.18x
💻 LocalNitro1.478s (~)2.028s (+1.1%)0.550s451.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.677s (+80.6% 🔺)13.672s (+70.1% 🔺)1.995s71.00x
▲ VercelNext.js (Turbopack)12.418s (+73.8% 🔺)14.184s (+59.5% 🔺)1.766s71.06x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)2.476s (~)3.034s (-0.8%)0.558s401.00x
🐘 PostgresNitro2.595s (-4.4%)3.085s (+0.9%)0.490s391.05x
🐘 PostgresExpress2.846s (+9.7% 🔺)3.194s (+3.5%)0.347s381.15x
💻 LocalNitro3.052s (-5.2% 🟢)3.675s (-8.3% 🟢)0.623s331.23x
💻 LocalNext.js (Turbopack)3.099s (-5.0%)3.675s (-9.1% 🟢)0.577s331.25x
💻 LocalExpress3.179s (-2.1%)3.913s (-1.6%)0.734s311.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.472s (+63.0% 🔺)24.735s (+60.0% 🔺)2.262s51.00x
▲ VercelNext.js (Turbopack)26.936s (+64.1% 🔺)28.462s (+54.0% 🔺)1.527s51.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.166s (-0.8%)1.006s (~)0.840s601.00x
🐘 PostgresNitro0.209s (-7.8% 🟢)1.006s (~)0.797s601.26x
🐘 PostgresExpress0.237s (+12.5% 🔺)1.007s (~)0.770s601.42x
💻 LocalNitro0.431s (-7.9% 🟢)1.004s (~)0.573s602.59x
💻 LocalExpress0.455s (+5.5% 🔺)1.005s (~)0.550s602.73x
💻 LocalNext.js (Turbopack)0.564s (-3.5%)1.022s (+1.7%)0.457s593.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.803s (-0.8%)3.477s (-2.0%)1.674s181.00x
▲ VercelExpress1.994s (-3.8%)3.433s (-10.6% 🟢)1.439s181.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.253s (-5.5% 🟢)1.006s (~)0.752s901.00x
🐘 PostgresNitro0.326s (-3.3%)1.006s (~)0.680s901.29x
🐘 PostgresExpress0.356s (+10.2% 🔺)1.018s (~)0.662s891.41x
💻 LocalNitro2.085s (-2.3%)2.686s (~)0.602s348.23x
💻 LocalExpress2.121s (-1.8%)2.766s (+1.1%)0.645s338.37x
💻 LocalNext.js (Turbopack)2.599s (-6.8% 🟢)3.113s (-4.5%)0.514s2910.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.670s (+8.4% 🔺)4.342s (+7.1% 🔺)1.672s211.00x
▲ VercelNext.js (Turbopack)3.196s (+47.3% 🔺)5.002s (+27.3% 🔺)1.806s191.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.490s (+3.8%)3.343s (+11.1% 🔺)2.853s361.00x
🐘 PostgresNitro0.519s (-1.7%)1.059s (+2.6%)0.540s1141.06x
🐘 PostgresExpress0.574s (+11.3% 🔺)1.118s (+4.7%)0.544s1081.17x
💻 LocalNext.js (Turbopack)9.280s (-5.1% 🟢)10.195s (-5.4% 🟢)0.916s1218.95x
💻 LocalExpress9.668s (-4.2%)10.528s (-5.4% 🟢)0.860s1219.74x
💻 LocalNitro9.701s (-2.2%)10.779s (-2.3%)1.077s1219.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.551s (+48.6% 🔺)5.489s (+35.4% 🔺)1.938s221.00x
▲ VercelNext.js (Turbopack)5.084s (+8.5% 🔺)7.510s (+14.7% 🔺)2.426s161.43x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.138s (-2.7%)1.968s (~)0.010s (-16.1% 🟢)2.018s (~)0.880s101.00x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.000s (~)0.001s (~)2.010s (~)0.863s101.01x
💻 LocalNitro1.155s (~)2.004s (~)0.010s (-14.0% 🟢)2.017s (~)0.862s101.01x
💻 LocalExpress1.159s (+0.6%)2.005s (~)0.012s (+2.5%)2.020s (~)0.860s101.02x
🐘 PostgresExpress1.169s (+1.7%)1.997s (~)0.001s (+10.0% 🔺)2.012s (~)0.842s101.03x
🐘 PostgresNitro1.193s (+2.3%)1.995s (~)0.001s (+9.1% 🔺)2.011s (~)0.817s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.409s (+17.7% 🔺)3.498s (+14.0% 🔺)3.398s (+94.0% 🔺)7.469s (+38.2% 🔺)5.061s101.00x
▲ VercelNext.js (Turbopack)2.673s (+29.4% 🔺)3.825s (+10.9% 🔺)3.340s (+137.0% 🔺)7.691s (+42.3% 🔺)5.018s101.11x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.535s (-4.3%)2.002s (~)0.005s (-1.3%)2.024s (~)0.489s301.00x
💻 LocalExpress1.544s (~)2.010s (~)0.012s (-4.9%)2.026s (~)0.482s301.01x
🐘 PostgresNext.js (Turbopack)1.547s (~)2.009s (~)0.005s (-0.6%)2.026s (~)0.479s301.01x
💻 LocalNext.js (Turbopack)1.555s (-1.4%)1.972s (~)0.013s (+1.0%)2.027s (~)0.472s301.01x
💻 LocalNitro1.566s (+0.8%)2.010s (~)0.013s (+5.0%)2.026s (~)0.460s301.02x
🐘 PostgresExpress1.602s (+3.5%)2.002s (~)0.005s (-7.2% 🟢)2.026s (~)0.424s301.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.722s (+13.1% 🔺)8.052s (+9.2% 🔺)0.240s (-24.5% 🟢)8.776s (+6.3% 🔺)2.054s71.00x
▲ VercelExpress7.606s (+29.2% 🔺)9.161s (+27.1% 🔺)0.500s (+131.5% 🔺)10.151s (+28.8% 🔺)2.545s61.13x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.764s (-7.0% 🟢)1.065s (-3.0%)0.000s (-100.0% 🟢)1.079s (-3.6%)0.314s561.00x
🐘 PostgresExpress0.830s (+7.2% 🔺)1.104s (+5.3% 🔺)0.000s (+Infinity% 🔺)1.119s (+5.2% 🔺)0.289s541.09x
🐘 PostgresNext.js (Turbopack)0.965s (+5.3% 🔺)1.429s (+11.9% 🔺)0.000s (+11.9% 🔺)1.436s (+11.8% 🔺)0.471s421.26x
💻 LocalExpress1.212s (-4.4%)1.981s (-1.6%)0.000s (+45.2% 🔺)1.983s (-1.6%)0.771s311.59x
💻 LocalNitro1.256s (-4.8%)1.982s (~)0.000s (-33.3% 🟢)1.984s (~)0.729s311.64x
💻 LocalNext.js (Turbopack)1.323s (-0.9%)1.981s (~)0.000s (+66.7% 🔺)2.017s (~)0.694s301.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.029s (+3.1%)5.021s (~)0.000s (~)5.476s (-1.1%)1.447s111.00x
▲ VercelNext.js (Turbopack)4.175s (+26.8% 🔺)5.055s (+7.5% 🔺)0.000s (-100.0% 🟢)5.543s (+5.5% 🔺)1.367s111.04x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.783s (+5.2% 🔺)2.435s (+8.0% 🔺)0.000s (+8.0% 🔺)2.451s (+8.0% 🔺)0.668s251.00x
🐘 PostgresNitro1.937s (+1.0%)2.410s (-3.5%)0.000s (-100.0% 🟢)2.450s (-2.9%)0.512s261.09x
🐘 PostgresNext.js (Turbopack)2.470s (+21.6% 🔺)2.907s (+11.3% 🔺)0.000s (-100.0% 🟢)2.914s (+10.3% 🔺)0.444s211.39x
💻 LocalExpress3.357s (-7.6% 🟢)3.904s (-7.7% 🟢)0.001s (+40.6% 🔺)3.909s (-7.6% 🟢)0.551s161.88x
💻 LocalNitro3.367s (-5.6% 🟢)3.965s (-1.5%)0.001s (-22.8% 🟢)3.971s (-1.5%)0.604s161.89x
💻 LocalNext.js (Turbopack)3.441s (-6.1% 🟢)3.868s (-7.7% 🟢)0.001s (-47.3% 🟢)3.907s (-7.8% 🟢)0.466s161.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.023s (+27.2% 🔺)7.515s (+30.8% 🔺)0.000s (-100.0% 🟢)8.032s (+28.4% 🔺)2.009s81.00x
▲ VercelNext.js (Turbopack)6.120s (+32.1% 🔺)7.138s (+18.6% 🔺)0.000s (+25.0% 🔺)7.761s (+18.0% 🔺)1.641s81.02x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNext.js (Turbopack)13/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres16/21
Nitro🐘 Postgres15/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)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@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 diff in depth — runtime span wiring, the builders codegen across all three generated route templates (workflow-only bundleFinal, combined, and watch-mode combined) plus the sveltekit/astro regex post-processing, the new semantic conventions, and the tests.

The instrumentation is clean, well-scoped, and well-tested. The span-hierarchy change (workflow.execute now nests under workflow.route.flow) is correctly reflected in the linked-mode tests and doesn't regress continuous mode (it still parents to the run-origin via withTraceContext). All three codegen templates declare workflowRouteModuleBodyStartedAt and reference it via the new options, and the sveltekit/astro regex ([^)]* capture) still matches the variable-reference options. CI E2E is fully green.

Main suggestion (re: the turbo-mode question): there is no dedicated turbo span attribute — turbo is only inferable from the workflow.run_started.skip_preloadevent. Recommend adding a workflow.turbo tag on workflow.execute so runs/invocations are trivially filterable by turbo mode (details inline). Everything else is minor/optional; nothing blocking.

skipPreload: boolean
) => {
span?.addEvent('workflow.run_started.create.start', {
'workflow.run_started.skip_preload': skipPreload,

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.

Turbo mode is only observable indirectly here. This workflow.run_started.skip_preload event attribute is currently the only turbo signal on the trace, and it's an awkward proxy for "is this run in turbo mode?":

  • It's a span event attribute, not a span tag, so it's harder to facet/filter on in most backends (Datadog, etc.).
  • The name skip_preload doesn't say "turbo" — someone asking the turbo question wouldn't know to look here.
  • The event is only emitted when run_started.create runs (first delivery). On redeliveries/replays (attempt > 1) there's no event at all, so those invocations carry no turbo signal.

turbo is already computed once per invocation at line 535 (and since turbo requires runInput, this skipPreload=true branch is exactly turbo). Consider also setting a dedicated tag on the workflow.execute span — e.g. right after the turbo constant is computed, where span is in scope:

span?.setAttributes(Attribute.WorkflowTurbo(turbo));

backed by a new export const WorkflowTurbo = SemanticConvention<boolean>('workflow.turbo'). That puts a boolean turbo tag on every workflow.execute span (true on the turbo first delivery, false on replays — which is the precise per-invocation distinction we want), and keeps the existing skip_preload event as the finer-grained marker.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a dedicated workflow.turbo semantic convention and set it on every workflow.execute span after the per-invocation turbo decision is computed. I kept the existing workflow.run_started.skip_preload event attribute as the lower-level marker for the run_started create call.

),
...(routeModuleBodyInitMs === undefined
? {}
: Attribute.WorkflowRouteModuleBodyInitMs(routeModuleBodyInitMs)),

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.

routeModuleBodyInitMs is a process-level constant (computed once at entrypoint construction) but emitted on everyworkflow.route.flow span, so it's identical across all requests from a given instance. Fine for queryability — just noting it isn't per-request.

Also worth setting expectations on what it measures: the marker is captured at the top of the generated module body, i.e. afterimport { workflowEntrypoint } from 'workflow/runtime' has fully evaluated. So it won't capture the @workflow/core import/eval cost (the likely dominant cold-start contributor) and will typically read ~0 in the workflow-only template, where the only body work between the marker and the workflowEntrypoint() call is a string-literal assignment. The PR description already acknowledges this; flagging only so the attribute's usefulness isn't over-read.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the PR description to make this explicit: workflow.route.module_body_init_ms is computed once per route entrypoint, emitted on each request from that in-memory entrypoint, and starts after static ESM imports have already evaluated. No code change here since the behavior is intentional.

return cachedHandler(req);
invocationCount += 1;
const handlerCached = cachedHandler !== undefined;
const spanKind = await getSpanKind('SERVER');

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.

Micro-nit (optional, fine to leave): getSpanKind('SERVER') plus the attributes object are now built on every flow request — including the warm path and when no OTEL SDK is registered — whereas previously the warm path was just return cachedHandler(req). The cost is negligible (awaiting an already-memoized once() promise; trace() awaits the same lazy values anyway), so no action needed; just calling it out since this route is perf-sensitive.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Acknowledged. I left this as-is: the added warm-path work remains a memoized span-kind lookup plus attribute construction, and trace() already hits the same memoized OTel handles. Keeping the route-level span always available seems worth that small overhead for this incident path.

},
async (span) => {
if (!cachedHandler) {
cachedHandler = await trace('workflow.route.init', async () => {

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.

Pre-existing behavior (not introduced by this PR), but now more visible in the spans: two concurrent requests on a cold instance can both observe cachedHandler === undefined and each run getWorldHandlers() + handler(), producing two workflow.route.init spans for a single instance (and handler_cached=false on both). Harmless — handlers are stateless and it's effectively last-write-wins — and the new test covers the sequential case. Not a blocker; just noting that under concurrency the span counts can briefly exceed the "once per warm instance" framing in the PR description.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good call. I updated the PR description to stop implying a strict once-per-instance invariant: it now says workflow.route.init is emitted when the cached handler is created, and that concurrent cold requests can briefly produce more than one init span because the pre-existing cache assignment is last-write-wins.


if (options?.routeModuleBodyStartedAt) {
fields.push(
`routeModuleBodyStartedAt: ${options.routeModuleBodyStartedAt}`

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.

routeModuleBodyStartedAt is interpolated raw here (unlike namespace, which is JSON.stringify'd) because it must emit a code identifier reference, not a string literal — correct by design, and the sole caller passes a hardcoded 'workflowRouteModuleBodyStartedAt'. Since this makes the param a code-injection surface if it's ever called with untrusted input, consider a one-line doc comment on the option noting the value must be a code identifier, not data.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a doc comment on routeModuleBodyStartedAt noting that it is emitted as a raw code identifier/expression in generated route files, not serialized data.

);

/** Route pattern for the request */
export const HttpRoute = SemanticConvention<string>('http.route');

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.

Tiny organizational nit: http.route is defined here in the "Route attributes" block, while the other standard OTEL Http* conventions (HttpRequestMethod, HttpResponseStatusCode, UrlFull, …) are grouped together around lines 286–309. Consider moving it next to them for discoverability.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: moved HttpRoute down next to the other standard HTTP semantic convention helpers and left the workflow-specific route attributes grouped separately.

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

Approving. The workflow.turbo attribute (the main ask from the review) plus the smaller nits are all addressed in c9b83f3 — I'd independently prepared the same set of changes, so rather than push a duplicate commit (which would have collided on the identical WorkflowTurbo/HttpRoute additions) I verified your version instead.

What's now in:

  • workflow.turbo boolean tag on the workflow.execute span (Attribute.WorkflowTurbo(turbo)), so turbo invocations are trivially filterable — true only on the turbo first delivery, false on replays/redeliveries. Complements the finer-grained workflow.run_started.skip_preload event.
  • http.route convention regrouped with the other standard OTEL Http* conventions.
  • Doc comment on createWorkflowEntrypointOptionsCode's routeModuleBodyStartedAt noting it's emitted raw as a code identifier (not data).
  • workflow.turbo assertion added to the linked-mode test.

Verified locally on c9b83f3 (built deps from source): @workflow/core + @workflow/builders typecheck clean, runtime-trace-mode.test.ts (11) and constants.test.ts (7) green. CI E2E was already fully green.

The earlier observations I left (module_body_init_ms being a per-process constant emitted on every span; tiny warm-path overhead; pre-existing concurrent cold-start init) are all non-blocking — fine as-is.

@pranaygp
pranaygp merged commit 2bf5257 into mainJun 23, 2026
108 of 110 checks passed
@pranaygp
pranaygp deleted the kk/otel-flow-module-loading branch June 23, 2026 21:28
@github-actionsgithub-actionsBot mentioned this pull request Jun 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2bf5257 (AI decision).

The core instrumentation in this PR builds directly on main-only runtime architecture: stable's workflowEntrypoint creates the queue handler eagerly and synchronously at module load (no lazy cachedHandler/getWorldHandlers() caching, no getSpanKind), so the new workflow.route.flow/workflow.route.init/workflow.route.get_world_handlers spans have nothing to wrap there. The workflow.turbo attribute and workflow.run_started.skip_preload event instrument the turbo first-delivery path, which does not exist on stable, and the modified runtime-trace-mode.test.ts is also absent on stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2bf5257f97fc4fea036717a7882dfd39bf2b3804

pranaygp added a commit that referenced this pull request Jun 23, 2026
…testing
* origin/main:
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
[web-shared] reskin json viewer (no duplicates, better colours and navigation) (#2434)
Send occurredAt with workflow events (#2580)
docs: use actual eve logo and tidy OSS nav dropdown (#2586)
Display occurredAt in trace details (#2581)
fix(next): discover root entrypoints (#2564)
[core] Turbo: skip the unused run_started event-log preload (#2569)
fix(next): prewarm SWC plugin cache (#2538)
[world-vercel] Use v3 stream endpoint (supports transparent reconnect on timeout) (#2424)
[core] Retry stream reopen against the reconnect budget (#2334)
Add Platformatic World to worlds-manifest.json (#1450)
docs: derive section landing-page cards from the page tree + lint drift (#2567)
pranaygp added a commit that referenced this pull request Jun 26, 2026
* origin/main: (46 commits)
[web] Fix HTTP/2 in bundled server build so observability reads work (#2632)
[core] Fix turbo-mode step-body writes racing run_started (#2629)
fix(world-postgres): rename setup command (#2644)
chore: ignore workflow swc caches (#2640)
fix(ai): ignore intentional stream aborts (#2635)
fix(nitro): reload steps during Vite HMR (#2572)
[world] Make @workflow/world own its Zod dependency (#2622)
feat(web-shared): extend cn with custom tailwind-merge class groups (#2619)
[world-vercel] Enable HTTP/2 for the events API and stream writes (#2573)
Remove shadow from off-screen marker indicator button (#2614)
Fix discovery of dotted JS workflow imports (#2594)
Version Packages (beta) (#2597)
Thread occurredAt into web-shared entities (#2608)
Propagate trace context to vercel-workflow.com in workbench instrumentation (#2601)
Reduce workflow build log noise (#2565)
Version Packages (beta) (#2596)
Use text-gray-900 for trace shortcut helper text (#2595)
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
...
# Conflicts:
#	workbench/nextjs-turbopack/instrumentation.ts
#	workbench/nextjs-webpack/instrumentation.ts
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.

2 participants

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

Trace /flow route initialization - #2592

Merged
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading
Jun 23, 2026
Merged

Trace /flow route initialization#2592
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

This instruments the Workflow SDK /flow route path so we can explain delay before the route makes its first workflow-server run_started POST.

Instrumentation contract

workflow.route.flow

New server span around each generated /flow route request.

Attributes:

  • workflow.route.type: currently flow
  • workflow.route.handler_cached: whether this invocation reused the in-memory route handler
  • workflow.route.invocation_count: per-process invocation count for this route entrypoint
  • workflow.route.entrypoint_age_ms: milliseconds since workflowEntrypoint() constructed this route entrypoint
  • workflow.route.module_body_init_ms: process-level milliseconds from the generated route module-body timestamp to workflowEntrypoint() construction, when generated routes provide the timestamp
  • http.request.method: request method
  • http.route: /.well-known/workflow/v1/flow
  • http.response.status_code: response status when the handler returns a Response

workflow.route.init

New child span under workflow.route.flow, emitted when the route creates the cached queue handler.

Use this to separate first-hit handler construction from normal warm-route request handling. Under concurrent cold requests, more than one request can observe an empty cache and emit workflow.route.init; that is pre-existing harmless last-write-wins behavior.

workflow.route.get_world_handlers

New child span under workflow.route.init around getWorldHandlers().

Use this to identify latency from loading/creating the world handler layer during route setup.

workflow.route.get_world

New child span under workflow.route.flow, emitted for each workflow invocation before workflow.execute.

Use this to identify latency from resolving the per-invocation world before SDK runtime work posts run_started.

workflow.execute <workflowName>

Existing workflow execution span. This PR adds:

Attributes:

  • workflow.turbo: whether this invocation is using the turbo first-delivery path

Span events:

  • workflow.run_started.create.start: emitted immediately before world.events.create(... run_started ...), i.e. right before the first workflow-server run_started POST begins

Event attributes:

  • workflow.run_started.skip_preload: whether the run_started create call is using the turbo skipPreload path

Reading the signal

  • Gap from workflow.route.flow start to workflow.run_started.create.start: SDK/runtime work before the first workflow-server run_started POST begins.
  • Large workflow.route.module_body_init_ms: generated route module body work after static imports, before the SDK route handler is created. This value is computed once per route entrypoint and emitted on every request from that in-memory entrypoint.
  • Near-zero workflow.route.entrypoint_age_ms: the route entrypoint was just created for this request, so this request hit a cold SDK route instance.
  • workflow.route.handler_cached=false plus workflow.route.init: the request paid route handler creation. Sequentially this happens once per warm instance; concurrent cold requests may briefly produce more than one init span.
  • workflow.route.handler_cached=true: route handler was already created; delay is not from handler construction.
  • workflow.turbo=true: this invocation is the turbo first-delivery path. workflow.run_started.skip_preload=true is the finer-grained marker for the associated run_started create call.

Static ESM import evaluation still happens before generated route body code can run, so this does not directly measure the entire import graph. It gives us the earliest SDK-controlled marker in the generated /flow module, plus the exact pre-run_started marker inside the SDK handler.

Testing

  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec vitest run packages/core/src/runtime-trace-mode.test.ts packages/builders/src/constants.test.ts
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/core typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/builders typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec biome check .changeset/flow-route-init-traces.md packages/builders/src/base-builder.ts packages/builders/src/constants.ts packages/builders/src/constants.test.ts packages/core/src/runtime.ts packages/core/src/runtime-trace-mode.test.ts packages/core/src/telemetry/semantic-conventions.ts (exit 0; reports existing complexity/unused-suppression warnings)
  • Previous full @workflow/core suite under Node 24: all 1322 tests passed, but Vitest exited 1 due an existing pending-fetch shutdown error; packages/core/src/abort-controller-step.test.ts passed by itself.

@vercel

vercelBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9b83f3

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

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

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

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

@github-actions

github-actionsBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production144112301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7281110788360

❌ Failed Tests

▲ Vercel Production (1 failed)

nuxt (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
❌ nuxt124127
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ 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


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jun 23, 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.044s (-5.7% 🟢)1.006s (~)0.962s101.00x
💻 LocalExpress0.046s (-2.1%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.049s (-6.3% 🟢)1.008s (~)0.959s101.11x
🐘 PostgresNext.js (Turbopack)0.056s (+1.1%)1.011s (~)0.955s101.26x
🐘 PostgresNitro0.061s (-6.0% 🟢)1.013s (~)0.952s101.39x
🐘 PostgresExpress0.072s (+3.9%)1.013s (~)0.940s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.292s (+26.2% 🔺)2.213s (+5.2% 🔺)1.921s101.00x
▲ VercelNext.js (Turbopack)0.314s (+16.1% 🔺)2.353s (+15.1% 🔺)2.039s101.08x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.079s (-0.6%)2.006s (~)0.928s101.00x
💻 LocalExpress1.083s (~)2.007s (~)0.923s101.00x
🐘 PostgresNext.js (Turbopack)1.086s (~)2.011s (~)0.925s101.01x
💻 LocalNext.js (Turbopack)1.086s (~)2.006s (~)0.920s101.01x
🐘 PostgresNitro1.101s (~)2.011s (~)0.911s101.02x
🐘 PostgresExpress1.103s (+1.0%)2.011s (~)0.908s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.507s (+8.9% 🔺)3.416s (+20.7% 🔺)1.909s101.00x
▲ VercelNext.js (Turbopack)1.588s (+10.7% 🔺)3.280s (-2.2%)1.692s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.443s (~)11.022s (~)0.580s31.00x
💻 LocalNitro10.446s (~)11.022s (~)0.576s31.00x
💻 LocalExpress10.463s (~)11.022s (~)0.560s31.00x
🐘 PostgresNext.js (Turbopack)10.471s (~)11.017s (~)0.547s31.00x
🐘 PostgresExpress10.484s (~)11.021s (~)0.537s31.00x
🐘 PostgresNitro10.498s (~)11.021s (~)0.523s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.720s (+3.3%)14.402s (+3.8%)1.682s31.00x
▲ VercelNext.js (Turbopack)12.862s (+9.0% 🔺)14.699s (+5.4% 🔺)1.837s31.01x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.569s (~)14.028s (~)0.459s51.00x
💻 LocalExpress13.602s (~)14.028s (~)0.426s51.00x
🐘 PostgresNitro13.641s (-0.6%)14.020s (~)0.379s51.01x
💻 LocalNext.js (Turbopack)13.681s (+0.7%)14.028s (~)0.347s51.01x
🐘 PostgresExpress13.701s (+0.9%)14.022s (~)0.320s51.01x
🐘 PostgresNext.js (Turbopack)13.721s (~)14.021s (~)0.299s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)18.660s (+7.6% 🔺)20.341s (+6.2% 🔺)1.681s31.00x
▲ VercelExpress19.542s (+18.9% 🔺)21.199s (+17.8% 🔺)1.657s31.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.055s (-1.0%)13.025s (~)0.970s71.00x
💻 LocalExpress12.145s (-1.6%)13.027s (~)0.882s71.01x
🐘 PostgresNitro12.162s (-0.9%)13.020s (~)0.858s71.01x
💻 LocalNext.js (Turbopack)12.189s (-0.5%)13.025s (~)0.836s71.01x
🐘 PostgresNext.js (Turbopack)12.218s (+1.0%)13.023s (~)0.805s71.01x
🐘 PostgresExpress12.276s (+1.4%)13.018s (~)0.742s71.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.857s (+35.3% 🔺)26.850s (+33.2% 🔺)1.994s41.00x
▲ VercelNext.js (Turbopack)25.505s (+31.8% 🔺)27.773s (+30.8% 🔺)2.268s41.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.165s (-1.0%)2.008s (~)0.843s151.00x
🐘 PostgresNitro1.194s (+1.0%)2.007s (~)0.813s151.02x
🐘 PostgresExpress1.213s (+3.8%)2.008s (~)0.795s151.04x
💻 LocalNext.js (Turbopack)1.380s (-7.8% 🟢)2.006s (~)0.626s151.18x
💻 LocalExpress1.385s (~)2.006s (~)0.621s151.19x
💻 LocalNitro1.396s (-2.9%)2.006s (~)0.611s151.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.793s (+29.9% 🔺)4.385s (+15.7% 🔺)1.592s71.00x
▲ VercelExpress3.109s (+53.2% 🔺)5.068s (+44.8% 🔺)1.959s61.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.301s (-0.9%)3.009s (+3.1%)1.708s101.00x
🐘 PostgresExpress1.343s (-3.6%)2.509s (-6.2% 🟢)1.166s121.03x
🐘 PostgresNitro1.348s (+0.6%)2.510s (~)1.162s121.04x
💻 LocalNitro2.313s (-3.9%)2.736s (-6.2% 🟢)0.423s111.78x
💻 LocalNext.js (Turbopack)2.391s (-12.6% 🟢)3.008s (-3.2%)0.618s101.84x
💻 LocalExpress2.529s (+7.3% 🔺)2.918s (~)0.389s111.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.579s (+7.1% 🔺)5.199s (+3.0%)1.620s61.00x
▲ VercelExpress3.756s (+3.4%)5.354s (+3.7%)1.598s61.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.591s (-3.0%)4.010s (-5.9% 🟢)2.419s81.00x
🐘 PostgresExpress1.637s (+3.2%)4.136s (-3.9%)2.499s81.03x
🐘 PostgresNext.js (Turbopack)2.944s (+4.6%)5.349s (-3.0%)2.406s61.85x
💻 LocalNext.js (Turbopack)4.978s (-9.0% 🟢)5.349s (-11.1% 🟢)0.371s63.13x
💻 LocalNitro5.957s (-7.5% 🟢)6.616s (-8.3% 🟢)0.658s53.74x
💻 LocalExpress6.009s (-5.0%)6.619s (-5.7% 🟢)0.611s53.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.831s (+38.8% 🔺)6.612s (+27.1% 🔺)1.781s51.00x
▲ VercelExpress4.852s (+58.3% 🔺)6.870s (+36.2% 🔺)2.018s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.170s (~)2.007s (~)0.837s151.00x
🐘 PostgresNitro1.178s (-2.4%)2.007s (~)0.829s151.01x
🐘 PostgresExpress1.182s (~)2.008s (~)0.826s151.01x
💻 LocalNitro1.357s (-4.5%)2.006s (~)0.649s151.16x
💻 LocalNext.js (Turbopack)1.399s (~)2.006s (~)0.607s151.20x
💻 LocalExpress1.452s (+1.0%)2.007s (~)0.555s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.794s (-51.7% 🟢)4.333s (-41.8% 🟢)1.539s81.00x
▲ VercelExpress2.871s (+39.1% 🔺)4.642s (+32.7% 🔺)1.772s71.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.294s (-5.7% 🟢)2.508s (+1.5%)1.214s121.00x
🐘 PostgresNext.js (Turbopack)1.306s (+1.3%)2.918s (~)1.611s111.01x
🐘 PostgresExpress1.328s (-1.0%)2.222s (-10.1% 🟢)0.894s141.03x
💻 LocalNitro2.318s (-10.2% 🟢)3.008s (-3.2%)0.691s101.79x
💻 LocalNext.js (Turbopack)2.607s (-1.2%)3.108s (+3.3%)0.502s102.01x
💻 LocalExpress2.613s (+7.9% 🔺)3.009s (-3.2%)0.396s102.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.700s (-22.0% 🟢)5.332s (-23.3% 🟢)1.631s61.00x
▲ VercelExpress4.302s (-10.2% 🟢)5.906s (-11.0% 🟢)1.604s61.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.610s (+1.5%)4.011s (-3.0%)2.401s81.00x
🐘 PostgresNitro1.620s (-2.4%)4.137s (+3.1%)2.517s81.01x
🐘 PostgresNext.js (Turbopack)3.012s (+5.4% 🔺)5.848s (~)2.837s61.87x
💻 LocalNext.js (Turbopack)4.998s (-12.6% 🟢)5.683s (-5.5% 🟢)0.685s63.11x
💻 LocalExpress6.217s (-7.2% 🟢)7.016s (-2.8%)0.799s53.86x
💻 LocalNitro6.501s (-4.1%)7.020s (-2.8%)0.518s54.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.961s (-15.5% 🟢)5.681s (-9.8% 🟢)1.720s61.00x
▲ VercelNext.js (Turbopack)4.577s (+46.5% 🔺)6.497s (+25.9% 🔺)1.920s51.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.507s (-0.9%)1.006s (~)0.500s601.00x
🐘 PostgresNitro0.538s (-9.6% 🟢)1.006s (-3.4%)0.468s601.06x
🐘 PostgresExpress0.561s (+5.1% 🔺)1.023s (~)0.462s591.11x
💻 LocalNext.js (Turbopack)0.565s (-6.5% 🟢)1.005s (-1.7%)0.440s601.11x
💻 LocalNitro0.580s (-1.0%)1.022s (+1.6%)0.442s591.15x
💻 LocalExpress0.584s (+1.9%)1.005s (~)0.421s601.15x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.239s (+39.5% 🔺)6.064s (+21.4% 🔺)1.825s111.00x
▲ VercelExpress4.460s (+59.4% 🔺)6.406s (+42.8% 🔺)1.946s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.221s (-1.9%)2.007s (-1.1%)0.786s451.00x
🐘 PostgresNitro1.294s (-8.5% 🟢)2.007s (-2.3%)0.713s451.06x
🐘 PostgresExpress1.379s (+10.3% 🔺)2.030s (+1.1%)0.651s451.13x
💻 LocalNext.js (Turbopack)1.406s (-4.2%)2.005s (~)0.599s451.15x
💻 LocalExpress1.436s (-1.3%)2.006s (~)0.570s451.18x
💻 LocalNitro1.478s (~)2.028s (+1.1%)0.550s451.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.677s (+80.6% 🔺)13.672s (+70.1% 🔺)1.995s71.00x
▲ VercelNext.js (Turbopack)12.418s (+73.8% 🔺)14.184s (+59.5% 🔺)1.766s71.06x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)2.476s (~)3.034s (-0.8%)0.558s401.00x
🐘 PostgresNitro2.595s (-4.4%)3.085s (+0.9%)0.490s391.05x
🐘 PostgresExpress2.846s (+9.7% 🔺)3.194s (+3.5%)0.347s381.15x
💻 LocalNitro3.052s (-5.2% 🟢)3.675s (-8.3% 🟢)0.623s331.23x
💻 LocalNext.js (Turbopack)3.099s (-5.0%)3.675s (-9.1% 🟢)0.577s331.25x
💻 LocalExpress3.179s (-2.1%)3.913s (-1.6%)0.734s311.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.472s (+63.0% 🔺)24.735s (+60.0% 🔺)2.262s51.00x
▲ VercelNext.js (Turbopack)26.936s (+64.1% 🔺)28.462s (+54.0% 🔺)1.527s51.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.166s (-0.8%)1.006s (~)0.840s601.00x
🐘 PostgresNitro0.209s (-7.8% 🟢)1.006s (~)0.797s601.26x
🐘 PostgresExpress0.237s (+12.5% 🔺)1.007s (~)0.770s601.42x
💻 LocalNitro0.431s (-7.9% 🟢)1.004s (~)0.573s602.59x
💻 LocalExpress0.455s (+5.5% 🔺)1.005s (~)0.550s602.73x
💻 LocalNext.js (Turbopack)0.564s (-3.5%)1.022s (+1.7%)0.457s593.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.803s (-0.8%)3.477s (-2.0%)1.674s181.00x
▲ VercelExpress1.994s (-3.8%)3.433s (-10.6% 🟢)1.439s181.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.253s (-5.5% 🟢)1.006s (~)0.752s901.00x
🐘 PostgresNitro0.326s (-3.3%)1.006s (~)0.680s901.29x
🐘 PostgresExpress0.356s (+10.2% 🔺)1.018s (~)0.662s891.41x
💻 LocalNitro2.085s (-2.3%)2.686s (~)0.602s348.23x
💻 LocalExpress2.121s (-1.8%)2.766s (+1.1%)0.645s338.37x
💻 LocalNext.js (Turbopack)2.599s (-6.8% 🟢)3.113s (-4.5%)0.514s2910.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.670s (+8.4% 🔺)4.342s (+7.1% 🔺)1.672s211.00x
▲ VercelNext.js (Turbopack)3.196s (+47.3% 🔺)5.002s (+27.3% 🔺)1.806s191.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.490s (+3.8%)3.343s (+11.1% 🔺)2.853s361.00x
🐘 PostgresNitro0.519s (-1.7%)1.059s (+2.6%)0.540s1141.06x
🐘 PostgresExpress0.574s (+11.3% 🔺)1.118s (+4.7%)0.544s1081.17x
💻 LocalNext.js (Turbopack)9.280s (-5.1% 🟢)10.195s (-5.4% 🟢)0.916s1218.95x
💻 LocalExpress9.668s (-4.2%)10.528s (-5.4% 🟢)0.860s1219.74x
💻 LocalNitro9.701s (-2.2%)10.779s (-2.3%)1.077s1219.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.551s (+48.6% 🔺)5.489s (+35.4% 🔺)1.938s221.00x
▲ VercelNext.js (Turbopack)5.084s (+8.5% 🔺)7.510s (+14.7% 🔺)2.426s161.43x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.138s (-2.7%)1.968s (~)0.010s (-16.1% 🟢)2.018s (~)0.880s101.00x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.000s (~)0.001s (~)2.010s (~)0.863s101.01x
💻 LocalNitro1.155s (~)2.004s (~)0.010s (-14.0% 🟢)2.017s (~)0.862s101.01x
💻 LocalExpress1.159s (+0.6%)2.005s (~)0.012s (+2.5%)2.020s (~)0.860s101.02x
🐘 PostgresExpress1.169s (+1.7%)1.997s (~)0.001s (+10.0% 🔺)2.012s (~)0.842s101.03x
🐘 PostgresNitro1.193s (+2.3%)1.995s (~)0.001s (+9.1% 🔺)2.011s (~)0.817s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.409s (+17.7% 🔺)3.498s (+14.0% 🔺)3.398s (+94.0% 🔺)7.469s (+38.2% 🔺)5.061s101.00x
▲ VercelNext.js (Turbopack)2.673s (+29.4% 🔺)3.825s (+10.9% 🔺)3.340s (+137.0% 🔺)7.691s (+42.3% 🔺)5.018s101.11x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.535s (-4.3%)2.002s (~)0.005s (-1.3%)2.024s (~)0.489s301.00x
💻 LocalExpress1.544s (~)2.010s (~)0.012s (-4.9%)2.026s (~)0.482s301.01x
🐘 PostgresNext.js (Turbopack)1.547s (~)2.009s (~)0.005s (-0.6%)2.026s (~)0.479s301.01x
💻 LocalNext.js (Turbopack)1.555s (-1.4%)1.972s (~)0.013s (+1.0%)2.027s (~)0.472s301.01x
💻 LocalNitro1.566s (+0.8%)2.010s (~)0.013s (+5.0%)2.026s (~)0.460s301.02x
🐘 PostgresExpress1.602s (+3.5%)2.002s (~)0.005s (-7.2% 🟢)2.026s (~)0.424s301.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.722s (+13.1% 🔺)8.052s (+9.2% 🔺)0.240s (-24.5% 🟢)8.776s (+6.3% 🔺)2.054s71.00x
▲ VercelExpress7.606s (+29.2% 🔺)9.161s (+27.1% 🔺)0.500s (+131.5% 🔺)10.151s (+28.8% 🔺)2.545s61.13x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.764s (-7.0% 🟢)1.065s (-3.0%)0.000s (-100.0% 🟢)1.079s (-3.6%)0.314s561.00x
🐘 PostgresExpress0.830s (+7.2% 🔺)1.104s (+5.3% 🔺)0.000s (+Infinity% 🔺)1.119s (+5.2% 🔺)0.289s541.09x
🐘 PostgresNext.js (Turbopack)0.965s (+5.3% 🔺)1.429s (+11.9% 🔺)0.000s (+11.9% 🔺)1.436s (+11.8% 🔺)0.471s421.26x
💻 LocalExpress1.212s (-4.4%)1.981s (-1.6%)0.000s (+45.2% 🔺)1.983s (-1.6%)0.771s311.59x
💻 LocalNitro1.256s (-4.8%)1.982s (~)0.000s (-33.3% 🟢)1.984s (~)0.729s311.64x
💻 LocalNext.js (Turbopack)1.323s (-0.9%)1.981s (~)0.000s (+66.7% 🔺)2.017s (~)0.694s301.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.029s (+3.1%)5.021s (~)0.000s (~)5.476s (-1.1%)1.447s111.00x
▲ VercelNext.js (Turbopack)4.175s (+26.8% 🔺)5.055s (+7.5% 🔺)0.000s (-100.0% 🟢)5.543s (+5.5% 🔺)1.367s111.04x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.783s (+5.2% 🔺)2.435s (+8.0% 🔺)0.000s (+8.0% 🔺)2.451s (+8.0% 🔺)0.668s251.00x
🐘 PostgresNitro1.937s (+1.0%)2.410s (-3.5%)0.000s (-100.0% 🟢)2.450s (-2.9%)0.512s261.09x
🐘 PostgresNext.js (Turbopack)2.470s (+21.6% 🔺)2.907s (+11.3% 🔺)0.000s (-100.0% 🟢)2.914s (+10.3% 🔺)0.444s211.39x
💻 LocalExpress3.357s (-7.6% 🟢)3.904s (-7.7% 🟢)0.001s (+40.6% 🔺)3.909s (-7.6% 🟢)0.551s161.88x
💻 LocalNitro3.367s (-5.6% 🟢)3.965s (-1.5%)0.001s (-22.8% 🟢)3.971s (-1.5%)0.604s161.89x
💻 LocalNext.js (Turbopack)3.441s (-6.1% 🟢)3.868s (-7.7% 🟢)0.001s (-47.3% 🟢)3.907s (-7.8% 🟢)0.466s161.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.023s (+27.2% 🔺)7.515s (+30.8% 🔺)0.000s (-100.0% 🟢)8.032s (+28.4% 🔺)2.009s81.00x
▲ VercelNext.js (Turbopack)6.120s (+32.1% 🔺)7.138s (+18.6% 🔺)0.000s (+25.0% 🔺)7.761s (+18.0% 🔺)1.641s81.02x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNext.js (Turbopack)13/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres16/21
Nitro🐘 Postgres15/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)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@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 diff in depth — runtime span wiring, the builders codegen across all three generated route templates (workflow-only bundleFinal, combined, and watch-mode combined) plus the sveltekit/astro regex post-processing, the new semantic conventions, and the tests.

The instrumentation is clean, well-scoped, and well-tested. The span-hierarchy change (workflow.execute now nests under workflow.route.flow) is correctly reflected in the linked-mode tests and doesn't regress continuous mode (it still parents to the run-origin via withTraceContext). All three codegen templates declare workflowRouteModuleBodyStartedAt and reference it via the new options, and the sveltekit/astro regex ([^)]* capture) still matches the variable-reference options. CI E2E is fully green.

Main suggestion (re: the turbo-mode question): there is no dedicated turbo span attribute — turbo is only inferable from the workflow.run_started.skip_preloadevent. Recommend adding a workflow.turbo tag on workflow.execute so runs/invocations are trivially filterable by turbo mode (details inline). Everything else is minor/optional; nothing blocking.

skipPreload: boolean
) => {
span?.addEvent('workflow.run_started.create.start', {
'workflow.run_started.skip_preload': skipPreload,

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.

Turbo mode is only observable indirectly here. This workflow.run_started.skip_preload event attribute is currently the only turbo signal on the trace, and it's an awkward proxy for "is this run in turbo mode?":

  • It's a span event attribute, not a span tag, so it's harder to facet/filter on in most backends (Datadog, etc.).
  • The name skip_preload doesn't say "turbo" — someone asking the turbo question wouldn't know to look here.
  • The event is only emitted when run_started.create runs (first delivery). On redeliveries/replays (attempt > 1) there's no event at all, so those invocations carry no turbo signal.

turbo is already computed once per invocation at line 535 (and since turbo requires runInput, this skipPreload=true branch is exactly turbo). Consider also setting a dedicated tag on the workflow.execute span — e.g. right after the turbo constant is computed, where span is in scope:

span?.setAttributes(Attribute.WorkflowTurbo(turbo));

backed by a new export const WorkflowTurbo = SemanticConvention<boolean>('workflow.turbo'). That puts a boolean turbo tag on every workflow.execute span (true on the turbo first delivery, false on replays — which is the precise per-invocation distinction we want), and keeps the existing skip_preload event as the finer-grained marker.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a dedicated workflow.turbo semantic convention and set it on every workflow.execute span after the per-invocation turbo decision is computed. I kept the existing workflow.run_started.skip_preload event attribute as the lower-level marker for the run_started create call.

),
...(routeModuleBodyInitMs === undefined
? {}
: Attribute.WorkflowRouteModuleBodyInitMs(routeModuleBodyInitMs)),

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.

routeModuleBodyInitMs is a process-level constant (computed once at entrypoint construction) but emitted on everyworkflow.route.flow span, so it's identical across all requests from a given instance. Fine for queryability — just noting it isn't per-request.

Also worth setting expectations on what it measures: the marker is captured at the top of the generated module body, i.e. afterimport { workflowEntrypoint } from 'workflow/runtime' has fully evaluated. So it won't capture the @workflow/core import/eval cost (the likely dominant cold-start contributor) and will typically read ~0 in the workflow-only template, where the only body work between the marker and the workflowEntrypoint() call is a string-literal assignment. The PR description already acknowledges this; flagging only so the attribute's usefulness isn't over-read.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the PR description to make this explicit: workflow.route.module_body_init_ms is computed once per route entrypoint, emitted on each request from that in-memory entrypoint, and starts after static ESM imports have already evaluated. No code change here since the behavior is intentional.

return cachedHandler(req);
invocationCount += 1;
const handlerCached = cachedHandler !== undefined;
const spanKind = await getSpanKind('SERVER');

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.

Micro-nit (optional, fine to leave): getSpanKind('SERVER') plus the attributes object are now built on every flow request — including the warm path and when no OTEL SDK is registered — whereas previously the warm path was just return cachedHandler(req). The cost is negligible (awaiting an already-memoized once() promise; trace() awaits the same lazy values anyway), so no action needed; just calling it out since this route is perf-sensitive.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Acknowledged. I left this as-is: the added warm-path work remains a memoized span-kind lookup plus attribute construction, and trace() already hits the same memoized OTel handles. Keeping the route-level span always available seems worth that small overhead for this incident path.

},
async (span) => {
if (!cachedHandler) {
cachedHandler = await trace('workflow.route.init', async () => {

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.

Pre-existing behavior (not introduced by this PR), but now more visible in the spans: two concurrent requests on a cold instance can both observe cachedHandler === undefined and each run getWorldHandlers() + handler(), producing two workflow.route.init spans for a single instance (and handler_cached=false on both). Harmless — handlers are stateless and it's effectively last-write-wins — and the new test covers the sequential case. Not a blocker; just noting that under concurrency the span counts can briefly exceed the "once per warm instance" framing in the PR description.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good call. I updated the PR description to stop implying a strict once-per-instance invariant: it now says workflow.route.init is emitted when the cached handler is created, and that concurrent cold requests can briefly produce more than one init span because the pre-existing cache assignment is last-write-wins.


if (options?.routeModuleBodyStartedAt) {
fields.push(
`routeModuleBodyStartedAt: ${options.routeModuleBodyStartedAt}`

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.

routeModuleBodyStartedAt is interpolated raw here (unlike namespace, which is JSON.stringify'd) because it must emit a code identifier reference, not a string literal — correct by design, and the sole caller passes a hardcoded 'workflowRouteModuleBodyStartedAt'. Since this makes the param a code-injection surface if it's ever called with untrusted input, consider a one-line doc comment on the option noting the value must be a code identifier, not data.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a doc comment on routeModuleBodyStartedAt noting that it is emitted as a raw code identifier/expression in generated route files, not serialized data.

);

/** Route pattern for the request */
export const HttpRoute = SemanticConvention<string>('http.route');

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.

Tiny organizational nit: http.route is defined here in the "Route attributes" block, while the other standard OTEL Http* conventions (HttpRequestMethod, HttpResponseStatusCode, UrlFull, …) are grouped together around lines 286–309. Consider moving it next to them for discoverability.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: moved HttpRoute down next to the other standard HTTP semantic convention helpers and left the workflow-specific route attributes grouped separately.

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

Approving. The workflow.turbo attribute (the main ask from the review) plus the smaller nits are all addressed in c9b83f3 — I'd independently prepared the same set of changes, so rather than push a duplicate commit (which would have collided on the identical WorkflowTurbo/HttpRoute additions) I verified your version instead.

What's now in:

  • workflow.turbo boolean tag on the workflow.execute span (Attribute.WorkflowTurbo(turbo)), so turbo invocations are trivially filterable — true only on the turbo first delivery, false on replays/redeliveries. Complements the finer-grained workflow.run_started.skip_preload event.
  • http.route convention regrouped with the other standard OTEL Http* conventions.
  • Doc comment on createWorkflowEntrypointOptionsCode's routeModuleBodyStartedAt noting it's emitted raw as a code identifier (not data).
  • workflow.turbo assertion added to the linked-mode test.

Verified locally on c9b83f3 (built deps from source): @workflow/core + @workflow/builders typecheck clean, runtime-trace-mode.test.ts (11) and constants.test.ts (7) green. CI E2E was already fully green.

The earlier observations I left (module_body_init_ms being a per-process constant emitted on every span; tiny warm-path overhead; pre-existing concurrent cold-start init) are all non-blocking — fine as-is.

@pranaygp
pranaygp merged commit 2bf5257 into mainJun 23, 2026
108 of 110 checks passed
@pranaygp
pranaygp deleted the kk/otel-flow-module-loading branch June 23, 2026 21:28
@github-actionsgithub-actionsBot mentioned this pull request Jun 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2bf5257 (AI decision).

The core instrumentation in this PR builds directly on main-only runtime architecture: stable's workflowEntrypoint creates the queue handler eagerly and synchronously at module load (no lazy cachedHandler/getWorldHandlers() caching, no getSpanKind), so the new workflow.route.flow/workflow.route.init/workflow.route.get_world_handlers spans have nothing to wrap there. The workflow.turbo attribute and workflow.run_started.skip_preload event instrument the turbo first-delivery path, which does not exist on stable, and the modified runtime-trace-mode.test.ts is also absent on stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2bf5257f97fc4fea036717a7882dfd39bf2b3804

pranaygp added a commit that referenced this pull request Jun 23, 2026
…testing
* origin/main:
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
[web-shared] reskin json viewer (no duplicates, better colours and navigation) (#2434)
Send occurredAt with workflow events (#2580)
docs: use actual eve logo and tidy OSS nav dropdown (#2586)
Display occurredAt in trace details (#2581)
fix(next): discover root entrypoints (#2564)
[core] Turbo: skip the unused run_started event-log preload (#2569)
fix(next): prewarm SWC plugin cache (#2538)
[world-vercel] Use v3 stream endpoint (supports transparent reconnect on timeout) (#2424)
[core] Retry stream reopen against the reconnect budget (#2334)
Add Platformatic World to worlds-manifest.json (#1450)
docs: derive section landing-page cards from the page tree + lint drift (#2567)
pranaygp added a commit that referenced this pull request Jun 26, 2026
* origin/main: (46 commits)
[web] Fix HTTP/2 in bundled server build so observability reads work (#2632)
[core] Fix turbo-mode step-body writes racing run_started (#2629)
fix(world-postgres): rename setup command (#2644)
chore: ignore workflow swc caches (#2640)
fix(ai): ignore intentional stream aborts (#2635)
fix(nitro): reload steps during Vite HMR (#2572)
[world] Make @workflow/world own its Zod dependency (#2622)
feat(web-shared): extend cn with custom tailwind-merge class groups (#2619)
[world-vercel] Enable HTTP/2 for the events API and stream writes (#2573)
Remove shadow from off-screen marker indicator button (#2614)
Fix discovery of dotted JS workflow imports (#2594)
Version Packages (beta) (#2597)
Thread occurredAt into web-shared entities (#2608)
Propagate trace context to vercel-workflow.com in workbench instrumentation (#2601)
Reduce workflow build log noise (#2565)
Version Packages (beta) (#2596)
Use text-gray-900 for trace shortcut helper text (#2595)
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
...
# Conflicts:
#	workbench/nextjs-turbopack/instrumentation.ts
#	workbench/nextjs-webpack/instrumentation.ts
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.

2 participants

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

Trace /flow route initialization - #2592

Merged
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading
Jun 23, 2026
Merged

Trace /flow route initialization#2592
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

This instruments the Workflow SDK /flow route path so we can explain delay before the route makes its first workflow-server run_started POST.

Instrumentation contract

workflow.route.flow

New server span around each generated /flow route request.

Attributes:

  • workflow.route.type: currently flow
  • workflow.route.handler_cached: whether this invocation reused the in-memory route handler
  • workflow.route.invocation_count: per-process invocation count for this route entrypoint
  • workflow.route.entrypoint_age_ms: milliseconds since workflowEntrypoint() constructed this route entrypoint
  • workflow.route.module_body_init_ms: process-level milliseconds from the generated route module-body timestamp to workflowEntrypoint() construction, when generated routes provide the timestamp
  • http.request.method: request method
  • http.route: /.well-known/workflow/v1/flow
  • http.response.status_code: response status when the handler returns a Response

workflow.route.init

New child span under workflow.route.flow, emitted when the route creates the cached queue handler.

Use this to separate first-hit handler construction from normal warm-route request handling. Under concurrent cold requests, more than one request can observe an empty cache and emit workflow.route.init; that is pre-existing harmless last-write-wins behavior.

workflow.route.get_world_handlers

New child span under workflow.route.init around getWorldHandlers().

Use this to identify latency from loading/creating the world handler layer during route setup.

workflow.route.get_world

New child span under workflow.route.flow, emitted for each workflow invocation before workflow.execute.

Use this to identify latency from resolving the per-invocation world before SDK runtime work posts run_started.

workflow.execute <workflowName>

Existing workflow execution span. This PR adds:

Attributes:

  • workflow.turbo: whether this invocation is using the turbo first-delivery path

Span events:

  • workflow.run_started.create.start: emitted immediately before world.events.create(... run_started ...), i.e. right before the first workflow-server run_started POST begins

Event attributes:

  • workflow.run_started.skip_preload: whether the run_started create call is using the turbo skipPreload path

Reading the signal

  • Gap from workflow.route.flow start to workflow.run_started.create.start: SDK/runtime work before the first workflow-server run_started POST begins.
  • Large workflow.route.module_body_init_ms: generated route module body work after static imports, before the SDK route handler is created. This value is computed once per route entrypoint and emitted on every request from that in-memory entrypoint.
  • Near-zero workflow.route.entrypoint_age_ms: the route entrypoint was just created for this request, so this request hit a cold SDK route instance.
  • workflow.route.handler_cached=false plus workflow.route.init: the request paid route handler creation. Sequentially this happens once per warm instance; concurrent cold requests may briefly produce more than one init span.
  • workflow.route.handler_cached=true: route handler was already created; delay is not from handler construction.
  • workflow.turbo=true: this invocation is the turbo first-delivery path. workflow.run_started.skip_preload=true is the finer-grained marker for the associated run_started create call.

Static ESM import evaluation still happens before generated route body code can run, so this does not directly measure the entire import graph. It gives us the earliest SDK-controlled marker in the generated /flow module, plus the exact pre-run_started marker inside the SDK handler.

Testing

  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec vitest run packages/core/src/runtime-trace-mode.test.ts packages/builders/src/constants.test.ts
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/core typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/builders typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec biome check .changeset/flow-route-init-traces.md packages/builders/src/base-builder.ts packages/builders/src/constants.ts packages/builders/src/constants.test.ts packages/core/src/runtime.ts packages/core/src/runtime-trace-mode.test.ts packages/core/src/telemetry/semantic-conventions.ts (exit 0; reports existing complexity/unused-suppression warnings)
  • Previous full @workflow/core suite under Node 24: all 1322 tests passed, but Vitest exited 1 due an existing pending-fetch shutdown error; packages/core/src/abort-controller-step.test.ts passed by itself.

@vercel

vercelBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9b83f3

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

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

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

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

@github-actions

github-actionsBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production144112301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7281110788360

❌ Failed Tests

▲ Vercel Production (1 failed)

nuxt (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
❌ nuxt124127
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ 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


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jun 23, 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.044s (-5.7% 🟢)1.006s (~)0.962s101.00x
💻 LocalExpress0.046s (-2.1%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.049s (-6.3% 🟢)1.008s (~)0.959s101.11x
🐘 PostgresNext.js (Turbopack)0.056s (+1.1%)1.011s (~)0.955s101.26x
🐘 PostgresNitro0.061s (-6.0% 🟢)1.013s (~)0.952s101.39x
🐘 PostgresExpress0.072s (+3.9%)1.013s (~)0.940s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.292s (+26.2% 🔺)2.213s (+5.2% 🔺)1.921s101.00x
▲ VercelNext.js (Turbopack)0.314s (+16.1% 🔺)2.353s (+15.1% 🔺)2.039s101.08x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.079s (-0.6%)2.006s (~)0.928s101.00x
💻 LocalExpress1.083s (~)2.007s (~)0.923s101.00x
🐘 PostgresNext.js (Turbopack)1.086s (~)2.011s (~)0.925s101.01x
💻 LocalNext.js (Turbopack)1.086s (~)2.006s (~)0.920s101.01x
🐘 PostgresNitro1.101s (~)2.011s (~)0.911s101.02x
🐘 PostgresExpress1.103s (+1.0%)2.011s (~)0.908s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.507s (+8.9% 🔺)3.416s (+20.7% 🔺)1.909s101.00x
▲ VercelNext.js (Turbopack)1.588s (+10.7% 🔺)3.280s (-2.2%)1.692s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.443s (~)11.022s (~)0.580s31.00x
💻 LocalNitro10.446s (~)11.022s (~)0.576s31.00x
💻 LocalExpress10.463s (~)11.022s (~)0.560s31.00x
🐘 PostgresNext.js (Turbopack)10.471s (~)11.017s (~)0.547s31.00x
🐘 PostgresExpress10.484s (~)11.021s (~)0.537s31.00x
🐘 PostgresNitro10.498s (~)11.021s (~)0.523s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.720s (+3.3%)14.402s (+3.8%)1.682s31.00x
▲ VercelNext.js (Turbopack)12.862s (+9.0% 🔺)14.699s (+5.4% 🔺)1.837s31.01x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.569s (~)14.028s (~)0.459s51.00x
💻 LocalExpress13.602s (~)14.028s (~)0.426s51.00x
🐘 PostgresNitro13.641s (-0.6%)14.020s (~)0.379s51.01x
💻 LocalNext.js (Turbopack)13.681s (+0.7%)14.028s (~)0.347s51.01x
🐘 PostgresExpress13.701s (+0.9%)14.022s (~)0.320s51.01x
🐘 PostgresNext.js (Turbopack)13.721s (~)14.021s (~)0.299s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)18.660s (+7.6% 🔺)20.341s (+6.2% 🔺)1.681s31.00x
▲ VercelExpress19.542s (+18.9% 🔺)21.199s (+17.8% 🔺)1.657s31.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.055s (-1.0%)13.025s (~)0.970s71.00x
💻 LocalExpress12.145s (-1.6%)13.027s (~)0.882s71.01x
🐘 PostgresNitro12.162s (-0.9%)13.020s (~)0.858s71.01x
💻 LocalNext.js (Turbopack)12.189s (-0.5%)13.025s (~)0.836s71.01x
🐘 PostgresNext.js (Turbopack)12.218s (+1.0%)13.023s (~)0.805s71.01x
🐘 PostgresExpress12.276s (+1.4%)13.018s (~)0.742s71.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.857s (+35.3% 🔺)26.850s (+33.2% 🔺)1.994s41.00x
▲ VercelNext.js (Turbopack)25.505s (+31.8% 🔺)27.773s (+30.8% 🔺)2.268s41.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.165s (-1.0%)2.008s (~)0.843s151.00x
🐘 PostgresNitro1.194s (+1.0%)2.007s (~)0.813s151.02x
🐘 PostgresExpress1.213s (+3.8%)2.008s (~)0.795s151.04x
💻 LocalNext.js (Turbopack)1.380s (-7.8% 🟢)2.006s (~)0.626s151.18x
💻 LocalExpress1.385s (~)2.006s (~)0.621s151.19x
💻 LocalNitro1.396s (-2.9%)2.006s (~)0.611s151.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.793s (+29.9% 🔺)4.385s (+15.7% 🔺)1.592s71.00x
▲ VercelExpress3.109s (+53.2% 🔺)5.068s (+44.8% 🔺)1.959s61.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.301s (-0.9%)3.009s (+3.1%)1.708s101.00x
🐘 PostgresExpress1.343s (-3.6%)2.509s (-6.2% 🟢)1.166s121.03x
🐘 PostgresNitro1.348s (+0.6%)2.510s (~)1.162s121.04x
💻 LocalNitro2.313s (-3.9%)2.736s (-6.2% 🟢)0.423s111.78x
💻 LocalNext.js (Turbopack)2.391s (-12.6% 🟢)3.008s (-3.2%)0.618s101.84x
💻 LocalExpress2.529s (+7.3% 🔺)2.918s (~)0.389s111.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.579s (+7.1% 🔺)5.199s (+3.0%)1.620s61.00x
▲ VercelExpress3.756s (+3.4%)5.354s (+3.7%)1.598s61.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.591s (-3.0%)4.010s (-5.9% 🟢)2.419s81.00x
🐘 PostgresExpress1.637s (+3.2%)4.136s (-3.9%)2.499s81.03x
🐘 PostgresNext.js (Turbopack)2.944s (+4.6%)5.349s (-3.0%)2.406s61.85x
💻 LocalNext.js (Turbopack)4.978s (-9.0% 🟢)5.349s (-11.1% 🟢)0.371s63.13x
💻 LocalNitro5.957s (-7.5% 🟢)6.616s (-8.3% 🟢)0.658s53.74x
💻 LocalExpress6.009s (-5.0%)6.619s (-5.7% 🟢)0.611s53.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.831s (+38.8% 🔺)6.612s (+27.1% 🔺)1.781s51.00x
▲ VercelExpress4.852s (+58.3% 🔺)6.870s (+36.2% 🔺)2.018s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.170s (~)2.007s (~)0.837s151.00x
🐘 PostgresNitro1.178s (-2.4%)2.007s (~)0.829s151.01x
🐘 PostgresExpress1.182s (~)2.008s (~)0.826s151.01x
💻 LocalNitro1.357s (-4.5%)2.006s (~)0.649s151.16x
💻 LocalNext.js (Turbopack)1.399s (~)2.006s (~)0.607s151.20x
💻 LocalExpress1.452s (+1.0%)2.007s (~)0.555s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.794s (-51.7% 🟢)4.333s (-41.8% 🟢)1.539s81.00x
▲ VercelExpress2.871s (+39.1% 🔺)4.642s (+32.7% 🔺)1.772s71.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.294s (-5.7% 🟢)2.508s (+1.5%)1.214s121.00x
🐘 PostgresNext.js (Turbopack)1.306s (+1.3%)2.918s (~)1.611s111.01x
🐘 PostgresExpress1.328s (-1.0%)2.222s (-10.1% 🟢)0.894s141.03x
💻 LocalNitro2.318s (-10.2% 🟢)3.008s (-3.2%)0.691s101.79x
💻 LocalNext.js (Turbopack)2.607s (-1.2%)3.108s (+3.3%)0.502s102.01x
💻 LocalExpress2.613s (+7.9% 🔺)3.009s (-3.2%)0.396s102.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.700s (-22.0% 🟢)5.332s (-23.3% 🟢)1.631s61.00x
▲ VercelExpress4.302s (-10.2% 🟢)5.906s (-11.0% 🟢)1.604s61.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.610s (+1.5%)4.011s (-3.0%)2.401s81.00x
🐘 PostgresNitro1.620s (-2.4%)4.137s (+3.1%)2.517s81.01x
🐘 PostgresNext.js (Turbopack)3.012s (+5.4% 🔺)5.848s (~)2.837s61.87x
💻 LocalNext.js (Turbopack)4.998s (-12.6% 🟢)5.683s (-5.5% 🟢)0.685s63.11x
💻 LocalExpress6.217s (-7.2% 🟢)7.016s (-2.8%)0.799s53.86x
💻 LocalNitro6.501s (-4.1%)7.020s (-2.8%)0.518s54.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.961s (-15.5% 🟢)5.681s (-9.8% 🟢)1.720s61.00x
▲ VercelNext.js (Turbopack)4.577s (+46.5% 🔺)6.497s (+25.9% 🔺)1.920s51.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.507s (-0.9%)1.006s (~)0.500s601.00x
🐘 PostgresNitro0.538s (-9.6% 🟢)1.006s (-3.4%)0.468s601.06x
🐘 PostgresExpress0.561s (+5.1% 🔺)1.023s (~)0.462s591.11x
💻 LocalNext.js (Turbopack)0.565s (-6.5% 🟢)1.005s (-1.7%)0.440s601.11x
💻 LocalNitro0.580s (-1.0%)1.022s (+1.6%)0.442s591.15x
💻 LocalExpress0.584s (+1.9%)1.005s (~)0.421s601.15x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.239s (+39.5% 🔺)6.064s (+21.4% 🔺)1.825s111.00x
▲ VercelExpress4.460s (+59.4% 🔺)6.406s (+42.8% 🔺)1.946s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.221s (-1.9%)2.007s (-1.1%)0.786s451.00x
🐘 PostgresNitro1.294s (-8.5% 🟢)2.007s (-2.3%)0.713s451.06x
🐘 PostgresExpress1.379s (+10.3% 🔺)2.030s (+1.1%)0.651s451.13x
💻 LocalNext.js (Turbopack)1.406s (-4.2%)2.005s (~)0.599s451.15x
💻 LocalExpress1.436s (-1.3%)2.006s (~)0.570s451.18x
💻 LocalNitro1.478s (~)2.028s (+1.1%)0.550s451.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.677s (+80.6% 🔺)13.672s (+70.1% 🔺)1.995s71.00x
▲ VercelNext.js (Turbopack)12.418s (+73.8% 🔺)14.184s (+59.5% 🔺)1.766s71.06x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)2.476s (~)3.034s (-0.8%)0.558s401.00x
🐘 PostgresNitro2.595s (-4.4%)3.085s (+0.9%)0.490s391.05x
🐘 PostgresExpress2.846s (+9.7% 🔺)3.194s (+3.5%)0.347s381.15x
💻 LocalNitro3.052s (-5.2% 🟢)3.675s (-8.3% 🟢)0.623s331.23x
💻 LocalNext.js (Turbopack)3.099s (-5.0%)3.675s (-9.1% 🟢)0.577s331.25x
💻 LocalExpress3.179s (-2.1%)3.913s (-1.6%)0.734s311.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.472s (+63.0% 🔺)24.735s (+60.0% 🔺)2.262s51.00x
▲ VercelNext.js (Turbopack)26.936s (+64.1% 🔺)28.462s (+54.0% 🔺)1.527s51.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.166s (-0.8%)1.006s (~)0.840s601.00x
🐘 PostgresNitro0.209s (-7.8% 🟢)1.006s (~)0.797s601.26x
🐘 PostgresExpress0.237s (+12.5% 🔺)1.007s (~)0.770s601.42x
💻 LocalNitro0.431s (-7.9% 🟢)1.004s (~)0.573s602.59x
💻 LocalExpress0.455s (+5.5% 🔺)1.005s (~)0.550s602.73x
💻 LocalNext.js (Turbopack)0.564s (-3.5%)1.022s (+1.7%)0.457s593.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.803s (-0.8%)3.477s (-2.0%)1.674s181.00x
▲ VercelExpress1.994s (-3.8%)3.433s (-10.6% 🟢)1.439s181.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.253s (-5.5% 🟢)1.006s (~)0.752s901.00x
🐘 PostgresNitro0.326s (-3.3%)1.006s (~)0.680s901.29x
🐘 PostgresExpress0.356s (+10.2% 🔺)1.018s (~)0.662s891.41x
💻 LocalNitro2.085s (-2.3%)2.686s (~)0.602s348.23x
💻 LocalExpress2.121s (-1.8%)2.766s (+1.1%)0.645s338.37x
💻 LocalNext.js (Turbopack)2.599s (-6.8% 🟢)3.113s (-4.5%)0.514s2910.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.670s (+8.4% 🔺)4.342s (+7.1% 🔺)1.672s211.00x
▲ VercelNext.js (Turbopack)3.196s (+47.3% 🔺)5.002s (+27.3% 🔺)1.806s191.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.490s (+3.8%)3.343s (+11.1% 🔺)2.853s361.00x
🐘 PostgresNitro0.519s (-1.7%)1.059s (+2.6%)0.540s1141.06x
🐘 PostgresExpress0.574s (+11.3% 🔺)1.118s (+4.7%)0.544s1081.17x
💻 LocalNext.js (Turbopack)9.280s (-5.1% 🟢)10.195s (-5.4% 🟢)0.916s1218.95x
💻 LocalExpress9.668s (-4.2%)10.528s (-5.4% 🟢)0.860s1219.74x
💻 LocalNitro9.701s (-2.2%)10.779s (-2.3%)1.077s1219.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.551s (+48.6% 🔺)5.489s (+35.4% 🔺)1.938s221.00x
▲ VercelNext.js (Turbopack)5.084s (+8.5% 🔺)7.510s (+14.7% 🔺)2.426s161.43x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.138s (-2.7%)1.968s (~)0.010s (-16.1% 🟢)2.018s (~)0.880s101.00x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.000s (~)0.001s (~)2.010s (~)0.863s101.01x
💻 LocalNitro1.155s (~)2.004s (~)0.010s (-14.0% 🟢)2.017s (~)0.862s101.01x
💻 LocalExpress1.159s (+0.6%)2.005s (~)0.012s (+2.5%)2.020s (~)0.860s101.02x
🐘 PostgresExpress1.169s (+1.7%)1.997s (~)0.001s (+10.0% 🔺)2.012s (~)0.842s101.03x
🐘 PostgresNitro1.193s (+2.3%)1.995s (~)0.001s (+9.1% 🔺)2.011s (~)0.817s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.409s (+17.7% 🔺)3.498s (+14.0% 🔺)3.398s (+94.0% 🔺)7.469s (+38.2% 🔺)5.061s101.00x
▲ VercelNext.js (Turbopack)2.673s (+29.4% 🔺)3.825s (+10.9% 🔺)3.340s (+137.0% 🔺)7.691s (+42.3% 🔺)5.018s101.11x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.535s (-4.3%)2.002s (~)0.005s (-1.3%)2.024s (~)0.489s301.00x
💻 LocalExpress1.544s (~)2.010s (~)0.012s (-4.9%)2.026s (~)0.482s301.01x
🐘 PostgresNext.js (Turbopack)1.547s (~)2.009s (~)0.005s (-0.6%)2.026s (~)0.479s301.01x
💻 LocalNext.js (Turbopack)1.555s (-1.4%)1.972s (~)0.013s (+1.0%)2.027s (~)0.472s301.01x
💻 LocalNitro1.566s (+0.8%)2.010s (~)0.013s (+5.0%)2.026s (~)0.460s301.02x
🐘 PostgresExpress1.602s (+3.5%)2.002s (~)0.005s (-7.2% 🟢)2.026s (~)0.424s301.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.722s (+13.1% 🔺)8.052s (+9.2% 🔺)0.240s (-24.5% 🟢)8.776s (+6.3% 🔺)2.054s71.00x
▲ VercelExpress7.606s (+29.2% 🔺)9.161s (+27.1% 🔺)0.500s (+131.5% 🔺)10.151s (+28.8% 🔺)2.545s61.13x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.764s (-7.0% 🟢)1.065s (-3.0%)0.000s (-100.0% 🟢)1.079s (-3.6%)0.314s561.00x
🐘 PostgresExpress0.830s (+7.2% 🔺)1.104s (+5.3% 🔺)0.000s (+Infinity% 🔺)1.119s (+5.2% 🔺)0.289s541.09x
🐘 PostgresNext.js (Turbopack)0.965s (+5.3% 🔺)1.429s (+11.9% 🔺)0.000s (+11.9% 🔺)1.436s (+11.8% 🔺)0.471s421.26x
💻 LocalExpress1.212s (-4.4%)1.981s (-1.6%)0.000s (+45.2% 🔺)1.983s (-1.6%)0.771s311.59x
💻 LocalNitro1.256s (-4.8%)1.982s (~)0.000s (-33.3% 🟢)1.984s (~)0.729s311.64x
💻 LocalNext.js (Turbopack)1.323s (-0.9%)1.981s (~)0.000s (+66.7% 🔺)2.017s (~)0.694s301.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.029s (+3.1%)5.021s (~)0.000s (~)5.476s (-1.1%)1.447s111.00x
▲ VercelNext.js (Turbopack)4.175s (+26.8% 🔺)5.055s (+7.5% 🔺)0.000s (-100.0% 🟢)5.543s (+5.5% 🔺)1.367s111.04x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.783s (+5.2% 🔺)2.435s (+8.0% 🔺)0.000s (+8.0% 🔺)2.451s (+8.0% 🔺)0.668s251.00x
🐘 PostgresNitro1.937s (+1.0%)2.410s (-3.5%)0.000s (-100.0% 🟢)2.450s (-2.9%)0.512s261.09x
🐘 PostgresNext.js (Turbopack)2.470s (+21.6% 🔺)2.907s (+11.3% 🔺)0.000s (-100.0% 🟢)2.914s (+10.3% 🔺)0.444s211.39x
💻 LocalExpress3.357s (-7.6% 🟢)3.904s (-7.7% 🟢)0.001s (+40.6% 🔺)3.909s (-7.6% 🟢)0.551s161.88x
💻 LocalNitro3.367s (-5.6% 🟢)3.965s (-1.5%)0.001s (-22.8% 🟢)3.971s (-1.5%)0.604s161.89x
💻 LocalNext.js (Turbopack)3.441s (-6.1% 🟢)3.868s (-7.7% 🟢)0.001s (-47.3% 🟢)3.907s (-7.8% 🟢)0.466s161.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.023s (+27.2% 🔺)7.515s (+30.8% 🔺)0.000s (-100.0% 🟢)8.032s (+28.4% 🔺)2.009s81.00x
▲ VercelNext.js (Turbopack)6.120s (+32.1% 🔺)7.138s (+18.6% 🔺)0.000s (+25.0% 🔺)7.761s (+18.0% 🔺)1.641s81.02x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNext.js (Turbopack)13/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres16/21
Nitro🐘 Postgres15/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)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@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 diff in depth — runtime span wiring, the builders codegen across all three generated route templates (workflow-only bundleFinal, combined, and watch-mode combined) plus the sveltekit/astro regex post-processing, the new semantic conventions, and the tests.

The instrumentation is clean, well-scoped, and well-tested. The span-hierarchy change (workflow.execute now nests under workflow.route.flow) is correctly reflected in the linked-mode tests and doesn't regress continuous mode (it still parents to the run-origin via withTraceContext). All three codegen templates declare workflowRouteModuleBodyStartedAt and reference it via the new options, and the sveltekit/astro regex ([^)]* capture) still matches the variable-reference options. CI E2E is fully green.

Main suggestion (re: the turbo-mode question): there is no dedicated turbo span attribute — turbo is only inferable from the workflow.run_started.skip_preloadevent. Recommend adding a workflow.turbo tag on workflow.execute so runs/invocations are trivially filterable by turbo mode (details inline). Everything else is minor/optional; nothing blocking.

skipPreload: boolean
) => {
span?.addEvent('workflow.run_started.create.start', {
'workflow.run_started.skip_preload': skipPreload,

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.

Turbo mode is only observable indirectly here. This workflow.run_started.skip_preload event attribute is currently the only turbo signal on the trace, and it's an awkward proxy for "is this run in turbo mode?":

  • It's a span event attribute, not a span tag, so it's harder to facet/filter on in most backends (Datadog, etc.).
  • The name skip_preload doesn't say "turbo" — someone asking the turbo question wouldn't know to look here.
  • The event is only emitted when run_started.create runs (first delivery). On redeliveries/replays (attempt > 1) there's no event at all, so those invocations carry no turbo signal.

turbo is already computed once per invocation at line 535 (and since turbo requires runInput, this skipPreload=true branch is exactly turbo). Consider also setting a dedicated tag on the workflow.execute span — e.g. right after the turbo constant is computed, where span is in scope:

span?.setAttributes(Attribute.WorkflowTurbo(turbo));

backed by a new export const WorkflowTurbo = SemanticConvention<boolean>('workflow.turbo'). That puts a boolean turbo tag on every workflow.execute span (true on the turbo first delivery, false on replays — which is the precise per-invocation distinction we want), and keeps the existing skip_preload event as the finer-grained marker.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a dedicated workflow.turbo semantic convention and set it on every workflow.execute span after the per-invocation turbo decision is computed. I kept the existing workflow.run_started.skip_preload event attribute as the lower-level marker for the run_started create call.

),
...(routeModuleBodyInitMs === undefined
? {}
: Attribute.WorkflowRouteModuleBodyInitMs(routeModuleBodyInitMs)),

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.

routeModuleBodyInitMs is a process-level constant (computed once at entrypoint construction) but emitted on everyworkflow.route.flow span, so it's identical across all requests from a given instance. Fine for queryability — just noting it isn't per-request.

Also worth setting expectations on what it measures: the marker is captured at the top of the generated module body, i.e. afterimport { workflowEntrypoint } from 'workflow/runtime' has fully evaluated. So it won't capture the @workflow/core import/eval cost (the likely dominant cold-start contributor) and will typically read ~0 in the workflow-only template, where the only body work between the marker and the workflowEntrypoint() call is a string-literal assignment. The PR description already acknowledges this; flagging only so the attribute's usefulness isn't over-read.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the PR description to make this explicit: workflow.route.module_body_init_ms is computed once per route entrypoint, emitted on each request from that in-memory entrypoint, and starts after static ESM imports have already evaluated. No code change here since the behavior is intentional.

return cachedHandler(req);
invocationCount += 1;
const handlerCached = cachedHandler !== undefined;
const spanKind = await getSpanKind('SERVER');

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.

Micro-nit (optional, fine to leave): getSpanKind('SERVER') plus the attributes object are now built on every flow request — including the warm path and when no OTEL SDK is registered — whereas previously the warm path was just return cachedHandler(req). The cost is negligible (awaiting an already-memoized once() promise; trace() awaits the same lazy values anyway), so no action needed; just calling it out since this route is perf-sensitive.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Acknowledged. I left this as-is: the added warm-path work remains a memoized span-kind lookup plus attribute construction, and trace() already hits the same memoized OTel handles. Keeping the route-level span always available seems worth that small overhead for this incident path.

},
async (span) => {
if (!cachedHandler) {
cachedHandler = await trace('workflow.route.init', async () => {

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.

Pre-existing behavior (not introduced by this PR), but now more visible in the spans: two concurrent requests on a cold instance can both observe cachedHandler === undefined and each run getWorldHandlers() + handler(), producing two workflow.route.init spans for a single instance (and handler_cached=false on both). Harmless — handlers are stateless and it's effectively last-write-wins — and the new test covers the sequential case. Not a blocker; just noting that under concurrency the span counts can briefly exceed the "once per warm instance" framing in the PR description.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good call. I updated the PR description to stop implying a strict once-per-instance invariant: it now says workflow.route.init is emitted when the cached handler is created, and that concurrent cold requests can briefly produce more than one init span because the pre-existing cache assignment is last-write-wins.


if (options?.routeModuleBodyStartedAt) {
fields.push(
`routeModuleBodyStartedAt: ${options.routeModuleBodyStartedAt}`

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.

routeModuleBodyStartedAt is interpolated raw here (unlike namespace, which is JSON.stringify'd) because it must emit a code identifier reference, not a string literal — correct by design, and the sole caller passes a hardcoded 'workflowRouteModuleBodyStartedAt'. Since this makes the param a code-injection surface if it's ever called with untrusted input, consider a one-line doc comment on the option noting the value must be a code identifier, not data.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a doc comment on routeModuleBodyStartedAt noting that it is emitted as a raw code identifier/expression in generated route files, not serialized data.

);

/** Route pattern for the request */
export const HttpRoute = SemanticConvention<string>('http.route');

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.

Tiny organizational nit: http.route is defined here in the "Route attributes" block, while the other standard OTEL Http* conventions (HttpRequestMethod, HttpResponseStatusCode, UrlFull, …) are grouped together around lines 286–309. Consider moving it next to them for discoverability.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: moved HttpRoute down next to the other standard HTTP semantic convention helpers and left the workflow-specific route attributes grouped separately.

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

Approving. The workflow.turbo attribute (the main ask from the review) plus the smaller nits are all addressed in c9b83f3 — I'd independently prepared the same set of changes, so rather than push a duplicate commit (which would have collided on the identical WorkflowTurbo/HttpRoute additions) I verified your version instead.

What's now in:

  • workflow.turbo boolean tag on the workflow.execute span (Attribute.WorkflowTurbo(turbo)), so turbo invocations are trivially filterable — true only on the turbo first delivery, false on replays/redeliveries. Complements the finer-grained workflow.run_started.skip_preload event.
  • http.route convention regrouped with the other standard OTEL Http* conventions.
  • Doc comment on createWorkflowEntrypointOptionsCode's routeModuleBodyStartedAt noting it's emitted raw as a code identifier (not data).
  • workflow.turbo assertion added to the linked-mode test.

Verified locally on c9b83f3 (built deps from source): @workflow/core + @workflow/builders typecheck clean, runtime-trace-mode.test.ts (11) and constants.test.ts (7) green. CI E2E was already fully green.

The earlier observations I left (module_body_init_ms being a per-process constant emitted on every span; tiny warm-path overhead; pre-existing concurrent cold-start init) are all non-blocking — fine as-is.

@pranaygp
pranaygp merged commit 2bf5257 into mainJun 23, 2026
108 of 110 checks passed
@pranaygp
pranaygp deleted the kk/otel-flow-module-loading branch June 23, 2026 21:28
@github-actionsgithub-actionsBot mentioned this pull request Jun 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2bf5257 (AI decision).

The core instrumentation in this PR builds directly on main-only runtime architecture: stable's workflowEntrypoint creates the queue handler eagerly and synchronously at module load (no lazy cachedHandler/getWorldHandlers() caching, no getSpanKind), so the new workflow.route.flow/workflow.route.init/workflow.route.get_world_handlers spans have nothing to wrap there. The workflow.turbo attribute and workflow.run_started.skip_preload event instrument the turbo first-delivery path, which does not exist on stable, and the modified runtime-trace-mode.test.ts is also absent on stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2bf5257f97fc4fea036717a7882dfd39bf2b3804

pranaygp added a commit that referenced this pull request Jun 23, 2026
…testing
* origin/main:
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
[web-shared] reskin json viewer (no duplicates, better colours and navigation) (#2434)
Send occurredAt with workflow events (#2580)
docs: use actual eve logo and tidy OSS nav dropdown (#2586)
Display occurredAt in trace details (#2581)
fix(next): discover root entrypoints (#2564)
[core] Turbo: skip the unused run_started event-log preload (#2569)
fix(next): prewarm SWC plugin cache (#2538)
[world-vercel] Use v3 stream endpoint (supports transparent reconnect on timeout) (#2424)
[core] Retry stream reopen against the reconnect budget (#2334)
Add Platformatic World to worlds-manifest.json (#1450)
docs: derive section landing-page cards from the page tree + lint drift (#2567)
pranaygp added a commit that referenced this pull request Jun 26, 2026
* origin/main: (46 commits)
[web] Fix HTTP/2 in bundled server build so observability reads work (#2632)
[core] Fix turbo-mode step-body writes racing run_started (#2629)
fix(world-postgres): rename setup command (#2644)
chore: ignore workflow swc caches (#2640)
fix(ai): ignore intentional stream aborts (#2635)
fix(nitro): reload steps during Vite HMR (#2572)
[world] Make @workflow/world own its Zod dependency (#2622)
feat(web-shared): extend cn with custom tailwind-merge class groups (#2619)
[world-vercel] Enable HTTP/2 for the events API and stream writes (#2573)
Remove shadow from off-screen marker indicator button (#2614)
Fix discovery of dotted JS workflow imports (#2594)
Version Packages (beta) (#2597)
Thread occurredAt into web-shared entities (#2608)
Propagate trace context to vercel-workflow.com in workbench instrumentation (#2601)
Reduce workflow build log noise (#2565)
Version Packages (beta) (#2596)
Use text-gray-900 for trace shortcut helper text (#2595)
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
...
# Conflicts:
#	workbench/nextjs-turbopack/instrumentation.ts
#	workbench/nextjs-webpack/instrumentation.ts
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.

2 participants

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

Trace /flow route initialization - #2592

Merged
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading
Jun 23, 2026
Merged

Trace /flow route initialization#2592
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

This instruments the Workflow SDK /flow route path so we can explain delay before the route makes its first workflow-server run_started POST.

Instrumentation contract

workflow.route.flow

New server span around each generated /flow route request.

Attributes:

  • workflow.route.type: currently flow
  • workflow.route.handler_cached: whether this invocation reused the in-memory route handler
  • workflow.route.invocation_count: per-process invocation count for this route entrypoint
  • workflow.route.entrypoint_age_ms: milliseconds since workflowEntrypoint() constructed this route entrypoint
  • workflow.route.module_body_init_ms: process-level milliseconds from the generated route module-body timestamp to workflowEntrypoint() construction, when generated routes provide the timestamp
  • http.request.method: request method
  • http.route: /.well-known/workflow/v1/flow
  • http.response.status_code: response status when the handler returns a Response

workflow.route.init

New child span under workflow.route.flow, emitted when the route creates the cached queue handler.

Use this to separate first-hit handler construction from normal warm-route request handling. Under concurrent cold requests, more than one request can observe an empty cache and emit workflow.route.init; that is pre-existing harmless last-write-wins behavior.

workflow.route.get_world_handlers

New child span under workflow.route.init around getWorldHandlers().

Use this to identify latency from loading/creating the world handler layer during route setup.

workflow.route.get_world

New child span under workflow.route.flow, emitted for each workflow invocation before workflow.execute.

Use this to identify latency from resolving the per-invocation world before SDK runtime work posts run_started.

workflow.execute <workflowName>

Existing workflow execution span. This PR adds:

Attributes:

  • workflow.turbo: whether this invocation is using the turbo first-delivery path

Span events:

  • workflow.run_started.create.start: emitted immediately before world.events.create(... run_started ...), i.e. right before the first workflow-server run_started POST begins

Event attributes:

  • workflow.run_started.skip_preload: whether the run_started create call is using the turbo skipPreload path

Reading the signal

  • Gap from workflow.route.flow start to workflow.run_started.create.start: SDK/runtime work before the first workflow-server run_started POST begins.
  • Large workflow.route.module_body_init_ms: generated route module body work after static imports, before the SDK route handler is created. This value is computed once per route entrypoint and emitted on every request from that in-memory entrypoint.
  • Near-zero workflow.route.entrypoint_age_ms: the route entrypoint was just created for this request, so this request hit a cold SDK route instance.
  • workflow.route.handler_cached=false plus workflow.route.init: the request paid route handler creation. Sequentially this happens once per warm instance; concurrent cold requests may briefly produce more than one init span.
  • workflow.route.handler_cached=true: route handler was already created; delay is not from handler construction.
  • workflow.turbo=true: this invocation is the turbo first-delivery path. workflow.run_started.skip_preload=true is the finer-grained marker for the associated run_started create call.

Static ESM import evaluation still happens before generated route body code can run, so this does not directly measure the entire import graph. It gives us the earliest SDK-controlled marker in the generated /flow module, plus the exact pre-run_started marker inside the SDK handler.

Testing

  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec vitest run packages/core/src/runtime-trace-mode.test.ts packages/builders/src/constants.test.ts
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/core typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/builders typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec biome check .changeset/flow-route-init-traces.md packages/builders/src/base-builder.ts packages/builders/src/constants.ts packages/builders/src/constants.test.ts packages/core/src/runtime.ts packages/core/src/runtime-trace-mode.test.ts packages/core/src/telemetry/semantic-conventions.ts (exit 0; reports existing complexity/unused-suppression warnings)
  • Previous full @workflow/core suite under Node 24: all 1322 tests passed, but Vitest exited 1 due an existing pending-fetch shutdown error; packages/core/src/abort-controller-step.test.ts passed by itself.

@vercel

vercelBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9b83f3

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

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

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

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

@github-actions

github-actionsBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production144112301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7281110788360

❌ Failed Tests

▲ Vercel Production (1 failed)

nuxt (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
❌ nuxt124127
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ 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


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jun 23, 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.044s (-5.7% 🟢)1.006s (~)0.962s101.00x
💻 LocalExpress0.046s (-2.1%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.049s (-6.3% 🟢)1.008s (~)0.959s101.11x
🐘 PostgresNext.js (Turbopack)0.056s (+1.1%)1.011s (~)0.955s101.26x
🐘 PostgresNitro0.061s (-6.0% 🟢)1.013s (~)0.952s101.39x
🐘 PostgresExpress0.072s (+3.9%)1.013s (~)0.940s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.292s (+26.2% 🔺)2.213s (+5.2% 🔺)1.921s101.00x
▲ VercelNext.js (Turbopack)0.314s (+16.1% 🔺)2.353s (+15.1% 🔺)2.039s101.08x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.079s (-0.6%)2.006s (~)0.928s101.00x
💻 LocalExpress1.083s (~)2.007s (~)0.923s101.00x
🐘 PostgresNext.js (Turbopack)1.086s (~)2.011s (~)0.925s101.01x
💻 LocalNext.js (Turbopack)1.086s (~)2.006s (~)0.920s101.01x
🐘 PostgresNitro1.101s (~)2.011s (~)0.911s101.02x
🐘 PostgresExpress1.103s (+1.0%)2.011s (~)0.908s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.507s (+8.9% 🔺)3.416s (+20.7% 🔺)1.909s101.00x
▲ VercelNext.js (Turbopack)1.588s (+10.7% 🔺)3.280s (-2.2%)1.692s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.443s (~)11.022s (~)0.580s31.00x
💻 LocalNitro10.446s (~)11.022s (~)0.576s31.00x
💻 LocalExpress10.463s (~)11.022s (~)0.560s31.00x
🐘 PostgresNext.js (Turbopack)10.471s (~)11.017s (~)0.547s31.00x
🐘 PostgresExpress10.484s (~)11.021s (~)0.537s31.00x
🐘 PostgresNitro10.498s (~)11.021s (~)0.523s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.720s (+3.3%)14.402s (+3.8%)1.682s31.00x
▲ VercelNext.js (Turbopack)12.862s (+9.0% 🔺)14.699s (+5.4% 🔺)1.837s31.01x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.569s (~)14.028s (~)0.459s51.00x
💻 LocalExpress13.602s (~)14.028s (~)0.426s51.00x
🐘 PostgresNitro13.641s (-0.6%)14.020s (~)0.379s51.01x
💻 LocalNext.js (Turbopack)13.681s (+0.7%)14.028s (~)0.347s51.01x
🐘 PostgresExpress13.701s (+0.9%)14.022s (~)0.320s51.01x
🐘 PostgresNext.js (Turbopack)13.721s (~)14.021s (~)0.299s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)18.660s (+7.6% 🔺)20.341s (+6.2% 🔺)1.681s31.00x
▲ VercelExpress19.542s (+18.9% 🔺)21.199s (+17.8% 🔺)1.657s31.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.055s (-1.0%)13.025s (~)0.970s71.00x
💻 LocalExpress12.145s (-1.6%)13.027s (~)0.882s71.01x
🐘 PostgresNitro12.162s (-0.9%)13.020s (~)0.858s71.01x
💻 LocalNext.js (Turbopack)12.189s (-0.5%)13.025s (~)0.836s71.01x
🐘 PostgresNext.js (Turbopack)12.218s (+1.0%)13.023s (~)0.805s71.01x
🐘 PostgresExpress12.276s (+1.4%)13.018s (~)0.742s71.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.857s (+35.3% 🔺)26.850s (+33.2% 🔺)1.994s41.00x
▲ VercelNext.js (Turbopack)25.505s (+31.8% 🔺)27.773s (+30.8% 🔺)2.268s41.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.165s (-1.0%)2.008s (~)0.843s151.00x
🐘 PostgresNitro1.194s (+1.0%)2.007s (~)0.813s151.02x
🐘 PostgresExpress1.213s (+3.8%)2.008s (~)0.795s151.04x
💻 LocalNext.js (Turbopack)1.380s (-7.8% 🟢)2.006s (~)0.626s151.18x
💻 LocalExpress1.385s (~)2.006s (~)0.621s151.19x
💻 LocalNitro1.396s (-2.9%)2.006s (~)0.611s151.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.793s (+29.9% 🔺)4.385s (+15.7% 🔺)1.592s71.00x
▲ VercelExpress3.109s (+53.2% 🔺)5.068s (+44.8% 🔺)1.959s61.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.301s (-0.9%)3.009s (+3.1%)1.708s101.00x
🐘 PostgresExpress1.343s (-3.6%)2.509s (-6.2% 🟢)1.166s121.03x
🐘 PostgresNitro1.348s (+0.6%)2.510s (~)1.162s121.04x
💻 LocalNitro2.313s (-3.9%)2.736s (-6.2% 🟢)0.423s111.78x
💻 LocalNext.js (Turbopack)2.391s (-12.6% 🟢)3.008s (-3.2%)0.618s101.84x
💻 LocalExpress2.529s (+7.3% 🔺)2.918s (~)0.389s111.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.579s (+7.1% 🔺)5.199s (+3.0%)1.620s61.00x
▲ VercelExpress3.756s (+3.4%)5.354s (+3.7%)1.598s61.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.591s (-3.0%)4.010s (-5.9% 🟢)2.419s81.00x
🐘 PostgresExpress1.637s (+3.2%)4.136s (-3.9%)2.499s81.03x
🐘 PostgresNext.js (Turbopack)2.944s (+4.6%)5.349s (-3.0%)2.406s61.85x
💻 LocalNext.js (Turbopack)4.978s (-9.0% 🟢)5.349s (-11.1% 🟢)0.371s63.13x
💻 LocalNitro5.957s (-7.5% 🟢)6.616s (-8.3% 🟢)0.658s53.74x
💻 LocalExpress6.009s (-5.0%)6.619s (-5.7% 🟢)0.611s53.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.831s (+38.8% 🔺)6.612s (+27.1% 🔺)1.781s51.00x
▲ VercelExpress4.852s (+58.3% 🔺)6.870s (+36.2% 🔺)2.018s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.170s (~)2.007s (~)0.837s151.00x
🐘 PostgresNitro1.178s (-2.4%)2.007s (~)0.829s151.01x
🐘 PostgresExpress1.182s (~)2.008s (~)0.826s151.01x
💻 LocalNitro1.357s (-4.5%)2.006s (~)0.649s151.16x
💻 LocalNext.js (Turbopack)1.399s (~)2.006s (~)0.607s151.20x
💻 LocalExpress1.452s (+1.0%)2.007s (~)0.555s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.794s (-51.7% 🟢)4.333s (-41.8% 🟢)1.539s81.00x
▲ VercelExpress2.871s (+39.1% 🔺)4.642s (+32.7% 🔺)1.772s71.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.294s (-5.7% 🟢)2.508s (+1.5%)1.214s121.00x
🐘 PostgresNext.js (Turbopack)1.306s (+1.3%)2.918s (~)1.611s111.01x
🐘 PostgresExpress1.328s (-1.0%)2.222s (-10.1% 🟢)0.894s141.03x
💻 LocalNitro2.318s (-10.2% 🟢)3.008s (-3.2%)0.691s101.79x
💻 LocalNext.js (Turbopack)2.607s (-1.2%)3.108s (+3.3%)0.502s102.01x
💻 LocalExpress2.613s (+7.9% 🔺)3.009s (-3.2%)0.396s102.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.700s (-22.0% 🟢)5.332s (-23.3% 🟢)1.631s61.00x
▲ VercelExpress4.302s (-10.2% 🟢)5.906s (-11.0% 🟢)1.604s61.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.610s (+1.5%)4.011s (-3.0%)2.401s81.00x
🐘 PostgresNitro1.620s (-2.4%)4.137s (+3.1%)2.517s81.01x
🐘 PostgresNext.js (Turbopack)3.012s (+5.4% 🔺)5.848s (~)2.837s61.87x
💻 LocalNext.js (Turbopack)4.998s (-12.6% 🟢)5.683s (-5.5% 🟢)0.685s63.11x
💻 LocalExpress6.217s (-7.2% 🟢)7.016s (-2.8%)0.799s53.86x
💻 LocalNitro6.501s (-4.1%)7.020s (-2.8%)0.518s54.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.961s (-15.5% 🟢)5.681s (-9.8% 🟢)1.720s61.00x
▲ VercelNext.js (Turbopack)4.577s (+46.5% 🔺)6.497s (+25.9% 🔺)1.920s51.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.507s (-0.9%)1.006s (~)0.500s601.00x
🐘 PostgresNitro0.538s (-9.6% 🟢)1.006s (-3.4%)0.468s601.06x
🐘 PostgresExpress0.561s (+5.1% 🔺)1.023s (~)0.462s591.11x
💻 LocalNext.js (Turbopack)0.565s (-6.5% 🟢)1.005s (-1.7%)0.440s601.11x
💻 LocalNitro0.580s (-1.0%)1.022s (+1.6%)0.442s591.15x
💻 LocalExpress0.584s (+1.9%)1.005s (~)0.421s601.15x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.239s (+39.5% 🔺)6.064s (+21.4% 🔺)1.825s111.00x
▲ VercelExpress4.460s (+59.4% 🔺)6.406s (+42.8% 🔺)1.946s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.221s (-1.9%)2.007s (-1.1%)0.786s451.00x
🐘 PostgresNitro1.294s (-8.5% 🟢)2.007s (-2.3%)0.713s451.06x
🐘 PostgresExpress1.379s (+10.3% 🔺)2.030s (+1.1%)0.651s451.13x
💻 LocalNext.js (Turbopack)1.406s (-4.2%)2.005s (~)0.599s451.15x
💻 LocalExpress1.436s (-1.3%)2.006s (~)0.570s451.18x
💻 LocalNitro1.478s (~)2.028s (+1.1%)0.550s451.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.677s (+80.6% 🔺)13.672s (+70.1% 🔺)1.995s71.00x
▲ VercelNext.js (Turbopack)12.418s (+73.8% 🔺)14.184s (+59.5% 🔺)1.766s71.06x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)2.476s (~)3.034s (-0.8%)0.558s401.00x
🐘 PostgresNitro2.595s (-4.4%)3.085s (+0.9%)0.490s391.05x
🐘 PostgresExpress2.846s (+9.7% 🔺)3.194s (+3.5%)0.347s381.15x
💻 LocalNitro3.052s (-5.2% 🟢)3.675s (-8.3% 🟢)0.623s331.23x
💻 LocalNext.js (Turbopack)3.099s (-5.0%)3.675s (-9.1% 🟢)0.577s331.25x
💻 LocalExpress3.179s (-2.1%)3.913s (-1.6%)0.734s311.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.472s (+63.0% 🔺)24.735s (+60.0% 🔺)2.262s51.00x
▲ VercelNext.js (Turbopack)26.936s (+64.1% 🔺)28.462s (+54.0% 🔺)1.527s51.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.166s (-0.8%)1.006s (~)0.840s601.00x
🐘 PostgresNitro0.209s (-7.8% 🟢)1.006s (~)0.797s601.26x
🐘 PostgresExpress0.237s (+12.5% 🔺)1.007s (~)0.770s601.42x
💻 LocalNitro0.431s (-7.9% 🟢)1.004s (~)0.573s602.59x
💻 LocalExpress0.455s (+5.5% 🔺)1.005s (~)0.550s602.73x
💻 LocalNext.js (Turbopack)0.564s (-3.5%)1.022s (+1.7%)0.457s593.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.803s (-0.8%)3.477s (-2.0%)1.674s181.00x
▲ VercelExpress1.994s (-3.8%)3.433s (-10.6% 🟢)1.439s181.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.253s (-5.5% 🟢)1.006s (~)0.752s901.00x
🐘 PostgresNitro0.326s (-3.3%)1.006s (~)0.680s901.29x
🐘 PostgresExpress0.356s (+10.2% 🔺)1.018s (~)0.662s891.41x
💻 LocalNitro2.085s (-2.3%)2.686s (~)0.602s348.23x
💻 LocalExpress2.121s (-1.8%)2.766s (+1.1%)0.645s338.37x
💻 LocalNext.js (Turbopack)2.599s (-6.8% 🟢)3.113s (-4.5%)0.514s2910.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.670s (+8.4% 🔺)4.342s (+7.1% 🔺)1.672s211.00x
▲ VercelNext.js (Turbopack)3.196s (+47.3% 🔺)5.002s (+27.3% 🔺)1.806s191.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.490s (+3.8%)3.343s (+11.1% 🔺)2.853s361.00x
🐘 PostgresNitro0.519s (-1.7%)1.059s (+2.6%)0.540s1141.06x
🐘 PostgresExpress0.574s (+11.3% 🔺)1.118s (+4.7%)0.544s1081.17x
💻 LocalNext.js (Turbopack)9.280s (-5.1% 🟢)10.195s (-5.4% 🟢)0.916s1218.95x
💻 LocalExpress9.668s (-4.2%)10.528s (-5.4% 🟢)0.860s1219.74x
💻 LocalNitro9.701s (-2.2%)10.779s (-2.3%)1.077s1219.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.551s (+48.6% 🔺)5.489s (+35.4% 🔺)1.938s221.00x
▲ VercelNext.js (Turbopack)5.084s (+8.5% 🔺)7.510s (+14.7% 🔺)2.426s161.43x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.138s (-2.7%)1.968s (~)0.010s (-16.1% 🟢)2.018s (~)0.880s101.00x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.000s (~)0.001s (~)2.010s (~)0.863s101.01x
💻 LocalNitro1.155s (~)2.004s (~)0.010s (-14.0% 🟢)2.017s (~)0.862s101.01x
💻 LocalExpress1.159s (+0.6%)2.005s (~)0.012s (+2.5%)2.020s (~)0.860s101.02x
🐘 PostgresExpress1.169s (+1.7%)1.997s (~)0.001s (+10.0% 🔺)2.012s (~)0.842s101.03x
🐘 PostgresNitro1.193s (+2.3%)1.995s (~)0.001s (+9.1% 🔺)2.011s (~)0.817s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.409s (+17.7% 🔺)3.498s (+14.0% 🔺)3.398s (+94.0% 🔺)7.469s (+38.2% 🔺)5.061s101.00x
▲ VercelNext.js (Turbopack)2.673s (+29.4% 🔺)3.825s (+10.9% 🔺)3.340s (+137.0% 🔺)7.691s (+42.3% 🔺)5.018s101.11x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.535s (-4.3%)2.002s (~)0.005s (-1.3%)2.024s (~)0.489s301.00x
💻 LocalExpress1.544s (~)2.010s (~)0.012s (-4.9%)2.026s (~)0.482s301.01x
🐘 PostgresNext.js (Turbopack)1.547s (~)2.009s (~)0.005s (-0.6%)2.026s (~)0.479s301.01x
💻 LocalNext.js (Turbopack)1.555s (-1.4%)1.972s (~)0.013s (+1.0%)2.027s (~)0.472s301.01x
💻 LocalNitro1.566s (+0.8%)2.010s (~)0.013s (+5.0%)2.026s (~)0.460s301.02x
🐘 PostgresExpress1.602s (+3.5%)2.002s (~)0.005s (-7.2% 🟢)2.026s (~)0.424s301.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.722s (+13.1% 🔺)8.052s (+9.2% 🔺)0.240s (-24.5% 🟢)8.776s (+6.3% 🔺)2.054s71.00x
▲ VercelExpress7.606s (+29.2% 🔺)9.161s (+27.1% 🔺)0.500s (+131.5% 🔺)10.151s (+28.8% 🔺)2.545s61.13x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.764s (-7.0% 🟢)1.065s (-3.0%)0.000s (-100.0% 🟢)1.079s (-3.6%)0.314s561.00x
🐘 PostgresExpress0.830s (+7.2% 🔺)1.104s (+5.3% 🔺)0.000s (+Infinity% 🔺)1.119s (+5.2% 🔺)0.289s541.09x
🐘 PostgresNext.js (Turbopack)0.965s (+5.3% 🔺)1.429s (+11.9% 🔺)0.000s (+11.9% 🔺)1.436s (+11.8% 🔺)0.471s421.26x
💻 LocalExpress1.212s (-4.4%)1.981s (-1.6%)0.000s (+45.2% 🔺)1.983s (-1.6%)0.771s311.59x
💻 LocalNitro1.256s (-4.8%)1.982s (~)0.000s (-33.3% 🟢)1.984s (~)0.729s311.64x
💻 LocalNext.js (Turbopack)1.323s (-0.9%)1.981s (~)0.000s (+66.7% 🔺)2.017s (~)0.694s301.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.029s (+3.1%)5.021s (~)0.000s (~)5.476s (-1.1%)1.447s111.00x
▲ VercelNext.js (Turbopack)4.175s (+26.8% 🔺)5.055s (+7.5% 🔺)0.000s (-100.0% 🟢)5.543s (+5.5% 🔺)1.367s111.04x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.783s (+5.2% 🔺)2.435s (+8.0% 🔺)0.000s (+8.0% 🔺)2.451s (+8.0% 🔺)0.668s251.00x
🐘 PostgresNitro1.937s (+1.0%)2.410s (-3.5%)0.000s (-100.0% 🟢)2.450s (-2.9%)0.512s261.09x
🐘 PostgresNext.js (Turbopack)2.470s (+21.6% 🔺)2.907s (+11.3% 🔺)0.000s (-100.0% 🟢)2.914s (+10.3% 🔺)0.444s211.39x
💻 LocalExpress3.357s (-7.6% 🟢)3.904s (-7.7% 🟢)0.001s (+40.6% 🔺)3.909s (-7.6% 🟢)0.551s161.88x
💻 LocalNitro3.367s (-5.6% 🟢)3.965s (-1.5%)0.001s (-22.8% 🟢)3.971s (-1.5%)0.604s161.89x
💻 LocalNext.js (Turbopack)3.441s (-6.1% 🟢)3.868s (-7.7% 🟢)0.001s (-47.3% 🟢)3.907s (-7.8% 🟢)0.466s161.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.023s (+27.2% 🔺)7.515s (+30.8% 🔺)0.000s (-100.0% 🟢)8.032s (+28.4% 🔺)2.009s81.00x
▲ VercelNext.js (Turbopack)6.120s (+32.1% 🔺)7.138s (+18.6% 🔺)0.000s (+25.0% 🔺)7.761s (+18.0% 🔺)1.641s81.02x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNext.js (Turbopack)13/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres16/21
Nitro🐘 Postgres15/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)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@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 diff in depth — runtime span wiring, the builders codegen across all three generated route templates (workflow-only bundleFinal, combined, and watch-mode combined) plus the sveltekit/astro regex post-processing, the new semantic conventions, and the tests.

The instrumentation is clean, well-scoped, and well-tested. The span-hierarchy change (workflow.execute now nests under workflow.route.flow) is correctly reflected in the linked-mode tests and doesn't regress continuous mode (it still parents to the run-origin via withTraceContext). All three codegen templates declare workflowRouteModuleBodyStartedAt and reference it via the new options, and the sveltekit/astro regex ([^)]* capture) still matches the variable-reference options. CI E2E is fully green.

Main suggestion (re: the turbo-mode question): there is no dedicated turbo span attribute — turbo is only inferable from the workflow.run_started.skip_preloadevent. Recommend adding a workflow.turbo tag on workflow.execute so runs/invocations are trivially filterable by turbo mode (details inline). Everything else is minor/optional; nothing blocking.

skipPreload: boolean
) => {
span?.addEvent('workflow.run_started.create.start', {
'workflow.run_started.skip_preload': skipPreload,

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.

Turbo mode is only observable indirectly here. This workflow.run_started.skip_preload event attribute is currently the only turbo signal on the trace, and it's an awkward proxy for "is this run in turbo mode?":

  • It's a span event attribute, not a span tag, so it's harder to facet/filter on in most backends (Datadog, etc.).
  • The name skip_preload doesn't say "turbo" — someone asking the turbo question wouldn't know to look here.
  • The event is only emitted when run_started.create runs (first delivery). On redeliveries/replays (attempt > 1) there's no event at all, so those invocations carry no turbo signal.

turbo is already computed once per invocation at line 535 (and since turbo requires runInput, this skipPreload=true branch is exactly turbo). Consider also setting a dedicated tag on the workflow.execute span — e.g. right after the turbo constant is computed, where span is in scope:

span?.setAttributes(Attribute.WorkflowTurbo(turbo));

backed by a new export const WorkflowTurbo = SemanticConvention<boolean>('workflow.turbo'). That puts a boolean turbo tag on every workflow.execute span (true on the turbo first delivery, false on replays — which is the precise per-invocation distinction we want), and keeps the existing skip_preload event as the finer-grained marker.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a dedicated workflow.turbo semantic convention and set it on every workflow.execute span after the per-invocation turbo decision is computed. I kept the existing workflow.run_started.skip_preload event attribute as the lower-level marker for the run_started create call.

),
...(routeModuleBodyInitMs === undefined
? {}
: Attribute.WorkflowRouteModuleBodyInitMs(routeModuleBodyInitMs)),

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.

routeModuleBodyInitMs is a process-level constant (computed once at entrypoint construction) but emitted on everyworkflow.route.flow span, so it's identical across all requests from a given instance. Fine for queryability — just noting it isn't per-request.

Also worth setting expectations on what it measures: the marker is captured at the top of the generated module body, i.e. afterimport { workflowEntrypoint } from 'workflow/runtime' has fully evaluated. So it won't capture the @workflow/core import/eval cost (the likely dominant cold-start contributor) and will typically read ~0 in the workflow-only template, where the only body work between the marker and the workflowEntrypoint() call is a string-literal assignment. The PR description already acknowledges this; flagging only so the attribute's usefulness isn't over-read.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the PR description to make this explicit: workflow.route.module_body_init_ms is computed once per route entrypoint, emitted on each request from that in-memory entrypoint, and starts after static ESM imports have already evaluated. No code change here since the behavior is intentional.

return cachedHandler(req);
invocationCount += 1;
const handlerCached = cachedHandler !== undefined;
const spanKind = await getSpanKind('SERVER');

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.

Micro-nit (optional, fine to leave): getSpanKind('SERVER') plus the attributes object are now built on every flow request — including the warm path and when no OTEL SDK is registered — whereas previously the warm path was just return cachedHandler(req). The cost is negligible (awaiting an already-memoized once() promise; trace() awaits the same lazy values anyway), so no action needed; just calling it out since this route is perf-sensitive.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Acknowledged. I left this as-is: the added warm-path work remains a memoized span-kind lookup plus attribute construction, and trace() already hits the same memoized OTel handles. Keeping the route-level span always available seems worth that small overhead for this incident path.

},
async (span) => {
if (!cachedHandler) {
cachedHandler = await trace('workflow.route.init', async () => {

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.

Pre-existing behavior (not introduced by this PR), but now more visible in the spans: two concurrent requests on a cold instance can both observe cachedHandler === undefined and each run getWorldHandlers() + handler(), producing two workflow.route.init spans for a single instance (and handler_cached=false on both). Harmless — handlers are stateless and it's effectively last-write-wins — and the new test covers the sequential case. Not a blocker; just noting that under concurrency the span counts can briefly exceed the "once per warm instance" framing in the PR description.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good call. I updated the PR description to stop implying a strict once-per-instance invariant: it now says workflow.route.init is emitted when the cached handler is created, and that concurrent cold requests can briefly produce more than one init span because the pre-existing cache assignment is last-write-wins.


if (options?.routeModuleBodyStartedAt) {
fields.push(
`routeModuleBodyStartedAt: ${options.routeModuleBodyStartedAt}`

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.

routeModuleBodyStartedAt is interpolated raw here (unlike namespace, which is JSON.stringify'd) because it must emit a code identifier reference, not a string literal — correct by design, and the sole caller passes a hardcoded 'workflowRouteModuleBodyStartedAt'. Since this makes the param a code-injection surface if it's ever called with untrusted input, consider a one-line doc comment on the option noting the value must be a code identifier, not data.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a doc comment on routeModuleBodyStartedAt noting that it is emitted as a raw code identifier/expression in generated route files, not serialized data.

);

/** Route pattern for the request */
export const HttpRoute = SemanticConvention<string>('http.route');

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.

Tiny organizational nit: http.route is defined here in the "Route attributes" block, while the other standard OTEL Http* conventions (HttpRequestMethod, HttpResponseStatusCode, UrlFull, …) are grouped together around lines 286–309. Consider moving it next to them for discoverability.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: moved HttpRoute down next to the other standard HTTP semantic convention helpers and left the workflow-specific route attributes grouped separately.

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

Approving. The workflow.turbo attribute (the main ask from the review) plus the smaller nits are all addressed in c9b83f3 — I'd independently prepared the same set of changes, so rather than push a duplicate commit (which would have collided on the identical WorkflowTurbo/HttpRoute additions) I verified your version instead.

What's now in:

  • workflow.turbo boolean tag on the workflow.execute span (Attribute.WorkflowTurbo(turbo)), so turbo invocations are trivially filterable — true only on the turbo first delivery, false on replays/redeliveries. Complements the finer-grained workflow.run_started.skip_preload event.
  • http.route convention regrouped with the other standard OTEL Http* conventions.
  • Doc comment on createWorkflowEntrypointOptionsCode's routeModuleBodyStartedAt noting it's emitted raw as a code identifier (not data).
  • workflow.turbo assertion added to the linked-mode test.

Verified locally on c9b83f3 (built deps from source): @workflow/core + @workflow/builders typecheck clean, runtime-trace-mode.test.ts (11) and constants.test.ts (7) green. CI E2E was already fully green.

The earlier observations I left (module_body_init_ms being a per-process constant emitted on every span; tiny warm-path overhead; pre-existing concurrent cold-start init) are all non-blocking — fine as-is.

@pranaygp
pranaygp merged commit 2bf5257 into mainJun 23, 2026
108 of 110 checks passed
@pranaygp
pranaygp deleted the kk/otel-flow-module-loading branch June 23, 2026 21:28
@github-actionsgithub-actionsBot mentioned this pull request Jun 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2bf5257 (AI decision).

The core instrumentation in this PR builds directly on main-only runtime architecture: stable's workflowEntrypoint creates the queue handler eagerly and synchronously at module load (no lazy cachedHandler/getWorldHandlers() caching, no getSpanKind), so the new workflow.route.flow/workflow.route.init/workflow.route.get_world_handlers spans have nothing to wrap there. The workflow.turbo attribute and workflow.run_started.skip_preload event instrument the turbo first-delivery path, which does not exist on stable, and the modified runtime-trace-mode.test.ts is also absent on stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2bf5257f97fc4fea036717a7882dfd39bf2b3804

pranaygp added a commit that referenced this pull request Jun 23, 2026
…testing
* origin/main:
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
[web-shared] reskin json viewer (no duplicates, better colours and navigation) (#2434)
Send occurredAt with workflow events (#2580)
docs: use actual eve logo and tidy OSS nav dropdown (#2586)
Display occurredAt in trace details (#2581)
fix(next): discover root entrypoints (#2564)
[core] Turbo: skip the unused run_started event-log preload (#2569)
fix(next): prewarm SWC plugin cache (#2538)
[world-vercel] Use v3 stream endpoint (supports transparent reconnect on timeout) (#2424)
[core] Retry stream reopen against the reconnect budget (#2334)
Add Platformatic World to worlds-manifest.json (#1450)
docs: derive section landing-page cards from the page tree + lint drift (#2567)
pranaygp added a commit that referenced this pull request Jun 26, 2026
* origin/main: (46 commits)
[web] Fix HTTP/2 in bundled server build so observability reads work (#2632)
[core] Fix turbo-mode step-body writes racing run_started (#2629)
fix(world-postgres): rename setup command (#2644)
chore: ignore workflow swc caches (#2640)
fix(ai): ignore intentional stream aborts (#2635)
fix(nitro): reload steps during Vite HMR (#2572)
[world] Make @workflow/world own its Zod dependency (#2622)
feat(web-shared): extend cn with custom tailwind-merge class groups (#2619)
[world-vercel] Enable HTTP/2 for the events API and stream writes (#2573)
Remove shadow from off-screen marker indicator button (#2614)
Fix discovery of dotted JS workflow imports (#2594)
Version Packages (beta) (#2597)
Thread occurredAt into web-shared entities (#2608)
Propagate trace context to vercel-workflow.com in workbench instrumentation (#2601)
Reduce workflow build log noise (#2565)
Version Packages (beta) (#2596)
Use text-gray-900 for trace shortcut helper text (#2595)
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
...
# Conflicts:
#	workbench/nextjs-turbopack/instrumentation.ts
#	workbench/nextjs-webpack/instrumentation.ts
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.

2 participants

@karthikscale3@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Trace /flow route initialization - #2592

Merged
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading
Jun 23, 2026
Merged

Trace /flow route initialization#2592
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

This instruments the Workflow SDK /flow route path so we can explain delay before the route makes its first workflow-server run_started POST.

Instrumentation contract

workflow.route.flow

New server span around each generated /flow route request.

Attributes:

  • workflow.route.type: currently flow
  • workflow.route.handler_cached: whether this invocation reused the in-memory route handler
  • workflow.route.invocation_count: per-process invocation count for this route entrypoint
  • workflow.route.entrypoint_age_ms: milliseconds since workflowEntrypoint() constructed this route entrypoint
  • workflow.route.module_body_init_ms: process-level milliseconds from the generated route module-body timestamp to workflowEntrypoint() construction, when generated routes provide the timestamp
  • http.request.method: request method
  • http.route: /.well-known/workflow/v1/flow
  • http.response.status_code: response status when the handler returns a Response

workflow.route.init

New child span under workflow.route.flow, emitted when the route creates the cached queue handler.

Use this to separate first-hit handler construction from normal warm-route request handling. Under concurrent cold requests, more than one request can observe an empty cache and emit workflow.route.init; that is pre-existing harmless last-write-wins behavior.

workflow.route.get_world_handlers

New child span under workflow.route.init around getWorldHandlers().

Use this to identify latency from loading/creating the world handler layer during route setup.

workflow.route.get_world

New child span under workflow.route.flow, emitted for each workflow invocation before workflow.execute.

Use this to identify latency from resolving the per-invocation world before SDK runtime work posts run_started.

workflow.execute <workflowName>

Existing workflow execution span. This PR adds:

Attributes:

  • workflow.turbo: whether this invocation is using the turbo first-delivery path

Span events:

  • workflow.run_started.create.start: emitted immediately before world.events.create(... run_started ...), i.e. right before the first workflow-server run_started POST begins

Event attributes:

  • workflow.run_started.skip_preload: whether the run_started create call is using the turbo skipPreload path

Reading the signal

  • Gap from workflow.route.flow start to workflow.run_started.create.start: SDK/runtime work before the first workflow-server run_started POST begins.
  • Large workflow.route.module_body_init_ms: generated route module body work after static imports, before the SDK route handler is created. This value is computed once per route entrypoint and emitted on every request from that in-memory entrypoint.
  • Near-zero workflow.route.entrypoint_age_ms: the route entrypoint was just created for this request, so this request hit a cold SDK route instance.
  • workflow.route.handler_cached=false plus workflow.route.init: the request paid route handler creation. Sequentially this happens once per warm instance; concurrent cold requests may briefly produce more than one init span.
  • workflow.route.handler_cached=true: route handler was already created; delay is not from handler construction.
  • workflow.turbo=true: this invocation is the turbo first-delivery path. workflow.run_started.skip_preload=true is the finer-grained marker for the associated run_started create call.

Static ESM import evaluation still happens before generated route body code can run, so this does not directly measure the entire import graph. It gives us the earliest SDK-controlled marker in the generated /flow module, plus the exact pre-run_started marker inside the SDK handler.

Testing

  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec vitest run packages/core/src/runtime-trace-mode.test.ts packages/builders/src/constants.test.ts
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/core typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/builders typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec biome check .changeset/flow-route-init-traces.md packages/builders/src/base-builder.ts packages/builders/src/constants.ts packages/builders/src/constants.test.ts packages/core/src/runtime.ts packages/core/src/runtime-trace-mode.test.ts packages/core/src/telemetry/semantic-conventions.ts (exit 0; reports existing complexity/unused-suppression warnings)
  • Previous full @workflow/core suite under Node 24: all 1322 tests passed, but Vitest exited 1 due an existing pending-fetch shutdown error; packages/core/src/abort-controller-step.test.ts passed by itself.

@vercel

vercelBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9b83f3

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

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

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

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

@github-actions

github-actionsBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production144112301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7281110788360

❌ Failed Tests

▲ Vercel Production (1 failed)

nuxt (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
❌ nuxt124127
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ 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


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jun 23, 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.044s (-5.7% 🟢)1.006s (~)0.962s101.00x
💻 LocalExpress0.046s (-2.1%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.049s (-6.3% 🟢)1.008s (~)0.959s101.11x
🐘 PostgresNext.js (Turbopack)0.056s (+1.1%)1.011s (~)0.955s101.26x
🐘 PostgresNitro0.061s (-6.0% 🟢)1.013s (~)0.952s101.39x
🐘 PostgresExpress0.072s (+3.9%)1.013s (~)0.940s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.292s (+26.2% 🔺)2.213s (+5.2% 🔺)1.921s101.00x
▲ VercelNext.js (Turbopack)0.314s (+16.1% 🔺)2.353s (+15.1% 🔺)2.039s101.08x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.079s (-0.6%)2.006s (~)0.928s101.00x
💻 LocalExpress1.083s (~)2.007s (~)0.923s101.00x
🐘 PostgresNext.js (Turbopack)1.086s (~)2.011s (~)0.925s101.01x
💻 LocalNext.js (Turbopack)1.086s (~)2.006s (~)0.920s101.01x
🐘 PostgresNitro1.101s (~)2.011s (~)0.911s101.02x
🐘 PostgresExpress1.103s (+1.0%)2.011s (~)0.908s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.507s (+8.9% 🔺)3.416s (+20.7% 🔺)1.909s101.00x
▲ VercelNext.js (Turbopack)1.588s (+10.7% 🔺)3.280s (-2.2%)1.692s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.443s (~)11.022s (~)0.580s31.00x
💻 LocalNitro10.446s (~)11.022s (~)0.576s31.00x
💻 LocalExpress10.463s (~)11.022s (~)0.560s31.00x
🐘 PostgresNext.js (Turbopack)10.471s (~)11.017s (~)0.547s31.00x
🐘 PostgresExpress10.484s (~)11.021s (~)0.537s31.00x
🐘 PostgresNitro10.498s (~)11.021s (~)0.523s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.720s (+3.3%)14.402s (+3.8%)1.682s31.00x
▲ VercelNext.js (Turbopack)12.862s (+9.0% 🔺)14.699s (+5.4% 🔺)1.837s31.01x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.569s (~)14.028s (~)0.459s51.00x
💻 LocalExpress13.602s (~)14.028s (~)0.426s51.00x
🐘 PostgresNitro13.641s (-0.6%)14.020s (~)0.379s51.01x
💻 LocalNext.js (Turbopack)13.681s (+0.7%)14.028s (~)0.347s51.01x
🐘 PostgresExpress13.701s (+0.9%)14.022s (~)0.320s51.01x
🐘 PostgresNext.js (Turbopack)13.721s (~)14.021s (~)0.299s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)18.660s (+7.6% 🔺)20.341s (+6.2% 🔺)1.681s31.00x
▲ VercelExpress19.542s (+18.9% 🔺)21.199s (+17.8% 🔺)1.657s31.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.055s (-1.0%)13.025s (~)0.970s71.00x
💻 LocalExpress12.145s (-1.6%)13.027s (~)0.882s71.01x
🐘 PostgresNitro12.162s (-0.9%)13.020s (~)0.858s71.01x
💻 LocalNext.js (Turbopack)12.189s (-0.5%)13.025s (~)0.836s71.01x
🐘 PostgresNext.js (Turbopack)12.218s (+1.0%)13.023s (~)0.805s71.01x
🐘 PostgresExpress12.276s (+1.4%)13.018s (~)0.742s71.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.857s (+35.3% 🔺)26.850s (+33.2% 🔺)1.994s41.00x
▲ VercelNext.js (Turbopack)25.505s (+31.8% 🔺)27.773s (+30.8% 🔺)2.268s41.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.165s (-1.0%)2.008s (~)0.843s151.00x
🐘 PostgresNitro1.194s (+1.0%)2.007s (~)0.813s151.02x
🐘 PostgresExpress1.213s (+3.8%)2.008s (~)0.795s151.04x
💻 LocalNext.js (Turbopack)1.380s (-7.8% 🟢)2.006s (~)0.626s151.18x
💻 LocalExpress1.385s (~)2.006s (~)0.621s151.19x
💻 LocalNitro1.396s (-2.9%)2.006s (~)0.611s151.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.793s (+29.9% 🔺)4.385s (+15.7% 🔺)1.592s71.00x
▲ VercelExpress3.109s (+53.2% 🔺)5.068s (+44.8% 🔺)1.959s61.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.301s (-0.9%)3.009s (+3.1%)1.708s101.00x
🐘 PostgresExpress1.343s (-3.6%)2.509s (-6.2% 🟢)1.166s121.03x
🐘 PostgresNitro1.348s (+0.6%)2.510s (~)1.162s121.04x
💻 LocalNitro2.313s (-3.9%)2.736s (-6.2% 🟢)0.423s111.78x
💻 LocalNext.js (Turbopack)2.391s (-12.6% 🟢)3.008s (-3.2%)0.618s101.84x
💻 LocalExpress2.529s (+7.3% 🔺)2.918s (~)0.389s111.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.579s (+7.1% 🔺)5.199s (+3.0%)1.620s61.00x
▲ VercelExpress3.756s (+3.4%)5.354s (+3.7%)1.598s61.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.591s (-3.0%)4.010s (-5.9% 🟢)2.419s81.00x
🐘 PostgresExpress1.637s (+3.2%)4.136s (-3.9%)2.499s81.03x
🐘 PostgresNext.js (Turbopack)2.944s (+4.6%)5.349s (-3.0%)2.406s61.85x
💻 LocalNext.js (Turbopack)4.978s (-9.0% 🟢)5.349s (-11.1% 🟢)0.371s63.13x
💻 LocalNitro5.957s (-7.5% 🟢)6.616s (-8.3% 🟢)0.658s53.74x
💻 LocalExpress6.009s (-5.0%)6.619s (-5.7% 🟢)0.611s53.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.831s (+38.8% 🔺)6.612s (+27.1% 🔺)1.781s51.00x
▲ VercelExpress4.852s (+58.3% 🔺)6.870s (+36.2% 🔺)2.018s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.170s (~)2.007s (~)0.837s151.00x
🐘 PostgresNitro1.178s (-2.4%)2.007s (~)0.829s151.01x
🐘 PostgresExpress1.182s (~)2.008s (~)0.826s151.01x
💻 LocalNitro1.357s (-4.5%)2.006s (~)0.649s151.16x
💻 LocalNext.js (Turbopack)1.399s (~)2.006s (~)0.607s151.20x
💻 LocalExpress1.452s (+1.0%)2.007s (~)0.555s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.794s (-51.7% 🟢)4.333s (-41.8% 🟢)1.539s81.00x
▲ VercelExpress2.871s (+39.1% 🔺)4.642s (+32.7% 🔺)1.772s71.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.294s (-5.7% 🟢)2.508s (+1.5%)1.214s121.00x
🐘 PostgresNext.js (Turbopack)1.306s (+1.3%)2.918s (~)1.611s111.01x
🐘 PostgresExpress1.328s (-1.0%)2.222s (-10.1% 🟢)0.894s141.03x
💻 LocalNitro2.318s (-10.2% 🟢)3.008s (-3.2%)0.691s101.79x
💻 LocalNext.js (Turbopack)2.607s (-1.2%)3.108s (+3.3%)0.502s102.01x
💻 LocalExpress2.613s (+7.9% 🔺)3.009s (-3.2%)0.396s102.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.700s (-22.0% 🟢)5.332s (-23.3% 🟢)1.631s61.00x
▲ VercelExpress4.302s (-10.2% 🟢)5.906s (-11.0% 🟢)1.604s61.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.610s (+1.5%)4.011s (-3.0%)2.401s81.00x
🐘 PostgresNitro1.620s (-2.4%)4.137s (+3.1%)2.517s81.01x
🐘 PostgresNext.js (Turbopack)3.012s (+5.4% 🔺)5.848s (~)2.837s61.87x
💻 LocalNext.js (Turbopack)4.998s (-12.6% 🟢)5.683s (-5.5% 🟢)0.685s63.11x
💻 LocalExpress6.217s (-7.2% 🟢)7.016s (-2.8%)0.799s53.86x
💻 LocalNitro6.501s (-4.1%)7.020s (-2.8%)0.518s54.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.961s (-15.5% 🟢)5.681s (-9.8% 🟢)1.720s61.00x
▲ VercelNext.js (Turbopack)4.577s (+46.5% 🔺)6.497s (+25.9% 🔺)1.920s51.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.507s (-0.9%)1.006s (~)0.500s601.00x
🐘 PostgresNitro0.538s (-9.6% 🟢)1.006s (-3.4%)0.468s601.06x
🐘 PostgresExpress0.561s (+5.1% 🔺)1.023s (~)0.462s591.11x
💻 LocalNext.js (Turbopack)0.565s (-6.5% 🟢)1.005s (-1.7%)0.440s601.11x
💻 LocalNitro0.580s (-1.0%)1.022s (+1.6%)0.442s591.15x
💻 LocalExpress0.584s (+1.9%)1.005s (~)0.421s601.15x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.239s (+39.5% 🔺)6.064s (+21.4% 🔺)1.825s111.00x
▲ VercelExpress4.460s (+59.4% 🔺)6.406s (+42.8% 🔺)1.946s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.221s (-1.9%)2.007s (-1.1%)0.786s451.00x
🐘 PostgresNitro1.294s (-8.5% 🟢)2.007s (-2.3%)0.713s451.06x
🐘 PostgresExpress1.379s (+10.3% 🔺)2.030s (+1.1%)0.651s451.13x
💻 LocalNext.js (Turbopack)1.406s (-4.2%)2.005s (~)0.599s451.15x
💻 LocalExpress1.436s (-1.3%)2.006s (~)0.570s451.18x
💻 LocalNitro1.478s (~)2.028s (+1.1%)0.550s451.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.677s (+80.6% 🔺)13.672s (+70.1% 🔺)1.995s71.00x
▲ VercelNext.js (Turbopack)12.418s (+73.8% 🔺)14.184s (+59.5% 🔺)1.766s71.06x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)2.476s (~)3.034s (-0.8%)0.558s401.00x
🐘 PostgresNitro2.595s (-4.4%)3.085s (+0.9%)0.490s391.05x
🐘 PostgresExpress2.846s (+9.7% 🔺)3.194s (+3.5%)0.347s381.15x
💻 LocalNitro3.052s (-5.2% 🟢)3.675s (-8.3% 🟢)0.623s331.23x
💻 LocalNext.js (Turbopack)3.099s (-5.0%)3.675s (-9.1% 🟢)0.577s331.25x
💻 LocalExpress3.179s (-2.1%)3.913s (-1.6%)0.734s311.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.472s (+63.0% 🔺)24.735s (+60.0% 🔺)2.262s51.00x
▲ VercelNext.js (Turbopack)26.936s (+64.1% 🔺)28.462s (+54.0% 🔺)1.527s51.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.166s (-0.8%)1.006s (~)0.840s601.00x
🐘 PostgresNitro0.209s (-7.8% 🟢)1.006s (~)0.797s601.26x
🐘 PostgresExpress0.237s (+12.5% 🔺)1.007s (~)0.770s601.42x
💻 LocalNitro0.431s (-7.9% 🟢)1.004s (~)0.573s602.59x
💻 LocalExpress0.455s (+5.5% 🔺)1.005s (~)0.550s602.73x
💻 LocalNext.js (Turbopack)0.564s (-3.5%)1.022s (+1.7%)0.457s593.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.803s (-0.8%)3.477s (-2.0%)1.674s181.00x
▲ VercelExpress1.994s (-3.8%)3.433s (-10.6% 🟢)1.439s181.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.253s (-5.5% 🟢)1.006s (~)0.752s901.00x
🐘 PostgresNitro0.326s (-3.3%)1.006s (~)0.680s901.29x
🐘 PostgresExpress0.356s (+10.2% 🔺)1.018s (~)0.662s891.41x
💻 LocalNitro2.085s (-2.3%)2.686s (~)0.602s348.23x
💻 LocalExpress2.121s (-1.8%)2.766s (+1.1%)0.645s338.37x
💻 LocalNext.js (Turbopack)2.599s (-6.8% 🟢)3.113s (-4.5%)0.514s2910.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.670s (+8.4% 🔺)4.342s (+7.1% 🔺)1.672s211.00x
▲ VercelNext.js (Turbopack)3.196s (+47.3% 🔺)5.002s (+27.3% 🔺)1.806s191.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.490s (+3.8%)3.343s (+11.1% 🔺)2.853s361.00x
🐘 PostgresNitro0.519s (-1.7%)1.059s (+2.6%)0.540s1141.06x
🐘 PostgresExpress0.574s (+11.3% 🔺)1.118s (+4.7%)0.544s1081.17x
💻 LocalNext.js (Turbopack)9.280s (-5.1% 🟢)10.195s (-5.4% 🟢)0.916s1218.95x
💻 LocalExpress9.668s (-4.2%)10.528s (-5.4% 🟢)0.860s1219.74x
💻 LocalNitro9.701s (-2.2%)10.779s (-2.3%)1.077s1219.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.551s (+48.6% 🔺)5.489s (+35.4% 🔺)1.938s221.00x
▲ VercelNext.js (Turbopack)5.084s (+8.5% 🔺)7.510s (+14.7% 🔺)2.426s161.43x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.138s (-2.7%)1.968s (~)0.010s (-16.1% 🟢)2.018s (~)0.880s101.00x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.000s (~)0.001s (~)2.010s (~)0.863s101.01x
💻 LocalNitro1.155s (~)2.004s (~)0.010s (-14.0% 🟢)2.017s (~)0.862s101.01x
💻 LocalExpress1.159s (+0.6%)2.005s (~)0.012s (+2.5%)2.020s (~)0.860s101.02x
🐘 PostgresExpress1.169s (+1.7%)1.997s (~)0.001s (+10.0% 🔺)2.012s (~)0.842s101.03x
🐘 PostgresNitro1.193s (+2.3%)1.995s (~)0.001s (+9.1% 🔺)2.011s (~)0.817s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.409s (+17.7% 🔺)3.498s (+14.0% 🔺)3.398s (+94.0% 🔺)7.469s (+38.2% 🔺)5.061s101.00x
▲ VercelNext.js (Turbopack)2.673s (+29.4% 🔺)3.825s (+10.9% 🔺)3.340s (+137.0% 🔺)7.691s (+42.3% 🔺)5.018s101.11x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.535s (-4.3%)2.002s (~)0.005s (-1.3%)2.024s (~)0.489s301.00x
💻 LocalExpress1.544s (~)2.010s (~)0.012s (-4.9%)2.026s (~)0.482s301.01x
🐘 PostgresNext.js (Turbopack)1.547s (~)2.009s (~)0.005s (-0.6%)2.026s (~)0.479s301.01x
💻 LocalNext.js (Turbopack)1.555s (-1.4%)1.972s (~)0.013s (+1.0%)2.027s (~)0.472s301.01x
💻 LocalNitro1.566s (+0.8%)2.010s (~)0.013s (+5.0%)2.026s (~)0.460s301.02x
🐘 PostgresExpress1.602s (+3.5%)2.002s (~)0.005s (-7.2% 🟢)2.026s (~)0.424s301.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.722s (+13.1% 🔺)8.052s (+9.2% 🔺)0.240s (-24.5% 🟢)8.776s (+6.3% 🔺)2.054s71.00x
▲ VercelExpress7.606s (+29.2% 🔺)9.161s (+27.1% 🔺)0.500s (+131.5% 🔺)10.151s (+28.8% 🔺)2.545s61.13x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.764s (-7.0% 🟢)1.065s (-3.0%)0.000s (-100.0% 🟢)1.079s (-3.6%)0.314s561.00x
🐘 PostgresExpress0.830s (+7.2% 🔺)1.104s (+5.3% 🔺)0.000s (+Infinity% 🔺)1.119s (+5.2% 🔺)0.289s541.09x
🐘 PostgresNext.js (Turbopack)0.965s (+5.3% 🔺)1.429s (+11.9% 🔺)0.000s (+11.9% 🔺)1.436s (+11.8% 🔺)0.471s421.26x
💻 LocalExpress1.212s (-4.4%)1.981s (-1.6%)0.000s (+45.2% 🔺)1.983s (-1.6%)0.771s311.59x
💻 LocalNitro1.256s (-4.8%)1.982s (~)0.000s (-33.3% 🟢)1.984s (~)0.729s311.64x
💻 LocalNext.js (Turbopack)1.323s (-0.9%)1.981s (~)0.000s (+66.7% 🔺)2.017s (~)0.694s301.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.029s (+3.1%)5.021s (~)0.000s (~)5.476s (-1.1%)1.447s111.00x
▲ VercelNext.js (Turbopack)4.175s (+26.8% 🔺)5.055s (+7.5% 🔺)0.000s (-100.0% 🟢)5.543s (+5.5% 🔺)1.367s111.04x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.783s (+5.2% 🔺)2.435s (+8.0% 🔺)0.000s (+8.0% 🔺)2.451s (+8.0% 🔺)0.668s251.00x
🐘 PostgresNitro1.937s (+1.0%)2.410s (-3.5%)0.000s (-100.0% 🟢)2.450s (-2.9%)0.512s261.09x
🐘 PostgresNext.js (Turbopack)2.470s (+21.6% 🔺)2.907s (+11.3% 🔺)0.000s (-100.0% 🟢)2.914s (+10.3% 🔺)0.444s211.39x
💻 LocalExpress3.357s (-7.6% 🟢)3.904s (-7.7% 🟢)0.001s (+40.6% 🔺)3.909s (-7.6% 🟢)0.551s161.88x
💻 LocalNitro3.367s (-5.6% 🟢)3.965s (-1.5%)0.001s (-22.8% 🟢)3.971s (-1.5%)0.604s161.89x
💻 LocalNext.js (Turbopack)3.441s (-6.1% 🟢)3.868s (-7.7% 🟢)0.001s (-47.3% 🟢)3.907s (-7.8% 🟢)0.466s161.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.023s (+27.2% 🔺)7.515s (+30.8% 🔺)0.000s (-100.0% 🟢)8.032s (+28.4% 🔺)2.009s81.00x
▲ VercelNext.js (Turbopack)6.120s (+32.1% 🔺)7.138s (+18.6% 🔺)0.000s (+25.0% 🔺)7.761s (+18.0% 🔺)1.641s81.02x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNext.js (Turbopack)13/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres16/21
Nitro🐘 Postgres15/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)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@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 diff in depth — runtime span wiring, the builders codegen across all three generated route templates (workflow-only bundleFinal, combined, and watch-mode combined) plus the sveltekit/astro regex post-processing, the new semantic conventions, and the tests.

The instrumentation is clean, well-scoped, and well-tested. The span-hierarchy change (workflow.execute now nests under workflow.route.flow) is correctly reflected in the linked-mode tests and doesn't regress continuous mode (it still parents to the run-origin via withTraceContext). All three codegen templates declare workflowRouteModuleBodyStartedAt and reference it via the new options, and the sveltekit/astro regex ([^)]* capture) still matches the variable-reference options. CI E2E is fully green.

Main suggestion (re: the turbo-mode question): there is no dedicated turbo span attribute — turbo is only inferable from the workflow.run_started.skip_preloadevent. Recommend adding a workflow.turbo tag on workflow.execute so runs/invocations are trivially filterable by turbo mode (details inline). Everything else is minor/optional; nothing blocking.

skipPreload: boolean
) => {
span?.addEvent('workflow.run_started.create.start', {
'workflow.run_started.skip_preload': skipPreload,

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.

Turbo mode is only observable indirectly here. This workflow.run_started.skip_preload event attribute is currently the only turbo signal on the trace, and it's an awkward proxy for "is this run in turbo mode?":

  • It's a span event attribute, not a span tag, so it's harder to facet/filter on in most backends (Datadog, etc.).
  • The name skip_preload doesn't say "turbo" — someone asking the turbo question wouldn't know to look here.
  • The event is only emitted when run_started.create runs (first delivery). On redeliveries/replays (attempt > 1) there's no event at all, so those invocations carry no turbo signal.

turbo is already computed once per invocation at line 535 (and since turbo requires runInput, this skipPreload=true branch is exactly turbo). Consider also setting a dedicated tag on the workflow.execute span — e.g. right after the turbo constant is computed, where span is in scope:

span?.setAttributes(Attribute.WorkflowTurbo(turbo));

backed by a new export const WorkflowTurbo = SemanticConvention<boolean>('workflow.turbo'). That puts a boolean turbo tag on every workflow.execute span (true on the turbo first delivery, false on replays — which is the precise per-invocation distinction we want), and keeps the existing skip_preload event as the finer-grained marker.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a dedicated workflow.turbo semantic convention and set it on every workflow.execute span after the per-invocation turbo decision is computed. I kept the existing workflow.run_started.skip_preload event attribute as the lower-level marker for the run_started create call.

),
...(routeModuleBodyInitMs === undefined
? {}
: Attribute.WorkflowRouteModuleBodyInitMs(routeModuleBodyInitMs)),

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.

routeModuleBodyInitMs is a process-level constant (computed once at entrypoint construction) but emitted on everyworkflow.route.flow span, so it's identical across all requests from a given instance. Fine for queryability — just noting it isn't per-request.

Also worth setting expectations on what it measures: the marker is captured at the top of the generated module body, i.e. afterimport { workflowEntrypoint } from 'workflow/runtime' has fully evaluated. So it won't capture the @workflow/core import/eval cost (the likely dominant cold-start contributor) and will typically read ~0 in the workflow-only template, where the only body work between the marker and the workflowEntrypoint() call is a string-literal assignment. The PR description already acknowledges this; flagging only so the attribute's usefulness isn't over-read.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the PR description to make this explicit: workflow.route.module_body_init_ms is computed once per route entrypoint, emitted on each request from that in-memory entrypoint, and starts after static ESM imports have already evaluated. No code change here since the behavior is intentional.

return cachedHandler(req);
invocationCount += 1;
const handlerCached = cachedHandler !== undefined;
const spanKind = await getSpanKind('SERVER');

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.

Micro-nit (optional, fine to leave): getSpanKind('SERVER') plus the attributes object are now built on every flow request — including the warm path and when no OTEL SDK is registered — whereas previously the warm path was just return cachedHandler(req). The cost is negligible (awaiting an already-memoized once() promise; trace() awaits the same lazy values anyway), so no action needed; just calling it out since this route is perf-sensitive.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Acknowledged. I left this as-is: the added warm-path work remains a memoized span-kind lookup plus attribute construction, and trace() already hits the same memoized OTel handles. Keeping the route-level span always available seems worth that small overhead for this incident path.

},
async (span) => {
if (!cachedHandler) {
cachedHandler = await trace('workflow.route.init', async () => {

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.

Pre-existing behavior (not introduced by this PR), but now more visible in the spans: two concurrent requests on a cold instance can both observe cachedHandler === undefined and each run getWorldHandlers() + handler(), producing two workflow.route.init spans for a single instance (and handler_cached=false on both). Harmless — handlers are stateless and it's effectively last-write-wins — and the new test covers the sequential case. Not a blocker; just noting that under concurrency the span counts can briefly exceed the "once per warm instance" framing in the PR description.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good call. I updated the PR description to stop implying a strict once-per-instance invariant: it now says workflow.route.init is emitted when the cached handler is created, and that concurrent cold requests can briefly produce more than one init span because the pre-existing cache assignment is last-write-wins.


if (options?.routeModuleBodyStartedAt) {
fields.push(
`routeModuleBodyStartedAt: ${options.routeModuleBodyStartedAt}`

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.

routeModuleBodyStartedAt is interpolated raw here (unlike namespace, which is JSON.stringify'd) because it must emit a code identifier reference, not a string literal — correct by design, and the sole caller passes a hardcoded 'workflowRouteModuleBodyStartedAt'. Since this makes the param a code-injection surface if it's ever called with untrusted input, consider a one-line doc comment on the option noting the value must be a code identifier, not data.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a doc comment on routeModuleBodyStartedAt noting that it is emitted as a raw code identifier/expression in generated route files, not serialized data.

);

/** Route pattern for the request */
export const HttpRoute = SemanticConvention<string>('http.route');

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.

Tiny organizational nit: http.route is defined here in the "Route attributes" block, while the other standard OTEL Http* conventions (HttpRequestMethod, HttpResponseStatusCode, UrlFull, …) are grouped together around lines 286–309. Consider moving it next to them for discoverability.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: moved HttpRoute down next to the other standard HTTP semantic convention helpers and left the workflow-specific route attributes grouped separately.

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

Approving. The workflow.turbo attribute (the main ask from the review) plus the smaller nits are all addressed in c9b83f3 — I'd independently prepared the same set of changes, so rather than push a duplicate commit (which would have collided on the identical WorkflowTurbo/HttpRoute additions) I verified your version instead.

What's now in:

  • workflow.turbo boolean tag on the workflow.execute span (Attribute.WorkflowTurbo(turbo)), so turbo invocations are trivially filterable — true only on the turbo first delivery, false on replays/redeliveries. Complements the finer-grained workflow.run_started.skip_preload event.
  • http.route convention regrouped with the other standard OTEL Http* conventions.
  • Doc comment on createWorkflowEntrypointOptionsCode's routeModuleBodyStartedAt noting it's emitted raw as a code identifier (not data).
  • workflow.turbo assertion added to the linked-mode test.

Verified locally on c9b83f3 (built deps from source): @workflow/core + @workflow/builders typecheck clean, runtime-trace-mode.test.ts (11) and constants.test.ts (7) green. CI E2E was already fully green.

The earlier observations I left (module_body_init_ms being a per-process constant emitted on every span; tiny warm-path overhead; pre-existing concurrent cold-start init) are all non-blocking — fine as-is.

@pranaygp
pranaygp merged commit 2bf5257 into mainJun 23, 2026
108 of 110 checks passed
@pranaygp
pranaygp deleted the kk/otel-flow-module-loading branch June 23, 2026 21:28
@github-actionsgithub-actionsBot mentioned this pull request Jun 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2bf5257 (AI decision).

The core instrumentation in this PR builds directly on main-only runtime architecture: stable's workflowEntrypoint creates the queue handler eagerly and synchronously at module load (no lazy cachedHandler/getWorldHandlers() caching, no getSpanKind), so the new workflow.route.flow/workflow.route.init/workflow.route.get_world_handlers spans have nothing to wrap there. The workflow.turbo attribute and workflow.run_started.skip_preload event instrument the turbo first-delivery path, which does not exist on stable, and the modified runtime-trace-mode.test.ts is also absent on stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2bf5257f97fc4fea036717a7882dfd39bf2b3804

pranaygp added a commit that referenced this pull request Jun 23, 2026
…testing
* origin/main:
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
[web-shared] reskin json viewer (no duplicates, better colours and navigation) (#2434)
Send occurredAt with workflow events (#2580)
docs: use actual eve logo and tidy OSS nav dropdown (#2586)
Display occurredAt in trace details (#2581)
fix(next): discover root entrypoints (#2564)
[core] Turbo: skip the unused run_started event-log preload (#2569)
fix(next): prewarm SWC plugin cache (#2538)
[world-vercel] Use v3 stream endpoint (supports transparent reconnect on timeout) (#2424)
[core] Retry stream reopen against the reconnect budget (#2334)
Add Platformatic World to worlds-manifest.json (#1450)
docs: derive section landing-page cards from the page tree + lint drift (#2567)
pranaygp added a commit that referenced this pull request Jun 26, 2026
* origin/main: (46 commits)
[web] Fix HTTP/2 in bundled server build so observability reads work (#2632)
[core] Fix turbo-mode step-body writes racing run_started (#2629)
fix(world-postgres): rename setup command (#2644)
chore: ignore workflow swc caches (#2640)
fix(ai): ignore intentional stream aborts (#2635)
fix(nitro): reload steps during Vite HMR (#2572)
[world] Make @workflow/world own its Zod dependency (#2622)
feat(web-shared): extend cn with custom tailwind-merge class groups (#2619)
[world-vercel] Enable HTTP/2 for the events API and stream writes (#2573)
Remove shadow from off-screen marker indicator button (#2614)
Fix discovery of dotted JS workflow imports (#2594)
Version Packages (beta) (#2597)
Thread occurredAt into web-shared entities (#2608)
Propagate trace context to vercel-workflow.com in workbench instrumentation (#2601)
Reduce workflow build log noise (#2565)
Version Packages (beta) (#2596)
Use text-gray-900 for trace shortcut helper text (#2595)
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
...
# Conflicts:
#	workbench/nextjs-turbopack/instrumentation.ts
#	workbench/nextjs-webpack/instrumentation.ts
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.

2 participants

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

Trace /flow route initialization - #2592

Merged
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading
Jun 23, 2026
Merged

Trace /flow route initialization#2592
pranaygp merged 3 commits into
mainfrom
kk/otel-flow-module-loading

Conversation

@karthikscale3

@karthikscale3karthikscale3 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

This instruments the Workflow SDK /flow route path so we can explain delay before the route makes its first workflow-server run_started POST.

Instrumentation contract

workflow.route.flow

New server span around each generated /flow route request.

Attributes:

  • workflow.route.type: currently flow
  • workflow.route.handler_cached: whether this invocation reused the in-memory route handler
  • workflow.route.invocation_count: per-process invocation count for this route entrypoint
  • workflow.route.entrypoint_age_ms: milliseconds since workflowEntrypoint() constructed this route entrypoint
  • workflow.route.module_body_init_ms: process-level milliseconds from the generated route module-body timestamp to workflowEntrypoint() construction, when generated routes provide the timestamp
  • http.request.method: request method
  • http.route: /.well-known/workflow/v1/flow
  • http.response.status_code: response status when the handler returns a Response

workflow.route.init

New child span under workflow.route.flow, emitted when the route creates the cached queue handler.

Use this to separate first-hit handler construction from normal warm-route request handling. Under concurrent cold requests, more than one request can observe an empty cache and emit workflow.route.init; that is pre-existing harmless last-write-wins behavior.

workflow.route.get_world_handlers

New child span under workflow.route.init around getWorldHandlers().

Use this to identify latency from loading/creating the world handler layer during route setup.

workflow.route.get_world

New child span under workflow.route.flow, emitted for each workflow invocation before workflow.execute.

Use this to identify latency from resolving the per-invocation world before SDK runtime work posts run_started.

workflow.execute <workflowName>

Existing workflow execution span. This PR adds:

Attributes:

  • workflow.turbo: whether this invocation is using the turbo first-delivery path

Span events:

  • workflow.run_started.create.start: emitted immediately before world.events.create(... run_started ...), i.e. right before the first workflow-server run_started POST begins

Event attributes:

  • workflow.run_started.skip_preload: whether the run_started create call is using the turbo skipPreload path

Reading the signal

  • Gap from workflow.route.flow start to workflow.run_started.create.start: SDK/runtime work before the first workflow-server run_started POST begins.
  • Large workflow.route.module_body_init_ms: generated route module body work after static imports, before the SDK route handler is created. This value is computed once per route entrypoint and emitted on every request from that in-memory entrypoint.
  • Near-zero workflow.route.entrypoint_age_ms: the route entrypoint was just created for this request, so this request hit a cold SDK route instance.
  • workflow.route.handler_cached=false plus workflow.route.init: the request paid route handler creation. Sequentially this happens once per warm instance; concurrent cold requests may briefly produce more than one init span.
  • workflow.route.handler_cached=true: route handler was already created; delay is not from handler construction.
  • workflow.turbo=true: this invocation is the turbo first-delivery path. workflow.run_started.skip_preload=true is the finer-grained marker for the associated run_started create call.

Static ESM import evaluation still happens before generated route body code can run, so this does not directly measure the entire import graph. It gives us the earliest SDK-controlled marker in the generated /flow module, plus the exact pre-run_started marker inside the SDK handler.

Testing

  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec vitest run packages/core/src/runtime-trace-mode.test.ts packages/builders/src/constants.test.ts
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/core typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm --filter @workflow/builders typecheck
  • PATH=/Users/karthikkalyanaraman/.nvm/versions/node/v24.13.1/bin:$PATH pnpm exec biome check .changeset/flow-route-init-traces.md packages/builders/src/base-builder.ts packages/builders/src/constants.ts packages/builders/src/constants.test.ts packages/core/src/runtime.ts packages/core/src/runtime-trace-mode.test.ts packages/core/src/telemetry/semantic-conventions.ts (exit 0; reports existing complexity/unused-suppression warnings)
  • Previous full @workflow/core suite under Node 24: all 1322 tests passed, but Vitest exited 1 due an existing pending-fetch shutdown error; packages/core/src/abort-controller-step.test.ts passed by itself.

@vercel

vercelBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9b83f3

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

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

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

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

@github-actions

github-actionsBot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production144112301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7281110788360

❌ Failed Tests

▲ Vercel Production (1 failed)

nuxt (1 failed):

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
❌ nuxt124127
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ 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


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actionsBot commented Jun 23, 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.044s (-5.7% 🟢)1.006s (~)0.962s101.00x
💻 LocalExpress0.046s (-2.1%)1.006s (~)0.959s101.05x
💻 LocalNext.js (Turbopack)0.049s (-6.3% 🟢)1.008s (~)0.959s101.11x
🐘 PostgresNext.js (Turbopack)0.056s (+1.1%)1.011s (~)0.955s101.26x
🐘 PostgresNitro0.061s (-6.0% 🟢)1.013s (~)0.952s101.39x
🐘 PostgresExpress0.072s (+3.9%)1.013s (~)0.940s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.292s (+26.2% 🔺)2.213s (+5.2% 🔺)1.921s101.00x
▲ VercelNext.js (Turbopack)0.314s (+16.1% 🔺)2.353s (+15.1% 🔺)2.039s101.08x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.079s (-0.6%)2.006s (~)0.928s101.00x
💻 LocalExpress1.083s (~)2.007s (~)0.923s101.00x
🐘 PostgresNext.js (Turbopack)1.086s (~)2.011s (~)0.925s101.01x
💻 LocalNext.js (Turbopack)1.086s (~)2.006s (~)0.920s101.01x
🐘 PostgresNitro1.101s (~)2.011s (~)0.911s101.02x
🐘 PostgresExpress1.103s (+1.0%)2.011s (~)0.908s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.507s (+8.9% 🔺)3.416s (+20.7% 🔺)1.909s101.00x
▲ VercelNext.js (Turbopack)1.588s (+10.7% 🔺)3.280s (-2.2%)1.692s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.443s (~)11.022s (~)0.580s31.00x
💻 LocalNitro10.446s (~)11.022s (~)0.576s31.00x
💻 LocalExpress10.463s (~)11.022s (~)0.560s31.00x
🐘 PostgresNext.js (Turbopack)10.471s (~)11.017s (~)0.547s31.00x
🐘 PostgresExpress10.484s (~)11.021s (~)0.537s31.00x
🐘 PostgresNitro10.498s (~)11.021s (~)0.523s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.720s (+3.3%)14.402s (+3.8%)1.682s31.00x
▲ VercelNext.js (Turbopack)12.862s (+9.0% 🔺)14.699s (+5.4% 🔺)1.837s31.01x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.569s (~)14.028s (~)0.459s51.00x
💻 LocalExpress13.602s (~)14.028s (~)0.426s51.00x
🐘 PostgresNitro13.641s (-0.6%)14.020s (~)0.379s51.01x
💻 LocalNext.js (Turbopack)13.681s (+0.7%)14.028s (~)0.347s51.01x
🐘 PostgresExpress13.701s (+0.9%)14.022s (~)0.320s51.01x
🐘 PostgresNext.js (Turbopack)13.721s (~)14.021s (~)0.299s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)18.660s (+7.6% 🔺)20.341s (+6.2% 🔺)1.681s31.00x
▲ VercelExpress19.542s (+18.9% 🔺)21.199s (+17.8% 🔺)1.657s31.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.055s (-1.0%)13.025s (~)0.970s71.00x
💻 LocalExpress12.145s (-1.6%)13.027s (~)0.882s71.01x
🐘 PostgresNitro12.162s (-0.9%)13.020s (~)0.858s71.01x
💻 LocalNext.js (Turbopack)12.189s (-0.5%)13.025s (~)0.836s71.01x
🐘 PostgresNext.js (Turbopack)12.218s (+1.0%)13.023s (~)0.805s71.01x
🐘 PostgresExpress12.276s (+1.4%)13.018s (~)0.742s71.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express24.857s (+35.3% 🔺)26.850s (+33.2% 🔺)1.994s41.00x
▲ VercelNext.js (Turbopack)25.505s (+31.8% 🔺)27.773s (+30.8% 🔺)2.268s41.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.165s (-1.0%)2.008s (~)0.843s151.00x
🐘 PostgresNitro1.194s (+1.0%)2.007s (~)0.813s151.02x
🐘 PostgresExpress1.213s (+3.8%)2.008s (~)0.795s151.04x
💻 LocalNext.js (Turbopack)1.380s (-7.8% 🟢)2.006s (~)0.626s151.18x
💻 LocalExpress1.385s (~)2.006s (~)0.621s151.19x
💻 LocalNitro1.396s (-2.9%)2.006s (~)0.611s151.20x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.793s (+29.9% 🔺)4.385s (+15.7% 🔺)1.592s71.00x
▲ VercelExpress3.109s (+53.2% 🔺)5.068s (+44.8% 🔺)1.959s61.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.301s (-0.9%)3.009s (+3.1%)1.708s101.00x
🐘 PostgresExpress1.343s (-3.6%)2.509s (-6.2% 🟢)1.166s121.03x
🐘 PostgresNitro1.348s (+0.6%)2.510s (~)1.162s121.04x
💻 LocalNitro2.313s (-3.9%)2.736s (-6.2% 🟢)0.423s111.78x
💻 LocalNext.js (Turbopack)2.391s (-12.6% 🟢)3.008s (-3.2%)0.618s101.84x
💻 LocalExpress2.529s (+7.3% 🔺)2.918s (~)0.389s111.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.579s (+7.1% 🔺)5.199s (+3.0%)1.620s61.00x
▲ VercelExpress3.756s (+3.4%)5.354s (+3.7%)1.598s61.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.591s (-3.0%)4.010s (-5.9% 🟢)2.419s81.00x
🐘 PostgresExpress1.637s (+3.2%)4.136s (-3.9%)2.499s81.03x
🐘 PostgresNext.js (Turbopack)2.944s (+4.6%)5.349s (-3.0%)2.406s61.85x
💻 LocalNext.js (Turbopack)4.978s (-9.0% 🟢)5.349s (-11.1% 🟢)0.371s63.13x
💻 LocalNitro5.957s (-7.5% 🟢)6.616s (-8.3% 🟢)0.658s53.74x
💻 LocalExpress6.009s (-5.0%)6.619s (-5.7% 🟢)0.611s53.78x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.831s (+38.8% 🔺)6.612s (+27.1% 🔺)1.781s51.00x
▲ VercelExpress4.852s (+58.3% 🔺)6.870s (+36.2% 🔺)2.018s51.00x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.170s (~)2.007s (~)0.837s151.00x
🐘 PostgresNitro1.178s (-2.4%)2.007s (~)0.829s151.01x
🐘 PostgresExpress1.182s (~)2.008s (~)0.826s151.01x
💻 LocalNitro1.357s (-4.5%)2.006s (~)0.649s151.16x
💻 LocalNext.js (Turbopack)1.399s (~)2.006s (~)0.607s151.20x
💻 LocalExpress1.452s (+1.0%)2.007s (~)0.555s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.794s (-51.7% 🟢)4.333s (-41.8% 🟢)1.539s81.00x
▲ VercelExpress2.871s (+39.1% 🔺)4.642s (+32.7% 🔺)1.772s71.03x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.294s (-5.7% 🟢)2.508s (+1.5%)1.214s121.00x
🐘 PostgresNext.js (Turbopack)1.306s (+1.3%)2.918s (~)1.611s111.01x
🐘 PostgresExpress1.328s (-1.0%)2.222s (-10.1% 🟢)0.894s141.03x
💻 LocalNitro2.318s (-10.2% 🟢)3.008s (-3.2%)0.691s101.79x
💻 LocalNext.js (Turbopack)2.607s (-1.2%)3.108s (+3.3%)0.502s102.01x
💻 LocalExpress2.613s (+7.9% 🔺)3.009s (-3.2%)0.396s102.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.700s (-22.0% 🟢)5.332s (-23.3% 🟢)1.631s61.00x
▲ VercelExpress4.302s (-10.2% 🟢)5.906s (-11.0% 🟢)1.604s61.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.610s (+1.5%)4.011s (-3.0%)2.401s81.00x
🐘 PostgresNitro1.620s (-2.4%)4.137s (+3.1%)2.517s81.01x
🐘 PostgresNext.js (Turbopack)3.012s (+5.4% 🔺)5.848s (~)2.837s61.87x
💻 LocalNext.js (Turbopack)4.998s (-12.6% 🟢)5.683s (-5.5% 🟢)0.685s63.11x
💻 LocalExpress6.217s (-7.2% 🟢)7.016s (-2.8%)0.799s53.86x
💻 LocalNitro6.501s (-4.1%)7.020s (-2.8%)0.518s54.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.961s (-15.5% 🟢)5.681s (-9.8% 🟢)1.720s61.00x
▲ VercelNext.js (Turbopack)4.577s (+46.5% 🔺)6.497s (+25.9% 🔺)1.920s51.16x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.507s (-0.9%)1.006s (~)0.500s601.00x
🐘 PostgresNitro0.538s (-9.6% 🟢)1.006s (-3.4%)0.468s601.06x
🐘 PostgresExpress0.561s (+5.1% 🔺)1.023s (~)0.462s591.11x
💻 LocalNext.js (Turbopack)0.565s (-6.5% 🟢)1.005s (-1.7%)0.440s601.11x
💻 LocalNitro0.580s (-1.0%)1.022s (+1.6%)0.442s591.15x
💻 LocalExpress0.584s (+1.9%)1.005s (~)0.421s601.15x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.239s (+39.5% 🔺)6.064s (+21.4% 🔺)1.825s111.00x
▲ VercelExpress4.460s (+59.4% 🔺)6.406s (+42.8% 🔺)1.946s101.05x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.221s (-1.9%)2.007s (-1.1%)0.786s451.00x
🐘 PostgresNitro1.294s (-8.5% 🟢)2.007s (-2.3%)0.713s451.06x
🐘 PostgresExpress1.379s (+10.3% 🔺)2.030s (+1.1%)0.651s451.13x
💻 LocalNext.js (Turbopack)1.406s (-4.2%)2.005s (~)0.599s451.15x
💻 LocalExpress1.436s (-1.3%)2.006s (~)0.570s451.18x
💻 LocalNitro1.478s (~)2.028s (+1.1%)0.550s451.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.677s (+80.6% 🔺)13.672s (+70.1% 🔺)1.995s71.00x
▲ VercelNext.js (Turbopack)12.418s (+73.8% 🔺)14.184s (+59.5% 🔺)1.766s71.06x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)2.476s (~)3.034s (-0.8%)0.558s401.00x
🐘 PostgresNitro2.595s (-4.4%)3.085s (+0.9%)0.490s391.05x
🐘 PostgresExpress2.846s (+9.7% 🔺)3.194s (+3.5%)0.347s381.15x
💻 LocalNitro3.052s (-5.2% 🟢)3.675s (-8.3% 🟢)0.623s331.23x
💻 LocalNext.js (Turbopack)3.099s (-5.0%)3.675s (-9.1% 🟢)0.577s331.25x
💻 LocalExpress3.179s (-2.1%)3.913s (-1.6%)0.734s311.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express22.472s (+63.0% 🔺)24.735s (+60.0% 🔺)2.262s51.00x
▲ VercelNext.js (Turbopack)26.936s (+64.1% 🔺)28.462s (+54.0% 🔺)1.527s51.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.166s (-0.8%)1.006s (~)0.840s601.00x
🐘 PostgresNitro0.209s (-7.8% 🟢)1.006s (~)0.797s601.26x
🐘 PostgresExpress0.237s (+12.5% 🔺)1.007s (~)0.770s601.42x
💻 LocalNitro0.431s (-7.9% 🟢)1.004s (~)0.573s602.59x
💻 LocalExpress0.455s (+5.5% 🔺)1.005s (~)0.550s602.73x
💻 LocalNext.js (Turbopack)0.564s (-3.5%)1.022s (+1.7%)0.457s593.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.803s (-0.8%)3.477s (-2.0%)1.674s181.00x
▲ VercelExpress1.994s (-3.8%)3.433s (-10.6% 🟢)1.439s181.11x
▲ VercelNitro⚠️missing----

🔍 Observability: Next.js (Turbopack) | Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.253s (-5.5% 🟢)1.006s (~)0.752s901.00x
🐘 PostgresNitro0.326s (-3.3%)1.006s (~)0.680s901.29x
🐘 PostgresExpress0.356s (+10.2% 🔺)1.018s (~)0.662s891.41x
💻 LocalNitro2.085s (-2.3%)2.686s (~)0.602s348.23x
💻 LocalExpress2.121s (-1.8%)2.766s (+1.1%)0.645s338.37x
💻 LocalNext.js (Turbopack)2.599s (-6.8% 🟢)3.113s (-4.5%)0.514s2910.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.670s (+8.4% 🔺)4.342s (+7.1% 🔺)1.672s211.00x
▲ VercelNext.js (Turbopack)3.196s (+47.3% 🔺)5.002s (+27.3% 🔺)1.806s191.20x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.490s (+3.8%)3.343s (+11.1% 🔺)2.853s361.00x
🐘 PostgresNitro0.519s (-1.7%)1.059s (+2.6%)0.540s1141.06x
🐘 PostgresExpress0.574s (+11.3% 🔺)1.118s (+4.7%)0.544s1081.17x
💻 LocalNext.js (Turbopack)9.280s (-5.1% 🟢)10.195s (-5.4% 🟢)0.916s1218.95x
💻 LocalExpress9.668s (-4.2%)10.528s (-5.4% 🟢)0.860s1219.74x
💻 LocalNitro9.701s (-2.2%)10.779s (-2.3%)1.077s1219.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.551s (+48.6% 🔺)5.489s (+35.4% 🔺)1.938s221.00x
▲ VercelNext.js (Turbopack)5.084s (+8.5% 🔺)7.510s (+14.7% 🔺)2.426s161.43x
▲ VercelNitro⚠️missing----

🔍 Observability: Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.138s (-2.7%)1.968s (~)0.010s (-16.1% 🟢)2.018s (~)0.880s101.00x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.000s (~)0.001s (~)2.010s (~)0.863s101.01x
💻 LocalNitro1.155s (~)2.004s (~)0.010s (-14.0% 🟢)2.017s (~)0.862s101.01x
💻 LocalExpress1.159s (+0.6%)2.005s (~)0.012s (+2.5%)2.020s (~)0.860s101.02x
🐘 PostgresExpress1.169s (+1.7%)1.997s (~)0.001s (+10.0% 🔺)2.012s (~)0.842s101.03x
🐘 PostgresNitro1.193s (+2.3%)1.995s (~)0.001s (+9.1% 🔺)2.011s (~)0.817s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.409s (+17.7% 🔺)3.498s (+14.0% 🔺)3.398s (+94.0% 🔺)7.469s (+38.2% 🔺)5.061s101.00x
▲ VercelNext.js (Turbopack)2.673s (+29.4% 🔺)3.825s (+10.9% 🔺)3.340s (+137.0% 🔺)7.691s (+42.3% 🔺)5.018s101.11x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.535s (-4.3%)2.002s (~)0.005s (-1.3%)2.024s (~)0.489s301.00x
💻 LocalExpress1.544s (~)2.010s (~)0.012s (-4.9%)2.026s (~)0.482s301.01x
🐘 PostgresNext.js (Turbopack)1.547s (~)2.009s (~)0.005s (-0.6%)2.026s (~)0.479s301.01x
💻 LocalNext.js (Turbopack)1.555s (-1.4%)1.972s (~)0.013s (+1.0%)2.027s (~)0.472s301.01x
💻 LocalNitro1.566s (+0.8%)2.010s (~)0.013s (+5.0%)2.026s (~)0.460s301.02x
🐘 PostgresExpress1.602s (+3.5%)2.002s (~)0.005s (-7.2% 🟢)2.026s (~)0.424s301.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)6.722s (+13.1% 🔺)8.052s (+9.2% 🔺)0.240s (-24.5% 🟢)8.776s (+6.3% 🔺)2.054s71.00x
▲ VercelExpress7.606s (+29.2% 🔺)9.161s (+27.1% 🔺)0.500s (+131.5% 🔺)10.151s (+28.8% 🔺)2.545s61.13x
▲ VercelNitro⚠️missing-----

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.764s (-7.0% 🟢)1.065s (-3.0%)0.000s (-100.0% 🟢)1.079s (-3.6%)0.314s561.00x
🐘 PostgresExpress0.830s (+7.2% 🔺)1.104s (+5.3% 🔺)0.000s (+Infinity% 🔺)1.119s (+5.2% 🔺)0.289s541.09x
🐘 PostgresNext.js (Turbopack)0.965s (+5.3% 🔺)1.429s (+11.9% 🔺)0.000s (+11.9% 🔺)1.436s (+11.8% 🔺)0.471s421.26x
💻 LocalExpress1.212s (-4.4%)1.981s (-1.6%)0.000s (+45.2% 🔺)1.983s (-1.6%)0.771s311.59x
💻 LocalNitro1.256s (-4.8%)1.982s (~)0.000s (-33.3% 🟢)1.984s (~)0.729s311.64x
💻 LocalNext.js (Turbopack)1.323s (-0.9%)1.981s (~)0.000s (+66.7% 🔺)2.017s (~)0.694s301.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.029s (+3.1%)5.021s (~)0.000s (~)5.476s (-1.1%)1.447s111.00x
▲ VercelNext.js (Turbopack)4.175s (+26.8% 🔺)5.055s (+7.5% 🔺)0.000s (-100.0% 🟢)5.543s (+5.5% 🔺)1.367s111.04x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.783s (+5.2% 🔺)2.435s (+8.0% 🔺)0.000s (+8.0% 🔺)2.451s (+8.0% 🔺)0.668s251.00x
🐘 PostgresNitro1.937s (+1.0%)2.410s (-3.5%)0.000s (-100.0% 🟢)2.450s (-2.9%)0.512s261.09x
🐘 PostgresNext.js (Turbopack)2.470s (+21.6% 🔺)2.907s (+11.3% 🔺)0.000s (-100.0% 🟢)2.914s (+10.3% 🔺)0.444s211.39x
💻 LocalExpress3.357s (-7.6% 🟢)3.904s (-7.7% 🟢)0.001s (+40.6% 🔺)3.909s (-7.6% 🟢)0.551s161.88x
💻 LocalNitro3.367s (-5.6% 🟢)3.965s (-1.5%)0.001s (-22.8% 🟢)3.971s (-1.5%)0.604s161.89x
💻 LocalNext.js (Turbopack)3.441s (-6.1% 🟢)3.868s (-7.7% 🟢)0.001s (-47.3% 🟢)3.907s (-7.8% 🟢)0.466s161.93x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.023s (+27.2% 🔺)7.515s (+30.8% 🔺)0.000s (-100.0% 🟢)8.032s (+28.4% 🔺)2.009s81.00x
▲ VercelNext.js (Turbopack)6.120s (+32.1% 🔺)7.138s (+18.6% 🔺)0.000s (+25.0% 🔺)7.761s (+18.0% 🔺)1.641s81.02x
▲ VercelNitro⚠️missing-----

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNext.js (Turbopack)13/21
▲ VercelExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres14/21
Next.js (Turbopack)🐘 Postgres16/21
Nitro🐘 Postgres15/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)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@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 diff in depth — runtime span wiring, the builders codegen across all three generated route templates (workflow-only bundleFinal, combined, and watch-mode combined) plus the sveltekit/astro regex post-processing, the new semantic conventions, and the tests.

The instrumentation is clean, well-scoped, and well-tested. The span-hierarchy change (workflow.execute now nests under workflow.route.flow) is correctly reflected in the linked-mode tests and doesn't regress continuous mode (it still parents to the run-origin via withTraceContext). All three codegen templates declare workflowRouteModuleBodyStartedAt and reference it via the new options, and the sveltekit/astro regex ([^)]* capture) still matches the variable-reference options. CI E2E is fully green.

Main suggestion (re: the turbo-mode question): there is no dedicated turbo span attribute — turbo is only inferable from the workflow.run_started.skip_preloadevent. Recommend adding a workflow.turbo tag on workflow.execute so runs/invocations are trivially filterable by turbo mode (details inline). Everything else is minor/optional; nothing blocking.

skipPreload: boolean
) => {
span?.addEvent('workflow.run_started.create.start', {
'workflow.run_started.skip_preload': skipPreload,

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.

Turbo mode is only observable indirectly here. This workflow.run_started.skip_preload event attribute is currently the only turbo signal on the trace, and it's an awkward proxy for "is this run in turbo mode?":

  • It's a span event attribute, not a span tag, so it's harder to facet/filter on in most backends (Datadog, etc.).
  • The name skip_preload doesn't say "turbo" — someone asking the turbo question wouldn't know to look here.
  • The event is only emitted when run_started.create runs (first delivery). On redeliveries/replays (attempt > 1) there's no event at all, so those invocations carry no turbo signal.

turbo is already computed once per invocation at line 535 (and since turbo requires runInput, this skipPreload=true branch is exactly turbo). Consider also setting a dedicated tag on the workflow.execute span — e.g. right after the turbo constant is computed, where span is in scope:

span?.setAttributes(Attribute.WorkflowTurbo(turbo));

backed by a new export const WorkflowTurbo = SemanticConvention<boolean>('workflow.turbo'). That puts a boolean turbo tag on every workflow.execute span (true on the turbo first delivery, false on replays — which is the precise per-invocation distinction we want), and keeps the existing skip_preload event as the finer-grained marker.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a dedicated workflow.turbo semantic convention and set it on every workflow.execute span after the per-invocation turbo decision is computed. I kept the existing workflow.run_started.skip_preload event attribute as the lower-level marker for the run_started create call.

),
...(routeModuleBodyInitMs === undefined
? {}
: Attribute.WorkflowRouteModuleBodyInitMs(routeModuleBodyInitMs)),

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.

routeModuleBodyInitMs is a process-level constant (computed once at entrypoint construction) but emitted on everyworkflow.route.flow span, so it's identical across all requests from a given instance. Fine for queryability — just noting it isn't per-request.

Also worth setting expectations on what it measures: the marker is captured at the top of the generated module body, i.e. afterimport { workflowEntrypoint } from 'workflow/runtime' has fully evaluated. So it won't capture the @workflow/core import/eval cost (the likely dominant cold-start contributor) and will typically read ~0 in the workflow-only template, where the only body work between the marker and the workflowEntrypoint() call is a string-literal assignment. The PR description already acknowledges this; flagging only so the attribute's usefulness isn't over-read.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Updated the PR description to make this explicit: workflow.route.module_body_init_ms is computed once per route entrypoint, emitted on each request from that in-memory entrypoint, and starts after static ESM imports have already evaluated. No code change here since the behavior is intentional.

return cachedHandler(req);
invocationCount += 1;
const handlerCached = cachedHandler !== undefined;
const spanKind = await getSpanKind('SERVER');

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.

Micro-nit (optional, fine to leave): getSpanKind('SERVER') plus the attributes object are now built on every flow request — including the warm path and when no OTEL SDK is registered — whereas previously the warm path was just return cachedHandler(req). The cost is negligible (awaiting an already-memoized once() promise; trace() awaits the same lazy values anyway), so no action needed; just calling it out since this route is perf-sensitive.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Acknowledged. I left this as-is: the added warm-path work remains a memoized span-kind lookup plus attribute construction, and trace() already hits the same memoized OTel handles. Keeping the route-level span always available seems worth that small overhead for this incident path.

},
async (span) => {
if (!cachedHandler) {
cachedHandler = await trace('workflow.route.init', async () => {

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.

Pre-existing behavior (not introduced by this PR), but now more visible in the spans: two concurrent requests on a cold instance can both observe cachedHandler === undefined and each run getWorldHandlers() + handler(), producing two workflow.route.init spans for a single instance (and handler_cached=false on both). Harmless — handlers are stateless and it's effectively last-write-wins — and the new test covers the sequential case. Not a blocker; just noting that under concurrency the span counts can briefly exceed the "once per warm instance" framing in the PR description.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good call. I updated the PR description to stop implying a strict once-per-instance invariant: it now says workflow.route.init is emitted when the cached handler is created, and that concurrent cold requests can briefly produce more than one init span because the pre-existing cache assignment is last-write-wins.


if (options?.routeModuleBodyStartedAt) {
fields.push(
`routeModuleBodyStartedAt: ${options.routeModuleBodyStartedAt}`

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.

routeModuleBodyStartedAt is interpolated raw here (unlike namespace, which is JSON.stringify'd) because it must emit a code identifier reference, not a string literal — correct by design, and the sole caller passes a hardcoded 'workflowRouteModuleBodyStartedAt'. Since this makes the param a code-injection surface if it's ever called with untrusted input, consider a one-line doc comment on the option noting the value must be a code identifier, not data.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: added a doc comment on routeModuleBodyStartedAt noting that it is emitted as a raw code identifier/expression in generated route files, not serialized data.

);

/** Route pattern for the request */
export const HttpRoute = SemanticConvention<string>('http.route');

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.

Tiny organizational nit: http.route is defined here in the "Route attributes" block, while the other standard OTEL Http* conventions (HttpRequestMethod, HttpResponseStatusCode, UrlFull, …) are grouped together around lines 286–309. Consider moving it next to them for discoverability.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in c9b83f3: moved HttpRoute down next to the other standard HTTP semantic convention helpers and left the workflow-specific route attributes grouped separately.

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

Approving. The workflow.turbo attribute (the main ask from the review) plus the smaller nits are all addressed in c9b83f3 — I'd independently prepared the same set of changes, so rather than push a duplicate commit (which would have collided on the identical WorkflowTurbo/HttpRoute additions) I verified your version instead.

What's now in:

  • workflow.turbo boolean tag on the workflow.execute span (Attribute.WorkflowTurbo(turbo)), so turbo invocations are trivially filterable — true only on the turbo first delivery, false on replays/redeliveries. Complements the finer-grained workflow.run_started.skip_preload event.
  • http.route convention regrouped with the other standard OTEL Http* conventions.
  • Doc comment on createWorkflowEntrypointOptionsCode's routeModuleBodyStartedAt noting it's emitted raw as a code identifier (not data).
  • workflow.turbo assertion added to the linked-mode test.

Verified locally on c9b83f3 (built deps from source): @workflow/core + @workflow/builders typecheck clean, runtime-trace-mode.test.ts (11) and constants.test.ts (7) green. CI E2E was already fully green.

The earlier observations I left (module_body_init_ms being a per-process constant emitted on every span; tiny warm-path overhead; pre-existing concurrent cold-start init) are all non-blocking — fine as-is.

@pranaygp
pranaygp merged commit 2bf5257 into mainJun 23, 2026
108 of 110 checks passed
@pranaygp
pranaygp deleted the kk/otel-flow-module-loading branch June 23, 2026 21:28
@github-actionsgithub-actionsBot mentioned this pull request Jun 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2bf5257 (AI decision).

The core instrumentation in this PR builds directly on main-only runtime architecture: stable's workflowEntrypoint creates the queue handler eagerly and synchronously at module load (no lazy cachedHandler/getWorldHandlers() caching, no getSpanKind), so the new workflow.route.flow/workflow.route.init/workflow.route.get_world_handlers spans have nothing to wrap there. The workflow.turbo attribute and workflow.run_started.skip_preload event instrument the turbo first-delivery path, which does not exist on stable, and the modified runtime-trace-mode.test.ts is also absent on stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

2bf5257f97fc4fea036717a7882dfd39bf2b3804

pranaygp added a commit that referenced this pull request Jun 23, 2026
…testing
* origin/main:
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
[web-shared] reskin json viewer (no duplicates, better colours and navigation) (#2434)
Send occurredAt with workflow events (#2580)
docs: use actual eve logo and tidy OSS nav dropdown (#2586)
Display occurredAt in trace details (#2581)
fix(next): discover root entrypoints (#2564)
[core] Turbo: skip the unused run_started event-log preload (#2569)
fix(next): prewarm SWC plugin cache (#2538)
[world-vercel] Use v3 stream endpoint (supports transparent reconnect on timeout) (#2424)
[core] Retry stream reopen against the reconnect budget (#2334)
Add Platformatic World to worlds-manifest.json (#1450)
docs: derive section landing-page cards from the page tree + lint drift (#2567)
pranaygp added a commit that referenced this pull request Jun 26, 2026
* origin/main: (46 commits)
[web] Fix HTTP/2 in bundled server build so observability reads work (#2632)
[core] Fix turbo-mode step-body writes racing run_started (#2629)
fix(world-postgres): rename setup command (#2644)
chore: ignore workflow swc caches (#2640)
fix(ai): ignore intentional stream aborts (#2635)
fix(nitro): reload steps during Vite HMR (#2572)
[world] Make @workflow/world own its Zod dependency (#2622)
feat(web-shared): extend cn with custom tailwind-merge class groups (#2619)
[world-vercel] Enable HTTP/2 for the events API and stream writes (#2573)
Remove shadow from off-screen marker indicator button (#2614)
Fix discovery of dotted JS workflow imports (#2594)
Version Packages (beta) (#2597)
Thread occurredAt into web-shared entities (#2608)
Propagate trace context to vercel-workflow.com in workbench instrumentation (#2601)
Reduce workflow build log noise (#2565)
Version Packages (beta) (#2596)
Use text-gray-900 for trace shortcut helper text (#2595)
Trace /flow route initialization (#2592)
Version Packages (beta) (#2591)
feat(web): add trace step shortcut helper (#2582)
...
# Conflicts:
#	workbench/nextjs-turbopack/instrumentation.ts
#	workbench/nextjs-webpack/instrumentation.ts
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.

2 participants

@karthikscale3@pranaygp