Open this page when a new surface needs a transport. Four options, scored on the four things that actually decide it: how the option behaves in infrastructure you do not control, what happens when the connection drops, whether the client can speak mid-stream, and what the choice costs you. Every claim links to the source that states it.
One note before the table. The thing most people call “SSE” in an agent chat is row two, not row one: the SSE wire format read from a POST with fetch. That hybrid is popular because it keeps the format and buys a request body, and it is worth naming explicitly, because it also quietly gives up the reconnection that makes row one attractive.
| Transport | Behind a proxy or CDN | Reconnection | Direction | What it costs you |
|---|---|---|---|---|
SSE via EventSource A never-ending text/event-stream response, parsed by the browser. Fields: event, data, id, retry; blank line ends a message; a leading colon is a comment. | Ordinary HTTP, so intermediaries understand it — but a buffering proxy will hold it. nginx buffers by default; X-Accel-Buffering: no asks it not to, and proxy_ignore_headers can veto that request. | The only option with it built in. MDN: “By default, if the connection between the client and server closes, the connection is restarted.” The id: field is replayed as the Last-Event-ID request header on reconnect. | Server to client only. | The constructor takes a URL and withCredentials, nothing else — the spec gives its request no author-controlled method, headers or body, so it is GET-only and you cannot POST a conversation with it. Also subject to the 6-connections-per-browser-per-domain limit outside HTTP/2. |
Chunked HTTP (POST + fetch) A normal POST whose response body you read incrementally from response.body, usually carrying SSE-formatted frames. This is what most “SSE” chat implementations actually are. | Same exposure as SSE — same buffering, same read timeouts. On HTTP/1.1 the framing is Transfer-Encoding: chunked; on HTTP/2 that header is disallowed outright and streaming happens in DATA frames instead. | None. You write it. Nothing replays a cursor for you, and nothing tells the server where the client got to. | Server to client, but the request carries a body — which is the whole reason to choose it over EventSource. | Every affordance EventSource gave you for free is now yours to build: reconnect, backoff, resume cursor, parse. In exchange you get headers, a body, POST, and full AbortSignal integration. |
WebSocket An HTTP GET with Upgrade: websocket answered by 101 Switching Protocols, after which the connection is a two-way frame pipe and is no longer HTTP. | The most fragile through infrastructure you do not control. The HTTP/1.1 Upgrade mechanism is explicitly disallowed in HTTP/2, and a proxy that does not specifically support upgrades will not pass it. MDN says almost nothing about intermediaries here, which is itself informative. | MDN documents none, and demonstrates reconnecting by constructing a new socket by hand. Note the honest framing: MDN does not state that there is no automatic reconnection — it never mentions one, in contrast to SSE where it says the connection is restarted. | Genuinely bidirectional. The only option here where the client can push mid-stream without a second request. | You give up HTTP semantics — caching, status codes, standard proxy handling, ordinary observability. MDN also notes the WebSocket interface “doesn’t support backpressure” (WebSocketStream is the alternative that does), and that an open socket may keep the page out of the bfcache. |
Polling Ordinary short request/response pairs on an interval, against a job the server keeps for you. | The most robust option by a distance, because there is nothing unusual to break. No long-lived connection, no upgrade, no buffering behaviour that matters. | Not applicable — every poll is a fresh request, so a dropped connection costs one interval. Resumption is structural rather than bolted on. | Client-initiated only. | Latency quantised to the poll interval, and request volume that scales with users × frequency. But note what it forces you to build: the answer has to live somewhere between polls, which means you have already built most of resumability. |
Run this when a stream works locally and arrives all at once in production — correct content, 200 status, no errors anywhere. In that situation the client code is usually innocent.
proxy_buffering defaults to on. “When buffering is enabled, nginx receives a response from the proxied server as soon as possible, saving it into the buffers… When buffering is disabled, the response is passed to a client synchronously, immediately as it is received.” This is the single most common cause of a stream arriving all at once.
X-Accel-Buffering: no is a request, not a command
The same directive documents the per-response escape hatch — “buffering can also be enabled or disabled by passing ‘yes’ or ‘no’ in the ‘X-Accel-Buffering’ response header field” — and then notes it “can be disabled using the proxy_ignore_headers directive.” In someone else’s infrastructure, assume it may already be ignored.
A quiet stream gets cut at 60 seconds
proxy_read_timeout defaults to 60s and applies “only between two successive read operations, not for the transmission of the whole response.” A stream can run for hours if it writes something often enough — which is what an SSE comment line (a line starting with a colon) is for.
Cloudflare inspects a prefix by default
Response Body Buffering has two modes: Standard, the default, which “allows Cloudflare products to inspect a prefix of the response body,” and None, “strictly no buffering.” Inspecting a prefix is enough to stall the first tokens. Cloudflare’s changelog warns that None “may break security functionality that requires body inspection, including the Web Application Firewall (WAF) and Bot Management.” Vendor documentation for a vendor product.
CloudFront streams chunked responses, and only chunked ones
“CloudFront supports only the chunked value of the Transfer-Encoding header. If your origin returns Transfer-Encoding: chunked, CloudFront returns the object to the client as the object is received at the edge location.” Its origin response timeout has the same between-packets semantics as nginx’s read timeout. Vendor documentation.
HTTP/2 disallows Transfer-Encoding entirely
“HTTP/2 disallows all uses of the Transfer-Encoding header. HTTP/2 and later provide more efficient mechanisms for data streaming than chunked transfer. Usage of the header in HTTP/2 may likely result in a specific protocol error.” Streaming and chunked encoding are not synonyms, and a deploy guide telling you to set that header is giving you HTTP/1.1 advice.
Six connections per domain, unless HTTP/2
MDN: SSE “suffers from a limitation to the maximum number of open connections… the limit is per browser and is set to a very low number (6)” per browser and domain. “When using HTTP/2, the maximum number of simultaneous HTTP streams is negotiated between the server and the client (defaults to 100).” Negotiated, not fixed.
Compression: check the framework, not nginx
nginx’s gzip module documentation says nothing about streaming or flushing, and its defaults make the usual advice moot (gzip off; gzip_types text/html; gzip_min_length measured from Content-Length, which a chunked stream has no such header for). Compression genuinely can break streaming, but cite the framework that says so — Vercel’s troubleshooting page blames “compressing proxy middleware” and prescribes Content-Encoding: none — rather than attributing it to nginx.
Every line below is defensive against something in the checklist above. This is the set the AI SDK ships by default — verified from the package source, because the documentation names only the last-but-one of them:
'content-type': 'text/event-stream'
'cache-control': 'no-cache'
'connection': 'keep-alive'
'x-vercel-ai-ui-message-stream': 'v1'
'x-accel-buffering': 'no' // disable nginx bufferingSource: ui-message-stream-headers.ts. Two more show up in Vercel’s troubleshooting pages rather than in its reference docs: Content-Encoding: none when a compressing proxy is in the path, and Transfer-Encoding: chunked on some HTTP/1.1 deployments — the second of which is exactly the header HTTP/2 forbids, so apply it knowing which protocol you are on.
Every claim on these pages links to its source. If a source looks wrong or out of date, check the resource list and tell your teaching agent — the course is meant to be corrected.