The idea: the server wants to speak first

In a chat, Bob's message reaches the server first, and the server has to get it to Alice. Plain HTTP cannot do that: the client sends a request, the server sends one response, done. The server never starts a conversation. There are three common ways around it. Polling keeps asking. Server-Sent Events (SSE) keep one response open forever. WebSocket turns the connection into a two-way pipe.

Three timelines between Alice's browser and the server. Long polling: a GET is held, Bob's message answers it with 200, the browser sends the next GET at once; a second message that arrives meanwhile waits for that next GET. SSE: one GET answered with 200 text/event-stream, then events id 1 and id 2 on the same response. WebSocket: GET with Upgrade, 101 Switching Protocols, then text frames in both directions
Polling pays a full request for every answer; SSE and WebSocket set up one connection and then only send the messages.

The canvas runs all three side by side on one clock: 1 tick is 1 second and every message on the wire takes 1 second. Each lane counts HTTP requests, bytes of headers and framing, payload bytes and how late each message is. Pick an event and press Do It (start with Connect), or run a demo.

Polling: short and long

Short polling asks on a timer, here every 3 s: GET /poll?since=2. The server answers at once, usually with 204 No Content. A message waits up to one interval, and almost every request is wasted (Demo 4).

Long polling sends the same request, but the server holds it until it has something, or until a timeout (8 s here, 20–30 s in practice) ends it with an empty answer. When the answer arrives, the client polls again at once. A single message arrives after 1 s, as fast as the other methods (Demo 2). The weak spot is the moment between two polls: a message that arrives while the next request is still on its way has no request to answer, and waits a round trip (Demo 3: a burst: 2 s instead of 1). Every answer also costs a full request with its headers and cookies, hundreds of bytes for a 2-byte message.

The since parameter makes polling robust: after any failure, the client just asks for everything after the last id it saw.

Server-Sent Events

With SSE the browser makes one ordinary request with new EventSource("/events"). The server answers 200 with Content-Type: text/event-stream and then never finishes the response. Each message is a few text lines on it:

id: 3
data: are you there?

: keepalive

An empty line ends an event, and a line starting with : is a comment the browser ignores. That is useful as a keepalive. SSE only goes from the server to the client: Alice's own messages still go as separate POST requests (Demo 5). In exchange it is plain HTTP, so it works through proxies, HTTP/2 and existing authentication. The browser also reconnects by itself: after a drop it waits the retry: time and sends Last-Event-ID with the last id it saw (1 in Demo 6), so the server can replay what was missed.

WebSocket

new WebSocket("wss://chat.example/ws") also starts as an HTTP request, but asks to switch protocols:

GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

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

The accept value is a hash of the key, which proves the server really speaks WebSocket. After the 101 the same TCP connection carries frames, not HTTP. A small text frame from the server has a 2-byte header. A frame from the browser has 6, because client frames are masked with a 4-byte key (that stops a malicious page from making a frame that a confused proxy would read as an HTTP request). Both sides can send at any time. There are control frames too: ping, pong (sent back automatically) and close.

Bytes on the wire for one 2-byte chat message: long polling about 680 bytes (450 request headers, 200 response headers, 30 JSON), SSE 15 bytes (13 framing, 2 data), WebSocket 4 bytes to the browser (2 header, 2 data) and 8 bytes from the browser (6 header with the mask key, 2 data)
For a 2-byte message, a poll answer costs hundreds of bytes of headers; an SSE event or a WebSocket frame costs a few bytes.

When the connection drops

Networks drop: a phone changes cell, a laptop sleeps, a server restarts. Demo 6 drops everything for 3 s while Bob sends a message:

how it comes backwhat about missed messageslatency in demo 6
long pollingthe next poll, after a retry delaysince=1 asks for them5 s
SSEthe browser reconnects by itselfLast-Event-ID: 1; the server replays5 s
WebSocketyour code must reconnect (onclose, back-off)nothing built in: the app sends its own resume since 17 s

WebSocket has no message ids and no resume. Libraries such as Socket.IO add both, plus heartbeats and fallbacks. Without that layer, a message sent during the gap is simply gone.

Idle connections and proxies

Load balancers and proxies close connections that carry no bytes for a while. Demo 7 puts in a proxy with a 6 s idle timeout. The held long poll gets 504 from the proxy and has to be sent again. SSE writes a : keepalive comment and WebSocket a ping every 5 s, so their connections stay open. Keep the long-poll hold time and the keepalive interval below every idle timeout on the path.

At scale the costs change shape (Demo 8, computed with real timers for 1 000 clients that receive nothing for a minute). Long polling with a 30 s hold makes 2 000 requests and moves about 1.3 MB of headers; short polling every 3 s makes 20 000 requests. SSE and WebSocket make no requests and send only tens of kilobytes of keepalives. Instead they hold 1 000 open connections all the time, so the server needs an event loop that can wait on many sockets cheaply (see the event loop).

Which one to use

short pollinglong pollingSSEWebSocket
directionclient asksclient asksserver → clientboth ways
latencyup to one interval1 trip, 2–3 in a burst1 trip1 trip
cost per messagea whole requesta whole requesta few bytes2–6 bytes
reconnect and resumetrivialsincebuilt inyour code
works throughanythinganything with long timeoutsHTTP proxies, HTTP/2proxies that allow Upgrade
good forrare updatesfallbackfeeds, notifications, LLM token streamschat, games, collaboration

For how one HTTP request and response travel, see How an HTTP Connection Works; for HTTP/2 streams, which let many SSE streams share one connection, see HTTP/1.1 vs HTTP/2 vs HTTP/3. All three methods ride on one TCP socket (TCP sockets).

What the page leaves out

TLS on top of each connection, HTTP/2 and HTTP/3 transports (SSE over HTTP/2 shares one connection; WebSocket over HTTP/2 uses extended CONNECT), message fragmentation and binary frames, compression (permessage-deflate), subprotocols, authentication of the upgrade, back-pressure, WebTransport, and the six-connections-per-host limit that hurts SSE over HTTP/1.1. Real timers are longer than the page's, and messages here never overtake each other.