Why SSE over WebSockets
Part of “Why this over that”: short posts about technology choices I’ve actually made, with the reversal conditions stated honestly.
Every time I’ve built a realtime feature lately (live dashboards, streaming AI assistant replies, payment status updates), I’ve reached for server-sent events, and every time someone has asked “why not WebSockets?” This post is the long-form answer.
The short-form answer: most “realtime” features are one-way, and paying the bidirectional tax for a one-way flow buys you nothing but operational surface.
Look at your arrows
Draw the data flow for the realtime features you’ve actually shipped. A dashboard: server to client. Notifications: server to client. An AI assistant streaming tokens: server to client. Order status: server to client. The client’s only contribution is “here’s what I’m subscribed to,” which is an ordinary HTTP request.
WebSockets exist for genuinely bidirectional, low-latency conversation: multiplayer games, collaborative editing, chat where clients send constantly. That’s a real category. It’s just not most of what we build. Most of what we build is a subscription, and SSE is subscription-shaped.
What SSE gives you for free
SSE is just a long-lived HTTP response with a text/event-stream content type. That “just HTTP” property does an enormous amount of quiet work:
- Reconnection is built in. The browser’s
EventSourcereconnects automatically, and sends theLast-Event-IDheader when it does. Give your events IDs and resume from the right place, and you get gap-free delivery over flaky networks without writing a reconnection state machine. WebSocket reconnection, by contrast, is a library you write yourself, badly, and you know it: heartbeats, backoff, missed-message replay, all on you. - Every middlebox already understands it. Load balancers, proxies, auth layers, rate limiters, CDNs: it’s an HTTP response. WebSockets need the Upgrade dance to survive every hop; on the networks I design for, corporate proxies and mobile carriers mangle upgraded connections far more often than they mangle plain HTTP.
- Your existing auth just works. Same cookies, same headers, same middleware. No separate authentication story for a second protocol.
- Debugging is curl.
curl -Nagainst the endpoint and watch events flow. The observability you already have (status codes, request logs) applies unchanged.
The known cost: SSE is one-directional, text-based, and on HTTP/1.1 browsers cap concurrent connections per origin (HTTP/2 dissolves that limit). Client-to-server messages ride ordinary POSTs, which for occasional actions (send a message, ack a notification) is not just acceptable, it’s cleaner: those are requests with responses and error handling, not frames fired into a socket.
Where WebSockets earn their keep
Reversal conditions, honestly: sustained high-frequency client-to-server traffic (games, cursors, collaborative editing), binary streams, or protocol-level multiplexing needs. If the client talks as much as it listens, use the tool built for conversation.
But audit yourself: if the client mostly listens and occasionally POSTs, the WebSocket is cosplay. You’ve built a second transport, a second auth path, and a hand-rolled reconnection protocol to avoid… sending an HTTP request.
The takeaway
Choose by arrow direction. One-way flow: SSE, and inherit twenty-five years of HTTP infrastructure. True conversation: WebSockets, eyes open about the machinery you now own. The boring transport that fits is worth more than the powerful one that doesn’t, which regular readers will recognise as this blog’s entire thesis.
Also in this series: Why SQLite over Postgres, Why Go for the service, Rust for the tool, and Why markdown files over a CMS.
No account, no tracking. One vote per reader.