Two ways to stream from server to client. One is a socket. The other is just an HTTP response that never ends β and it's the right default more often than people think.
Server-Sent Events is a one-way stream: the server pushes, the client listens. WebSockets are bidirectional. That single axis decides almost everything else.
Content-Type: text/event-stream and simply never closes. The server writes text
events over time; the browser's EventSource parses them. No upgrade, no framing protocol.
It's almost comically simple β line-oriented UTF-8 text, events separated by a blank line:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
id: 42
event: price
data: {"symbol":"ACME","px":19.20}
id: 43
data: line one
data: line two
: this is a comment / heartbeat to keep the connection warm
retry: 5000
data: β the payload; multiple data: lines are joined with newlines.id: β an event ID the browser remembers.event: β a named event type you can listen for.retry: β how long the browser waits before reconnecting.: β a comment line, commonly used as a keepalive ping.
The browser reconnects automatically when the stream drops, and it sends the last ID it saw back in the
Last-Event-ID header. The server can resume exactly where it left off. You get
at-least-once resumable delivery for free.
// SSE β the browser handles reconnection + Last-Event-ID for you
const es = new EventSource("/stream");
es.addEventListener("price", (e) => render(JSON.parse(e.data)));
es.onerror = () => {/* browser auto-reconnects; no manual retry needed */};
// WebSocket β you own reconnection, heartbeats, and backoff
let ws;
function connect() {
ws = new WebSocket("wss://example.com/feed");
ws.onmessage = (e) => render(JSON.parse(e.data));
ws.onclose = () => setTimeout(connect, backoff()); // your job
}
connect();
| Dimension | SSE | WebSockets |
|---|---|---|
| Direction | Server β client only | Bidirectional |
| Protocol | Plain HTTP (no upgrade) | Upgrade to ws frames |
| Payload | UTF-8 text only | Text or binary |
| Auto-reconnect | Built in | Roll your own |
| Resume (missed events) | Built in (Last-Event-ID) | Roll your own |
| Works with HTTP auth / cookies / proxies | Yes, natively | Partially |
| Multiplexing | Per-connection limit on HTTP/1.1; fixed by HTTP/2 | One connection multiplexes messages |
Last-Event-ID) for free.