Skip to content
Back to blog
Building software

WebSocket vs SSE vs polling: how each works and when to use it

Polling sends a new request each round, SSE holds one response open for server events, WebSocket upgrades to two-way frames. Reconnects, proxies and firewalls.

Intermediate9 min read

On this page

The short answer: if data only flows from the server to the browser (job status, notifications, sensor readings), start with SSE (Server-Sent Events). If both sides send often, as in chat or collaborative editing, use WebSocket. Polling fits when data changes rarely and a delay of several seconds is fine, or when the network in between won't tolerate long-lived connections.

The three differ in what the connection looks like on the wire, and that difference reaches every proxy, load balancer and firewall along the path. Below is each one at the request and response level, how it recovers from a dropped connection, and what has to be configured in the middle.

Polling sends a new GET every few seconds and mostly gets 204; SSE keeps one response open and the server writes events; WebSocket upgrades once and frames go both ways

How does polling work?

The client sends an HTTP request at intervals to ask whether anything is new. RFC 6202 (April 2011) describes two kinds.

With short polling, the client asks on a fixed schedule; if there is nothing new, the server returns an empty response and the client waits for the next round. The catch is that server and network load strongly depend on the acceptable latency. If updates must arrive within seconds, the polling rate can become a real burden.

GET /api/updates?since=1041 HTTP/1.1
Host: app.example.com

HTTP/1.1 204 No Content

With long polling, the server doesn't answer straight away. It holds the request until there is an update, or until a timeout. The client usually sends the next request as soon as a response arrives, so the server almost always has one request waiting.

RFC 6202 lists the costs. Three come up most:

The upside is that polling is plain HTTP throughout. RFC 6202 notes that most proxies work well with HTTP long polling, because every response sent is a complete one.

How do server-sent events work?

The client sends one GET; the server replies with text/event-stream, never finishes the response, and keeps appending events to it. In the browser this is the EventSource interface from the WHATWG HTML Standard.

const source = new EventSource("/api/events");
source.addEventListener("job", (e) => console.log(e.lastEventId, e.data));

On the wire:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-store

: keep-alive

id: 1042
event: job
data: {"job": 7, "state": "running"}

id: 1043
event: job
data: {"job": 7, "state": "done"}

The format has only a few rules (WHATWG, 9.2.6):

Line Meaning
data: Event data; several data: lines are joined with newlines
event: Event name; without it the event is a message
id: Sets the last event ID used on reconnect
retry: Sets the reconnection delay in milliseconds
A line starting with : A comment; ignored
A blank line Dispatches the event built so far

Event streams must always be encoded as UTF-8, so raw binary is out. SSE is also a one-way connection: anything the client needs to send goes in a separate request, such as an ordinary POST.

How does WebSocket work?

A WebSocket starts as an HTTP request asking to switch protocols, and after that it is no longer HTTP. RFC 6455 (December 2011) gives this handshake (RFC 6455):

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

The server builds Sec-WebSocket-Accept by appending the fixed GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 to the key, then taking the SHA-1 hash and base64-encoding it, which proves it actually speaks WebSocket. The RFC is explicit that WebSocket is an independent TCP-based protocol whose only relationship to HTTP is that the handshake is read as an Upgrade request.

After the 101, data moves in frames. Each frame has an opcode: 0x1 text, 0x2 binary, 0x8 close, 0x9 ping, 0xA pong (RFC 6455, 5.2). Either side can send at any time over the one TCP connection.

One detail matters for network people. A client must mask all frames it sends, with a fresh random 32-bit key per frame, and the server must not mask its frames. While the protocol was being designed, an experiment poisoned caching proxies deployed in the wild by sending data formatted as a GET request over an upgraded connection. Masking stops the attacker's script from controlling the bytes on the wire. The key travels inside the frame, though, so anyone who can read the frame can unmask it: masking is not encryption. Confidentiality comes from TLS on wss://, whose default port is 443.

Two browser-side limits are worth knowing. The WebSocket interface doesn't support backpressure: if messages arrive faster than the app handles them, they pile up in memory. And a page with an open WebSocket may not be added to the bfcache, so close the connection when the user is done with the page.

What happens when the connection drops?

SSE reconnects by itself; with WebSocket you write the reconnect. In my view this is the difference that matters most in practice.

SSE. If the connection closes, the browser reconnects automatically after the reconnection time, which starts at an implementation-defined value probably in the region of a few seconds and can be changed with retry:. On reconnect it sends a Last-Event-ID header with the last id: it saw:

GET /api/events HTTP/1.1
Accept: text/event-stream
Last-Event-ID: 1043

A server that keeps recent events can resume at 1044 instead of resending everything. The browser stops reconnecting in two cases: the server answers 204 No Content, or the response is not a 200 or not text/event-stream, which fails the connection, with no further attempts. The second case means a single 502 from a proxy can end the stream for good. The app should listen for error and open a new EventSource itself when readyState is CLOSED.

WebSocket. RFC 6455 defines nothing like Last-Event-ID. If you need to resume from the last message, that goes into your own application protocol, for example by having the client send its last sequence number as the first message after reconnecting. On timing, the RFC says clients SHOULD use some form of backoff: delay the first attempt by a random amount, where between 0 and 5 seconds is a reasonable start, and wait longer after each failure, for example with truncated binary exponential backoff. Without it, a server outage followed by every client reconnecting at once amounts to a denial-of-service attack.

Polling. There is no connection to lose; the next request is the reconnect. Send a cursor such as since= every time so nothing is missed between rounds.

Which way can data flow with each one?

Side by side:

Polling SSE WebSocket
Direction Client asks, server answers Server to client Both ways
Connection New request each round One long-lived response One TCP connection after the upgrade
Payload Anything HTTP can carry UTF-8 text Text or binary frames
Reconnect Every round is one Browser does it, with Last-Event-ID Your code does it
What intermediaries see Normal HTTP A very long HTTP response HTTP only during the handshake

How do proxies, load balancers and firewalls treat each one?

Very differently. Polling is ordinary HTTP. SSE is an HTTP response that doesn't end. WebSocket is HTTP only at the start, then a different protocol on the same TCP connection. Most real-world trouble comes from the following.

Idle timeouts

Most intermediaries close connections that go quiet. nginx is a clear example: by default a proxied WebSocket connection is closed if the backend sends no data within 60 seconds. Raise proxy_read_timeout, or have the backend send ping frames.

The fix for each:

  • WebSocket: send ping frames, which the RFC says may serve as a keepalive. The receiver must answer with a pong.
  • SSE: send a comment line such as : keep-alive. The HTML Standard suggests one every 15 seconds or so, because some legacy proxies drop HTTP connections after a short timeout.
  • Long polling: set the server's hold time below the intermediaries' timeouts. RFC 6202 calls 30 seconds generally a safer value, although experiments succeeded with up to 120.

Buffering

SSE only works if each event reaches the browser when it is written, but a reverse proxy may buffer the response first. In nginx, proxy_buffering is on by default, so events wait at the proxy until a buffer fills. Turn it off in config, or have the backend send X-Accel-Buffering: no on the stream's response. The HTML Standard warns about the same class of problem: chunking done by a layer unaware of timing can hurt the reliability of the protocol.

The Upgrade header

Upgrade and Connection are hop-by-hop headers, so a reverse proxy does not pass them to the backend on its own. Without extra config, the backend sees a plain GET and never upgrades. In nginx:

location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 300s;
}

location /api/events {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_read_timeout 300s;
}

A consequence follows: each WebSocket or SSE connection stays on one backend for its whole life, and the load balancer can only move a client when it reconnects. Draining or restarting one backend therefore makes all of its clients reconnect at the same moment, which is one more reason for backoff.

Connection limits and HTTP/2

Without HTTP/2, SSE runs into the browser's connection limit: 6 per browser per domain, across all tabs. If each tab opens one stream, the seventh tab on the same site can't connect. Over HTTP/2 the number of concurrent streams is negotiated and defaults to 100. The HTML Standard also suggests sharing one EventSource through a shared worker.

Classic WebSocket uses a TCP connection of its own. RFC 8441 (September 2018) defines how to run WebSocket over a single stream of an HTTP/2 connection using extended CONNECT, which the server must first advertise with SETTINGS_ENABLE_CONNECT_PROTOCOL.

Firewalls and inspection

On the security side I always check two things:

  • What an HTTP-aware device sees. Polling and SSE stay HTTP end to end, so policies based on URL, method or headers keep working. After the 101, WebSocket is frames of a different protocol; a device that only parses HTTP sees the handshake and nothing after it. With wss:// it is all inside TLS, so the device must decrypt to see even the handshake.
  • The Origin check at the handshake. A script on any page the browser has open can try to open a WebSocket to your server. The RFC says servers meant only for certain sites SHOULD verify the Origin field and reply 403 Forbidden when it isn't one they expect.

Which one should you choose?

Work through these in order:

  1. Does the client need to send often while it is also receiving? If yes (chat, games, shared editing), use WebSocket, and plan to write reconnect, backoff and resume-from-last-message yourself.
  2. Does data flow one way, from the server, as text? If yes, use SSE. You get reconnects and Last-Event-ID for free, and it stays HTTP that intermediaries understand. Turn off buffering and send keep-alive comments.
  3. Does data change rarely, or do clients sit behind proxies you don't control? If yes, polling is the simplest option, easy to test, and cacheable and loggable like any other HTTP.
  4. What proxies, load balancers and firewalls are in the path? Check each one's idle timeout, buffering and Upgrade forwarding before you commit. All three approaches work end to end; one intermediary configured the wrong way is enough to drop connections or hold events.

Whichever you pick, give every message a sequence number and have the client send the last one it saw each time it reconnects or polls. All three can drop, and the sequence number is what keeps data from going missing.

Sources

Keep reading