Reach for WebSockets by reflex and you often pay for a full-duplex socket to push data that only ever flows one way. Most "real-time" features — a live order status, a notification badge, a progress bar, tokens streaming out of an LLM — are the server telling the browser something, and nothing coming back on the same channel. For that shape, Server-Sent Events do the job over a single long-lived HTTP response: the browser opens an EventSource, the server keeps the connection open and writes data: lines, and the browser fires a message event for each one.
The reason SSE is underused is that it looks too simple to be a real-time transport. But it's specified in the HTML Living Standard, and the spec hands you two things WebSockets make you build yourself: automatic reconnection when the connection drops, and a Last-Event-ID header the browser resends on reconnect so the server can replay whatever the client missed. WebSockets, defined in RFC 6455, give you a raw bidirectional frame pipe and no opinion about any of that.
| SSE | WebSocket | Long polling | |
|---|---|---|---|
| Direction | Server → client only | Bidirectional | Server → client (per request) |
| Transport | Plain HTTP | Upgraded TCP socket | Plain HTTP |
| Auto-reconnect | Built in | You build it | Trivial (just request again) |
| Missed-message replay | Last-Event-ID, built in | You build it | Cursor in your own request |
| Binary payloads | No (text/UTF-8) | Yes | Yes |
| Works through old proxies | Usually | Sometimes blocked | Always |
| Overhead | One connection | One connection | New request each cycle |
Long polling is the fallback, not a peer: the client sends a request, the server holds it open until there's data or a timeout, then the client immediately asks again. It works literally everywhere because it's just HTTP requests, which is exactly why it's the compatibility floor — reach for it when a corporate proxy strips the SSE stream or refuses the WebSocket upgrade, not as a default.
Every one of these gives you a byte pipe and nothing else. Ordering, delivery guarantees, refreshing an expired auth token mid-stream, and recovering the messages sent while the client was offline are your application protocol to design — the transport won't do it for you. This is the part teams skip, and it's the part that pages someone.
The honest default is to start with SSE and only upgrade to WebSockets when the client genuinely needs to send low-latency messages back on the same channel — collaborative editing, multiplayer, a chat where typing indicators matter. Picking the smaller transport isn't about saving bytes; it's that SSE's reconnect and replay are already written and tested, so choosing it means there's less of your own protocol left to get wrong at 3am.
Cover photo by Stanislav Kondratiev on Pexels.
