Skip to content

[world] Re-enqueue active runs on world restart#1534

Merged
VaguelySerious merged 4 commits into
mainfrom
peter/reenqueue-active-runs-on-restart
Mar 30, 2026
Merged

[world] Re-enqueue active runs on world restart#1534
VaguelySerious merged 4 commits into
mainfrom
peter/reenqueue-active-runs-on-restart

Conversation

@VaguelySerious
Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious commented Mar 27, 2026

Summary

  • When a world (local or postgres) is stopped and restarted, runs in pending or running state now get re-enqueued automatically during start()
  • For world-local: scans filesystem storage for active runs and re-enqueues them via the in-process queue
  • For world-postgres: queries the database for active runs and re-enqueues them via graphile-worker, covering edge cases where jobs exhausted retry attempts or were lost during shutdown
  • Safe by design: the workflow handler's event-log replay makes duplicate enqueues idempotent — re-enqueued runs simply resume from where they left off

Closes #1531

Test plan

  • Added reenqueue.test.ts for world-local: verifies pending/running runs are re-enqueued after restart, completed/failed runs are not, and empty data dirs are handled
  • Added reenqueue.test.ts for world-postgres: verifies active runs trigger graphile-worker jobs on start, no-op when no active runs, and pagination works correctly
  • All existing tests pass (254 world-local, 104 world-postgres)

🤖 Generated with Claude Code

When a world (local or postgres) is stopped and restarted, runs that were
pending or running would get stuck because their in-memory queue messages
were lost. On start(), both worlds now scan storage for active runs and
re-enqueue them. The workflow handler's event-log replay makes this
idempotent — duplicate enqueues are safe no-ops.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@VaguelySerious VaguelySerious requested a review from a team as a code owner March 27, 2026 16:49
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 27, 2026

🦋 Changeset detected

Latest commit: dff6564

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

This PR includes changesets to release 20 packages
Name Type
@workflow/world Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/cli Patch
@workflow/core Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/world-testing Patch
@workflow/world-vercel Patch
workflow Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/ai Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt 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
Copy Markdown
Contributor

vercel Bot commented Mar 27, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 27, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 846 0 67 913
✅ 💻 Local Development 818 0 178 996
✅ 📦 Local Production 818 0 178 996
✅ 🐘 Local Postgres 818 0 178 996
✅ 🪟 Windows 75 0 8 83
❌ 🌍 Community Worlds 130 59 24 213
✅ 📋 Other 207 0 42 249
Total 3712 59 675 4446

❌ Failed Tests

🌍 Community Worlds (59 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KN01HAE3DW3DW8Z8XM22HFMD
  • webhookWorkflow | wrun_01KN01HMC32NJ5KTS59740DAER
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KN01T3JSPJPWE4XAZ2JAR755

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KN01HAE3DW3DW8Z8XM22HFMD
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KN01T3JSPJPWE4XAZ2JAR755

turso (54 failed):

  • addTenWorkflow | wrun_01KN01G1KZXCTT68X309SNZENA
  • addTenWorkflow | wrun_01KN01G1KZXCTT68X309SNZENA
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KN01J740REH29M4Y4WV4HR7F
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KN01G9R4PCFSHAZEEG050Z68
  • promiseRaceWorkflow | wrun_01KN01GGS3FW6BCT6V3CR933HA
  • promiseAnyWorkflow | wrun_01KN01GK90AS97F70W1CE2JD7F
  • importedStepOnlyWorkflow | wrun_01KN01JN749CVFV9QCF1NR90QZ
  • hookWorkflow | wrun_01KN01GZRJKAC8GB9E5NNCHK2B
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KN01HAE3DW3DW8Z8XM22HFMD
  • webhookWorkflow | wrun_01KN01HMC32NJ5KTS59740DAER
  • sleepingWorkflow | wrun_01KN01HVAK7X8BE51XVT2D4P3B
  • parallelSleepWorkflow | wrun_01KN01J732ETSQD7AR52D481MF
  • nullByteWorkflow | wrun_01KN01JBYD0D84Y5J1CGZ4NX1Y
  • workflowAndStepMetadataWorkflow | wrun_01KN01JE7YTM9PNRG364N5KKZ2
  • fetchWorkflow | wrun_01KN01NHGF546KDC0WHRGDH4YD
  • promiseRaceStressTestWorkflow | wrun_01KN01NQ1Z3985WMKZN74W55JZ
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • error handling not registered WorkflowNotRegisteredError fails the run when workflow does not exist
  • error handling not registered StepNotRegisteredError fails the step but workflow can catch it
  • error handling not registered StepNotRegisteredError fails the run when not caught in workflow
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KN01SEYM0C9Z0H6FEJMEPQ1J
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KN01T3JSPJPWE4XAZ2JAR755
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KN01TVBQ4QYTTFYGC1ZJ5T2T
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KN01VFWQ34ZRK5Q16H0EKYRF
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KN01VSC63VTNV364493AHE3B
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KN01VZ3QNG1MMQQSCZHR8F66
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KN01W1F1E7H9H2CAPBFBJ2K3
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KN01WHMG5ENFMKQ17P38W9CJ
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KN01WS7FDKNRY1VYSPN17VRN
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KN01X1JKBWTWS1APR2CQVZ35
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KN01X8GC03YB97MC0B71QVJN
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KN01XHEDZ01RSZYQ97S6NSPX
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KN01XRCZYZC9DBKS59ZBKWS3
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KN01XZF334WMTV54HCT8ES1Z
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KN01YC7Y0R7498M1XEZ5HA4Z
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KN01YMM6YYFKRHA12VETYJD8
  • cancelRun - cancelling a running workflow | wrun_01KN01YWXPQQHG71XBK2Q3DVFA
  • cancelRun via CLI - cancelling a running workflow | wrun_01KN01Z6KBK3MEHCX9PA32GNBB
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KN01ZM4SK2XS08597A4PWM5B
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KN020BE23NCF4HTCV1KDPYHP
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KN020P11X76YDXGK2G3PDT3P

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 76 0 7
✅ example 76 0 7
✅ express 76 0 7
✅ fastify 76 0 7
✅ hono 76 0 7
✅ nextjs-turbopack 81 0 2
✅ nextjs-webpack 81 0 2
✅ nitro 76 0 7
✅ nuxt 76 0 7
✅ sveltekit 76 0 7
✅ vite 76 0 7
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 69 0 14
✅ express-stable 69 0 14
✅ fastify-stable 69 0 14
✅ hono-stable 69 0 14
✅ nextjs-turbopack-canary 58 0 25
✅ nextjs-turbopack-stable 75 0 8
✅ nextjs-webpack-canary 58 0 25
✅ nextjs-webpack-stable 75 0 8
✅ nitro-stable 69 0 14
✅ nuxt-stable 69 0 14
✅ sveltekit-stable 69 0 14
✅ vite-stable 69 0 14
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 69 0 14
✅ express-stable 69 0 14
✅ fastify-stable 69 0 14
✅ hono-stable 69 0 14
✅ nextjs-turbopack-canary 58 0 25
✅ nextjs-turbopack-stable 75 0 8
✅ nextjs-webpack-canary 58 0 25
✅ nextjs-webpack-stable 75 0 8
✅ nitro-stable 69 0 14
✅ nuxt-stable 69 0 14
✅ sveltekit-stable 69 0 14
✅ vite-stable 69 0 14
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 69 0 14
✅ express-stable 69 0 14
✅ fastify-stable 69 0 14
✅ hono-stable 69 0 14
✅ nextjs-turbopack-canary 58 0 25
✅ nextjs-turbopack-stable 75 0 8
✅ nextjs-webpack-canary 58 0 25
✅ nextjs-webpack-stable 75 0 8
✅ nitro-stable 69 0 14
✅ nuxt-stable 69 0 14
✅ sveltekit-stable 69 0 14
✅ vite-stable 69 0 14
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 75 0 8
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 5 0 0
❌ mongodb 55 3 8
✅ redis-dev 5 0 0
❌ redis 56 2 8
✅ turso-dev 5 0 0
❌ turso 4 54 8
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 69 0 14
✅ e2e-local-postgres-nest-stable 69 0 14
✅ e2e-local-prod-nest-stable 69 0 14

📋 View full workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 27, 2026

📊 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.046s (-13.5% 🟢) 1.005s (-1.0%) 0.959s 10 1.00x
💻 Local Next.js (Turbopack) 0.049s 1.005s 0.956s 10 1.08x
🐘 Postgres Nitro 0.051s (-21.1% 🟢) 1.012s (~) 0.961s 10 1.11x
🌐 Redis Next.js (Turbopack) 0.054s 1.005s 0.951s 10 1.18x
🐘 Postgres Express 0.057s (-12.4% 🟢) 1.010s (~) 0.953s 10 1.26x
🐘 Postgres Next.js (Turbopack) 0.060s 1.012s 0.951s 10 1.32x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.454s (-42.8% 🟢) 2.497s (-18.6% 🟢) 2.044s 10 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 1.115s 2.006s 0.891s 10 1.00x
🌐 Redis Next.js (Turbopack) 1.123s 2.007s 0.884s 10 1.01x
🐘 Postgres Nitro 1.127s (-2.2%) 2.011s (~) 0.884s 10 1.01x
💻 Local Nitro 1.128s (-0.5%) 2.005s (~) 0.878s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.149s 2.012s 0.863s 10 1.03x
🐘 Postgres Express 1.150s (~) 2.011s (~) 0.861s 10 1.03x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.268s (-2.8%) 3.838s (-13.1% 🟢) 1.570s 10 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 10.642s (-2.4%) 11.021s (~) 0.379s 3 1.00x
🌐 Redis Next.js (Turbopack) 10.793s 11.023s 0.229s 3 1.01x
💻 Local Next.js (Turbopack) 10.822s 11.023s 0.201s 3 1.02x
🐘 Postgres Next.js (Turbopack) 10.891s 11.025s 0.134s 3 1.02x
💻 Local Nitro 10.905s (~) 11.024s (~) 0.119s 3 1.02x
🐘 Postgres Express 10.942s (+0.7%) 11.028s (~) 0.086s 3 1.03x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 20.326s (-8.9% 🟢) 21.867s (-9.7% 🟢) 1.542s 2 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 13.965s (-4.0%) 14.022s (-6.7% 🟢) 0.058s 5 1.00x
🌐 Redis Next.js (Turbopack) 14.273s 15.030s 0.757s 4 1.02x
🐘 Postgres Next.js (Turbopack) 14.531s 15.026s 0.495s 4 1.04x
🐘 Postgres Express 14.585s (+0.5%) 15.032s (~) 0.447s 4 1.04x
💻 Local Next.js (Turbopack) 14.639s 15.031s 0.391s 4 1.05x
💻 Local Nitro 14.931s (~) 15.030s (~) 0.099s 4 1.07x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 46.545s (+37.8% 🔺) 48.637s (+33.9% 🔺) 2.092s 2 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 12.833s (-8.2% 🟢) 13.021s (-9.9% 🟢) 0.188s 7 1.00x
🌐 Redis Next.js (Turbopack) 13.454s 14.026s 0.571s 7 1.05x
🐘 Postgres Next.js (Turbopack) 14.059s 14.739s 0.680s 7 1.10x
🐘 Postgres Express 14.127s (+1.6%) 15.026s (+7.2% 🔺) 0.898s 6 1.10x
💻 Local Next.js (Turbopack) 16.079s 16.699s 0.620s 6 1.25x
💻 Local Nitro 16.312s (~) 17.030s (~) 0.718s 6 1.27x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 56.724s (-2.4%) 58.236s (-3.3%) 1.512s 2 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.198s (-6.0% 🟢) 2.010s (~) 0.812s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.228s 2.011s 0.783s 15 1.03x
🐘 Postgres Express 1.276s (~) 2.012s (~) 0.735s 15 1.07x
🌐 Redis Next.js (Turbopack) 1.323s 2.007s 0.683s 15 1.10x
💻 Local Nitro 1.496s (-1.8%) 2.005s (~) 0.510s 15 1.25x
💻 Local Next.js (Turbopack) 1.537s 2.006s 0.468s 15 1.28x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.742s (+14.4% 🔺) 4.429s (+6.8% 🔺) 1.688s 7 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.260s (-2.6%) 3.010s (~) 0.750s 10 1.00x
🐘 Postgres Express 2.336s (-0.7%) 3.010s (~) 0.674s 10 1.03x
🐘 Postgres Next.js (Turbopack) 2.397s 3.011s 0.614s 10 1.06x
🌐 Redis Next.js (Turbopack) 2.661s 3.109s 0.448s 10 1.18x
💻 Local Nitro 2.812s (-3.4%) 3.009s (-6.2% 🟢) 0.196s 10 1.24x
💻 Local Next.js (Turbopack) 2.857s 3.453s 0.596s 9 1.26x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.879s (-7.1% 🟢) 4.662s (-3.6%) 1.783s 7 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 3.365s (-3.3%) 4.009s (~) 0.644s 8 1.00x
🐘 Postgres Express 3.463s (-1.0%) 4.011s (~) 0.548s 8 1.03x
🐘 Postgres Next.js (Turbopack) 3.698s 4.013s 0.314s 8 1.10x
🌐 Redis Next.js (Turbopack) 4.122s 4.869s 0.747s 7 1.22x
💻 Local Nitro 7.886s (+4.1%) 8.269s (+3.1%) 0.384s 4 2.34x
💻 Local Next.js (Turbopack) 8.073s 8.269s 0.195s 4 2.40x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.251s (-4.9%) 4.671s (-9.2% 🟢) 1.420s 7 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.205s (-4.9%) 2.009s (~) 0.804s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.236s 2.011s 0.775s 15 1.03x
🐘 Postgres Express 1.260s (~) 2.011s (~) 0.751s 15 1.05x
🌐 Redis Next.js (Turbopack) 1.331s 2.007s 0.676s 15 1.10x
💻 Local Nitro 1.522s (~) 2.005s (~) 0.482s 15 1.26x
💻 Local Next.js (Turbopack) 1.675s 2.151s 0.476s 14 1.39x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.679s (+2.8%) 4.335s (-2.4%) 1.656s 7 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.256s (-4.5%) 3.010s (~) 0.754s 10 1.00x
🐘 Postgres Express 2.343s (-0.8%) 3.010s (~) 0.666s 10 1.04x
🐘 Postgres Next.js (Turbopack) 2.393s 3.011s 0.618s 10 1.06x
🌐 Redis Next.js (Turbopack) 2.548s 3.008s 0.460s 10 1.13x
💻 Local Nitro 3.014s (+7.1% 🔺) 3.342s (+4.2%) 0.328s 9 1.34x
💻 Local Next.js (Turbopack) 3.075s 3.886s 0.811s 8 1.36x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.379s (-64.6% 🟢) 3.643s (-57.1% 🟢) 1.264s 9 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 3.358s (-2.6%) 4.011s (~) 0.653s 8 1.00x
🐘 Postgres Express 3.432s (-0.7%) 4.012s (~) 0.580s 8 1.02x
🐘 Postgres Next.js (Turbopack) 3.642s 4.011s 0.369s 8 1.08x
🌐 Redis Next.js (Turbopack) 4.165s 4.869s 0.704s 7 1.24x
💻 Local Next.js (Turbopack) 8.551s 9.270s 0.720s 4 2.55x
💻 Local Nitro 8.740s (+11.0% 🔺) 9.270s (+8.8% 🔺) 0.530s 4 2.60x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.094s (-38.4% 🟢) 4.618s (-30.8% 🟢) 1.524s 7 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.593s (-27.1% 🟢) 1.007s (~) 0.413s 60 1.00x
🌐 Redis Next.js (Turbopack) 0.704s 1.005s 0.301s 60 1.19x
🐘 Postgres Next.js (Turbopack) 0.767s 1.007s 0.240s 60 1.29x
🐘 Postgres Express 0.847s (+2.5%) 1.007s (~) 0.161s 60 1.43x
💻 Local Next.js (Turbopack) 0.852s 1.004s 0.152s 60 1.44x
💻 Local Nitro 0.952s (-6.2% 🟢) 1.076s (-37.4% 🟢) 0.124s 56 1.60x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 10.526s (-4.3%) 12.638s (-3.3%) 2.112s 6 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.378s (-27.6% 🟢) 2.008s (-1.1%) 0.631s 45 1.00x
🌐 Redis Next.js (Turbopack) 1.707s 2.006s 0.299s 45 1.24x
🐘 Postgres Next.js (Turbopack) 1.918s 2.054s 0.136s 44 1.39x
🐘 Postgres Express 1.935s (-3.2%) 2.077s (-10.3% 🟢) 0.141s 44 1.40x
💻 Local Next.js (Turbopack) 2.759s 3.041s 0.283s 30 2.00x
💻 Local Nitro 2.870s (-5.8% 🟢) 3.007s (-19.4% 🟢) 0.137s 30 2.08x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 34.175s (+1.1%) 36.286s (+1.4%) 2.110s 3 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.822s (-26.6% 🟢) 3.009s (-26.2% 🟢) 0.187s 40 1.00x
🌐 Redis Next.js (Turbopack) 3.469s 4.009s 0.540s 30 1.23x
🐘 Postgres Next.js (Turbopack) 3.881s 4.149s 0.268s 29 1.38x
🐘 Postgres Express 3.962s (-2.4%) 4.298s (-5.9% 🟢) 0.336s 28 1.40x
💻 Local Next.js (Turbopack) 8.869s 9.233s 0.364s 14 3.14x
💻 Local Nitro 8.948s (~) 9.479s (+1.6%) 0.531s 13 3.17x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 110.933s (+22.6% 🔺) 113.493s (+22.2% 🔺) 2.561s 2 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.232s (-19.4% 🟢) 1.007s (~) 0.775s 60 1.00x
🐘 Postgres Next.js (Turbopack) 0.268s 1.008s 0.741s 60 1.15x
🐘 Postgres Express 0.282s (~) 1.008s (~) 0.726s 60 1.21x
🌐 Redis Next.js (Turbopack) 0.398s 1.004s 0.606s 60 1.71x
💻 Local Nitro 0.582s (+3.1%) 1.004s (~) 0.422s 60 2.51x
💻 Local Next.js (Turbopack) 0.609s 1.022s 0.413s 59 2.62x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.854s (+43.0% 🔺) 5.059s (+32.1% 🔺) 2.205s 12 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.380s (-25.3% 🟢) 1.007s (~) 0.626s 90 1.00x
🐘 Postgres Next.js (Turbopack) 0.489s 1.008s 0.519s 90 1.28x
🐘 Postgres Express 0.497s (-1.0%) 1.008s (~) 0.511s 90 1.31x
🌐 Redis Next.js (Turbopack) 1.214s 2.006s 0.792s 45 3.19x
💻 Local Nitro 2.567s (+10.7% 🔺) 3.008s (~) 0.441s 30 6.75x
💻 Local Next.js (Turbopack) 2.685s 3.042s 0.357s 30 7.06x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.141s (+8.9% 🔺) 5.429s (+17.8% 🔺) 2.288s 17 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.561s (-27.7% 🟢) 1.016s (+0.7%) 0.455s 119 1.00x
🐘 Postgres Next.js (Turbopack) 0.765s 1.008s 0.243s 120 1.36x
🐘 Postgres Express 0.773s (-2.6%) 1.008s (~) 0.235s 120 1.38x
🌐 Redis Next.js (Turbopack) 2.842s 3.033s 0.191s 40 5.07x
💻 Local Next.js (Turbopack) 11.046s 11.573s 0.527s 11 19.71x
💻 Local Nitro 11.166s (+7.9% 🔺) 11.845s (+9.1% 🔺) 0.679s 11 19.92x
💻 Local Express ⚠️ missing - - - -

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 10.335s (+20.2% 🔺) 12.232s (+14.0% 🔺) 1.897s 10 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - -
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.147s (-28.6% 🟢) 0.998s (~) 0.001s (+9.1% 🔺) 1.010s (~) 0.863s 10 1.00x
🌐 Redis Next.js (Turbopack) 0.173s 1.000s 0.001s 1.008s 0.835s 10 1.17x
💻 Local Next.js (Turbopack) 0.178s 1.002s 0.012s 1.018s 0.841s 10 1.20x
💻 Local Nitro 0.190s (-8.6% 🟢) 1.004s (~) 0.011s (+12.2% 🔺) 1.017s (~) 0.827s 10 1.29x
🐘 Postgres Next.js (Turbopack) 0.195s 1.000s 0.001s 1.011s 0.816s 10 1.32x
🐘 Postgres Express 0.221s (+8.7% 🔺) 0.994s (~) 0.001s (+8.3% 🔺) 1.011s (~) 0.790s 10 1.50x
💻 Local Express ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.738s (-1.3%) 2.970s (~) 0.354s (+4.2%) 3.876s (-4.7%) 2.138s 10 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.470s (-22.1% 🟢) 1.005s (~) 0.003s (-21.4% 🟢) 1.020s (~) 0.550s 59 1.00x
🌐 Redis Next.js (Turbopack) 0.503s 1.000s 0.003s 1.012s 0.509s 60 1.07x
🐘 Postgres Express 0.614s (~) 1.004s (~) 0.004s (-19.4% 🟢) 1.023s (~) 0.410s 59 1.31x
🐘 Postgres Next.js (Turbopack) 0.628s 1.008s 0.006s 1.025s 0.397s 59 1.34x
💻 Local Next.js (Turbopack) 0.664s 1.009s 0.010s 1.024s 0.360s 59 1.41x
💻 Local Nitro 0.723s (-4.0%) 1.010s (~) 0.010s (-0.9%) 1.024s (~) 0.300s 59 1.54x
💻 Local Express ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.234s (~) 5.457s (-2.8%) 0.439s (+22.1% 🔺) 6.684s (-0.9%) 2.450s 9 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.871s (-9.8% 🟢) 1.010s (-13.2% 🟢) 0.000s (+169.0% 🔺) 1.045s (-12.9% 🟢) 0.174s 58 1.00x
🌐 Redis Next.js (Turbopack) 0.916s 1.034s 0.000s 1.039s 0.123s 58 1.05x
🐘 Postgres Express 0.971s (-1.4%) 1.187s (-4.4%) 0.000s (-5.9% 🟢) 1.205s (-4.3%) 0.235s 51 1.11x
🐘 Postgres Next.js (Turbopack) 0.994s 1.334s 0.000s 1.343s 0.350s 45 1.14x
💻 Local Nitro 1.235s (+1.0%) 2.020s (~) 0.000s (-9.1% 🟢) 2.023s (~) 0.788s 30 1.42x
💻 Local Next.js (Turbopack) 1.295s 2.020s 0.000s 2.024s 0.729s 30 1.49x
💻 Local Express ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.187s (+24.6% 🔺) 5.251s (+18.4% 🔺) 0.000s (-100.0% 🟢) 5.851s (+11.9% 🔺) 1.664s 11 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.628s (-6.9% 🟢) 2.069s (-1.4%) 0.000s (-33.3% 🟢) 2.098s (-0.7%) 0.469s 29 1.00x
🌐 Redis Next.js (Turbopack) 1.639s 2.035s 0.000s 2.040s 0.400s 30 1.01x
🐘 Postgres Express 1.774s (-1.9%) 2.140s (~) 0.000s (+55.4% 🔺) 2.153s (-0.9%) 0.379s 28 1.09x
🐘 Postgres Next.js (Turbopack) 1.858s 2.181s 0.000s 2.189s 0.331s 28 1.14x
💻 Local Nitro 3.466s (+2.0%) 4.033s (+1.6%) 0.000s (-28.9% 🟢) 4.036s (+1.5%) 0.570s 15 2.13x
💻 Local Next.js (Turbopack) 3.693s 4.232s 0.001s 4.237s 0.545s 15 2.27x
💻 Local Express ⚠️ missing - - - - -

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 9.929s (+107.4% 🔺) 11.274s (+89.0% 🔺) 0.007s (+Infinity% 🔺) 11.834s (+75.7% 🔺) 1.905s 6 1.00x
▲ Vercel Next.js (Turbopack) ⚠️ missing - - - - -
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

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

Winner determined by most benchmark wins

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

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Move the shared recovery logic into @workflow/world/src/recovery.ts
so both world-local and world-postgres import it instead of
duplicating the function.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@TooTallNate TooTallNate left a comment

Choose a reason for hiding this comment

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

Good feature — re-enqueueing active runs on restart is the right approach, and the shared reenqueueActiveRuns helper with cursor-based pagination is clean. The idempotency guarantee from event-log replay makes this safe by design.

What I verified:

  • Type signatures line up: Storage['runs'] (with list), Queue['queue'] (enqueue method), ValidQueueName template literal, and QueuePayload union all check out.
  • resolveData: 'none' is the right choice — avoids deserializing input/output blobs when only runId and workflowName are needed.
  • 'cancelled' is correctly excluded — it's a terminal state across all world implementations with no residual work needed.
  • CI is green across all environments (local, postgres, vercel, windows, community worlds).
  • Test coverage is solid: world-local tests exercise pending/running/completed/failed/empty scenarios; world-postgres tests cover active runs, empty, and pagination.

One blocking issue: reenqueueActiveRuns has no error handling — if runs.list() or queue() throws (e.g. corrupt storage, DB connection issue on restart), the entire start() call fails and the world never starts. This is a startup-path function so partial progress is better than total failure. See inline comment.

const page = await runs.list({
status,
resolveData: 'none',
pagination: { cursor },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: If runs.list() or enqueue() throws here (e.g. corrupt run on disk, transient DB error), the entire start() will reject and the world won't come up at all. For a best-effort recovery path this is too fragile — a single bad run shouldn't prevent startup.

Consider wrapping the inner enqueue in a try/catch (log and continue), and optionally wrapping the outer loop too:

for (const run of page.data) {
  try {
    const queueName: ValidQueueName = `__wkf_workflow_${run.workflowName}`;
    await enqueue(queueName, { runId: run.runId });
    reenqueued++;
  } catch (err) {
    console.warn(
      `[${label}] Failed to re-enqueue run ${run.runId}: ${err}`
    );
  }
}

This way a single bad run doesn't prevent the rest from being recovered, and the world still starts.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good call — wrapped individual enqueue calls in try/catch so a single corrupt/bad run logs a warning and the rest still get recovered. Fixed in dff6564.

}

beforeEach(() => {
vi.clearAllMocks();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit (non-blocking): The comment on lines 87-91 is a bit confusing — it acknowledges the mock returns the same runs for both 'pending' and 'running' queries, leading to 4 enqueues, but then only asserts 2 addJob calls by name rather than verifying toHaveBeenCalledTimes(4). The mismatch between the comment's explanation and what's actually asserted could confuse future readers.

Consider either:

  • Making the mock return distinct runs per status so the assertion is 1:1, or
  • Adding expect(workerUtilsMock.addJob).toHaveBeenCalledTimes(4) to match the comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — mockRunsList now takes a { pending, running } map so each status returns distinct runs and the assertion is toHaveBeenCalledTimes(2) matching exactly what we expect.

VaguelySerious and others added 2 commits March 30, 2026 11:49
- Wrap individual enqueue calls in try/catch so a single bad run
  doesn't prevent the world from starting
- Fix postgres test to return distinct runs per status instead of
  the same runs for both queries, making assertions 1:1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@TooTallNate TooTallNate left a comment

Choose a reason for hiding this comment

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

Both issues from my previous review have been addressed in dff6564:

  1. Error handling (was blocking): Individual enqueue calls are now wrapped in try/catch with console.warn — a single bad run no longer prevents the world from starting. The rest of the runs still get re-enqueued.

  2. Test mock clarity (was non-blocking): mockRunsList now takes a runsByStatus map with distinct runs per status, and the test asserts toHaveBeenCalledTimes(2) — the confusing comment about 4-vs-2 enqueues is gone.

CI failures are benchmark timeouts only, not related to this change.

LGTM.

@VaguelySerious VaguelySerious merged commit 329cdb3 into main Mar 30, 2026
101 of 105 checks passed
@VaguelySerious VaguelySerious deleted the peter/reenqueue-active-runs-on-restart branch March 30, 2026 20:48
@ghost ghost mentioned this pull request Mar 30, 2026
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.

Postgres World - Workflows doesn't recover after process crash

2 participants