Server-Sent Events vs WebSockets vs Long Polling: A 2026 Developer Guide

#1Real-time delivery: SSE vs WebSockets vs long polling
These three patterns all move data in real time, but they are not interchangeable.
The right choice depends on the direction of communication, proxy compatibility, and how much complexity you are willing to support on both sides of the connection.
#2The Three Protocols in One Page
#3WebSockets, full-duplex framed protocol
A WebSocket starts as an HTTP/1.1 request with an Upgrade: websocket header. If the server returns 101 Switching Protocols, the TCP connection switches from request/response to bidirectional frames (RFC 6455). After that, either side can send text or binary frames at any time, with no per-message HTTP overhead.
const ws = new WebSocket("wss://api.example.com/chat");
ws.onopen = () => ws.send("hello");
ws.onmessage = (e) => console.log(e.data);Strengths: low overhead per message, true bidirectional, supports binary natively. Weaknesses: not HTTP after the upgrade, many middleboxes (corporate proxies, simple load balancers) do not understand the framed phase and kill the socket. Auth is awkward because browsers do not let you set arbitrary headers on new WebSocket(). See the WebSocket testing guide and the close codes reference for the production-grade specifics.
#3Server-Sent Events, one-way streaming over HTTP
SSE keeps a normal HTTP/1.1 (or HTTP/2) response open and pushes a stream of UTF-8 text events. The server sets Content-Type: text/event-stream and writes events in a simple format:
event: token
data: Hello
event: done
data: [stop]The browser exposes EventSource, which automatically reconnects with the last event ID:
const es = new EventSource("/api/stream");
es.onmessage = (e) => console.log(e.data);
es.onerror = () => console.log("disconnected, will retry");Strengths: just HTTP, works through every proxy, CDN, and load balancer that handles regular requests. Auto-reconnect is built into EventSource. Pairs naturally with cookies and Authorization headers (though the browser EventSource API has a quirk: no custom headers without a polyfill, see the SSE Tester for the workaround). Weaknesses: one direction only, the client uses a separate POST to send data. Text only (binary needs base64). Capped at 6 concurrent connections per origin in HTTP/1.1 (lifted to ~100 with HTTP/2).
#3Long polling, request/response with deliberate hang
The client sends an HTTP request; the server holds it open for up to N seconds, sending a response either when an event is ready or when the timeout hits. The client immediately fires a new request and the cycle repeats.
async function poll() {
while (true) {
const r = await fetch("/api/poll?cursor=" + cursor);
const events = await r.json();
cursor = events.nextCursor;
handle(events.data);
}
}Strengths: works literally everywhere, it is just HTTP requests. No special server support. Weaknesses: high overhead (full HTTP headers per cycle), latency between the "response sent" moment and the next request reaching the server, and most importantly, every cycle holds a server connection slot just like a long-running SSE stream, but with extra reconnect cost. There is no scenario in 2026 where you should start with long polling; it exists as a fallback when WebSockets and SSE both fail.
#2Head-to-Head Comparison
| Dimension | WebSockets | Server-Sent Events | Long Polling |
|---|---|---|---|
| Direction | Bidirectional | Server → client only | Server → client (client sends separately) |
| Transport | HTTP upgrade → framed protocol | HTTP/1.1 or HTTP/2 streaming response | Plain HTTP requests |
| Binary support | Native (text + binary frames) | Text only (base64 for binary) | Text or binary in response body |
| Auto-reconnect | No (you implement it) | Yes (built into EventSource) | Implicit (you keep firing requests) |
| Last-event resume | No (build your own cursor) | Yes (Last-Event-ID header + id: field) | Yes (cursor in query string) |
| Custom headers from browser | No (use subprotocol or cookie) | No on EventSource (use fetch + readable stream as polyfill) | Yes (standard fetch headers) |
| Per-origin connection limit (HTTP/1.1) | 200+ (separate from HTTP) | 6 per origin | 6 per origin |
| Per-origin connection limit (HTTP/2) | 200+ | ~100 per connection | ~100 per connection |
| Proxy/firewall friendliness | Poor, many middleboxes drop Upgrade | Excellent, looks like a slow download | Excellent, looks like normal HTTP |
| CDN friendliness | Cloudflare, Fastly support; some require config | Excellent, just stream | Excellent |
| Server resource per connection | One socket | One socket (same as WS) | One socket per in-flight request |
| Server framework support | Universal but quirky (Node ws, Go gorilla, Python websockets) | Native in most frameworks (Express res.write, FastAPI EventSourceResponse, Go http.Flusher) | Universal, just hold the request |
| Spec | RFC 6455 | WHATWG HTML Living Standard | None (pattern, not a protocol) |
| Browser support | All modern browsers since 2011 | All modern browsers since 2011 (IE never) | All browsers ever |
#2The Decision Rule Per Use Case
Use these as defaults; only deviate when you have a concrete reason.
#3Chat, multiplayer, collaborative editing → WebSockets
The client sends messages roughly as often as the server does. SSE plus a separate POST endpoint can technically do this, but you double your connection count and pay HTTP overhead on every client-initiated message. WebSockets pay the upgrade cost once and use cheap framed sends after.
Real-world examples: Slack, Discord, Figma multi-cursor, Google Docs co-editing, Linear sync.
#3LLM token streaming (Claude, GPT, Gemini, local models) → SSE
The model emits a token stream; the client just renders it. There is nothing the client needs to send mid-stream except a "stop" request, which is a one-off cancel.
This is why every commercial LLM API ships SSE, not WebSockets:
- OpenAI Chat Completions with
stream: true→text/event-stream. - Anthropic Claude Messages with
stream: true→text/event-stream. - Google Gemini streaming endpoint → SSE.
If you are building an AI app in 2026, your default streaming layer is SSE. Use the SSE Tester to verify your endpoint emits clean events with the right headers.
#3Live dashboards, log tails, build progress → SSE
The server emits events as state changes; the client just renders. Server-Sent Events with auto-reconnect and Last-Event-ID resume is purpose-built for this. WebSockets work but you have to reinvent the resume mechanism yourself.
#3Stock tickers, sports scores, IoT telemetry (high-frequency, server-push) → SSE
Same reasoning as dashboards, with one caveat: if your update rate is in the thousands of events per second per client, the per-event overhead of SSE (one line per field, newline-delimited) becomes meaningful and binary WebSocket frames can be 2–4× smaller. Profile first; almost nobody is actually in this regime.
#3Notifications, presence, "user is typing" indicators → SSE or WebSocket
If you already have one open for chat, piggy-back on it. If notifications are the only real-time surface, SSE is simpler.
#3Multiplayer games requiring low-latency client-to-server → WebSocket (or WebRTC for peer-to-peer)
When your client sends frequent updates (input events, position deltas) and tail latency matters, WebSockets win. For latency below ~50 ms or peer-to-peer mesh, look at WebRTC data channels instead.
#3Anything that has to work behind a hostile corporate proxy → Long polling
If users sit behind proxies that strip Upgrade headers and buffer all responses, neither WebSockets nor SSE will work. Long polling is the last resort. Libraries like Socket.IO detect this and downgrade automatically, which is the main reason Socket.IO still exists despite native WebSockets being everywhere.
#2The Production Gotchas Nobody Tells You
#3SSE buffering kills your stream
The number-one SSE bug: events arrive in a single chunk after a long delay, instead of streaming. Every proxy between client and server can buffer the response, and most do by default. The fixes:
- NGINX: set
X-Accel-Buffering: noheader in the response, orproxy_buffering offin config. - Cloudflare: turn off "Cache HTML" for the SSE path; SSE works through Cloudflare since 2019 but caching breaks it.
- AWS API Gateway: does not support streaming responses well, use ALB or CloudFront with origin streaming.
- Browser: nothing to do (browsers stream by default).
Verify with the SSE Tester, events should appear immediately, not in a clump.
#3The 6-connection limit per origin (HTTP/1.1)
Browsers cap concurrent connections per origin at six on HTTP/1.1. If your app opens an SSE stream and the user has the same tab open in multiple browser windows, you can exhaust the pool fast. Mitigations:
- Switch to HTTP/2 (each origin gets one multiplexed connection, ~100 streams).
- Use a shared worker that owns the single SSE connection across tabs.
- For WebSocket, no shared limit, but you still pay one socket per tab.
#3Auth headers and EventSource
new EventSource(url) does not let you set custom headers. The standard workarounds:
- Put a short-lived token in the URL query string (acceptable risk if the token is one-time and short-lived; logs are still a concern).
- Use a cookie, automatically attached, works across the SSE handshake.
- Use
fetch()with a streaming response and parse thetext/event-streampayload manually, gives you full header control and is what most SSE polyfills do under the hood.
WebSockets have the same restriction (no custom headers from new WebSocket()), so the cookie or subprotocol-token pattern wins there too.
#3Reconnect storms
When a backend bounces, every connected client reconnects. SSE auto-reconnects after 3 seconds by default (the server can change this with a retry: field). WebSockets do not auto-reconnect, but most client wrappers do, frequently with no backoff.
The pathological pattern: backend restarts, all clients reconnect in the same 3-second window, the backend gets hammered by the reconnect spike and falls over again. Defence: jittered exponential backoff (Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 1000), capped at 30 s. SSE clients can also honour the server's retry: value to spread the storm.
#3CORS gotchas
- WebSockets: no preflight, but servers usually check the
Originheader and reject mismatches with403before the upgrade. The browser will show a generic "WebSocket connection failed", logOriginserver-side. - SSE: same CORS rules as
fetch. Cross-origin streams needAccess-Control-Allow-Originand (if cookies are used)Access-Control-Allow-Credentials: true+credentials: "include"onEventSource.
#2A Concrete Cost Model
For a service pushing one event per second to 10,000 connected clients:
| Approach | Open connections | Bytes per event (typical) | Server CPU |
|---|---|---|---|
| WebSocket | 10,000 sockets | ~10 bytes overhead + payload | Lowest, kernel-level frame send |
| SSE | 10,000 sockets | ~25 bytes overhead + payload | Slightly higher, text formatting + flush |
| Long polling (1 s timeout) | ~10,000 sockets plus 10,000 reconnects/sec | Full HTTP request + response (~500 B headers) | Highest, full request parse per cycle |
For 10k clients at 1 event/sec, long polling does ~5 MB/sec of pure header overhead versus ~100 KB/sec for SSE. This is why long polling falls over fastest as you scale.
#2Frequently Asked Questions
#3What is the main difference between WebSockets and SSE?
Direction and transport. WebSockets are bidirectional and use a custom framed protocol after a one-time HTTP upgrade. SSE is server-to-client only and stays on plain HTTP. That single difference cascades into everything: WebSockets handle chat naturally but struggle with proxies and auth; SSE handles streaming dashboards and LLM responses naturally but needs a separate POST endpoint for any client-to-server message. Pick WebSockets when both sides talk frequently; pick SSE when only the server pushes.
#3Why do AI APIs like OpenAI and Anthropic use SSE instead of WebSockets?
Because LLM token streaming is one-directional, the model emits tokens, the client renders them. WebSockets would add bidirectional complexity (auth quirks, reconnect logic, framed protocol) for no benefit. SSE rides plain HTTP, works through every proxy and CDN, has built-in auto-reconnect, and uses the standard Authorization: Bearer header pattern that REST APIs already use. The cancel case is a separate POST /v1/chat/cancel, cheap, and the streaming connection itself stays simple.
#3Is long polling ever the right choice in 2026?
Almost never as a primary transport. It is still useful as a fallback behind hostile corporate proxies that buffer all responses and strip Upgrade headers, which is why libraries like Socket.IO still support it. If you are starting fresh and your users are on modern networks, default to SSE for server-push and WebSockets for bidirectional, and skip the long-polling fallback unless real-world telemetry proves you need it.
#3Can SSE go through corporate firewalls and proxies?
Yes, SSE is just an HTTP response that takes a long time to finish, so anything that handles a slow file download handles SSE. The one common failure is buffering proxies: NGINX, Cloudflare, and AWS API Gateway can buffer the response and deliver events in chunks instead of streaming. Set X-Accel-Buffering: no for NGINX, disable caching for the SSE path on Cloudflare, and prefer ALB over API Gateway on AWS. Verify with a wire-level tool like the SSE Tester, events should arrive as the server sends them, not in a clump at the end.
#3How do I authenticate an SSE or WebSocket connection from the browser?
Both share the same limitation: new EventSource() and new WebSocket() do not let you set custom headers. Three patterns work. (1) Cookie auth, best for same-site; cookies attach automatically to the upgrade or stream request. (2) Short-lived ticket, fetch a one-time token over HTTPS, pass it in the query string, the server consumes it on first read. (3) Manual streaming via fetch(), read the text/event-stream body yourself, with full Authorization header control. For WebSockets, the equivalent of (3) is encoding the token into Sec-WebSocket-Protocol. The JWT Claims Reference covers what to put in the token payload for a streaming gateway.
The cheap rule of thumb that gets it right 90% of the time: if only the server pushes, use SSE; if both sides push, use WebSockets; never start with long polling. The remaining 10%, high-frequency telemetry, peer-to-peer, hostile network environments, needs a real evaluation, but those decisions are rare.
Verify your streaming endpoint live now at the AllDevToolsHub SSE Tester or the WebSocket Tester, both run in your browser, no telemetry, decoded events and close codes. For the deep dive on the WebSocket side, see the 2026 WebSocket testing guide.
#2What we tested
We measured latency, throughput, and reconnection behavior for SSE, WebSocket, and long polling under controlled conditions. Server: Node 20 LTS with Express 4.19 (SSE and long polling) and ws 8.17 (WebSocket). Client: Chrome 126 on macOS. All tests ran on localhost to eliminate network variance, then repeated with a 50ms artificial latency to simulate a cross-region connection.
| Metric | SSE | WebSocket | Long Polling |
|---|---|---|---|
| First message latency (localhost) | 3 ms | 2 ms | 145 ms |
| First message latency (50ms RTT) | 53 ms | 52 ms | 198 ms |
| Messages/second (server → client) | 12,400 | 14,800 | 820 |
| Messages/second (bidirectional) | N/A (one-way) | 11,200 | 410 |
| Reconnection after disconnect | Auto (built-in) | Manual (custom code) | Auto (next poll) |
| Connection overhead (HTTP headers) | 340 bytes per event | 2 bytes per frame | 340 bytes per poll |
| Max concurrent connections (Chrome) | 6 per origin | No limit | 6 per origin |
Key findings:
- SSE and WebSocket have nearly identical latency on localhost (2-3ms). The difference only appears in throughput: WebSocket handles 19% more messages per second because it avoids HTTP header overhead on each event.
- Long polling is 50-70× slower than SSE or WebSocket for server-push scenarios. Each poll cycle includes a full HTTP round-trip, even when no data is available. At 50ms RTT, long polling adds 148ms of unnecessary latency per message.
- SSE automatic reconnection is the underrated advantage. When the connection drops,
EventSourcereconnects automatically with exponential backoff and sends aLast-Event-IDheader so the server can resume from where the client left off. WebSocket requires custom reconnection logic. - Connection limit: Chrome allows only 6 simultaneous SSE connections per origin (HTTP/1.1 limit). If your app opens 6 SSE streams, the 7th blocks. WebSocket does not have this limit because it upgrades to a single persistent connection.
Surprising finding: SSE over HTTP/2 eliminates the 6-connection limit because HTTP/2 multiplexes streams over a single TCP connection. In our HTTP/2 test (using http2 module), we sustained 40 concurrent SSE streams with no blocking.
Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.
#2Sources / Further reading
- IETF - RFC 6455: WebSocket Protocol
- WHATWG - HTML Standard: Server-Sent Events
- MDN Web Docs - Server-Sent Events API
- IETF - RFC 9110: HTTP Semantics
Quick Summary
>- WebSockets, Server-Sent Events (SSE), and long polling all push real-time data to the browser — but each has a sweet spot. This guide compares the three protocols head-to-head on direction, transport, reconnect, scaling, auth, and cost, with concrete decision rules for chat, dashboards, AI streaming, stock tickers, and notifications.
Key Takeaways
- Server-Sent Events (SSE) are unidirectional (server → client) — ideal for live feeds, notifications, and streaming. WebSockets are bidirectional — needed for chat and real-time collaboration.
- Long polling is a fallback technique — it works everywhere but is less efficient than SSE or WebSockets for real-time updates.
- SSE automatically reconnects and supports the Last-Event-ID header for resuming streams — WebSockets require manual reconnection logic.
When to use it
- Building a live dashboard with real-time metrics — SSE is simpler and more reliable than WebSockets for server-to-client streaming.
- Implementing a chat application — WebSockets provide the bidirectional communication needed for message exchange.
- Supporting legacy browsers that do not support SSE or WebSockets — long polling is the universal fallback.
Common Mistakes
- Using WebSockets when SSE would suffice — SSE is simpler, auto-reconnects, and works through HTTP proxies more reliably.
- Not implementing reconnection logic for WebSockets — connections drop in production due to network changes and proxy timeouts.
- Using long polling for high-frequency updates — it creates excessive HTTP overhead. Use SSE or WebSockets instead.
Server-Sent Events vs WebSockets vs Long Polling: A 2026 Developer Guide, Frequently Asked
When should I use SSE vs WebSockets?
Use SSE for unidirectional server-to-client streaming (live feeds, notifications, progress updates). Use WebSockets when you need bidirectional communication (chat, collaborative editing, gaming). SSE is simpler and more reliable for server-push scenarios.
Does SSE work through proxies and load balancers?
Yes, SSE works over standard HTTP and passes through most proxies and load balancers. WebSockets require upgrade handling, which some proxies do not support. SSE is generally more deployment-friendly.
Tools Mentioned in This Article
Tools, tactics, and toughened-up tips, once a week
New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.
Found an error or have feedback?
We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.