Skip to content

perf(core): path-independent stream write batching (group commit in the server writable)#3078

Merged
karthikscale3 merged 6 commits into
mainfrom
kk/writable-batching
Jul 24, 2026
Merged

perf(core): path-independent stream write batching (group commit in the server writable)#3078
karthikscale3 merged 6 commits into
mainfrom
kk/writable-batching

Conversation

@karthikscale3

@karthikscale3 karthikscale3 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Problem

Batching was implemented in flushablePipe's coalescing loop, so it depended on which piping path ran. getWritable() batched; a raw ReadableStream crossing a workflow/step boundary uses native pipeTo(), which doesn't pull chunk N+1 until write(chunk N) resolves — and write() resolved only after the flush timer and the server round trip. The buffer never held more than one chunk: one request per token.

Fix

Batching moves into WorkflowServerWritableStream, identical on every path:

  • write() resolves when the chunk enters a bounded client buffer (WORKFLOW_STREAM_MAX_INFLIGHT_CHUNKS counts buffered and in-request chunks; new byte bound WORKFLOW_STREAM_MAX_BUFFERED_BYTES, default 8 MiB, documented).
  • The flush interval is a real group-commit window; chunks arriving while a request is in flight form the next writeMulti group. One request in flight preserves order; 1,000-chunk / 1 MiB per-request limits preserved.
  • Durability is an explicit barrier: close() drains first; flushablePipe (now a plain pump: lock-release + durability tracking only) adopts the barrier so step completion still implies durability; abort() drains the accepted prefix without closing (a producer error after acked writes can't lose data); a failed pipe drains before settling. Dispatch failures are sticky and surface at the next write/close/drain.

Trade-off

An open getWritable() stream's first chunk now waits up to the group-commit interval (10ms default) instead of dispatching eagerly — watch TTFC / buffer_dwell_ms after release.

Validation

1,572 core unit tests pass, including new regressions for: native-pipeTo batching, awaited per-chunk loops coalescing into one writeMulti, wire-cap splits, in-flight-inclusive backpressure on both bounds, source-error prefix delivery through abort, failed-pipe drain-before-reject, and turbo run-ready barrier gating. E2E tier requires a deployment and was not run here.

Addressed some of the streaming perf concerns for use cases mentioned in #1930

…eam (group commit)

Batching previously lived in flushablePipe's coalescing loop, so it only
engaged on paths that used flushablePipe (getWritable). A raw
ReadableStream crossing a workflow/step boundary is piped with native
pipeTo(), which does not pull chunk N+1 until write(chunk N) resolves —
and write() resolved only after the flush timer AND the server round
trip, so the buffer never held more than one chunk and every token
became its own server request.

The sink now group-commits:
- write() resolves when the chunk enters a bounded client buffer; the
  bound counts buffered AND in-request chunks
  (WORKFLOW_STREAM_MAX_INFLIGHT_CHUNKS, preserving its documented
  meaning) plus a byte bound (WORKFLOW_STREAM_MAX_BUFFERED_BYTES, new,
  default 8 MiB, documented in runtime-tuning). A full buffer applies
  backpressure until a group lands durably.
- The flush interval is a real group-commit window; chunks arriving
  while a request is in flight accumulate and form the next writeMulti
  group. One request in flight at a time preserves chunk order.
- Per-request wire limits (1,000 chunks / 1 MiB) split groups exactly
  as the coalescing pipe did; an oversized single chunk goes alone.
- Durability moved to an explicit barrier (STREAM_DRAIN_SYMBOL):
  close() drains before closing; flushablePipe adopts the barrier so
  lock-release completion (step completion) still means 'everything
  written is durable'; abort() DRAINS the accepted prefix (never
  closing) so a producer error after acked writes cannot lose data —
  native pipeTo aborts the sink on source failure; and a failed pipe
  drains before settling so a step failure is not persisted ahead of
  the emitted prefix. A dispatch failure retains the group, poisons
  the sink, and surfaces at the next write/close/drain.

flushablePipe is now a plain per-chunk pump responsible only for
lock-release completion and durability tracking; its coalescing
machinery and STREAM_WRITE_BATCH_SYMBOL are removed.

Covered: native-pipeTo batching (the regression), awaited per-chunk
loops coalescing into one writeMulti, in-flight accumulation, wire-cap
splits (count/byte/oversized), in-flight-inclusive backpressure for
both bounds, sequential fallback without writeMulti, source-error
prefix delivery through abort, failed-pipe drain-before-reject,
early-ack sticky errors, turbo run-ready barrier gating (incl. dwell
telemetry), drain-barrier adoption/rejection, and group-level flush
spans. 1,572 core unit tests pass; e2e tier requires a deployment and
was not run here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested review from a team and ijjk as code owners July 23, 2026 23:52
@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a57ba7c

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

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

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Jul 24, 2026 5:11am
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jul 24, 2026 5:11am
example-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workbench-astro-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workbench-express-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workbench-fastify-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workbench-hono-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workbench-nestjs-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workbench-nitro-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workbench-nuxt-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workbench-sveltekit-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workbench-vite-workflow Ready Ready Preview, Comment Jul 24, 2026 5:11am
workflow-docs Building Building Preview, Comment, Open in v0 Jul 24, 2026 5:11am
workflow-swc-playground Ready Ready Preview, Comment Jul 24, 2026 5:11am
workflow-tarballs Ready Ready Preview, Comment Jul 24, 2026 5:11am
workflow-web Ready Ready Preview, Comment Jul 24, 2026 5:11am

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit a57ba7c · Fri, 24 Jul 2026 05:30:38 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 291 (-69%) 💚 1419 🔴 (+25%) 🔻 1447 🔴 (+24%) 🔻 1770 🔴 (+31%) 🔻 30
TTFS stream 281 (-72%) 💚 1406 🔴 (+22%) 🔻 1442 🔴 (+23%) 🔻 1613 🔴 (+31%) 🔻 30
TTFS hook + stream 497 (+18%) 🔻 1645 🔴 (+14%) 1662 🔴 (+14%) 1843 🔴 (+16%) 🔻 30
STSO 1020 steps (1-20) 166 (-10%) 267 🔴 (-27%) 💚 319 🔴 (-33%) 💚 331 🔴 (-52%) 💚 19
STSO 1020 steps (101-120) 200 (-18%) 💚 279 🔴 (-30%) 💚 306 🔴 (-61%) 💚 330 🔴 (-66%) 💚 19
STSO 1020 steps (1001-1020) 490 (-0.8%) 568 🔴 (-2.7%) 714 🔴 (+14%) 881 🔴 (+27%) 🔻 19
WO 1020 steps 407715 (-8.3%) 407715 (-8.3%) 407715 (-8.3%) 407715 (-8.3%) 1
SL stream latency 98 (-2.0%) 178 🔴 (+23%) 🔻 230 🔴 (+43%) 🔻 253 🔴 (+29%) 🔻 30
SO stream overhead (text) 124 (-13%) 222 (-18%) 💚 403 (+24%) 🔻 530 (+5.8%) 30
SO stream overhead (structured) 115 (-9.4%) 215 (-15%) 💚 263 (-13%) 820 (+90%) 🔻 30
📜 Previous results (4)

5f57e09

Fri, 24 Jul 2026 05:05:30 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1201 (+29%) 🔻 1272 🔴 (+12%) 1297 🔴 (+11%) 1590 🔴 (+17%) 🔻 30
TTFS stream 1207 (+18%) 🔻 1265 🔴 (+9.7%) 1278 🔴 (+9.4%) 1337 🔴 (+8.6%) 30
TTFS hook + stream 527 (+25%) 🔻 1540 🔴 (+7.2%) 1687 🔴 (+16%) 🔻 1837 🔴 (+16%) 🔻 30
STSO 1020 steps (1-20) 167 (-9.7%) 270 🔴 (-26%) 💚 318 🔴 (-33%) 💚 494 🔴 (-28%) 💚 19
STSO 1020 steps (101-120) 183 (-25%) 💚 239 🔴 (-40%) 💚 316 🔴 (-60%) 💚 644 🔴 (-33%) 💚 19
STSO 1020 steps (1001-1020) 451 (-8.7%) 535 🔴 (-8.4%) 821 🔴 (+31%) 🔻 3616 🔴 (+423%) 🔻 19
WO 1020 steps 374593 (-16%) 💚 374593 (-16%) 💚 374593 (-16%) 💚 374593 (-16%) 💚 1
SL stream latency 84 (-16%) 💚 125 🔴 (-14%) 161 🔴 (±0%) 218 🔴 (+11%) 30
SO stream overhead (text) 98 (-31%) 💚 141 (-48%) 💚 182 (-44%) 💚 277 (-45%) 💚 30
SO stream overhead (structured) 101 (-20%) 💚 148 (-42%) 💚 174 (-42%) 💚 210 (-51%) 💚 30

b5718b0

Fri, 24 Jul 2026 01:05:26 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1286 (+34%) 🔻 1373 🔴 (+28%) 🔻 1393 🔴 (+23%) 🔻 1449 🔴 (+1.7%) 30
TTFS stream 1273 (+32%) 🔻 1360 🔴 (+30%) 🔻 1373 🔴 (+29%) 🔻 1544 🔴 (+37%) 🔻 30
TTFS hook + stream 1456 (+20%) 🔻 1583 🔴 (+22%) 🔻 1621 🔴 (+20%) 🔻 1683 🔴 (+19%) 🔻 30
STSO 1020 steps (1-20) 162 (-6.9%) 256 🔴 (+3.6%) 305 🔴 (+9.3%) 380 🔴 (+10%) 19
STSO 1020 steps (101-120) 188 (-0.5%) 256 🔴 (-7.2%) 318 🔴 (-3.6%) 320 🔴 (-15%) 💚 19
STSO 1020 steps (1001-1020) 500 (+9.4%) 583 🔴 (+13%) 658 🔴 (+24%) 🔻 787 🔴 (+27%) 🔻 19
WO 1020 steps 395092 (+2.2%) 395092 (+2.2%) 395092 (+2.2%) 395092 (+2.2%) 1
SL stream latency 86 (+7.5%) 148 🔴 (+12%) 169 🔴 (-5.6%) 371 🔴 (-1.1%) 30
SO stream overhead 111 (+2.8%) 165 (-13%) 207 (-5.0%) 320 (+25%) 🔻 30

7e5ad94

Fri, 24 Jul 2026 00:29:08 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1249 (+30%) 🔻 1330 🔴 (+24%) 🔻 1343 🔴 (+19%) 🔻 1380 🔴 (-3.2%) 30
TTFS stream 333 (-66%) 💚 1327 🔴 (+27%) 🔻 1345 🔴 (+26%) 🔻 1437 🔴 (+27%) 🔻 30
TTFS hook + stream 1454 (+20%) 🔻 1691 🔴 (+30%) 🔻 1777 🔴 (+31%) 🔻 2084 🔴 (+47%) 🔻 30
STSO 1020 steps (1-20) 183 (+5.2%) 278 🔴 (+13%) 318 🔴 (+14%) 387 🔴 (+13%) 19
STSO 1020 steps (101-120) 188 (-0.5%) 239 🔴 (-13%) 257 🔴 (-22%) 💚 279 🔴 (-26%) 💚 19
STSO 1020 steps (1001-1020) 480 (+5.0%) 621 🔴 (+21%) 🔻 745 🔴 (+40%) 🔻 776 🔴 (+25%) 🔻 19
WO 1020 steps 395743 (+2.4%) 395743 (+2.4%) 395743 (+2.4%) 395743 (+2.4%) 1
SL stream latency 117 (+46%) 🔻 175 🔴 (+33%) 🔻 204 🔴 (+14%) 419 🔴 (+12%) 30

9792b58

Fri, 24 Jul 2026 00:09:07 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1281 (+267%) 🔻 1362 🔴 (+34%) 🔻 1381 🔴 (+33%) 🔻 1479 🔴 (+6.5%) 30
TTFS stream 1265 (+38%) 🔻 1321 🔴 (+26%) 🔻 1369 🔴 (+29%) 🔻 1447 🔴 (±0%) 30
TTFS hook + stream 1597 (+41%) 🔻 1677 🔴 (+26%) 🔻 1816 🔴 (+28%) 🔻 1873 🔴 (+24%) 🔻 30
STSO 1020 steps (1-20) 175 (-0.6%) 260 🔴 (-6.1%) 313 🔴 (-30%) 💚 341 🔴 (-24%) 💚 19
STSO 1020 steps (101-120) 179 (-4.8%) 273 🔴 (-1.1%) 521 🔴 (+61%) 🔻 572 🔴 (+71%) 🔻 19
STSO 1020 steps (1001-1020) 530 (+17%) 🔻 616 🔴 (+8.5%) 647 🔴 (-6.5%) 762 🔴 (-8.7%) 19
WO 1020 steps 419068 (+6.7%) 419068 (+6.7%) 419068 (+6.7%) 419068 (+6.7%) 1
SL stream latency 120 (+50%) 🔻 174 🔴 (+38%) 🔻 185 🔴 (+27%) 🔻 224 🔴 (-47%) 💚 30
ℹ️ Metric definitions & methodology

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 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

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-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

📦 Local Production (1 failed)

nextjs-turbopack-stable (1 failed):

  • webhookWorkflow | wrun_41KY98HTJK0GKYSP0S315E2QCH

E2E Test Summary

Summary
Passed Failed Skipped Total
✅ ▲ Vercel Production 1455 0 239 1694
✅ 💻 Local Development 1621 0 227 1848
❌ 📦 Local Production 1620 1 227 1848
✅ 🐘 Local Postgres 1621 0 227 1848
✅ 🪟 Windows 154 0 0 154
✅ 📋 Other 1020 0 212 1232
✅ vercel-multi-region 27 0 0 27
Total 7518 1 1132 8651
Details by Category

✅ ▲ Vercel Production

App Passed Failed Skipped
✅ astro 126 0 28
✅ example 126 0 28
✅ express 126 0 28
✅ fastify 126 0 28
✅ hono 126 0 28
✅ nextjs-turbopack 151 0 3
✅ nextjs-webpack 151 0 3
✅ nitro 126 0 28
✅ nuxt 126 0 28
✅ sveltekit 145 0 9
✅ vite 126 0 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

❌ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
❌ nextjs-turbopack-stable 153 1 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack 154 0 0

✅ 📋 Other

App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 26
✅ e2e-local-dev-tanstack-start- 128 0 26
✅ e2e-local-postgres-nest-stable 128 0 26
✅ e2e-local-postgres-tanstack-start- 128 0 26
✅ e2e-local-prod-nest-stable 128 0 26
✅ e2e-local-prod-tanstack-start- 128 0 26
✅ e2e-vercel-prod-nest 126 0 28
✅ e2e-vercel-prod-tanstack-start 126 0 28

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

Comment thread packages/core/src/serialization.ts
Review (bot): a write landing between the dispatch loop's empty-buffer
exit and the reaction clearing the in-flight marker armed no timer
(scheduleGroupCommit saw the marker set) and was never dispatched on an
open stream — only a later write/close/drain would pick it up. The
settle reaction now re-dispatches when the buffer is non-empty, treating
the chunk as an in-request arrival; drain waiters settle with the new
chain. Regression test aims a write at the settle gap and asserts both
chunks flush without a close.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@VaguelySerious VaguelySerious left a comment

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.

AI review: no blocking issues

},
abort(reason) {
// Clean up timer to prevent leaks
async abort(reason) {

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.

AI Review: Note

abort() changes from "discard the buffer" to await drain() — it now delivers the accepted prefix over the network and only skips the server-side close. This is the correct choice given the early-ack contract (those chunks' write() already resolved to the producer), but it's a networked cancel/abort path, and world-local's synchronous cancel() masks exactly this class of behavior — it only exercises on the world-vercel prod E2E tier, which the PR notes was not run. Worth validating both abort triggers there before merge: source-error-during-pipeTo (deliver prefix) and explicit cancellation/teardown (where drain() may writeMulti to an already-terminal run and swallow the resulting conflict).

@karthikscale3 karthikscale3 Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

On validation: all 13 E2E Vercel Prod lanes pass on this PR, so the networked write/close/cancel paths are exercised against the real world-vercel transport. On the two specific triggers: (1) source-error-during-pipeTo prefix delivery is pinned by a unit regression at the sink layer (mock world, asserts delivery + no close); (2) teardown-drain into a terminal run rejects into abort's existing catch — swallowed by design (best-effort delivery on a stream being torn down), now noted in the abort comment (e4c8a82). Happy to add a dedicated e2e for the source-error trigger as a follow-up if you want it pinned at that tier too.

// skipped. A dispatch failure during this drain is already sticky
// and surfaces through the sink's error paths.
try {
await drain();

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.

AI Review: Note

Unlike the step-executor's inline flush (bounded at 500ms with a waitUntil fallback), this drain() inside abort() has no timeout. When abort is driven by pipeTo on a source error, a stalled world.streams.write/writeMulti would stall the abort with no upper bound. Low likelihood, but a bound (or a comment noting the reliance on the transport's own timeout) would make the teardown path's worst case explicit.

@karthikscale3 karthikscale3 Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documented in e4c8a82: the drain is deliberately un-timeboxed — a bound that gives up would drop acked chunks, the exact loss this path exists to prevent — and the comment now states the worst case explicitly: the World transport's finite timeout/retry budget terminates a stalled write with a rejection, which lands in the catch and completes the abort.

…t handling

Review note: the abort-path drain is deliberately un-timeboxed (a bound
would drop acked chunks); its worst case is owned by the World
transport's finite timeout/retry budget, and a teardown-driven drain
into an already-terminal run rejects into the existing catch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
karthikscale3 and others added 2 commits July 23, 2026 21:44
The native-pipeTo batching test flaked on a slow CI runner: a fixed
25ms wait raced the 10ms commit window plus scheduler jitter. All
'dispatch has happened' assertions now poll the expectation (bounded);
intentional negatives keep their fixed windows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for b610c46 (AI decision).

This rework builds directly on main-only streaming infrastructure: stable's @workflow/core has no world.streams.writeMulti API, no STREAM_WRITE_BATCH_SYMBOL/coalescing pipe, and none of the WORKFLOW_STREAM_MAX_* tuning knobs or flush telemetry this patch refactors (verified via git grep on origin/stable). The docs change also targets docs/content/docs/v5/configuration/runtime-tuning.mdx, which does not exist on stable. The diff would not apply meaningfully to stable's much older per-write streaming implementation.

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

b610c46f8143afe3c862fb9957f5b4b48e754b42

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.

2 participants