Show notes
Why AI streaming needs backpressure#
Streaming makes responses feel immediate, but it creates a flow-control problem. The provider emits events, the gateway receives and transforms them, and the client reads at its own pace. If every layer assumes the next one is always ready, buffers grow silently.
A fast local test can look perfect while a mobile client, a serverless function or a disconnected tab leaves work queued in memory. Treat streaming as a pipeline with several boundaries, not as a single socket.
Bound buffers and use the transport's flow control#
Put a bounded buffer at every meaningful boundary, with limits on bytes, events, tokens and elapsed time. When a buffer nears its ceiling, stop reading from the provider if the protocol and client library allow it. Otherwise cancel the upstream generation and close the downstream stream with a clear reason. An unbounded queue is deferred failure, not a performance feature.
Do not invent a second hidden queue on top of the transport. HTTP and HTTP/2 have their own windows, and frameworks differ in their write and drain semantics. Find out whether a successful write means the kernel accepted the bytes or the client consumed them, await the documented drain signal, set a maximum pause, and test through the real reverse proxy rather than a local process.
Cancellation and admission control#
Every stream needs an owner and a termination path, and cancellation has to travel both ways:
- If the user closes the page, detect the disconnect and cancel the provider request.
- If the provider fails, send a final error event or close the stream in a way the SDK can classify.
- If a downstream consumer stops reading, do not keep generating just because the upstream connection is open.
Concurrency needs a cost-aware admission policy. A stream that has produced only a few tokens still holds a connection, a queue slot and provider capacity. Track active streams, expected output budgets, buffered bytes and time since the last client read, and refuse or queue new work before every stream becomes slow. Keep interactive streams separate from batch and background work so neither class starves the other.
Measure stream health and handle slow readers#
Fast time to first token does not mean the stream is healthy; a request can start quickly and then stall on a full buffer or a slow reader. Measure time to first token, inter-token gaps, delivery lag, buffer occupancy, upstream generation rate, downstream consumption rate and total completion time. Record why each stream ended: completed, client disconnect, deadline, provider error, backpressure cancellation or gateway overload.
For slow readers, you can pause upstream reads, request less work, cancel after a grace period, or fall back to a non-streaming response when that is still useful. A brief pause may be fine for a short chat answer, while holding a huge backlog for a long agent trace whose client stopped reading is usually the wrong trade. Make the policy explicit and configurable per route.
Framing, recovery, proxies and SDKs#
Send complete protocol events rather than arbitrary chunks that a proxy or client must reassemble. Add sequence numbers or event IDs when clients may need to detect duplicates. Batch a few tokens per flush when that keeps responsiveness, and make sure heartbeats that keep idle connections visible can never be mistaken for model output.
Be honest about recovery. A broken connection does not tell the client whether the provider finished, partly generated or kept working. If you support resume, define a cursor, retention window and replay rule; if not, return a request ID and let the client decide whether to start over. Replaying the last visible text is not exactly-once generation, and side-effecting tools need state reconciliation before any retry.
Reverse proxies, CDNs and service meshes may buffer responses, compress events, enforce idle timeouts or change flush behavior, so test with HTTP/1.1, HTTP/2 and your real proxy settings. Client SDKs should expose incremental events, cancellation, timeouts and a final usage or status record, stop reading when the caller stops iterating, close resources on exceptions, and document whether usage arrives only at the end.
Test unhappy paths and budget streams#
Exercise a client that reads one event every few seconds, a client that disconnects during a tool call, a proxy that buffers to a threshold, a provider that pauses mid-response, and a gateway restart with active streams. Fill the buffer, hit the deadline, exceed the concurrency limit and send malformed upstream events. Confirm that memory returns to baseline, generation is cancelled when promised, and metrics name the real termination reason.
Budget streaming like any other workload: cap output tokens, stream duration, idle time, buffered bytes and concurrent connections, apply tenant and route quotas, and return a useful retry-after signal when you reject work. Enforcing these rules once in a gateway layer, such as an OpenAI-compatible router like Crazyrouter, saves each application team from rediscovering backpressure bugs in every client.
