WebSockets vs Server-Sent Events vs Long Polling
WebSockets, Server-Sent Events and long polling all push data to the browser. Here is how they compare on latency, scaling and proxies, and which one to pick.

10 min read · Last updated: October 25, 2026
For WebSockets vs Server-Sent Events, the short answer is this. Use Server-Sent Events (SSE) when only the server needs to push: feeds, notifications, AI token streams. Use WebSockets when both sides talk constantly: chat, multiplayer games, collaborative editing. Keep long polling as the fallback for networks where neither of the other two gets through.
Came here from Instagram, LinkedIn, X or YouTube? Here is the full source list you came for. Everything below is built from these, nothing else.
The specs
- RFC 6455: The WebSocket Protocol, IETF, December 2011
- RFC 6202: Known Issues and Best Practices for Long Polling and Streaming, IETF, April 2011
- RFC 8441: Bootstrapping WebSockets with HTTP/2, IETF, September 2018
- RFC 9220: Bootstrapping WebSockets with HTTP/3, IETF, June 2022
- HTML Living Standard: Server-sent events, WHATWG
The docs
- MDN: Using server-sent events
- MDN: EventSource
- MDN: WebSocket
- MDN: Using WebRTC data channels
- Socket.IO v4: Introduction
The infrastructure
Key takeaways
- SSE is plain HTTP, one direction, and reconnects by itself. WebSockets are a separate protocol (RFC 6455), two-way, and you write the reconnect logic.
- Over HTTP/1.1, browsers cap SSE at 6 open connections per browser and domain, according to MDN. Over HTTP/2 the default is 100 streams.
- Long polling has a worst-case latency of over three network transits per message, according to RFC 6202.
- Close code 1006 is never sent by a server. Your browser reports it when the connection died without a Close frame.
- nginx closes a proxied connection after 60 seconds without data by default. Send keepalives or raise
proxy_read_timeout. - Socket.IO is not a WebSocket implementation. A plain WebSocket client cannot connect to a Socket.IO server.
WebSockets vs Server-Sent Events vs long polling: the comparison table
All three let a server send data to a browser without the user refreshing. They differ in direction, protocol and how they survive the network. Long polling repeats ordinary HTTP requests. SSE holds one HTTP response open forever. WebSockets upgrade the connection to a separate two-way protocol. Every other difference follows from that.
| Long polling | Server-Sent Events | WebSockets | |
|---|---|---|---|
| Direction | Server to client, client uses normal requests | Server to client only | Both ways, full duplex |
| Protocol | Plain HTTP request and response | Plain HTTP, text/event-stream | ws:// or wss:// after an HTTP 101 upgrade |
| Worst-case latency | Over 3 network transits per message (RFC 6202) | No new request per message | No new request per message |
| Data format | Anything HTTP carries | UTF-8 text only | Text and binary |
| Reconnect | You write it | Built in, resumes with Last-Event-ID | You write it |
| Browser API | fetch or XHR | EventSource, Baseline since January 2020 | WebSocket, Baseline since July 2015 |
| HTTP/1.1 limit | Shares the per-domain connection pool | 6 per browser and domain | Not an HTTP request after the upgrade |
| Over HTTP/2 | Yes | Yes, up to 100 streams by default | Only if the server supports RFC 8441 |
| Proxy needs | Works with most proxies (RFC 6202) | Buffering off, keepalive comments | Upgrade headers forwarded, pings |
The Baseline dates come from MDN's browser compatibility data. Both APIs work in every current major browser.
What is the difference between WebSockets and SSE?
WebSockets give you a two-way channel over their own protocol, defined in RFC 6455 in December 2011. Server-Sent Events give you a one-way stream from server to browser over ordinary HTTP, defined in the WHATWG HTML standard. SSE is simpler to deploy and reconnects on its own. WebSockets win once the client needs to send frequent messages.
A WebSocket starts life as an HTTP request carrying an Upgrade: websocket header. The server answers HTTP/1.1 101 Switching Protocols, and from then on the connection is no longer HTTP. Both sides send frames, text or binary, whenever they like. RFC 6455 also requires the client to mask every frame it sends to the server.
SSE never leaves HTTP. The server responds with Content-Type: text/event-stream and simply never finishes the response. Each message is a few lines of text (event, data, id, retry) followed by a blank line. MDN notes that a line starting with a colon is a comment, which is how you send keepalives.
The part people underrate is reconnection. If an SSE stream drops, the browser waits (the spec suggests "probably in the region of a few seconds") and reconnects by itself, sending the last id it saw in a Last-Event-ID header. Your server reads it and replays what was missed. The server can change the wait with the retry field, or stop reconnection entirely with an HTTP 204.
If you have read our take on why a private API can skip HTTP status codes, SSE is the exception: that 204 is how you tell the browser to give up.
Here is a complete SSE endpoint in Node.js with resume support and keepalives:
import http from "node:http";
http.createServer((req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
});
let id = Number(req.headers["last-event-id"] ?? 0);
const tick = setInterval(() => {
id++;
res.write(`id: ${id}
data: ${JSON.stringify({ time: Date.now() })}
`);
}, 1000);
const keepalive = setInterval(() => res.write(`: ping
`), 15000);
req.on("close", () => {
clearInterval(tick);
clearInterval(keepalive);
});
}).listen(3000);
And the browser side is two lines:
const source = new EventSource("/events");
source.onmessage = (e) => console.log(JSON.parse(e.data));
The trade-off: SSE is UTF-8 text only. The spec gives you no way to choose another encoding, and there is no binary mode. If the server needs to push binary data, use a WebSocket.
What is the difference between WebSocket and long polling?
Long polling fakes server push with normal HTTP. The client sends a request, the server holds it open until there is data or a timeout, responds, and the client immediately asks again. A WebSocket keeps one connection open, so messages flow both ways with no new request. RFC 6202 puts long polling's worst-case latency at over three network transits.
RFC 6202, the IETF's own write-up of long polling from April 2011, lists the costs plainly:
- Header overhead. Every message is a full HTTP request and response, headers included. That hurts most when messages are small and frequent.
- Latency. "The maximal latency is over three network transits": the response, the new request, then the next response.
- Held resources. "For each client both a TCP/IP connection and an HTTP request are held open."
- Timeouts. Experiments succeeded with timeouts up to 120 seconds, "but generally 30 seconds is a safer value."
So why does it still exist? Because "most proxies work well with HTTP long polling," in the RFC's words. Each poll is an ordinary request that completes. Nothing on the network has to understand a new protocol or tolerate an endless response. That makes it the transport of last resort, and it is exactly why Socket.IO keeps it as a fallback.
Which one scales better?
All three hold one long-lived connection per connected client, so open connections are your ceiling whichever you pick. The differences sit around that. Long polling pays full HTTP headers and a new request on every message. SSE pays nothing extra per message but hits a 6-connection browser cap on HTTP/1.1. WebSockets need every proxy in the path configured to carry them.
The SSE cap catches people in production. MDN says that without HTTP/2 the limit is per browser and domain, "set to a very low number (6)," and that the issue is marked "Won't fix" in Chrome and Firefox. Open your dashboard in seven tabs and the seventh hangs. Two fixes:
- Serve SSE over HTTP/2. Streams share one connection, and the default limit is 100 negotiated streams.
- Share one stream across tabs. The WHATWG spec itself suggests a shared worker so every page reads from a single
EventSource.
WebSockets have their own scaling trap on the client. MDN warns that the WebSocket API "has no way to apply backpressure". If messages arrive faster than your code handles them, the page buffers until memory fills or the CPU pins at 100%. MDN points to WebSocketStream as the alternative that applies backpressure automatically.
On the server side, the hard part is the same for all three: a message for user A has to reach whichever machine holds user A's connection. That is a fan-out problem, and it is the core of how WhatsApp handles billions of messages.
How do proxies and load balancers treat each one?
Proxies are where real-time features usually break. nginx does not forward the Upgrade header by default because it is hop-by-hop, so WebSockets fail until you pass it explicitly. nginx also buffers proxied responses by default, which holds SSE events back. Both connection types get closed after 60 seconds without data.
This config handles both:
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 /events {
proxy_pass http://backend;
proxy_buffering off;
proxy_read_timeout 300s;
}
A few details from the nginx docs worth knowing:
proxy_http_version 1.1is only required on nginx versions before 1.29.7.proxy_read_timeoutdefaults to 60 seconds and is measured between two reads, not across the whole response. Any data resets it.- Instead of a longer timeout, the backend can send WebSocket ping frames to keep the connection alive.
- The backend can switch buffering off per response with an
X-Accel-Buffering: noheader, which is why it sits in the SSE code above.
The SSE spec adds its own warning: "Legacy proxy servers are known to, in certain cases, drop HTTP connections after a short timeout." Its recommendation is a comment line roughly every 15 seconds. It also notes that HTTP chunking can hurt the reliability of event streams.
Socket.IO vs WebSocket: what is the difference?
WebSocket is a protocol and a browser API. Socket.IO is a library that uses WebSocket as its main transport and adds its own protocol on top: long-polling fallback, automatic reconnection with exponential back-off, packet buffering, acknowledgements, broadcasting to rooms, and namespaces. Because it adds metadata to each packet, plain WebSocket clients and Socket.IO servers cannot talk to each other.
The Socket.IO docs are blunt about it: "Socket.IO is NOT a WebSocket implementation." The overhead is small. Their example socket.emit("hello", "world") goes over the wire as one WebSocket frame containing 42["hello","world"].
| You need | Plain WebSocket | Socket.IO |
|---|---|---|
| Reconnect with back-off | Write it yourself | Built in |
| Fallback when WebSocket is blocked | None | HTTP long polling |
| Messages queued while offline | Write it yourself | Packet buffering |
| Request and reply | Write it yourself | Acknowledgements with timeout |
| Rooms and broadcast | Write it yourself | Built in |
| Talk to any standard WebSocket client | Yes | No |
Pick Socket.IO when both ends are yours and you would otherwise rebuild that list by hand. Pick plain WebSocket when the other end is a client you do not control.
Does HTTP/2 replace WebSockets?
No. HTTP/2 makes SSE better, because many event streams share one connection instead of eating the 6-connection limit. But HTTP/2 Server Push was a resource preloading feature, not a messaging channel, and Chrome 106 disabled it by default. WebSockets were instead adapted to run over HTTP/2 by RFC 8441 and over HTTP/3 by RFC 9220.
Chrome's write-up on removing push cites low adoption (about 1.25% of HTTP/2 sites used it) and mixed performance results, and recommends 103 Early Hints and preload instead. Neither is a way to stream application data.
RFC 8441, from September 2018, lets a WebSocket open on a single HTTP/2 stream using an extended CONNECT request with a :protocol pseudo-header set to websocket. The server has to opt in with the SETTINGS_ENABLE_CONNECT_PROTOCOL setting. RFC 9220 did the same for HTTP/3 in June 2022. So the realistic answer for 2026: HTTP/2 helps SSE, and WebSockets still exist, now riding on newer HTTP when your server supports it.
When does WebRTC fit instead?
WebRTC data channels fit when two browsers should talk directly. MDN describes them as a way to "securely exchange arbitrary data" where "the data never passes through the web or application server." Everything is encrypted with DTLS. Use them for peer-to-peer file transfer, games and calls, not for server push.
Two practical limits from MDN. Keep messages small: if the max-message-size attribute is missing from the SDP, 64 kilobytes is assumed. And the peers still have to find each other first, which MDN describes as negotiating "out-of-band, using a web server or other means." In practice that signalling often runs over the WebSocket you already have.
What does WebSocket close code 1006 mean?
Close code 1006 means the connection closed abnormally, "without sending or receiving a Close control frame." RFC 6455 reserves it: an endpoint "MUST NOT" send 1006 in a Close frame. Your browser reports it locally when the connection simply died. The cause is somewhere in the path, not in your close handler.
| Code | Meaning in RFC 6455 | Reconnect? |
|---|---|---|
| 1000 | Normal closure, the purpose was fulfilled | Usually no |
| 1001 | Going away: server shutting down or page navigated away | Yes, after a delay |
| 1006 | Closed abnormally, no Close frame, set by the browser | Yes, with backoff |
| 1011 | Server hit an unexpected condition | Yes, with backoff |
When I see 1006, I check these in order:
- Did it die at exactly 60 seconds of silence? That is nginx's default
proxy_read_timeout. Add pings or raise it. - Did the handshake ever return 101? If a proxy did not forward
UpgradeandConnection, the socket never opened. - Did the server process restart or crash? Check its logs for the same timestamp.
- Did the client change network? Laptop sleep or Wi-Fi to mobile drops the TCP connection with no Close frame.
How do you reconnect a WebSocket automatically?
Listen for the close event and open a new WebSocket after a delay. RFC 6455 says the first attempt should wait a random amount of time, calling 0 to 5 seconds "a reasonable initial delay," and each failed attempt after that should wait longer, using something like truncated binary exponential backoff. The randomness stops every client reconnecting in the same second.
function connect(url, onMessage, attempt = 0) {
const ws = new WebSocket(url);
ws.onopen = () => { attempt = 0; };
ws.onmessage = (e) => onMessage(e.data);
ws.onclose = (e) => {
if (e.code === 1000) return;
const delay = Math.random() * Math.min(30000, 5000 * 2 ** attempt);
setTimeout(() => connect(url, onMessage, attempt + 1), delay);
};
}
The first retry waits 0 to 5 seconds, the next up to 10, then 20, capped at 30. A successful open resets the counter.
Reconnecting the socket is half the job. When it comes back, resend whatever subscriptions the client had, and ask the server for anything missed since the last message ID you processed. SSE does that second step for you with Last-Event-ID. With WebSockets you build it, or let Socket.IO's packet buffering cover the client side.
How do you debug WebSockets in Chrome?
Chrome DevTools shows every WebSocket message in the Network panel. Filter by WS, click the connection, and open the Messages tab. It lists the last 100 messages with payload, size and time. For Server-Sent Events, select the request and open the EventStream tab instead.
- Open DevTools and go to the Network panel.
- Click WS in the Filters bar.
- Click the URL of the WebSocket connection in the Name column.
- Check Headers first. The status should be
101 Switching Protocols. Anything else means the upgrade failed. - Open the Messages tab. Outgoing text messages are light green, incoming are white, opcodes are light yellow, and errors are light red.
- Click the connection name again to refresh the table.
For SSE, the EventStream tab works for EventSource, Fetch and XHR streams, and you can filter events with a regular expression.
Which should you pick?
Start with the direction of your data. If only the server talks, pick SSE. If both sides talk often, pick WebSockets. Reach for Socket.IO when you want rooms, acknowledgements and reconnection without writing them, WebRTC when two browsers should talk directly, and long polling only when the network blocks everything else.
| Your situation | Pick | Why |
|---|---|---|
| Streaming AI tokens into a chat UI | SSE | One direction, text, built-in reconnect |
| Notifications, live scores, dashboards | SSE | Server push only, plain HTTP |
| Chat with typing indicators | WebSockets or Socket.IO | Constant traffic both ways |
| Multiplayer game or collaborative editor | WebSockets | Low latency both ways, binary frames |
| Rooms, acks and fallback without writing them | Socket.IO | All built in, if you own both ends |
| Server pushes binary data | WebSockets | SSE is UTF-8 text only |
| Peer-to-peer files, video or games | WebRTC data channels | Data skips your server |
| Rare updates on locked-down networks | Long polling | Works through most proxies |
The one-line version: SSE until the client needs to talk back, then WebSockets.
If this is for an interview rather than production, this exact choice shows up in chat and feed designs. Our guide on why most developers fail system design interviews covers how to defend it out loud.
FAQ
Is SSE faster than WebSockets?
For server-to-client messages on an open connection, neither needs a new request per message, so the gap is small. The real differences are elsewhere: SSE is text only and runs over plain HTTP, while WebSockets carry binary data and let the client send on the same connection. Long polling is the slow one, at over three network transits worst case.
Can SSE send data from the client to the server?
No. An EventSource only receives. The client sends data with ordinary HTTP requests, such as a fetch POST, alongside the stream. For many apps, including AI chat, that split is all you need.
Do I still need long polling?
Rarely as your main transport. It is useful as a fallback when a network blocks WebSocket upgrades or streaming responses, because each poll is an ordinary HTTP request that completes. Socket.IO already falls back to it automatically.
Why does my WebSocket disconnect after exactly 60 seconds?
Almost always a proxy timeout. nginx closes a proxied connection when the backend sends nothing for 60 seconds, the default proxy_read_timeout. Have the server send ping frames, or raise the timeout in the location block that proxies the socket.
Open DevTools on your own app today, filter by WS, and check what close codes you are getting. Then try the free tools on Art of Code, and if you want the large-scale version of this problem, read how WhatsApp handles billions of messages.
Written by Adithya Guttha, Founder of Art of Code.
Sign in to like and save posts.
Keep reading



Discussion
Comments (0)
Want to join the conversation?
Sign in to commentNo comments yet. Be the first to comment!