Problem
PR #409 detaches dispatched upstream requests from client cancellation and drains their responses through executionCtx.waitUntil(). This preserves terminal usage, dumps, and performance telemetry when the remaining work completes within Cloudflare Workers' post-disconnect grace period.
Cloudflare limits all waitUntil() work for an HTTP invocation to 30 seconds after the response completes or the client disconnects. Promises still pending at that point are canceled. A sufficiently long upstream response can therefore still be truncated, so a stateless Worker cannot provide the absolute invariant that every dispatched request is consumed completely.
An already-dispatched fetch() cannot transfer its connection or ReadableStream ownership to another execution context. Durable ownership must be selected before dispatch.
Required outcome
- Select a client-independent owner before the upstream request is dispatched.
- Keep streaming visible output to the connected client without making that connection the owner of upstream work.
- Consume the complete upstream response after any client disconnect, with no 30-second grace-period dependency.
- Extract and persist terminal usage, dumps, performance data, and any response state exactly once.
- Prevent retries, reconnects, and workflow replay from double-dispatching or double-billing an upstream request.
- Preserve the dispatch-time disconnect gate: if the client is already gone, do not start a new request.
- Cover HTTP streaming and Responses WebSocket transports.
Architecture direction
Use a Workflow as the durable request owner before dispatch. A Workflow step has no wall-time limit and can read the upstream response and persist terminal accounting independently of the client invocation. Deliver live chunks to the frontend through a durable broker/buffer (for example a Durable Object plus persisted replay data). The frontend Worker only subscribes and forwards; it never owns the upstream connection.
A plain Service Binding does not detach lifetime from its caller. A Durable Object alone also needs careful persistence: Cloudflare does not guarantee that a plain outbound fetch() response body keeps a Durable Object alive, and deployments/runtime updates can terminate in-flight work.
References
Problem
PR #409 detaches dispatched upstream requests from client cancellation and drains their responses through
executionCtx.waitUntil(). This preserves terminal usage, dumps, and performance telemetry when the remaining work completes within Cloudflare Workers' post-disconnect grace period.Cloudflare limits all
waitUntil()work for an HTTP invocation to 30 seconds after the response completes or the client disconnects. Promises still pending at that point are canceled. A sufficiently long upstream response can therefore still be truncated, so a stateless Worker cannot provide the absolute invariant that every dispatched request is consumed completely.An already-dispatched
fetch()cannot transfer its connection orReadableStreamownership to another execution context. Durable ownership must be selected before dispatch.Required outcome
Architecture direction
Use a Workflow as the durable request owner before dispatch. A Workflow step has no wall-time limit and can read the upstream response and persist terminal accounting independently of the client invocation. Deliver live chunks to the frontend through a durable broker/buffer (for example a Durable Object plus persisted replay data). The frontend Worker only subscribes and forwards; it never owns the upstream connection.
A plain Service Binding does not detach lifetime from its caller. A Durable Object alone also needs careful persistence: Cloudflare does not guarantee that a plain outbound
fetch()response body keeps a Durable Object alive, and deployments/runtime updates can terminate in-flight work.References
waitUntil()limitwaitUntilsource permalink