The simplest way to stream
When the server needs to push a steady stream of updates to the browser — a live feed, a progress bar, or the tokens of a streaming AI response — Server-Sent Events (SSE) is often the right tool. It runs over plain HTTP, auto-reconnects, and is dead simple compared to WebSockets. It is the transport behind most streaming LLM APIs, and JSON is what travels over it.
The wire format
SSE is a long-lived HTTP response with a special content type and a tiny text format: each message is a data: line, terminated by a blank line.
Content-Type: text/event-stream
data: {"token":"Hello"}
data: {"token":" world"}
data: [DONE]Each data: payload is typically a JSON object. The double newline separates events. That is the whole protocol — no framing library required.
Consuming SSE in the browser
The native EventSource API handles connection and reconnection for you:
const es = new EventSource("/api/stream");
es.onmessage = (e) => {
if (e.data === "[DONE]") { es.close(); return; }
const msg = JSON.parse(e.data); // each event is JSON
appendToken(msg.token);
};
es.onerror = () => { /* EventSource auto-reconnects */ };For POST requests or custom headers (common with AI APIs), you read the stream manually with fetch and parse data: lines yourself.
Producing SSE on the server
// Node/Next.js route handler
const stream = new ReadableStream({
async start(controller) {
const enc = new TextEncoder();
for (const token of tokens) {
controller.enqueue(enc.encode(`data: ${JSON.stringify({ token })}\n\n`));
}
controller.enqueue(enc.encode("data: [DONE]\n\n"));
controller.close();
},
});
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });Each chunk is data: <json> followed by two newlines.
SSE vs WebSockets
| Aspect | SSE | WebSockets |
|---|---|---|
| Direction | Server → client only | Bidirectional |
| Protocol | Plain HTTP | Upgrade to ws:// |
| Reconnect | Automatic | Manual |
| Format | Text (UTF-8) | Text or binary |
| Best for | Feeds, notifications, AI token streams | Chat, multiplayer, collaboration |
If you only need the server to push to the client — which is exactly the streaming-AI case — SSE is simpler and more robust. Reach for WebSockets when the client must also send a continuous stream.
Gotchas
- Each event needs the trailing blank line, or the browser buffers it.
- Disable proxy/server buffering (e.g.
X-Accel-Buffering: nobehind nginx) so tokens flush immediately. - Send periodic keep-alive comments (
: ping) on idle streams to keep connections open. - One JSON object per event — do not split a JSON value across events.