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.
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.
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 back | what about missed messages | latency in demo 6 | |
|---|---|---|---|
| long polling | the next poll, after a retry delay | since=1 asks for them | 5 s |
| SSE | the browser reconnects by itself | Last-Event-ID: 1; the server replays | 5 s |
| WebSocket | your code must reconnect (onclose, back-off) | nothing built in: the app sends its own resume since 1 | 7 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 polling | long polling | SSE | WebSocket | |
|---|---|---|---|---|
| direction | client asks | client asks | server → client | both ways |
| latency | up to one interval | 1 trip, 2–3 in a burst | 1 trip | 1 trip |
| cost per message | a whole request | a whole request | a few bytes | 2–6 bytes |
| reconnect and resume | trivial | since | built in | your code |
| works through | anything | anything with long timeouts | HTTP proxies, HTTP/2 | proxies that allow Upgrade |
| good for | rare updates | fallback | feeds, notifications, LLM token streams | chat, 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.