WebSocket vs SSE vs Long Polling: Complete Guide 2026
Definitive comparison of WebSocket, Server-Sent Events (SSE), and Long Polling for real-time web apps. Performance benchmarks, code examples in JavaScript and Python, decision matrix, and migration paths.
Real-time communication is no longer optional. Users expect live notifications, collaborative editing, live dashboards, and instant chat β all without hitting refresh. The question isnβt whether to go real-time, but which protocol to use.
Three technologies dominate: WebSocket, Server-Sent Events (SSE), and Long Polling. Each solves the same problem differently, and picking the wrong one will haunt your architecture for years.
This guide cuts through the confusion with honest comparisons, working code examples, and a decision matrix you can use today.
How Each Protocol Works
Understanding the internals makes the tradeoffs obvious.
Long Polling
Long polling is HTTP β no new protocol, no browser APIs, just a clever use of existing infrastructure.
Client β Server: "Any updates?"
Server: [holds connection open, waiting...]
Server β Client: "Here's new data!" (after 30s)
Client β Server: "Any updates?" (immediately re-requests)
The client sends a request. The server holds it open until new data arrives (or a timeout hits). The client gets a response, processes it, and immediately fires another request. From the outside it looks real-time. Under the hood itβs a polling loop with patience.
Connection lifecycle:
- Client opens HTTP request
- Server blocks response until data is ready (or timeout ~30β60s)
- Server sends response
- Client processes data
- Client immediately opens new request
- Repeat forever
Server-Sent Events (SSE)
SSE is a W3C standard built on HTTP. One persistent connection, server pushes data to client whenever it wants. The client cannot send messages back through this channel.
Client β Server: GET /events (one-time)
Server β Client: data: {"type":"notification","msg":"Hello"}
Server β Client: data: {"type":"update","count":42}
Server β Client: [keeps connection open indefinitely]
The browserβs EventSource API handles reconnection automatically. Events are text-based (UTF-8). The protocol adds data:, event:, id:, and retry: fields.
SSE event format:
event: notification
data: {"message": "New order received", "orderId": "12345"}
id: 42
retry: 3000
(Note the blank line β it terminates each event)
WebSocket
WebSocket starts as an HTTP request (the handshake), then upgrades to a persistent, full-duplex TCP connection. Both sides can send data at any time, independently.
Client β Server: HTTP Upgrade request
Server β Client: 101 Switching Protocols
--- WebSocket connection established ---
Client β Server: bidirectional messages (any time, any direction)
Messages can be text or binary. Thereβs no built-in format β you define your own message schema. The connection stays alive until either side closes it.
Performance Comparison
Numbers matter. Hereβs an honest breakdown.
Latency
| Protocol | First Message Latency | Subsequent Message Latency |
|---|---|---|
| Long Polling | High (HTTP overhead per message) | High (new HTTP request each time) |
| SSE | Low (after initial connection) | Very low (persistent connection) |
| WebSocket | Low (after handshake) | Very low (persistent connection) |
Long polling adds 10β100ms of HTTP overhead per message cycle. SSE and WebSocket deliver subsequent messages at near-wire speed.
Throughput
WebSocket wins for high-frequency bidirectional traffic. SSE is efficient for server-to-client streams. Long polling degrades badly under load.
Rough throughput comparison (single server, 10k concurrent users):
| Protocol | Messages/sec | Server Memory | Connections |
|---|---|---|---|
| Long Polling | ~5,000 | High (thread per connection) | Many short-lived |
| SSE | ~50,000 | Medium | 10k persistent |
| WebSocket | ~100,000+ | Low (with async server) | 10k persistent |
Overhead per Message
Long polling HTTP headers add 500β800 bytes per message. WebSocket frames add 2β14 bytes. SSE adds ~5 bytes (data: prefix + newlines).
For a chat app sending 100 messages/second per user, WebSocket saves ~50KB/s in overhead per user compared to long polling.
Scalability
All three can scale horizontally, but the mechanism differs:
- Long Polling: Stateless HTTP β scales easily with any load balancer. No sticky sessions needed.
- SSE: Persistent connections require some stickiness or a pub/sub backend (Redis, etc.) to route messages to the right server.
- WebSocket: Same as SSE β stateful connections need sticky sessions or a message broker.
Code Examples
Long Polling in JavaScript + Node.js
Server (Node.js/Express):
const express = require('express');
const app = express();
let waitingClients = [];
let messages = [];
app.get('/poll', (req, res) => {
const lastId = parseInt(req.query.lastId) || 0;
const newMessages = messages.filter(m => m.id > lastId);
if (newMessages.length > 0) {
// Data available immediately
res.json({ messages: newMessages });
return;
}
// No data β hold the connection
const timeout = setTimeout(() => {
const idx = waitingClients.indexOf(res);
if (idx !== -1) waitingClients.splice(idx, 1);
res.json({ messages: [] }); // timeout, empty response
}, 30000);
waitingClients.push({ res, timeout });
});
app.post('/send', express.json(), (req, res) => {
const message = { id: messages.length + 1, text: req.body.text };
messages.push(message);
// Notify all waiting clients
waitingClients.forEach(({ res: clientRes, timeout }) => {
clearTimeout(timeout);
clientRes.json({ messages: [message] });
});
waitingClients = [];
res.sendStatus(200);
});
Client:
let lastId = 0;
async function poll() {
try {
const res = await fetch(`/poll?lastId=${lastId}`);
const data = await res.json();
data.messages.forEach(msg => {
lastId = Math.max(lastId, msg.id);
console.log('Received:', msg.text);
});
} catch (err) {
console.error('Poll failed:', err);
await new Promise(r => setTimeout(r, 1000)); // backoff on error
}
poll(); // immediately re-poll
}
poll(); // start the loop
Server-Sent Events in JavaScript + Python
Server (Python/FastAPI):
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import json
app = FastAPI()
subscribers = set()
async def event_stream(queue: asyncio.Queue):
try:
while True:
data = await queue.get()
yield f"data: {json.dumps(data)}\n\n"
except asyncio.CancelledError:
pass
@app.get("/events")
async def sse_endpoint():
queue = asyncio.Queue()
subscribers.add(queue)
async def cleanup():
subscribers.discard(queue)
return StreamingResponse(
event_stream(queue),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no", # disable nginx buffering
}
)
@app.post("/broadcast")
async def broadcast(message: dict):
for queue in subscribers.copy():
await queue.put(message)
return {"sent_to": len(subscribers)}
Client:
const source = new EventSource('/events');
source.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('SSE received:', data);
};
source.onerror = (err) => {
console.error('SSE error:', err);
// EventSource auto-reconnects β no manual handling needed
};
// Listen to named events
source.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
showToast(notification.message);
});
// Cleanup when done
function disconnect() {
source.close();
}
WebSocket in JavaScript + Node.js
Server (Node.js with ws):
const WebSocket = require('ws');
const http = require('http');
const server = http.createServer();
const wss = new WebSocket.Server({ server });
const clients = new Map();
wss.on('connection', (ws, req) => {
const clientId = Date.now().toString();
clients.set(clientId, ws);
console.log(`Client ${clientId} connected`);
ws.on('message', (rawData) => {
const message = JSON.parse(rawData);
switch (message.type) {
case 'chat':
// Broadcast to all clients
broadcast({ type: 'chat', from: clientId, text: message.text });
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong' }));
break;
}
});
ws.on('close', () => {
clients.delete(clientId);
console.log(`Client ${clientId} disconnected`);
});
ws.on('error', (err) => {
console.error(`Client ${clientId} error:`, err);
clients.delete(clientId);
});
// Send welcome message
ws.send(JSON.stringify({ type: 'connected', clientId }));
});
function broadcast(data, excludeId = null) {
const message = JSON.stringify(data);
clients.forEach((client, id) => {
if (id !== excludeId && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
server.listen(3000);
Client:
class ReliableWebSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectDelay = 1000;
this.maxDelay = 30000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectDelay = 1000; // reset backoff
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.onMessage(data);
};
this.ws.onclose = () => {
console.log(`Disconnected. Reconnecting in ${this.reconnectDelay}ms`);
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxDelay);
};
this.ws.onerror = (err) => {
console.error('WebSocket error:', err);
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
onMessage(data) {
// Override this
console.log('Received:', data);
}
}
const ws = new ReliableWebSocket('ws://localhost:3000');
ws.send({ type: 'chat', text: 'Hello!' });
Real-World Use Cases
Chat Applications
Best choice: WebSocket
Chat is bidirectional by nature. Users send messages, receive messages from others, see typing indicators, and get read receipts β all flowing both directions simultaneously.
SSE would require a separate HTTP endpoint for sending messages, which works but adds complexity. Long polling is viable for low-volume chat (Slack used it early on) but struggles at scale.
Live Notifications
Best choice: SSE
Notifications are inherently one-directional: server pushes to client. SSE is the simplest, most browser-native solution. The EventSource API auto-reconnects, handles Last-Event-ID for missed events, and integrates cleanly with HTTP infrastructure (CDNs, proxies, load balancers).
No WebSocket upgrade handshake, no binary framing β just HTTP streaming. Your existing CORS, authentication middleware, and load balancers work unchanged.
Live Dashboards
Best choice: SSE (for data display) or WebSocket (for interactive dashboards)
If the dashboard only displays server data (metrics, logs, analytics), SSE is perfect. One connection per client, server pushes updates.
If users can interact β filter data, change time ranges, trigger actions β WebSocket makes the bidirectional communication cleaner.
Collaborative Editing (Google Docs-style)
Best choice: WebSocket
Operational Transformation and CRDT algorithms require rapid, bidirectional state synchronization. A userβs edit must propagate to other users in milliseconds. WebSocketβs low overhead and persistent connection handle this well.
Financial Data / Trading
Best choice: WebSocket
High-frequency price feeds, order book updates, and trade execution all benefit from WebSocketβs minimal overhead and low latency. Many trading platforms use binary WebSocket frames (not JSON text) to squeeze out additional performance.
Legacy Browser Support
Best choice: Long Polling
If you must support IE 9 or environments where WebSocket is blocked by corporate proxies, long polling is the safe fallback. Libraries like Socket.io transparently fall back to long polling when WebSocket is unavailable.
Decision Matrix
| Scenario | Long Polling | SSE | WebSocket |
|---|---|---|---|
| Bidirectional communication | β | β | β |
| Server-to-client only | β | β (best) | β (overkill) |
| Simple HTTP infrastructure | β | β | β |
| CDN / proxy friendly | β | β | β |
| High message frequency (>10/sec) | β | β | β |
| Binary data transfer | β | β | β |
| Auto-reconnect built in | β | β | β |
| Low server memory per connection | β | β | β (async) |
| Works behind corporate proxies | β | β | Sometimes |
| Browser support | Universal | All modern + IE polyfill | All modern |
| Mobile battery efficiency | Poor | Good | Good |
Quick decision flowchart:
Does the client need to send messages?
βββ Yes β WebSocket
βββ No β Is the message rate > 5/sec?
βββ Yes β SSE
βββ No β Is proxy compatibility required?
βββ Yes β Long Polling
βββ No β SSE (simpler than WebSocket, good enough)
Migration Guide
Long Polling β SSE
SSE is a drop-in improvement for any server-to-client polling pattern.
Before (Long Polling):
// Client: repeated fetch loop
async function poll() {
const res = await fetch('/updates?since=' + lastEventId);
const data = await res.json();
processUpdates(data);
poll();
}
After (SSE):
// Client: single EventSource, no loop needed
const source = new EventSource('/stream');
source.onmessage = (e) => processUpdates(JSON.parse(e.data));
The server endpoint changes from returning JSON to streaming text/event-stream. The client loop disappears entirely.
Migration risk: Low. SSE works over HTTP/1.1 and HTTP/2. No new infrastructure needed. Roll out server-side first, then update client.
SSE β WebSocket
Necessary when you add client-to-server messaging.
Key changes:
- Replace
EventSourcewithWebSocket - Add message type routing on both client and server
- Implement reconnection logic (EventSource does this automatically; WebSocket does not)
- Update load balancer to support WebSocket upgrade (
Upgrade: websocketheader)
Migration risk: Medium. WebSocket requires infrastructure changes (proxies, load balancers must be configured). Plan for sticky sessions or a pub/sub message broker.
WebSocket β SSE (downgrade)
Sometimes justified when simplifying architecture. If you realize your WebSocket usage is 95% server-to-client with rare client messages, SSE + a REST endpoint for client actions is simpler and easier to scale.
Migration risk: Low for the communication layer. Audit client-sent messages first β they become POST requests.
Common Pitfalls
WebSocket Behind Nginx
Without the right config, Nginx drops WebSocket connections. Add this:
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 86400; # 24h β don't timeout idle WS connections
}
SSE and HTTP/2 Multiplexing
HTTP/2 multiplexes multiple streams over one TCP connection. Browsers limit HTTP/1.1 to 6 connections per domain β SSE consumes one of those slots. Under HTTP/2, this limit disappears. If youβre on HTTP/2, SSE scales much better than on HTTP/1.1.
Long Polling Timeout Tuning
Set server timeout at 25β30 seconds, not 60+. Many proxies and load balancers kill idle connections at 60 seconds. A 25-second server timeout ensures a clean response before the proxy cuts the connection.
WebSocket Heartbeats
Idle WebSocket connections can be killed by middleboxes. Implement a ping/pong heartbeat:
// Server-side heartbeat
setInterval(() => {
clients.forEach((ws) => {
if (ws.isAlive === false) { ws.terminate(); return; }
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
Summary
| Long Polling | SSE | WebSocket | |
|---|---|---|---|
| Best for | Legacy support, simple notifications | Live feeds, notifications, dashboards | Chat, gaming, collaborative apps |
| Complexity | Low | Low | Medium |
| Infrastructure | Standard HTTP | Standard HTTP | WebSocket-aware proxy |
| Performance | Poor at scale | Good | Best |
| Direction | Server β Client (via polling) | Server β Client | Bidirectional |
The default recommendation for new projects:
- Start with SSE if you only need server-to-client data flow
- Upgrade to WebSocket when you need bidirectional communication
- Use Long Polling only as a fallback for legacy environments or as a quick prototype
Avoid over-engineering. Most notification systems, live dashboards, and status feeds work perfectly with SSE and never need WebSocket. The extra complexity of bidirectional connections is only worth it when users actively send data back.
Explore related tools on DevPlaybook: REST vs GraphQL Comparison | API Testing Best Practices | Node.js Backend Tools
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.