For years the web had a single verb: ask and wait. You opened a page, the server answered, and the conversation was over. But chat, live quotes, notifications, or collaborative boards need something different: for data to arrive without you asking for it. That is what two technologies that are often confused and are actually very different are for: WebSocket and Server-Sent Events (SSE). One opens a permanent two-way channel; the other, a one-way stream of data. Choosing well between them is the difference between a product that responds and one that sinks under its own traffic.
The problem neither of them invented
Classic HTTP is a request/response protocol: the client sends a request and the server answers. If the server wants to tell you something new, it has no way to do it until you ask again. The first attempt at a solution was polling: the browser re-asked every few seconds “anything new?”. It works, but it wastes connections, saturates the network, and adds latency. Then came long polling, where the server holds the request open until there is data to send: it reduces the noise, but the client still keeps asking round after round.
SSE and, above all, WebSocket appear to end that story of comings and goings. The underlying idea is simple to state and complex to implement: keep a single connection open for as long as the conversation lasts. What travels inside and how is what really sets them apart.
WebSocket: a two-way channel, framed
WebSocket is defined in RFC 6455 and was born with a clear ambition: for the browser and the server to talk to each other at any time, in both directions, with low latency. The magic starts with a handshake over HTTP: the client sends a perfectly normal request with the Upgrade: websocket header and the Sec-WebSocket-Key. If the server accepts, it replies with 101 Switching Protocols and, from that moment on, the connection stops being HTTP and becomes a flow of frames of binary or text data.
That is the technical key: WebSocket does not carry data as an HTML response, but in frames with a binary header that specify their operation. The FIN bit marks whether it is the last part of a fragmented message; the opcode says whether it is text, binary, ping, or pong; the length can take 7 bits, 16, or 64 depending on the message size. That structure gives it a huge advantage over SSE: messages are framed and known to be complete without having to guess where each one ends.
The ping that keeps the connection alive
A connection that stays open for hours needs to know the other end is still there. Between a proxy, a firewall, or a mobile network, intermediaries can slip in that close “dormant” connections. For that, WebSocket defines control ping and pong frames: the server sends a ping and waits for the pong; if it does not arrive, the connection is considered dead and reopened. In addition, on the browser side there is the onclose event, to detect drops and retry. It is the least flashy part and yet the one that keeps a chat from going silent after ten minutes.
The browser API is a WebSocket object where you open a URL with the ws:// or wss:// scheme (the TLS version, the only sane one in production) and register the onopen, onmessage, and onerror events. On top of that framework sit higher-level libraries and protocols such as STOMP for messaging brokers or Socket.IO, which adds retries and rooms transparently.
Server-Sent Events: a one-way stream
If WebSocket is a conversation, Server-Sent Events are a loudspeaker: the server speaks, the client listens. The basis is a single HTTP connection that responds with the text/event-stream type and stays open indefinitely. The server keeps writing events separated by blank lines, each made of fields such as data:, event:, id:, and retry:. That format is specified in the HTML Living Standard (not in a separate RFC), which integrates it natively with the browser.
Its great technical advantage over WebSocket is simplicity and automatic reconnection. The native EventSource object handles reconnection for you when the network drops; if the server sends an id: field, the browser resends it in the Last-Event-ID header on reconnect, so the server can resume the stream exactly where it left off without losing messages. That resumption mechanism, so simple to write, is hard to replicate by hand with WebSocket, where rebuilding lost state requires extra logic.
When each option wins (and when it loses)
It is worth banishing the idea that one is “better” than the other: they are tools with different shapes. WebSocket wins when you need two-way communication: chat, multiplayer games, live collaborative editing, or updates that depend on what the client sends in turn. Its downside is that it loses the benefit of HTTP/2’s native multiplexing: each socket occupies its own connection, and managing subprotocols and masking (clients must mask their frames, for security) requires more work.
SSE benefits from everything HTTP already knows: it compresses easily with gzip, coexists with HTTP/2 multiplexing without clashing, and reuses caches and proxies. It is the natural choice for notifications, live dashboards, telemetry data, or feeds where only the server emits. Its limit is hard: it goes in one direction only and without binary data (you would have to encode it in base64, with the size and CPU cost that implies).
The decision, in short, is not about which sounds more modern, but about the topology of your problem: who speaks and who listens? If both, WebSocket. If only the server, and you also want cheap reconnection robustness, Server-Sent Events. Choosing the wrong piece not only complicates your code: it conditions the cost of your servers, the latency the user perceives, and the number of “wheels” you will have to reinvent to keep a conversation that, today, should be silent and invisible.





