WebSocket vs Server-Sent Events vs Long Polling: How to Choose a Real-Time Transport
A practical comparison of WebSocket, Server-Sent Events, and long polling: how each one moves data, what it costs to operate, and which fits chat, notifications, live feeds, and dashboards.
Panji Ihsanudin Fajri
17 min read1 reads

Contents
- What "real-time" actually means on the web
- Why the transport choice matters
- How long polling works
- Short polling vs long polling
- How Server-Sent Events work
- How WebSocket works
- The same feature, three ways
- Long polling version
- Server-Sent Events version
- WebSocket version
- Head-to-head comparison
- Scaling and operating each transport
- Choosing for real use cases
- Chat and collaboration: WebSocket
- Notifications and live feeds: Server-Sent Events
- Dashboards and metrics: SSE, or honest polling
- Rare updates and constrained environments: long polling
- A short case study: one product, two answers
- Common mistakes
- Best practices
- FAQ
- Is WebSocket faster than Server-Sent Events?
- Can SSE send data from the client to the server?
- Why do my SSE events arrive in bursts, or not at all, in production?
- Should my whole app move to WebSocket once I add chat?
- What about WebTransport and HTTP/3?
- Conclusion
Your dashboard shows a number that changed two seconds ago, but the page only finds out on the next manual refresh. A chat message reaches the server and then sits there while the other person stares at a still screen. WebSocket vs Server-Sent Events vs long polling is the decision behind every feature like this, and it is one most web developers have to make without a clear picture of how the three options actually differ.
All three techniques deliver data from server to browser without a page reload. What separates them is direction (one-way or two-way), how the connection behaves, and how much infrastructure each one demands from you. This article walks through the mechanics of all three, builds the same notification feature three ways, compares them head to head, and ends with a selection guide for chat, notifications, live feeds, and dashboards.
Summary: Long polling fakes real-time behavior with held HTTP requests — simple and universal, but wasteful at scale. Server-Sent Events (SSE) is a one-way stream over plain HTTP with reconnection built into the browser, a natural fit for notifications, feeds, and dashboards. WebSocket is a persistent, full-duplex connection built for two-way traffic such as chat and collaboration, paid for with connection state you have to manage yourself. Choose by the direction and frequency of your data.

What "real-time" actually means on the web
"Real-time" on the web rarely means hard real-time. It means that when something changes on the server, every interested browser finds out fast enough for the interface to feel live — within tens or hundreds of milliseconds, instead of on the next click or refresh.
The constraint that shapes all three techniques is that HTTP was designed as request–response. The client asks, the server answers, and the server has no way to initiate contact. A server cannot open a connection to a browser behind NAT, a firewall, and a mobile carrier's network. Every real-time technique is a workaround for that asymmetry: the client opens something first, and the server uses that opening to send data back — once per request (polling), as a continuous stream (SSE), or over a connection that upgrades into a different protocol altogether (WebSocket). Understanding that single constraint explains most of the trade-offs below.
Why the transport choice matters
Picking a transport is not a cosmetic decision. It determines how many connections your servers hold open at once, whether your load balancer and proxies need special configuration, how the feature behaves when a phone loses signal in an elevator, and how much heartbeat, retry, and resync code you will write and maintain.
A wrong choice usually does not break anything on day one. It shows up months later: a browser connection limit you did not know about, a proxy that quietly buffers your event stream, a fleet of servers spending most of their memory on idle sockets. The good news is that the decision hinges on two questions you can answer before writing any code. Which direction does the data flow? And how often does it flow?
How long polling works
Long polling is the oldest trick of the three, and it needs nothing beyond plain HTTP. The client sends a normal request. Instead of answering right away, the server holds the request open until new data exists or a timeout expires — commonly after 20 to 30 seconds. The moment a response lands, the client processes it and immediately fires the next request. To the user, updates appear the instant they are published. To the server, every waiting client occupies one open request and a little bookkeeping.
Short polling vs long polling
Short polling asks "anything new?" on a fixed interval, every few seconds, and the answer is usually "no". It is trivial to build and perfectly fine for data that changes rarely. Its costs are latency (half the polling interval on average) and waste: at a 5-second interval, one open tab generates over 17,000 requests a day, nearly all of them empty.
Long polling removes most of the waste. A request only completes when there is data or the timeout hits, so empty responses are rare and delivery is immediate when something does happen. The price is held connections. A thousand waiting clients means a thousand open requests, which event-driven servers — Node.js, Go, or async Python — absorb easily, while thread-per-request stacks feel each held request as a blocked thread.
How Server-Sent Events work
Server-Sent Events keeps a single HTTP response open and writes events into it as they occur. The browser creates an EventSource, the server replies with Content-Type: text/event-stream, and each event is a small block of text: one or more data: lines, terminated by a blank line. Optional fields add structure — event: names the type, id: attaches a sequence number, and retry: tells the browser how long to wait before reconnecting.
Two details make SSE comfortable in production. First, reconnection is part of the platform: if the connection drops, EventSource retries on its own and sends the last event ID it received in the Last-Event-ID header, so your server can replay exactly what that client missed. Second, SSE never leaves ordinary HTTP. There is no protocol upgrade, authentication works with the cookies you already have, and the only proxy rule to remember is to disable response buffering for the stream endpoint.
The limits are equally clear. The channel is one-way: if the client needs to send data, it uses regular fetch calls alongside the stream. Payloads are UTF-8 text, so binary data needs encoding. And on HTTP/1.1, browsers allow roughly six open connections per host, so several SSE tabs can starve each other — a problem that largely disappears under HTTP/2, where streams are multiplexed over one connection.
How WebSocket works
WebSocket, standardized in RFC 6455, begins life as an HTTP request carrying an Upgrade header. The server answers with 101 Switching Protocols, and from that moment the connection stops speaking HTTP. Both sides exchange small frames over the same TCP connection, in either direction, at any time. The framing is lean — a small message carries as little as two bytes of header — and the protocol reserves control frames for ping and pong, so either side can check whether the other is still there.
That freedom is the point, and it is also the bill. The browser's WebSocket API gives you send() and onmessage, and nothing else: no automatic reconnection, no delivery acknowledgment, no rooms, no presence. Whatever your feature needs beyond raw messages, you design yourself or adopt from a library. In exchange, WebSocket is the only one of the three where client-to-server messages are first-class, symmetric traffic — precisely what chat, multiplayer games, and collaborative editing are made of.

The same feature, three ways
Abstract differences get concrete fast when you build one feature all three ways. The feature here is notifications: the server learns about new events (a comment, a mention, a payment) and the open browser tab should show them as they happen.
Long polling version
The server endpoint holds each request until an event appears. Notice that the "real-time" behavior is entirely a convention between this handler and the client loop below it.
// Server (Express): answer only when there is news, or time out politely
app.get('/poll', async (req, res) => {
const deadline = Date.now() + 25_000;
while (Date.now() < deadline) {
const events = await getEventsSince(req.query.since ?? 0);
if (events.length > 0) return res.json(events);
await sleep(500);
}
res.json([]); // timeout: the client will ask again right away
});
// Client: process what arrived, then immediately ask again
async function poll(since = 0) {
const res = await fetch(`/poll?since=${since}`);
const events = await res.json();
events.forEach(showNotification);
const next = events.length ? events.at(-1).id : since;
poll(next);
}
Server-Sent Events version
The server writes into one long-lived response. The client is three lines, because reconnection and event parsing belong to the browser, as the MDN guide to using server-sent events describes.
// Server (Express): one response, kept open, events written as they happen
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const send = (event) => {
res.write(`id: ${event.id}\ndata: ${JSON.stringify(event)}\n\n`);
};
bus.on('notification', send);
req.on('close', () => bus.off('notification', send));
});
// Client: EventSource reconnects and tracks the last event ID for you
const source = new EventSource('/events');
source.onmessage = (e) => showNotification(JSON.parse(e.data));
WebSocket version
The server upgrades the connection and then treats messages as a conversation. Sending a notification is a push; receiving the user's reply travels the same path.
// Server (ws library): one socket per client, messages both ways
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (socket) => {
socket.on('message', (raw) => {
const msg = JSON.parse(raw);
for (const client of wss.clients) {
if (client !== socket) client.send(JSON.stringify(msg));
}
});
});
// Client: same socket sends and receives
const socket = new WebSocket('wss://example.com/chat');
socket.onmessage = (e) => renderMessage(JSON.parse(e.data));
sendButton.onclick = () =>
socket.send(JSON.stringify({ text: input.value }));
Read the three versions side by side and the pattern is hard to miss. Long polling spreads its complexity across a client loop and a deliberately slow endpoint. SSE concentrates it in one handler and lets the browser carry the rest. WebSocket starts out shortest and grows fastest, because every operational concern — reconnects, liveness, missed messages — becomes your code.
Head-to-head comparison
| Aspect | Long Polling | Server-Sent Events | WebSocket |
|---|---|---|---|
| Direction | Server to client (client asks) | Server to client only | Both directions |
| Connection | One HTTP request per batch | One HTTP response, held open | One upgraded connection |
| Protocol | Plain HTTP | Plain HTTP | HTTP handshake, then WebSocket frames (RFC 6455) |
| Reconnect | Client re-requests by design | Built into EventSource, with Last-Event-ID replay |
You build it yourself |
| Data format | Anything HTTP carries | UTF-8 text events | Text and binary frames |
| Browser support | Universal | All modern browsers via EventSource |
All modern browsers via WebSocket |
| Infrastructure | None special | Proxies must not buffer the stream | Load balancers must hold long-lived connections |
| Scaling pressure | Many short requests, server state per poll | One held connection per viewer | One held connection plus shared state per user |
| Best at | Rare updates, maximum compatibility | Notifications, feeds, dashboards | Chat, games, collaboration |
Two rows in that table decide most real projects. The first is direction: if your feature only ever pushes data toward the user, WebSocket's two-way machinery buys you nothing. The second is reconnect behavior. Connections drop constantly in the real world — tunnels, elevators, network switches — and the transport that treats reconnection as a built-in (SSE) or as the normal loop (long polling) will hold up better than one where your team has to design the recovery path from scratch (WebSocket).
Scaling and operating each transport
All three techniques trade requests for connections, and connections are the resource you end up managing. Ten thousand open tabs means ten thousand sockets your servers, load balancers, and firewalls must track, whether the protocol is SSE or WebSocket. Two operational habits matter regardless of which one you pick.
The first is heartbeats. Proxies and load balancers close connections that look idle, typically after 30 to 60 seconds. SSE servers send a comment line such as : ping every 15 to 30 seconds to keep the path warm; WebSocket uses its built-in ping/pong frames for the same job. Skip this and your feature will appear to work in development and die quietly in production.
The second is fan-out. A single server can broadcast to its own connections from memory. The moment you run two servers, an event published on server A must reach clients connected to server B, which means a shared backplane — most teams reach for Redis publish/subscribe, a pattern covered in this blog's Redis caching strategies guide. Long polling dodges much of this because each poll is a fresh, stateless request any server can answer. Held connections also reward event-driven servers: an async runtime keeps thousands of idle sockets cheaply, the same property explored in the Python asyncio guide, while a thread-per-connection stack pays a thread for every waiting user.

Choosing for real use cases
Chat and collaboration: WebSocket
Chat is symmetric. Users send about as much as they receive, and the extras — typing indicators, read receipts, presence, cursor positions in a shared document — are all small messages flying in both directions. Bolting those onto SSE means a stream for reading plus a fetch call for every keystroke-adjacent signal. WebSocket was designed for exactly this shape of traffic.
Notifications and live feeds: Server-Sent Events
Notifications, activity feeds, order-status trackers, and live scores share one shape: the server knows, the user watches. That is a one-way street, and SSE covers it with less code and less infrastructure than WebSocket — no handshake upgrade, no custom reconnect logic, and events that can be replayed after a dropped connection thanks to event IDs. This is also the pattern behind many "AI is typing" interfaces, where tokens stream from server to browser and the user's only write is the initial request.
Dashboards and metrics: SSE, or honest polling
A dashboard updating every second or faster belongs on SSE for the same reasons as a feed. But slow down and be honest about the data: if the numbers change every few minutes, a 60-second poll is simpler, cache-friendly, and kind to your connection budget. Holding ten thousand connections open for a chart that changes twice an hour is engineering theater.
Rare updates and constrained environments: long polling
Long polling remains the right answer when updates are infrequent but should still feel prompt, when the stack cannot hold long-lived streaming responses, or when the client is not a browser at all — a script, a device, a partner integration that only speaks request–response. It is also the fallback that keeps working through the strictest corporate proxies, because to the network it is indistinguishable from slow web traffic.

A short case study: one product, two answers
Consider a customer-support platform. The agent inbox is live in both directions: new tickets arrive, agents reply, assign, and annotate, and everyone sees who is online. That half of the product runs on WebSocket, and the team pays the operational price gladly — connection state, a Redis backplane, a reconnect protocol with message replay — because the feature is a conversation.
The same company's public status page serves thousands of anonymous visitors during an incident. Nothing travels from visitor to server; the page only announces updates. That half runs on SSE: one stream per visitor, automatic reconnection during network blips, replay of anything missed. The deciding questions were never benchmarks. They were direction (two-way vs one-way) and audience (dozens of agents vs thousands of viewers). Same company, same week, two different right answers — chosen per feature, not per stack.
Common mistakes
- Reaching for WebSocket because it feels modern. One-way data over WebSocket means you hand-build reconnection, heartbeats, and replay for a feature SSE would have covered natively.
- Forgetting heartbeats. The stream works on your machine, then dies after a minute behind the production load balancer. Idle timeouts are the default, not the exception.
- Letting a proxy buffer SSE. With response buffering on, events accumulate at the proxy and flush in bursts — or never. Disable buffering on the stream endpoint and verify through the real production path, not just localhost.
- Synchronized reconnect storms. After a deploy or outage, thousands of clients retrying on the same fixed interval will hammer a recovering server. Use exponential backoff with random jitter.
- Assuming a socket is a delivery guarantee. A WebSocket message sent while a phone was in a tunnel is simply gone. If loss matters, number your messages and let clients request a replay — the pattern SSE builds in with
Last-Event-ID. - Polling every second "because it is simple". At 10,000 connected users that is 10,000 requests per second, most answering "nothing changed". Simplicity at the endpoint can be complexity at the fleet level.
Best practices
- Decide direction and frequency first. Write down how often data changes and who initiates. Most transport arguments end right there.
- Give every event an ID on every transport. IDs let clients resume after a drop instead of refetching the world, and they make duplicates detectable.
- Centralize reconnect policy. One backoff implementation with jitter, shared by every live feature, beats five hand-rolled variants.
- Instrument connections and message rates from day one. Open connections and messages per second are the two numbers that predict your scaling work months in advance.
- Keep an HTTP fallback. A plain polling path saves you the day a customer's network blocks everything else, and it doubles as a debugging tool.
FAQ
Is WebSocket faster than Server-Sent Events?
For a single message, barely. Once a connection is open, both push data with comparable latency. WebSocket's leaner framing and binary support matter at high message rates — hundreds per second — not at a few notifications per minute.
Can SSE send data from the client to the server?
Not over the event stream; it is one-way by design. The client sends data with ordinary fetch requests alongside the stream. Most push-style features need so little upstream traffic that this split is a non-issue.
Why do my SSE events arrive in bursts, or not at all, in production?
Usually a proxy is buffering the response, or middleware is holding it for compression. Turn off buffering for the stream endpoint (nginx honors the X-Accel-Buffering: no response header), flush after every event, and send heartbeat comments so nothing in the path treats the connection as idle and dead.
Should my whole app move to WebSocket once I add chat?
No. Pages, forms, uploads, and cacheable reads belong on HTTP, where caching, CDNs, and simple semantics live — the concerns covered in the REST API design guide. Reserve WebSocket for the slices of the app that are genuinely conversational.
What about WebTransport and HTTP/3?
WebTransport adds streams and datagrams over HTTP/3 and looks promising for games and media. Browser support and operational tooling are still maturing, though, so WebSocket, SSE, and long polling remain the transports that production systems actually run on today.
Conclusion
Real-time features are not one problem, and they do not have one answer. Long polling is the universal fallback: stateless, proxy-proof, and wasteful in proportion to how live your data is. Server-Sent Events is the specialist for one-way streams, with reconnection and replay handled by the browser. WebSocket is the general-purpose conversation protocol, powerful exactly where traffic is two-way and demanding everywhere else.
Before your next live feature, answer the two questions from the flowchart — which direction, how often — and let the answers pick the transport. Then give your events IDs, add heartbeats and jittered reconnects, and the feature will still feel live when it has ten thousand users instead of ten.


