EP177 · October 4, 2026 · 7:19 · AI Dev Tools: The Crazyrouter Podcast

AI API Streaming Backpressure — Keep Tokens Flowing Without Melting Clients

A model can produce tokens faster than a browser, mobile connection, proxy or parser can read them, and unbounded buffers turn that mismatch into memory growth and wasted generation. This episode treats streaming as a resource contract: bound every buffer, honor transport flow control, propagate cancellation in both directions, and measure delivery lag as well as model latency.

Key takeaways

  • Set limits for bytes, events, tokens and elapsed time at every boundary, and cancel upstream generation when a buffer approaches its ceiling.
  • Use the transport's own flow control and await the documented drain signal instead of adding a hidden in-memory queue.
  • Propagate cancellation both ways so a closed tab stops provider generation and a provider failure reaches the client as a typed final event.
  • Measure inter-token gaps, delivery lag, buffer occupancy and the reason each stream ended, not just time to first token.
  • Pick an explicit, per-route policy for slow readers: pause, cancel after a grace period, or downgrade to a non-streaming response.
  • Test the full path through your real reverse proxy, CDN and HTTP versions, since intermediaries can buffer, compress or time out streams.

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.

Questions from this episode

What is backpressure in LLM streaming?
Backpressure is how a streaming pipeline copes when the model produces tokens faster than the client can consume them. Without it, buffers in the gateway or proxy grow without limit, wasting memory and provider generation. Bounded buffers, drain signals and cancellation keep the flow under control.
How do I stop the provider from generating after the user closes the tab?
The gateway needs to detect the client disconnect and cancel the upstream provider request, rather than continuing just because the upstream connection is still open. Test this deliberately with clients that disconnect mid-stream and verify that generation actually stops.
Why do my SSE AI streams arrive in bursts behind a proxy?
Reverse proxies, CDNs and service meshes can buffer responses, compress events or change flush behavior. Test the full network path with HTTP/1.1, HTTP/2 and your actual proxy settings, and send complete protocol events rather than arbitrary chunks.

More episodes

All episodes with show notes →

Try it on your own traffic

Crazyrouter gives you one OpenAI-compatible API key for Claude, GPT, Gemini, DeepSeek and more, billed pay-as-you-go.

Get an API key →