Why SSE, not WebSockets
Streaming a generation is one-way: server to client. SSE is built exactly for that — plain HTTP, automatic reconnection, no handshake protocol to manage. WebSockets are for genuine bidirectional real-time (collaboration, multiplayer). For AI token streaming, SSE is less code and fewer failure modes.
The server: emit typed frames
// Route Handler: stream typed SSE frames
export async function POST(req: Request) {
const { prompt } = await req.json();
const stream = new ReadableStream({
async start(controller) {
const enc = new TextEncoder();
for await (const tok of llm.stream(prompt)) {
controller.enqueue(enc.encode(`data: ${JSON.stringify({ t: tok })}\n\n`));
}
controller.enqueue(enc.encode('data: [DONE]\n\n'));
controller.close();
},
});
return new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } });
}The client: read, render, and cancel
The part most examples omit: users abandon generations. If you don't cancel the request, you keep paying for tokens no one will read. An AbortController wired to the fetch stops the stream — and the server should notice the disconnect and stop generating too.
const ac = new AbortController();
const res = await fetch('/api/chat', { method: 'POST', body, signal: ac.signal });
const reader = res.body!.getReader();
const dec = new TextDecoder();
try {
for (;;) {
const { value, done } = await reader.read();
if (done) break;
for (const f of dec.decode(value).split('\n\n')) {
if (f.startsWith('data: ')) {
const p = f.slice(6);
if (p === '[DONE]') return;
append(JSON.parse(p).t as string);
}
}
}
} catch (e) { /* aborted or network error — show a retry */ }
// ac.abort() on unmount / stop buttonCancellation isn't a nicety — it's cost control. An abandoned generation you don't cancel is tokens you pay for and throw away. Wire abort to the stop button and to component unmount.
Errors and backpressure
Mid-stream failures happen (rate limits, model errors). Emit a typed error frame so the client can show a retry instead of a half-message that just stops. And respect backpressure: the ReadableStream naturally pauses when the consumer is slow, so you don't buffer an unbounded queue of tokens in memory.
Key takeaways
- SSE is the right tool for one-way AI token streaming — simpler than WebSockets.
- Send typed frames (JSON per event) and a clear [DONE] sentinel.
- Wire an AbortController — cancellation is real cost control, not polish.
- Emit typed error frames so failures become retries, not frozen messages.