Skip to content

Conversation

@TooTallNate
Copy link
Member

@TooTallNate TooTallNate commented Dec 15, 2025

Screenshot 2025-12-15 at 15.26.38.png

Added endpoint health checks to the web UI and improved CORS support for health check endpoints.

What changed?

  • Added CORS headers to endpoint health check responses in the core package
  • Created a new EndpointsHealthStatus component that checks and displays the health of workflow endpoints
  • Integrated the health status component into the web UI toolbar
  • Added session storage caching for health check results
  • Implemented detailed tooltips showing endpoint status and response messages

Why make this change?

This change improves observability by providing immediate visual feedback about the health of critical workflow endpoints directly in the UI. It helps users quickly identify connectivity issues between the frontend and backend services, making troubleshooting easier. The CORS headers enable the health checks to work properly across different origins.

@changeset-bot
Copy link

changeset-bot bot commented Dec 15, 2025

🦋 Changeset detected

Latest commit: 61aefad

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

This PR includes changesets to release 5 packages
Name Type
@workflow/web Patch
@workflow/cli Patch
workflow Patch
@workflow/world-testing Patch
@workflow/ai Patch

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

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

@vercel
Copy link
Contributor

vercel bot commented Dec 15, 2025

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

Project Deployment Review Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Dec 23, 2025 1:21pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Dec 23, 2025 1:21pm
example-workflow Ready Ready Preview, Comment Dec 23, 2025 1:21pm
workbench-astro-workflow Ready Ready Preview, Comment Dec 23, 2025 1:21pm
workbench-express-workflow Ready Ready Preview, Comment Dec 23, 2025 1:21pm
workbench-fastify-workflow Ready Ready Preview, Comment Dec 23, 2025 1:21pm
workbench-hono-workflow Ready Ready Preview, Comment Dec 23, 2025 1:21pm
workbench-nitro-workflow Ready Ready Preview, Comment Dec 23, 2025 1:21pm
workbench-nuxt-workflow Ready Ready Preview, Comment Dec 23, 2025 1:21pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Dec 23, 2025 1:21pm
workbench-vite-workflow Ready Ready Preview, Comment Dec 23, 2025 1:21pm
workflow-docs Ready Ready Preview, Comment Dec 23, 2025 1:21pm

@github-actions
Copy link
Contributor

github-actions bot commented Dec 15, 2025

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 286 0 11 297
✅ 💻 Local Development 262 0 8 270
✅ 📦 Local Production 262 0 8 270
✅ 🐘 Local Postgres 262 0 8 270
❌ 🪟 Windows 0 27 0 27
❌ 🌍 Community Worlds 87 33 0 120
Total 1159 60 35 1254

❌ Failed Tests

🪟 Windows (27 failed)

nextjs-turbopack (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint - workflow and step endpoints respond to __health query parameter
🌍 Community Worlds (33 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter-dev (3 failed):

  • dev e2e should rebuild on step change
  • dev e2e should rebuild on workflow change
  • dev e2e should rebuild on adding workflow file

starter (27 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • webhook route with invalid token
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • retryAttemptCounterWorkflow
  • retryableAndFatalErrorWorkflow
  • stepDirectCallWorkflow - calling step functions directly outside workflow context
  • crossFileErrorWorkflow - stack traces work across imported modules
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check endpoint - workflow and step endpoints respond to __health query parameter

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 26 0 1
✅ example 26 0 1
✅ express 26 0 1
✅ fastify 26 0 1
✅ hono 26 0 1
✅ nextjs-turbopack 26 0 1
✅ nextjs-webpack 26 0 1
✅ nitro 26 0 1
✅ nuxt 26 0 1
✅ sveltekit 26 0 1
✅ vite 26 0 1
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 26 0 1
✅ express-stable 26 0 1
✅ fastify-stable 26 0 1
✅ hono-stable 26 0 1
✅ nextjs-turbopack-stable 27 0 0
✅ nextjs-webpack-stable 27 0 0
✅ nitro-stable 26 0 1
✅ nuxt-stable 26 0 1
✅ sveltekit-stable 26 0 1
✅ vite-stable 26 0 1
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 26 0 1
✅ express-stable 26 0 1
✅ fastify-stable 26 0 1
✅ hono-stable 26 0 1
✅ nextjs-turbopack-stable 27 0 0
✅ nextjs-webpack-stable 27 0 0
✅ nitro-stable 26 0 1
✅ nuxt-stable 26 0 1
✅ sveltekit-stable 26 0 1
✅ vite-stable 26 0 1
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 26 0 1
✅ express-stable 26 0 1
✅ fastify-stable 26 0 1
✅ hono-stable 26 0 1
✅ nextjs-turbopack-stable 27 0 0
✅ nextjs-webpack-stable 27 0 0
✅ nitro-stable 26 0 1
✅ nuxt-stable 26 0 1
✅ sveltekit-stable 26 0 1
✅ vite-stable 26 0 1
❌ 🪟 Windows
App Passed Failed Skipped
❌ nextjs-turbopack 0 27 0
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 0
❌ mongodb 26 1 0
✅ redis-dev 3 0 0
❌ redis 26 1 0
❌ starter-dev 0 3 0
❌ starter 0 27 0
✅ turso-dev 3 0 0
❌ turso 26 1 0

📋 View full workflow run

@github-actions
Copy link
Contributor

github-actions bot commented Dec 15, 2025

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.032s (-28.4% 🟢) 1.007s (~) 0.975s 10 1.00x
💻 Local Next.js (Turbopack) 0.039s (-4.2%) 1.018s (~) 0.979s 10 1.22x
🌐 Redis Next.js (Turbopack) 0.039s (-7.6% 🟢) 1.017s (~) 0.978s 10 1.23x
🌐 Starter Next.js (Turbopack) 0.039s (-3.2%) 1.015s (~) 0.975s 10 1.24x
💻 Local Express 0.046s (+7.3% 🔺) 1.007s (~) 0.961s 10 1.43x
🌐 Turso Next.js (Turbopack) 0.094s (-12.6% 🟢) 1.013s (~) 0.919s 10 2.96x
🌐 MongoDB Next.js (Turbopack) 0.121s (+102.9% 🔺) 1.015s (~) 0.894s 10 3.80x
🐘 Postgres Next.js (Turbopack) 0.303s (+24.0% 🔺) 1.021s (~) 0.717s 10 9.54x
🐘 Postgres Nitro 0.306s (+2.6%) 1.011s (~) 0.705s 10 9.62x
🐘 Postgres Express 0.336s (-11.5% 🟢) 1.014s (~) 0.678s 10 10.56x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.517s (-22.1% 🟢) 1.581s (+2.5%) 1.065s 10 1.00x
▲ Vercel Next.js (Turbopack) 0.518s (-29.5% 🟢) 1.638s (+1.9%) 1.120s 10 1.00x
▲ Vercel Nitro 0.526s (-10.4% 🟢) 1.408s (-0.6%) 0.882s 10 1.02x

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

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.080s (-3.0%) 2.006s (~) 0.926s 10 1.00x
💻 Local Next.js (Turbopack) 1.094s (~) 2.011s (~) 0.917s 10 1.01x
🌐 Starter Next.js (Turbopack) 1.097s (+1.0%) 2.010s (~) 0.913s 10 1.02x
🌐 Redis Next.js (Turbopack) 1.097s (~) 2.012s (~) 0.914s 10 1.02x
💻 Local Express 1.117s (~) 2.008s (~) 0.891s 10 1.03x
🌐 MongoDB Next.js (Turbopack) 1.279s (-2.4%) 2.014s (~) 0.735s 10 1.18x
🌐 Turso Next.js (Turbopack) 1.296s (-1.8%) 2.012s (~) 0.715s 10 1.20x
🐘 Postgres Next.js (Turbopack) 1.864s (+0.9%) 2.015s (~) 0.151s 10 1.73x
🐘 Postgres Nitro 2.163s (+0.7%) 3.014s (~) 0.852s 10 2.00x
🐘 Postgres Express 2.188s (-1.9%) 3.017s (~) 0.829s 10 2.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.603s (+3.3%) 3.567s (-0.9%) 0.964s 10 1.00x
▲ Vercel Express 2.739s (+10.8% 🔺) 3.653s (+6.6% 🔺) 0.915s 10 1.05x
▲ Vercel Next.js (Turbopack) 2.780s (+6.5% 🔺) 3.788s (+3.0%) 1.008s 10 1.07x

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

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 10.533s (-2.4%) 11.012s (~) 0.479s 5 1.00x
🌐 Starter Next.js (Turbopack) 10.598s (~) 11.011s (~) 0.413s 5 1.01x
💻 Local Next.js (Turbopack) 10.643s (~) 11.017s (~) 0.373s 5 1.01x
🌐 Redis Next.js (Turbopack) 10.689s (~) 11.019s (~) 0.329s 5 1.01x
💻 Local Express 10.810s (~) 11.014s (~) 0.204s 5 1.03x
🌐 Turso Next.js (Turbopack) 12.194s (~) 13.026s (~) 0.832s 5 1.16x
🌐 MongoDB Next.js (Turbopack) 12.233s (~) 13.028s (~) 0.795s 5 1.16x
🐘 Postgres Next.js (Turbopack) 15.310s (+0.6%) 16.031s (~) 0.721s 5 1.45x
🐘 Postgres Nitro 20.325s (-0.8%) 21.029s (~) 0.703s 5 1.93x
🐘 Postgres Express 20.387s (+0.9%) 21.036s (~) 0.649s 5 1.94x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 21.169s (+0.7%) 22.161s (+2.1%) 0.992s 5 1.00x
▲ Vercel Next.js (Turbopack) 21.322s (+0.6%) 22.296s (+1.2%) 0.975s 5 1.01x
▲ Vercel Nitro 21.362s (+0.9%) 21.864s (+0.6%) 0.503s 5 1.01x

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

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Starter 🥇 Next.js (Turbopack) 1.347s (+0.9%) 2.008s (~) 0.661s 15 1.00x
💻 Local Nitro 1.355s (-4.4%) 2.006s (~) 0.651s 15 1.01x
🌐 Redis Next.js (Turbopack) 1.356s (~) 2.010s (~) 0.654s 15 1.01x
💻 Local Next.js (Turbopack) 1.380s (-1.0%) 2.012s (~) 0.633s 15 1.02x
💻 Local Express 1.421s (+0.7%) 2.006s (~) 0.586s 15 1.05x
🐘 Postgres Next.js (Turbopack) 1.890s (-3.7%) 2.022s (-6.4% 🟢) 0.132s 15 1.40x
🌐 MongoDB Next.js (Turbopack) 2.114s (-0.7%) 3.017s (~) 0.903s 10 1.57x
🐘 Postgres Nitro 2.114s (-13.3% 🟢) 2.510s (-16.6% 🟢) 0.396s 12 1.57x
🌐 Turso Next.js (Turbopack) 2.204s (-0.9%) 3.013s (~) 0.809s 10 1.64x
🐘 Postgres Express 2.373s (-4.2%) 3.012s (~) 0.639s 10 1.76x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.803s (+8.8% 🔺) 3.766s (+7.0% 🔺) 0.963s 8 1.00x
▲ Vercel Nitro 2.845s (+8.0% 🔺) 3.681s (+0.8%) 0.836s 9 1.02x
▲ Vercel Next.js (Turbopack) 2.926s (+4.3%) 3.840s (+6.3% 🔺) 0.914s 8 1.04x

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

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 2.009s (-9.5% 🟢) 2.410s (-23.9% 🟢) 0.400s 13 1.00x
💻 Local Next.js (Turbopack) 2.179s (-2.2%) 3.131s (-1.4%) 0.951s 10 1.08x
💻 Local Express 2.242s (~) 3.191s (~) 0.949s 10 1.12x
🌐 Redis Next.js (Turbopack) 2.470s (~) 3.012s (~) 0.542s 10 1.23x
🌐 Starter Next.js (Turbopack) 2.488s (+1.1%) 3.009s (~) 0.520s 10 1.24x
🐘 Postgres Next.js (Turbopack) 2.751s (+4.5%) 3.032s (~) 0.281s 10 1.37x
🐘 Postgres Nitro 2.876s (-1.8%) 3.110s (-3.1%) 0.235s 10 1.43x
🐘 Postgres Express 3.095s (+6.1% 🔺) 3.781s (+21.5% 🔺) 0.686s 8 1.54x
🌐 MongoDB Next.js (Turbopack) 4.697s (+1.3%) 5.180s (~) 0.483s 6 2.34x
🌐 Turso Next.js (Turbopack) 4.723s (+1.1%) 5.182s (~) 0.459s 6 2.35x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.977s (-9.0% 🟢) 3.736s (-4.2%) 0.759s 9 1.00x
▲ Vercel Nitro 3.248s (-22.6% 🟢) 4.029s (-17.2% 🟢) 0.781s 8 1.09x
▲ Vercel Next.js (Turbopack) 3.319s (-11.8% 🟢) 3.983s (-11.0% 🟢) 0.664s 8 1.12x

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

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.355s (-3.7%) 2.006s (~) 0.651s 15 1.00x
🌐 Redis Next.js (Turbopack) 1.364s (+1.0%) 2.010s (~) 0.646s 15 1.01x
🌐 Starter Next.js (Turbopack) 1.393s (+2.4%) 2.007s (~) 0.615s 15 1.03x
💻 Local Next.js (Turbopack) 1.405s (~) 2.014s (~) 0.609s 15 1.04x
💻 Local Express 1.416s (-0.7%) 2.006s (~) 0.590s 15 1.04x
🐘 Postgres Next.js (Turbopack) 1.658s (-1.3%) 2.012s (~) 0.354s 15 1.22x
🐘 Postgres Nitro 1.664s (-18.2% 🟢) 2.009s (-29.1% 🟢) 0.345s 15 1.23x
🐘 Postgres Express 1.913s (+18.4% 🔺) 2.011s (~) 0.098s 15 1.41x
🌐 MongoDB Next.js (Turbopack) 2.115s (-2.0%) 3.014s (~) 0.899s 10 1.56x
🌐 Turso Next.js (Turbopack) 2.223s (+0.7%) 3.012s (~) 0.790s 10 1.64x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.632s (+1.1%) 3.781s (+7.7% 🔺) 1.148s 8 1.00x
▲ Vercel Next.js (Turbopack) 2.685s (-3.0%) 3.775s (+3.7%) 1.091s 8 1.02x
▲ Vercel Nitro 2.709s (+5.0%) 3.618s (+3.4%) 0.909s 9 1.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 2.107s (-6.2% 🟢) 3.081s (-3.4%) 0.974s 10 1.00x
💻 Local Express 2.207s (-2.5%) 3.159s (-1.8%) 0.952s 10 1.05x
💻 Local Next.js (Turbopack) 2.286s (+0.8%) 3.201s (+1.1%) 0.915s 10 1.09x
🌐 Redis Next.js (Turbopack) 2.451s (-0.6%) 3.011s (~) 0.560s 10 1.16x
🐘 Postgres Next.js (Turbopack) 2.466s (-6.4% 🟢) 3.018s (~) 0.552s 10 1.17x
🌐 Starter Next.js (Turbopack) 2.471s (-1.5%) 3.009s (~) 0.537s 10 1.17x
🐘 Postgres Nitro 2.639s (-10.7% 🟢) 3.037s (-2.6%) 0.398s 10 1.25x
🐘 Postgres Express 2.772s (-7.1% 🟢) 3.015s (-6.3% 🟢) 0.243s 10 1.32x
🌐 Turso Next.js (Turbopack) 4.645s (-1.9%) 5.180s (~) 0.535s 6 2.20x
🌐 MongoDB Next.js (Turbopack) 4.683s (+0.8%) 5.181s (~) 0.497s 6 2.22x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.004s (+0.9%) 3.805s (+7.8% 🔺) 0.801s 8 1.00x
▲ Vercel Nitro 3.063s (-0.6%) 3.546s (-3.2%) 0.483s 9 1.02x
▲ Vercel Next.js (Turbopack) 3.143s (+2.3%) 3.847s (+4.9%) 0.704s 8 1.05x

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

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.108s (-38.6% 🟢) 0.999s (+0.7%) 0.014s (-13.9% 🟢) 1.020s (~) 0.912s 10 1.00x
🌐 Starter Next.js (Turbopack) 0.127s (+1.8%) 1.005s (~) 0.000s (NaN%) 1.011s (~) 0.884s 10 1.17x
💻 Local Next.js (Turbopack) 0.137s (-1.6%) 1.003s (~) 0.017s (-22.7% 🟢) 1.027s (-0.5%) 0.891s 10 1.27x
🌐 Redis Next.js (Turbopack) 0.139s (-7.5% 🟢) 1.004s (~) 0.000s (+Infinity% 🔺) 1.013s (~) 0.875s 10 1.28x
💻 Local Express 0.182s (+1.3%) 0.994s (~) 0.018s (+1.7%) 1.026s (~) 0.844s 10 1.68x
🌐 Turso Next.js (Turbopack) 0.493s (-6.4% 🟢) 0.956s (+3.3%) 0.000s (-100.0% 🟢) 1.013s (~) 0.521s 10 4.56x
🌐 MongoDB Next.js (Turbopack) 0.500s (-5.3% 🟢) 0.945s (+2.5%) 0.000s (-100.0% 🟢) 1.014s (~) 0.514s 10 4.62x
🐘 Postgres Next.js (Turbopack) 1.272s (+9.1% 🔺) 1.771s (+6.3% 🔺) 0.000s (NaN%) 2.018s (+11.0% 🔺) 0.746s 10 11.77x
🐘 Postgres Nitro 1.399s (-40.0% 🟢) 1.695s (-37.4% 🟢) 0.000s (+Infinity% 🔺) 2.012s (-33.2% 🟢) 0.612s 10 12.94x
🐘 Postgres Express 2.295s (-3.9%) 2.747s (+3.5%) 0.000s (+Infinity% 🔺) 3.016s (~) 0.721s 10 21.23x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.531s (-1.6%) 3.079s (+2.1%) 0.539s (+107.2% 🔺) 4.103s (+11.7% 🔺) 1.572s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.623s (+4.0%) 3.247s (+3.8%) 0.456s (+72.8% 🔺) 4.147s (+9.6% 🔺) 1.524s 10 1.04x
▲ Vercel Nitro 2.713s (+12.2% 🔺) 3.265s (+11.4% 🔺) 0.360s (-9.7% 🟢) 4.123s (+11.1% 🔺) 1.410s 10 1.07x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 8/8
🐘 Postgres Next.js (Turbopack) 8/8
▲ Vercel Express 7/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 💻 Local 8/8
Next.js (Turbopack) 💻 Local 4/8
Nitro 💻 Local 8/8
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
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

Copy link
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);
Copy link
Contributor

Choose a reason for hiding this comment

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

The useEffect hook starts an async health check operation but lacks a cleanup function to handle component unmounting. This can cause React state update warnings if the component unmounts before the async operation completes.

View Details
📝 Patch Details
diff --git a/packages/web/src/components/display-utils/endpoints-health-status.tsx b/packages/web/src/components/display-utils/endpoints-health-status.tsx
index bad42f3..c0b19f2 100644
--- a/packages/web/src/components/display-utils/endpoints-health-status.tsx
+++ b/packages/web/src/components/display-utils/endpoints-health-status.tsx
@@ -58,17 +58,23 @@ function getConfigKey(config: WorldConfig): string {
 
 async function checkEndpointHealth(
   baseUrl: string,
-  endpoint: 'flow' | 'step'
+  endpoint: 'flow' | 'step',
+  signal?: AbortSignal
 ): Promise<{ success: boolean; message: string }> {
   try {
     const url = new URL(
       `/.well-known/workflow/v1/${endpoint}?__health`,
       baseUrl
     );
+    // Combine provided signal with timeout signal
+    const timeoutSignal = AbortSignal.timeout(5000);
+    const combinedSignal = signal
+      ? AbortSignal.any([signal, timeoutSignal])
+      : timeoutSignal;
+
     const response = await fetch(url.toString(), {
       method: 'POST',
-      // Short timeout for health checks
-      signal: AbortSignal.timeout(5000),
+      signal: combinedSignal,
     });
 
     if (response.ok) {
@@ -103,8 +109,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
       return;
     }
 
+    // Track whether the component is still mounted
+    let isMounted = true;
+    const abortController = new AbortController();
+
     // Otherwise, perform the health check
     const performHealthCheck = async () => {
+      if (!isMounted || abortController.signal.aborted) return;
       setIsChecking(true);
 
       // Determine base URL based on config
@@ -112,10 +123,13 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
       const baseUrl = `http://localhost:${port}`;
 
       const [flowResult, stepResult] = await Promise.all([
-        checkEndpointHealth(baseUrl, 'flow'),
-        checkEndpointHealth(baseUrl, 'step'),
+        checkEndpointHealth(baseUrl, 'flow', abortController.signal),
+        checkEndpointHealth(baseUrl, 'step', abortController.signal),
       ]);
 
+      // Only update state if the component is still mounted
+      if (!isMounted || abortController.signal.aborted) return;
+
       const result: HealthCheckResult = {
         flow: flowResult.success ? 'success' : 'error',
         step: stepResult.success ? 'success' : 'error',
@@ -130,6 +144,12 @@ export function EndpointsHealthStatus({ config }: EndpointsHealthStatusProps) {
     };
 
     performHealthCheck();
+
+    // Cleanup function: cancel pending requests and mark component as unmounted
+    return () => {
+      isMounted = false;
+      abortController.abort();
+    };
   }, [config]);
 
   const allSuccess =

Analysis

Missing cleanup function in useEffect allows state updates on unmounted component

What fails: The EndpointsHealthStatus component's useEffect hook (lines 96-133 in packages/web/src/components/display-utils/endpoints-health-status.tsx) initiates async fetch operations without a cleanup function. When the component unmounts before the async operation completes, the subsequent setHealthCheck() and setIsChecking() calls attempt to update state on an unmounted component.

How to reproduce:

  1. Navigate to a page containing the EndpointsHealthStatus component
  2. Immediately navigate away before the health check completes (within 5 seconds)
  3. In development mode with React's StrictMode, observe the warning in browser console

Result: React warning appears: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."

Expected: The component should implement proper cleanup to cancel pending async operations when unmounting, preventing state updates on unmounted components. Per React documentation on removing effect dependencies, effects with async operations should return cleanup functions that abort pending requests.

Fix implemented:

  • Added AbortController within the effect to manage async operation lifecycles
  • Added isMounted flag to track component mount state
  • Updated checkEndpointHealth() function to accept optional AbortSignal parameter
  • Combined component's abort signal with existing timeout signal using AbortSignal.any()
  • Added guard checks before state updates to prevent updates on unmounted components
  • Implemented cleanup function that aborts pending requests and marks component as unmounted

This ensures all pending fetch requests are cancelled when the component unmounts or the effect re-runs, preventing "state update on unmounted component" warnings and memory leaks.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds endpoint health monitoring to the web observability UI by introducing a new health status component that actively checks the workflow and step endpoints and displays their status in the toolbar. The implementation adds CORS support to the health check endpoints in the core runtime to enable cross-origin health checks from the web UI.

  • Added a new EndpointsHealthStatus component that performs health checks on workflow endpoints and caches results in session storage
  • Enhanced the core runtime's health check handler with CORS headers and OPTIONS preflight support
  • Integrated the health status display into the web UI toolbar alongside the existing connection status

Reviewed changes

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

Show a summary per file
File Description
packages/web/src/components/display-utils/endpoints-health-status.tsx New component that checks workflow/step endpoint health, caches results in sessionStorage, and displays status with detailed tooltips
packages/web/src/app/layout-client.tsx Integrates EndpointsHealthStatus component into the toolbar with appropriate spacing
packages/core/src/runtime.ts Adds CORS headers to health check responses and handles OPTIONS preflight requests for cross-origin support
.changeset/twenty-parents-type.md Documents CORS headers addition to core package
.changeset/empty-yaks-follow.md Documents endpoint health check feature addition to web package

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

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

The health check always uses localhost regardless of the backend configuration. When config.backend is 'vercel' or another non-local backend, this will fail to check the actual endpoints being used. The component should derive the appropriate base URL based on the backend type, similar to how other components like ConnectionStatus handle different backends.

Copilot uses AI. Check for mistakes.
* Allows the observability UI to check endpoint health from a different origin.
*/
const HEALTH_CHECK_CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

The CORS configuration uses a wildcard 'Access-Control-Allow-Origin: *' which allows any origin to access the health check endpoints. While this might be acceptable for non-sensitive health checks, it's worth considering whether this should be more restrictive, especially if the endpoints could leak information about the system's state or configuration. Consider documenting the security implications or restricting to known origins.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +56
// Create a unique key based on relevant config values
return `${config.backend || 'local'}-${config.port || '3000'}`;
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

The config key generation only considers 'backend' and 'port' fields, but this may not uniquely identify all possible backend configurations. For example, Vercel backends with different env/project/team combinations or Postgres backends with different database URLs would share the same cache key if they happen to use the same port. This could lead to cached health check results being incorrectly reused across different backend configurations. Consider including all relevant config fields that uniquely identify the backend (e.g., env, project, team, dataDir, postgresUrl).

Suggested change
// Create a unique key based on relevant config values
return `${config.backend || 'local'}-${config.port || '3000'}`;
// Create a unique key based on all relevant config values that uniquely identify the backend
// Include backend, port, and backend-specific fields
const keyObj: Record<string, unknown> = {
backend: config.backend || 'local',
port: config.port || '3000',
};
// Add backend-specific fields
if ('env' in config && config.env) keyObj.env = config.env;
if ('project' in config && config.project) keyObj.project = config.project;
if ('team' in config && config.team) keyObj.team = config.team;
if ('dataDir' in config && config.dataDir) keyObj.dataDir = config.dataDir;
if ('postgresUrl' in config && config.postgresUrl) keyObj.postgresUrl = config.postgresUrl;
// Add any other fields that may uniquely identify the backend as needed
return JSON.stringify(keyObj);

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +133
useEffect(() => {
const configKey = getConfigKey(config);
const cached = getSessionHealthCheck(configKey);

// If we have a cached result from this session, use it
if (cached) {
setHealthCheck(cached);
return;
}

// Otherwise, perform the health check
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
}, [config]);
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

The useEffect hook has 'config' as a dependency, but it references the entire config object. In React, this will trigger the health check every time any field in the config object changes (even if it's a new object reference with the same values). Consider using a more stable dependency like a memoized config key from getConfigKey(config), or add specific config fields that actually affect the health check (backend, port) to the dependency array.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +132
const performHealthCheck = async () => {
setIsChecking(true);

// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;

const [flowResult, stepResult] = await Promise.all([
checkEndpointHealth(baseUrl, 'flow'),
checkEndpointHealth(baseUrl, 'step'),
]);

const result: HealthCheckResult = {
flow: flowResult.success ? 'success' : 'error',
step: stepResult.success ? 'success' : 'error',
flowMessage: flowResult.message,
stepMessage: stepResult.message,
checkedAt: new Date().toISOString(),
};

setHealthCheck(result);
setSessionHealthCheck(configKey, result);
setIsChecking(false);
};

performHealthCheck();
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

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

The useEffect that performs health checks does not handle cleanup when the component unmounts or when the config changes mid-check. If the component unmounts or config changes while async health checks are in progress, the state updates on lines 127-129 will execute on an unmounted component, causing a React warning. Consider using an AbortController to cancel in-flight requests and check if the component is still mounted before calling setState.

Copilot uses AI. Check for mistakes.
@TooTallNate
Copy link
Member Author

@copilot open a new pull request to apply changes based on this feedback, this feedback, this feedback, and this feedback.

Copy link
Contributor

Copilot AI commented Dec 15, 2025

@TooTallNate I've opened a new pull request, #617, to work on those changes. Once the pull request is ready, I'll request review from you.

@pranaygp
Copy link
Collaborator

if the health check fails, we should make sure the component visually stands out and shows a link to a page on the docs that talks about enabling proxy

maybe a new error slug page "routes-not-reachable"


// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

this would fail if the user deployed this observability app on vercel right? could we use an env var like NEXT_PUBLIC_URL or something?

Copy link
Member

Choose a reason for hiding this comment

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

Seems to me like the World/runtime should have a canonical way of getting its public URL, which is partly in the versioning spec, but in the meantime, we should disable this feature for non-local worlds

Copy link
Member Author

Choose a reason for hiding this comment

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

The main motivation for this feature is to diagnose issues with Vercel deployments where workflows are not getting past the start() function (stuck in pending), so we should not disable for non-local worlds.

Copy link
Member

Choose a reason for hiding this comment

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

@TooTallNate sure, but this PR doesn't do that, since it only calls localhost from the web UI, which isn't on the same host as the vercel deployment. This PR would need to be extended to include a conditional for world-vercel that checks for DEPLOYMENT_ID or DEPLOYMENT_URL or whatever we use, and ping against that, and probably would also need to use the vercel auth to bypass the preview environment protection

Copy link
Member

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

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

The idea looks good. Whatever iteration Copilot is doing, please merge it back into this PR so it can be reviewed as a single package.

Then, we can either disable this for world-vercel (initially) and merge, then follow-up with world-vercel code, or fix the world-vercel use here directly and then merge

Copy link
Member

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

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

I also think we can merge the CORS part early - I extracted that to #624 if you want to ship that sooner

@VaguelySerious
Copy link
Member

Merged the CORS changes in #624 so this PR can focus on web changes only

Comment on lines +110 to +112
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Determine base URL based on config
const port = config.port || '3000';
const baseUrl = `http://localhost:${port}`;
// Determine base URL based on backend type
// For local backend: use localhost with configured port
// For deployed backends (vercel, postgres): use current origin
let baseUrl: string;
const backend = config.backend || 'local';
if (backend === 'local') {
const port = config.port || '3000';
baseUrl = `http://localhost:${port}`;
} else {
// For deployed backends, use the current window origin
baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
}

The health check component hard-codes http://localhost: for all backend types, but this will fail for deployed backends (Vercel, Postgres) where the server is not running on localhost.

View Details

Analysis

Health check hardcodes localhost, fails for deployed Vercel/Postgres backends

What fails: EndpointsHealthStatus component constructs health check URLs using hardcoded http://localhost: , which fails when the frontend is deployed to Vercel or a separate deployment. The component needs to check if endpoints at /.well-known/workflow/v1/flow and /.well-known/workflow/v1/step are available, but only attempts to access them on localhost.

How to reproduce:

  1. Deploy the observability UI frontend to Vercel (or any non-localhost domain)
  2. Set backend config to 'vercel' or 'postgres'
  3. Load the UI in a browser
  4. Observe the health status indicator showing "Endpoint issues" even though the endpoints are actually healthy

Result: Browser attempts to fetch from http://localhost:3000/.well-known/workflow/v1/flow which results in connection failures (localhost refers to the user's machine, not the server). The UI always shows "Endpoint issues" in the tooltip.

Expected: For 'vercel' and 'postgres' backends, the health check should use window.location.origin to construct the URL, so it checks endpoints on the current domain where the frontend is deployed. The localhost approach should only be used for the 'local' backend where the frontend actually runs on localhost.

Fix: Modified EndpointsHealthStatus to check the backend config type - if it's 'local', use http://localhost: (original behavior); otherwise, use window.location.origin to access endpoints on the current deployment domain.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants