Home/Learn/JavaScript & TypeScript/Streaming and Real-Time — SSE, WebSockets and ReadableStream

Streaming and Real-Time — SSE, WebSockets and ReadableStream

Advanced
Async

Three ways to get data as it arrives rather than all at once. Choosing between them is a design question you will be asked, and the answer is usually not WebSockets.

Overview

A normal request waits for the whole response. That is wrong for anything that produces output progressively — an AI response, a long import job, a live counter. Server-Sent Events give you a one-way stream over plain HTTP with automatic reconnection, and cover most cases. WebSockets give you a full-duplex connection, which you need only when the client also sends continuously. Streaming a fetch response body is the lowest-level option and is how you consume a token-by-token LLM response. Reaching for WebSockets when SSE would do is a common over-engineering answer in interviews.

Server-Sent Events

One-way, plain HTTP, reconnects by itself. The right default for server-to-client updates.

EventSource — the underused default
const es = new EventSource('/api/jobs/42/progress')

es.onmessage = (e) => setProgress(JSON.parse(e.data))
es.addEventListener('done', () => es.close())     // named events
es.onerror = () => { /* the browser retries automatically */ }

// Cleanup matters — an open EventSource keeps the connection alive
useEffect(() => {
  const es = new EventSource(url)
  es.onmessage = handle
  return () => es.close()
}, [url])

// The wire format is plain text:
//   event: progress
//   data: {"percent":40}
//   \n
// which means any backend can produce it with no library.

// Limits: one direction only, and browsers cap ~6 connections
// per origin on HTTP/1.1 (not an issue over HTTP/2).

WebSockets

Full duplex, persistent. Use when the client streams too — chat, collaborative editing, live cursors.

Duplex, and everything it does not handle for you
const ws = new WebSocket('wss://api.example.com/room/42')

ws.onopen = () => ws.send(JSON.stringify({ type: 'join', room: 42 }))
ws.onmessage = (e) => dispatch(JSON.parse(e.data))
ws.onclose = (e) => { if (!e.wasClean) scheduleReconnect() }

// What the browser does NOT give you, and you must build:
//   - reconnection with exponential backoff
//   - heartbeat/ping to detect a dead connection behind a proxy
//   - message queueing while disconnected
//   - resubscribing after reconnect
// This is why teams use a library rather than the raw API.

// readyState before sending, or it throws
if (ws.readyState === WebSocket.OPEN) ws.send(payload)

// Auth: you cannot set headers on a WebSocket handshake.
// Use a cookie, or a short-lived ticket in the query string.

Streaming a fetch Response

Reading the body in chunks. This is how a token-by-token AI reply reaches the screen.

ReadableStream, and cancelling it
const res = await fetch('/api/chat', { method: 'POST', body })
const reader = res.body.getReader()
const decoder = new TextDecoder()

while (true) {
  const { done, value } = await reader.read()
  if (done) break
  append(decoder.decode(value, { stream: true }))   // stream:true handles
}                                                   // split multi-byte chars

// Same thing with for await, which is cleaner
for await (const chunk of res.body.pipeThrough(new TextDecoderStream())) {
  append(chunk)
}

// Cancel when the user navigates away — otherwise the server keeps
// generating tokens nobody will read, and you keep paying for them
const controller = new AbortController()
fetch(url, { signal: controller.signal })
controller.abort()

// Choosing: SSE for server->client updates, WebSocket when the client
// also streams, fetch streaming for a single progressive response.

Key Points to Remember

  • 1SSE is one-way over plain HTTP with automatic reconnection — the right default for server-to-client updates
  • 2WebSockets are full duplex but give you no reconnection, heartbeat or queueing; you build all of it
  • 3You cannot set headers on a WebSocket handshake, so authenticate with a cookie or a short-lived ticket
  • 4Reading res.body with a reader streams a response progressively, which is how token-by-token AI output works
  • 5Always close an EventSource and abort a stream on unmount, or the connection and the server work continue

Interview Questions

Sign in to ask Aria
1

When would you choose Server-Sent Events over WebSockets?

Medium
2

What does a WebSocket not give you that a production app needs?

Hard
3

How would you render an AI response token by token as it arrives?

Medium

Ask Aria about Streaming and Real-Time — SSE, WebSockets and ReadableStream

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…