SSE is a "server → client" one-way broadcast; WebSocket is a "client ⇄ server" two-way intercom.
SSE (Server-Sent Events), as the name suggests, is a mechanism where the server unilaterally "feeds" data to the client. It is still HTTP underneath, except the connection is kept alive, and the server can push whenever it wants. The browser natively provides an EventSource API that is ready to use out of the box.
WebSocket, on the other hand, is a different protocol. After a handshake over HTTP, it "upgrades" this TCP connection into a full-duplex channel—the client can send, the server can send, and both sides can exchange at any time without waiting for each other.
They both look "real-time," but one only outputs and never takes input, while the other can both send and receive. This fundamental difference determines all the usage patterns that follow.
The main differences between the two are as follows:
| Dimension | SSE | WebSocket |
|---|---|---|
| Communication model | Half-duplex, one-way (server → client only) | Full-duplex, two-way (client ⇄ server) |
| Protocol layer | Application layer, carried over HTTP (HTTP/1.1 or HTTP/2) | Independent application-layer protocol; handshake borrows HTTP, then breaks away from HTTP semantics |
| Connection setup | Standard HTTP request, Accept: text/event-stream, keep connection open after response | HTTP request with Upgrade: websocket, server replies 101 Switching Protocols to complete protocol switch |
| Transmission unit | Text event stream (UTF-8 mandatory), messages separated by blank lines | Binary frames: include FIN / opcode / mask bit, supports both text and binary |
| Client sending messages | Not supported at protocol level; need separate fetch / XHR | Natively supported |
| Data types | Text only (UTF-8) | Text + binary (Blob / ArrayBuffer) |
| Reconnection | Browser EventSource native reconnection, with Last-Event-ID resume | No built-in; business layer must implement heartbeat + backoff reconnection |
| Message sequencing / resume | Yes: id: field + Last-Event-ID mechanism | No built-in; business layer must define custom sequence / ACK |
| Concurrent connections | Constrained by HTTP/1.1 ~6 connections per domain limit; multiple SSE easily exhaust connection pool | Single "upgraded connection" carries both directions, does not occupy that limit |
| Proxy / firewall | Uses standard HTTP long connection; but intermediate proxies may buffer responses (need to disable buffering) | Requires proxy to support Upgrade; some old proxies / security devices may block |
| Browser native API | EventSource (with auto-reconnect) | WebSocket (without reconnect) |
| Same-origin policy | Follows standard same-origin / CORS | CORS during handshake; afterwards not restricted by same-origin, and cannot customize request headers (auth via Cookie or query params) |
Strictly speaking, SSE is not a new protocol—it simply leaves an ordinary HTTP response "unclosed." The client sends a GET request with Accept: text/event-stream; the server replies with Content-Type: text/event-stream and does not end the response, then pushes whenever it wants. Over HTTP/1.1 it is a long connection; over HTTP/2 it multiplexes on a single connection. Intermediate Nginx, CDN, load balancers all treat it as ordinary HTTP, with almost zero adaptation cost—but there is a pitfall discussed in section 4.
WebSocket "defects" during the HTTP handshake phase. The client request carries:
text
Sec-WebSocket-Key is 16 random bytes Base64-encoded. The server must concatenate it with the fixed magic GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, compute SHA-1 then Base64, and return it as Sec-WebSocket-Accept (the standard example value given by RFC 6455 is exactly s3pPLMBiTxaQ9kYGzzhZRbK+xOo=). Only after the client verifies and receives 101 Switching Protocols does this TCP connection formally switch to the WebSocket protocol, completely leaving HTTP semantics behind.
Precisely because of this, old proxies, WAFs, and enterprise firewalls that don't understand Upgrade often cut the connection directly—this is the root cause of WebSocket's poor penetration and frequent blocking, not black magic.
SSE is a pure text event stream, and must be UTF-8. An event consists of several lines, with field prefixes as hard conventions:
sh
The rules are strict:
data: can span multiple lines, automatically concatenated by the client; event ends with a blank line.id: numbers the event; on reconnect the browser sends the last received id back as the Last-Event-ID request header (see section 3).event: custom event name, client listens via addEventListener('price', ...); if unspecified, falls to default onmessage.retry: specifies reconnect interval (milliseconds).curl can view the stream, extremely debug-friendly.WebSocket is message-based, encapsulated in frames. A message may be split into multiple frames, each carrying: FIN bit (whether last frame of message), opcode (0x1 text / 0x2 binary / 0x8 close / 0x9 ping / 0xA pong), payload length, and mask bit (spec mandate: client → server frames must be masked, server → client not masked). Handles both text and binary (received as string or Blob / ArrayBuffer). The cost: objects must be JSON.stringify / parse by yourself, and packetization and heartbeat formats are all up to the business layer.
This is the most overlooked yet most valuable point of SSE. EventSource natively reconnects on disconnect: once the connection drops, the browser auto-reconnects with internal backoff (about 3 seconds starting, implementation-dependent); if the server had sent events with id:, the reconnect request carries the Last-Event-ID header, and the server resumes pushing from that point—this is protocol-level resume guarantee.
WebSocket gives you nothing: on drop it fires onclose, no auto-reconnect, no message sequence, no resume. To keep alive you must build it yourself: application-layer heartbeat, timeout detection, exponential backoff reconnect in onclose, and if needed maintain your own message sequence / ACK for resume. The beloved "I have to reinvent the wheel again" segment.
A commonly misunderstood point: The WebSocket protocol does have ping / pong control frames (opcode
0x9/0xA), belonging to protocol-layer keepalive. But the browser'sWebSocketAPI does not expose the ability to send protocol-level pings—ping / pong are handled automatically by the browser at the lower layer (auto-replies pong on receiving server ping). So if the application layer wants to judge "is the connection still alive," it often still has to send its own business heartbeat messages.
Concurrent connection count (SSE's hidden pit). Browsers typically cap persistent HTTP/1.1 connections per domain at about 6. Each SSE stream occupies one long connection—if you open several SSE streams simultaneously on the frontend (e.g., one per dashboard), the connection pool is quickly exhausted, blocking even ordinary API requests from getting in queue. WebSocket is a single "upgraded connection" with bidirectional multiplexing, not occupying that limit. Mitigation: prefer HTTP/2 (multiplexing, one TCP carries multiple streams), or merge multiple businesses into one SSE multiplexed stream (differentiate via event:).
Proxy buffering (SSE's other hidden pit). When SSE passes through Nginx, CDN, responses are buffered by default—messages accumulate before being sent in batches, manifesting as "clearly pushing every second, but frontend receives only once every one or two seconds." The fix is to declare no buffering in response headers:
text
And ensure chunked transfer. WebSocket is generally unaffected by buffering, but conversely requires the proxy to correctly forward Upgrade:
text
Otherwise the handshake fails.
To sum up this section in one sentence: SSE's shortcoming is not in "capability," but in "two long-connection-related pitfalls" (connection count, buffering); WebSocket's shortcoming is not in "capability," but in "giving you nothing, you must build it all yourself."
In one sentence: As long as your need is "server unilaterally and continuously tells the client," SSE can basically handle it elegantly, and throws in auto-reconnect for free.
In one sentence: As long as "the client also needs to continuously send to the server," or you need to transfer binary, go with WebSocket.