Picture a chat app with one rule: a message must show up on your screen the moment someone sends it. You open the browser, you load the page, and then you wait. Nothing happens, because the browser never asks for anything, and the server, by the rules of HTTP, is not allowed to talk first. The server can only answer.
That one rule, the client asks and the server answers, is the root of everything in this post. It made sense in 1991, when the web was a pile of documents you fetched one at a time. It stops making sense the moment the page is a chat, a stock ticker, a delivery map or a build log. Every technique below is a different way of getting around that rule, and each one was invented because the previous one hurt in a specific way.
Quick vocabulary fix first, because I have seen these names mixed up often. The four things people usually line up are:
- Polling (also called short polling)
- Long polling
- Server-Sent Events, usually shortened to SSE
- WebSockets
1. Polling: asking again and again
This is the first thing anyone builds, and there is no inventor to credit, because it is just plain HTTP used the only way it allows. The client asks, the server answers, and to get fresh data the client asks again on a timer.
setInterval(async () => {
const res = await fetch("/api/messages?since=" + lastId);
const messages = await res.json();
render(messages);
}, 5000);If you think of it as "hitting the API over and over", you have it exactly right. The name even fits: you are polling, like a pollster knocking on doors.
The cost is easy to see once you do the math. Say 10,000 users have your page open and each polls every 5 seconds. That is 2,000 requests per second, and most of them come back with "nothing new". Each one pays for a full HTTP exchange, headers, authentication, a database lookup, and a response that carries no information. You are paying full price to be told "no".
You also face a trade-off that never goes away. Poll every 30 seconds and the data is up to 30 seconds stale. Poll every second and your server load multiplies. You choose between slow and expensive, and you cannot get both fast and cheap.
Polling is not useless, though. For something that changes rarely, like a deployment status checked every 30 seconds, it is simple, stateless, easy to cache and easy to debug. You can also make it cheaper with conditional requests (ETag and If-None-Match), so an unchanged resource returns a tiny 304 Not Modified instead of a full body. Boring, and often the right answer.
2. Long polling: ask once, wait for the answer
If most polls return "nothing", the fix is to stop answering "nothing". With long polling the client sends a request and the server holds it open until it actually has something to say, or until a timeout (often 20 to 60 seconds) expires. When it answers, the client immediately sends the next request.
async function listen() {
while (true) {
try {
const res = await fetch("/api/messages/wait?since=" + lastId);
if (res.status === 200) {
const messages = await res.json();
render(messages);
lastId = messages.at(-1).id;
}
// 204 means the server timed out with nothing new, just ask again
} catch (err) {
await sleep(2000); // network blip, back off a little
}
}
}Your description was close here, with one detail worth fixing: the connection is not kept forever. Each request ends when the server answers, and a new request starts right away. The client is always waiting with one request in flight, so from the user's side the update arrives the instant it exists.
Where it came from
This is the part of the story I find most interesting. In 1995 Netscape added a feature called server push, based on a content type named multipart/x-mixed-replace. The server kept the HTTP connection open and sent chunks of content, and each new chunk replaced the previous one in the browser. It was used for simple animations and for live-updating pages, years before anyone said "real time".
Then in 2004 Google shipped Gmail, and in 2005 Jesse James Garrett gave the new style a name: Ajax. Pages could now fetch data in the background with XMLHttpRequest. The next obvious question was how to make that background request feel instant. In March 2006, Alex Russell, who was working on the Dojo toolkit, named the family of tricks that held a request open to push data: Comet. The name was a joke on Ajax, since both are household cleaners in the United States. Long polling is the most common Comet technique, and Russell later co-authored the Dojo cometd client for the Bayeux protocol, an early attempt to standardize it.
Long polling shipped in production for years before it had a clean spec. It was finally written up in RFC 6202 (April 2011), titled "Known Issues and Best Practices for the Use of Long Polling and Streaming in Bidirectional HTTP". The authors say plainly that these techniques stretch the original semantics of HTTP, and that HTTP was not designed for bidirectional communication. That is an honest summary of the whole technique: it is a clever workaround built on a protocol that was never meant for it.
Why it hurts
- Held connections are not free. Every waiting client occupies a socket on your server and often a slot on a proxy or load balancer. Servers that spend a thread per request fall over quickly, which is part of why event-loop servers like Node.js became popular around the same time.
- A gap between requests. Between receiving a response and sending the next request, there is a small window. If two events happen in that window, you need a
sincecursor so the client does not miss any. Skipping that detail is the classic long-polling bug. - Timeouts everywhere. Proxies and load balancers cut idle connections. You tune your server timeout to be shorter than theirs and expect the occasional surprise.
- Heavy for chatty data. Under a constant stream of events, you pay for a full request and a full set of headers per message.
3. Server-Sent Events: one request, many answers
This is the one you said you were unsure about, and the name is "Server-Sent Events". It is the technique that most often goes unnoticed, which is a pity, because it fits a huge number of problems very well.
With SSE the browser opens one HTTP request, and the server never finishes the response. Instead it keeps writing small text messages into it over time. The connection stays open and the events keep flowing down, so the difference from long polling is that the connection is not closed after each message.
On the client you get a tiny built-in API:
const source = new EventSource("/api/stream");
source.onmessage = (event) => {
render(JSON.parse(event.data));
};
source.addEventListener("price-update", (event) => {
updateTicker(JSON.parse(event.data));
});On the wire it is plain text with a specific Content-Type: text/event-stream:
id: 42
event: price-update
data: {"symbol":"ACME","price":101.5}
id: 43
data: {"message":"hello"}
Each message ends with a blank line. That is the whole format.
What you get for free
The browser's EventSource does the annoying parts for you:
- Automatic reconnection. If the connection drops, the browser reconnects on its own.
- Resume where you stopped. Each message can carry an
id. On reconnect, the browser sends aLast-Event-IDheader so the server can replay what the client missed. That solves the "gap" problem from long polling at the protocol level. - It is just HTTP. It works with normal authentication, cookies, logging and proxies, and with HTTP/2 many streams can share one connection.
Limits you should know
- One direction only. Server to client. If the client needs to send data, it uses a normal
fetchorPOSTnext to the stream. For most "live feed" problems that is enough. - Text only. No binary frames. Send JSON, or encode if you must.
- Buffering can break it. Some proxies and servers buffer responses, which holds your events back until the buffer fills. You often need to disable buffering (for example
X-Accel-Buffering: noon nginx). - Connection limits on HTTP/1.1. Browsers cap connections per host (commonly six), so many tabs each holding an SSE stream can starve each other. HTTP/2 largely removes that problem.
Where it came from
SSE came out of the HTML5 effort. Ian Hickson, the editor of the HTML5 specification, wrote the proposal for server-sent DOM events, and Opera shipped the first implementation in 2006 (September, in Opera 9). That first version was awkward: you added a special <event-source> element to the page. After feedback from other browser engineers, it was redesigned into the simple EventSource object we use today, which looks much like XMLHttpRequest. Opera also wrote a nice article called "The Long Journey of Server-Sent Events" if you want the full story.
It then spent years in the shadow of WebSockets, which got far more attention. Today, with streaming responses everywhere, from live dashboards to the token-by-token output of AI chat interfaces, SSE has had a quiet comeback. If your data only flows downward, it is often the simplest correct answer.
4. WebSockets: a real two-way channel
You described WebSockets as "the open listening and sending connection", which is exactly right, and it is what makes them different from everything above. All the earlier techniques are HTTP bent into shape. A WebSocket is a different protocol, and it only borrows HTTP for the first moment.
The connection starts as a normal HTTP request that asks to switch protocols:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13If the server agrees, it replies 101 Switching Protocols, and from then on the same TCP connection carries small WebSocket frames in both directions, at any time, with very little overhead per message. No request and response pairs, no repeated headers.
const socket = new WebSocket("wss://example.com/chat");
socket.onopen = () => socket.send(JSON.stringify({ type: "join", room: "general" }));
socket.onmessage = (event) => render(JSON.parse(event.data));
socket.onclose = () => scheduleReconnect();It can also carry binary data, which makes it a good fit for games, collaborative editors and anything that needs low latency in both directions.
Where it came from
The story starts with a placeholder. Early versions of the HTML5 spec contained an API called TCPConnection, which was meant to give pages raw socket access. Raw sockets in a browser are a security nightmare, though, so something safer was needed. In June 2008, a series of discussions led by Michael Carter produced the first version of the protocol. The name WebSocket came shortly afterwards from Carter and Ian Hickson, talking on the #whatwg IRC channel, and Hickson wrote it into the HTML5 specification.
Work then moved from the WHATWG and W3C to the IETF in February 2010. The final protocol was published as RFC 6455 in December 2011, authored by Ian Fette (Google) and Alexey Melnikov (Isode). Its handshake was designed to look like HTTP so that it could pass through existing infrastructure.
In the years between the draft and the standard, browser support was uneven, so developers needed a safety net. In 2010 Guillermo Rauch, who later founded Vercel, released Socket.IO, a Node.js library that tried WebSocket first and quietly fell back to long polling or other techniques when it could not. A lot of apps were "using WebSockets" in those years without knowing which transport was actually running underneath. That is a nice reminder that these techniques are not rivals in a museum. They stack on top of each other.
The price of power
A persistent two-way connection is more capable, and also more work to operate:
- Stateful servers. Each connection lives on one server. Scaling out means a load balancer that supports it, and a way to deliver a message to the machine that holds a given user's socket, usually through a pub/sub system like Redis.
- You rebuild what SSE gave you. Reconnection, backoff, resuming after a gap, heartbeats to detect dead connections: all of it is yours to write (or to get from a library).
- Authentication is awkward. The browser WebSocket API does not let you set custom headers, so tokens often travel in the URL or in the first message.
- Not everything is a plain HTTP request anymore. Your usual HTTP tools, caching and middleware do not apply the same way.
Side by side
| Polling | Long polling | SSE | WebSockets | |
|---|---|---|---|---|
| Who starts it | Client, on a timer | Client, then again after every answer | Client, once | Client, once (HTTP upgrade) |
| Direction | Client asks | Client asks | Server to client | Both ways |
| Connection | New request each time | Held open, then renewed | One long-lived response | One long-lived socket |
| Latency | Up to the interval | Near instant | Near instant | Near instant |
| Wasted requests | Many | Few | None | None |
| Binary data | Yes | Yes | No (text) | Yes |
| Auto reconnect | You write the loop | You write the loop | Built into the browser | You write it |
| Server complexity | Lowest | Medium | Low to medium | Highest |
| Born | The beginning of HTTP | Mid 2000s (Comet, 2006) | 2006 (Opera), then HTML5 | 2008 draft, RFC 6455 in 2011 |
Which one should you use?
My rule of thumb, which you can bend as you like:
- The data changes rarely, or "a few seconds late" is fine? Use polling, with conditional requests. Boring and robust.
- You need near real time, you control the server, and you cannot use anything newer? Long polling works, and it is the fallback behind many libraries.
- The server pushes, the client mostly listens? Notifications, live scores, dashboards, build logs, streamed AI responses. Start with SSE. You get reconnection and resume for free.
- Both sides talk often and latency matters? Chat, multiplayer games, collaborative editing. Use WebSockets, and accept the operational cost.
The honest pattern in the industry is to start with the simplest option that meets the requirement and move up only when a real problem forces you to. Plenty of products run happily on SSE or even plain polling, and plenty of "we need WebSockets" projects turn out not to.
The pattern behind the story
Look at the timeline once more. Server push in 1995. Ajax in 2005. Comet in 2006. Opera's first SSE the same year. A WebSocket draft in 2008. Two RFCs in 2011. Every step is the web trying to escape its own founding rule, "the client asks, the server answers", without breaking the billions of servers, proxies and browsers already built on it. Polling ignores the rule. Long polling bends it. SSE stretches the response. WebSockets finally replace the protocol after the first handshake.
Understanding which of these you are using, and what it costs, is most of the work. The rest is writing the reconnect logic.
Further Reading
- RFC 6202, Known Issues and Best Practices for the Use of Long Polling and Streaming in Bidirectional HTTP (IETF, 2011)
- RFC 6455, The WebSocket Protocol (IETF, 2011)
- Alex Russell, "Comet: Low Latency Data for the Browser" (2006)
- Opera, "The Long Journey of Server-Sent Events (EventSource)"
- HTML Living Standard, Server-sent events
- MDN, Using server-sent events
- MDN, The WebSocket API