Zhongfarewell

Usage and Differences between SSE and WebSocket

SSE is a "server → client" one-way broadcast; WebSocket is a "client ⇄ server" two-way intercom.

#I. SSE and WebSocket

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.

#II. Core Differences

The main differences between the two are as follows:

DimensionSSEWebSocket
Communication modelHalf-duplex, one-way (server → client only)Full-duplex, two-way (client ⇄ server)
Protocol layerApplication layer, carried over HTTP (HTTP/1.1 or HTTP/2)Independent application-layer protocol; handshake borrows HTTP, then breaks away from HTTP semantics
Connection setupStandard HTTP request, Accept: text/event-stream, keep connection open after responseHTTP request with Upgrade: websocket, server replies 101 Switching Protocols to complete protocol switch
Transmission unitText event stream (UTF-8 mandatory), messages separated by blank linesBinary frames: include FIN / opcode / mask bit, supports both text and binary
Client sending messagesNot supported at protocol level; need separate fetch / XHRNatively supported
Data typesText only (UTF-8)Text + binary (Blob / ArrayBuffer)
ReconnectionBrowser EventSource native reconnection, with Last-Event-ID resumeNo built-in; business layer must implement heartbeat + backoff reconnection
Message sequencing / resumeYes: id: field + Last-Event-ID mechanismNo built-in; business layer must define custom sequence / ACK
Concurrent connectionsConstrained by HTTP/1.1 ~6 connections per domain limit; multiple SSE easily exhaust connection poolSingle "upgraded connection" carries both directions, does not occupy that limit
Proxy / firewallUses 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 APIEventSource (with auto-reconnect)WebSocket (without reconnect)
Same-origin policyFollows standard same-origin / CORSCORS during handshake; afterwards not restricted by same-origin, and cannot customize request headers (auth via Cookie or query params)

#III. Detailed Differences

#1. Protocol and Handshake: One Borrows the Road, One Builds Anew

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
GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13

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.

#2. Data Format and Packetization: Stream vs Frame

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
id: 1024 event: price data: {"symbol":"BTC","price":61234} data: Default message with no event name (triggers onmessage) retry: 3000

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).
  • Binary not supported—to push images or audio, SSE simply fails.
  • Benefit is human-readable: a single 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.

#3. Reconnection and Resume: One Out-of-the-Box, One All by Yourself

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's WebSocket API 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.

#4. Two Often-Overlooked Hard Constraints (Real Pitfalls in Selection)

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
Cache-Control: no-cache X-Accel-Buffering: no

And ensure chunked transfer. WebSocket is generally unaffected by buffering, but conversely requires the proxy to correctly forward Upgrade:

text
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";

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."


#IV. Practical Scenarios

#Scenarios Where SSE Is More Suitable

  • Server-initiated notifications: in-site messages, system announcements, approval to-dos, "push to you when there's news."
  • Real-time data dashboards: monitoring metrics, log streams, CI build progress—essentially all "server one-way pouring."
  • Stock/quote/score push: high-frequency updates but output-only.
  • AI streaming output: the "typewriter effect" where LLMs spit out one character at a time—SSE is the most handy vehicle (this is why OpenAI and major model gateways default to it).

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.

#Scenarios Where WebSocket Is More Suitable

  • Instant messaging / IM: both send and receive are high-frequency bidirectional, SSE one-way can't hold up.
  • Multiplayer collaborative editing: cursor positions, content changes synced bidirectionally in real time.
  • Real-time combat games: low-latency bidirectional + possibly state snapshots (binary).
  • Businesses requiring frequent client reporting: e.g., real-time trajectories, sensor data upload while also receiving commands.

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.