') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(world-local): queue - bound stalled delivery timeouts by AndrewBarba · Pull Request #3255 · vercel/workflow · GitHub
Skip to content

fix(world-local): queue - bound stalled delivery timeouts - #3255

Merged
karthikscale3 merged 1 commit into
mainfrom
barba/fix-world-local-queue-timeouts
Jul 31, 2026
Merged

fix(world-local): queue - bound stalled delivery timeouts#3255
karthikscale3 merged 1 commit into
mainfrom
barba/fix-world-local-queue-timeouts

Conversation

@AndrewBarba

@AndrewBarbaAndrewBarba commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Description

Two intermittent eve e2e failures timed out with no events while local-world queue deliveries remained in flight:

@workflow/world-local configured undici with headersTimeout: 0 and no body timeout. If a delivery handler accepted a connection and then stopped making progress, the transport never rejected, so the durable message could not be retried. The socket hang up errors seen during teardown were pending deliveries being terminated, not the initiating failure.

This intentionally brings world-local on main in line with the transport resilience policy recently added to world-vercel on stable in #3169: 30-second header and body stall limits, environment overrides, and 0 as an explicit unbounded opt-out. The retry implementation is different by design: world-vercel uses RetryAgent and event-specific POST retries, while world-local already owns a durable delivery loop, so transport timeouts redeliver the same queued message there.

This change bounds response-header and response-body waits at 30 seconds by default, keeps body consumption inside the retry boundary, and redelivers stalled durable messages. Both limits can be overridden with WORKFLOW_LOCAL_HEADERS_TIMEOUT_MS and WORKFLOW_LOCAL_BODY_TIMEOUT_MS; setting either to 0 preserves an explicit unbounded opt-out.

How did you test your changes?

  • pnpm --filter @workflow/world-local build
  • pnpm --filter @workflow/world-local typecheck
  • pnpm --filter @workflow/world-local test (12 files, 498 tests)
  • Added real HTTP socket tests proving header and body stalls are redelivered.
  • pnpm test:docs was attempted, but this fresh checkout cannot complete the required workspace build because Rust/wasm32-unknown-unknown is not installed; CI remains the full docs check.

PR Checklist - Required to merge

  • 📦 Added a patch changeset for @workflow/world-local
  • 🔒 Commit includes DCO sign-off
  • 📝 Pinged @vercel/workflow for review

Signed-off-by: Andrew Barba <barba@hey.com>
@AndrewBarba
AndrewBarba requested review from a team and ijjk as code ownersJuly 31, 2026 14:50
@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewJul 31, 2026 2:53pm
example-nextjs-workflow-webpackReadyReadyPreviewJul 31, 2026 2:53pm
example-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workbench-astro-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workbench-express-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workbench-fastify-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workbench-hono-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workbench-nestjs-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workbench-nitro-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workbench-nuxt-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workbench-sveltekit-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workbench-tanstack-start-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workbench-vite-workflowReadyReadyPreviewJul 31, 2026 2:53pm
workflow-docsReadyReadyPreview, v0Jul 31, 2026 2:53pm
workflow-swc-playgroundReadyReadyPreviewJul 31, 2026 2:53pm
workflow-tarballsReadyReadyPreviewJul 31, 2026 2:53pm
workflow-webReadyReadyPreviewJul 31, 2026 2:53pm

@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: aecf3da

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

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

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

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

@vercel
vercelBottemporarily deployed to Preview – workflow-docs July 31, 2026 14:50 Inactive
@AndrewBarba

Copy link
Copy Markdown
ContributorAuthor

@vercel/workflow ready for review.

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit aecf3da · Fri, 31 Jul 2026 15:11:37 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1239 (+23%) 🔻1300 🔴 (+19%) 🔻1323 🔴 (+20%) 🔻1350 🔴 (-16%) 💚30
TTFSstream251 (-32%) 💚1280 🔴 (+21%) 🔻1295 🔴 (+20%) 🔻1316 🔴 (+18%) 🔻30
TTFShook + stream654 (+31%) 🔻1583 🔴 (+13%)1673 🔴 (+14%)2050 🔴 (+37%) 🔻30
STSO1020 steps (inline)164 (-11%)447 (-9.0%)503 (-9.9%)692 (-8.5%)1016
STSO1020 steps (queue-hop)2521 (+23%) 🔻3373 (+21%) 🔻3373 (+21%) 🔻3373 (+21%) 🔻3
WO1020 steps388322 (-10%)388322 (-10%)388322 (-10%)388322 (-10%)1
SLstream latency101 (+17%) 🔻144 🔴 (-1.4%)159 🔴 (-14%)182 🔴 (-29%) 💚30
SOstream overhead (text)100 (-26%) 💚146 (-71%) 💚169 (-82%) 💚221 (-82%) 💚30
SOstream overhead (structured)108 (-3.6%)163 (-20%) 💚183 (-30%) 💚282 (-36%) 💚30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 423669ms → this run 378663ms (Δ -45006ms, -11%)

 150-200 ms █░░░┃ main 10 this 40 +30
200-250 ms ████████░░░░░░┃ main 63 this 119 +56
250-300 ms ██████████████░░░░░┃ main 108 this 156 +48
300-350 ms █████████████┃███ main 136 this 110 -26
350-400 ms ██████████████████░┃ main 143 this 159 +16
400-450 ms ████████████████████░░░┃ main 161 this 190 +29
450-500 ms ████████████████┃███ main 161 this 131 -30
500-550 ms ███████┃██████ main 111 this 65 -46
550-600 ms ██┃█████ main 63 this 20 -43
600-650 ms ┃███ main 28 this 7 -21
650-700 ms ┃█ main 13 this 9 -4
700-750 ms ┃ main 6 this 3 -3
750-800 ms ┃ main 6 this 6 +0
800-850 ms ┃ main 2 this 0 -2
850-900 ms ┃ main 1 this 0 -1
900-950 ms ┃ main 2 this 0 -2
1100-1150 ms ┃ main 2 this 0 -2
1150-1200 ms ┃ main 0 this 1 +1

1020 steps (queue-hop)

Cumulative STSO time: main 7586ms → this run 8500ms (Δ +914ms, +12%)

2000-2500 ms ┃███████████ main 1 this 0 -1
2500-3000 ms ███████████████████████┃ main 2 this 2 +0
3000-3500 ms ░░░░░░░░░░░┃ main 0 this 1 +1
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the Best column shows the fastest (warm-start) sample for comparison.

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production145502391694
✅ 💻 Local Development162102271848
✅ 📦 Local Production162102271848
✅ 🐘 Local Postgres162102271848
✅ 🪟 Windows15400154
✅ 📋 Other102002121232
✅ vercel-multi-region270027
Total7519011328651
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro126028
✅ example126028
✅ express126028
✅ fastify126028
✅ hono126028
✅ nextjs-turbopack15103
✅ nextjs-webpack15103
✅ nitro126028
✅ nuxt126028
✅ sveltekit14509
✅ vite126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128026
✅ e2e-local-dev-tanstack-start-128026
✅ e2e-local-postgres-nest-stable128026
✅ e2e-local-postgres-tanstack-start-128026
✅ e2e-local-prod-nest-stable128026
✅ e2e-local-prod-tanstack-start-128026
✅ e2e-vercel-prod-nest126028
✅ e2e-vercel-prod-tanstack-start126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@karthikscale3
karthikscale3 merged commit 2677653 into mainJul 31, 2026
107 checks passed
@karthikscale3
karthikscale3 deleted the barba/fix-world-local-queue-timeouts branch July 31, 2026 15:27
@github-actionsgithub-actionsBot mentioned this pull request Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 2677653 (AI decision).

The underlying hang (undici headersTimeout: 0 with no body timeout in packages/world-local/src/queue.ts) does exist on stable, but this fix depends on behavior that only exists on main: the transport-level retry/delivery-counter loop that redelivers the same durable message (the commit edits that try/catch, and stable's createQueue has no such catch at all — a transport throw there is not retried). Applying just the bounded timeouts to stable would convert stalled deliveries into dropped messages instead of redeliveries, and it also changes an existing default from unbounded to 30s on a maintenance line. If the hang is judged to affect stable users, backport the prerequisite transport-retry change together with this one via workflow_dispatch.

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

2677653759aa34c5c9fe28950fe1a02cec294551

pranaygp added a commit that referenced this pull request Jul 31, 2026
…ent-guard
* origin/main:
ci: stop deploying changeset-release/main, run its e2e against production (#3243)
fix(world-local): bound stalled queue deliveries (#3255)
Sort imports in runtime.ts and step-executor.ts (#3241)
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

@AndrewBarba@karthikscale3