WebSockets vs SSE vs Long Polling: Real-Time Communication Explained
Learn how WebSockets, Server-Sent Events, and long polling work. When to use each for real-time features like chat, notifications, and live dashboards.
Your app needs live updates. A chat message arrives. A trade executes. A build finishes. The user should see it immediately — without refreshing.
There are three ways to do this, and each has a different tradeoff.
The Problem with Normal HTTP
Standard HTTP is request-response. The client asks, the server answers. The server can't push data to the client unprompted.
For real-time features, you need either:
- The client to ask repeatedly (polling)
- A persistent connection the server can push over (WebSockets, SSE)
Long Polling
The simplest approach. The client makes a request. If there's no new data, the server holds the request open until something happens (or a timeout).
# FastAPI long polling endpoint
import asyncio
from fastapi import FastAPI
from datetime import datetime
app = FastAPI()
pending_updates: dict[str, asyncio.Event] = {}
new_data: dict[str, list] = {}
@app.get("/updates/{client_id}")
async def get_updates(client_id: str):
event = asyncio.Event()
pending_updates[client_id] = event
try:
# Wait up to 30 seconds for new data
await asyncio.wait_for(event.wait(), timeout=30)
updates = new_data.pop(client_id, [])
return {"updates": updates}
except asyncio.TimeoutError:
# No new data — client should reconnect
return {"updates": []}
finally:
pending_updates.pop(client_id, None)
def push_update(client_id: str, data: dict):
if client_id in pending_updates:
new_data.setdefault(client_id, []).append(data)
pending_updates[client_id].set()// Client
async function pollForUpdates(clientId) {
while (true) {
const res = await fetch(`/updates/${clientId}`);
const { updates } = await res.json();
if (updates.length > 0) {
handleUpdates(updates);
}
// Reconnects immediately after response
}
}Pros: Works everywhere. No special browser support needed. Works through proxies and firewalls.
Cons: High server resource usage — connections stay open. Latency equal to time between updates. Not efficient for high-frequency updates.
Best for: Low-frequency updates, simple notification systems, legacy browser support.
Server-Sent Events (SSE)
One-way persistent connection from server to client. The server streams data. The client listens.
import asyncio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import json
app = FastAPI()
async def event_stream(channel: str):
async for event in subscribe_to_channel(channel):
# SSE format: "data: ...\n\n"
yield f"data: {json.dumps(event)}\n\n"
# Send keep-alive every 30s to prevent timeout
if no_event_for_30s:
yield ": keep-alive\n\n"
@app.get("/stream/{channel}")
async def stream_events(channel: str):
return StreamingResponse(
event_stream(channel),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no", # Disable Nginx buffering
}
)// Client — built-in EventSource API
const es = new EventSource("/stream/trades");
es.onmessage = (event) => {
const trade = JSON.parse(event.data);
updateTradeDisplay(trade);
};
es.onerror = () => {
// Browser automatically reconnects
console.log("Reconnecting...");
};The browser's EventSource API handles reconnection automatically. On disconnect, it reconnects with a Last-Event-ID header so you can resume where you left off.
Pros: Simple. Browser handles reconnection. Works over plain HTTP/2. Lower overhead than WebSockets for one-way streams.
Cons: Server-to-client only — client can't send messages on the same connection. HTTP/1.1 limits to 6 connections per domain. Not supported in IE11 (but polyfills exist).
Best for: Live feeds (news, stock prices, social media), notifications, progress bars, live logs, dashboards where data only flows server→client.
WebSockets
Full-duplex connection. Both client and server can send messages at any time over the same connection.
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import dict
app = FastAPI()
class ConnectionManager:
def __init__(self):
self.connections: dict[str, list[WebSocket]] = {}
async def connect(self, room: str, ws: WebSocket):
await ws.accept()
self.connections.setdefault(room, []).append(ws)
async def disconnect(self, room: str, ws: WebSocket):
self.connections[room].remove(ws)
async def broadcast(self, room: str, message: dict):
for ws in self.connections.get(room, []):
await ws.send_json(message)
manager = ConnectionManager()
@app.websocket("/ws/chat/{room}")
async def chat_ws(ws: WebSocket, room: str, user_id: str):
await manager.connect(room, ws)
try:
while True:
data = await ws.receive_json()
# Broadcast to all clients in the room
await manager.broadcast(room, {
"user": user_id,
"message": data["message"],
"timestamp": datetime.utcnow().isoformat()
})
except WebSocketDisconnect:
await manager.disconnect(room, ws)
await manager.broadcast(room, {"user": user_id, "event": "left"})// Client
const ws = new WebSocket("wss://api.example.com/ws/chat/room-123?user_id=alice");
ws.onopen = () => {
ws.send(JSON.stringify({ message: "Hello!" }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
renderMessage(msg);
};
ws.onclose = () => {
// Implement reconnect with exponential backoff
setTimeout(reconnect, 1000);
};Pros: Bi-directional. Low latency (no HTTP overhead per message). Real-time feel. Efficient for high-frequency messages.
Cons: More complex server state — connections are stateful. Doesn't work transparently through all proxies. Harder to scale horizontally (connections are sticky).
Best for: Chat, multiplayer games, collaborative editing (Google Docs), trading platforms, anything needing client→server pushes.
Scaling WebSockets
WebSocket connections are stateful — a client stays connected to one server. When you have multiple servers, messages from Server A can't reach clients on Server B.
Fix: use a pub/sub layer (Redis) to fan out messages:
import aioredis
redis = aioredis.from_url("redis://localhost")
@app.websocket("/ws/chat/{room}")
async def chat_ws(ws: WebSocket, room: str):
await manager.connect(room, ws)
# Subscribe to this room's Redis channel
async with redis.pubsub() as pubsub:
await pubsub.subscribe(f"room:{room}")
async def receive_from_redis():
async for message in pubsub.listen():
if message["type"] == "message":
await ws.send_text(message["data"])
async def receive_from_client():
while True:
data = await ws.receive_text()
# Publish to Redis — all servers in the cluster receive it
await redis.publish(f"room:{room}", data)
await asyncio.gather(receive_from_redis(), receive_from_client())Every server subscribes to Redis. A message published from any server reaches all connected clients.
Comparison
| Long Polling | SSE | WebSockets | |
|---|---|---|---|
| Direction | Client→Server→Client | Server→Client | Both |
| Connection | Reopened per response | Persistent | Persistent |
| Protocol | HTTP | HTTP | WS (upgraded HTTP) |
| Browser support | All | All modern | All modern |
| Reconnect | Manual | Automatic | Manual |
| Proxy friendly | Yes | Yes | Sometimes |
| Scaling | Easy | Easy | Needs pub/sub |
| Best for | Simple notifications | Live feeds | Chat, games |
When to Use Each
Long polling: You need compatibility with very old browsers or restrictive corporate proxies. Low-frequency updates (once per minute or less).
SSE: Push-only data streams — notifications, live feeds, progress updates, streaming AI responses (ChatGPT uses SSE). Much simpler than WebSockets when you don't need client messages.
WebSockets: Chat, collaborative tools, games, trading platforms — anything where the client also needs to send real-time data.
Key Takeaways
- Long polling: client keeps asking — simple, but inefficient for frequent updates
- SSE: server pushes over HTTP — simple, automatic reconnect, one-way only
- WebSockets: full-duplex persistent connection — bi-directional, lowest latency, most complex
- For notifications and feeds, SSE is often the right choice (simpler than WebSockets)
- For chat and collaborative features, WebSockets are necessary
- WebSocket horizontal scaling needs a pub/sub layer (Redis) to fan out messages
- Most real-time features are server→client only — reach for SSE before WebSockets
Pick the simplest option that meets your needs. SSE handles most real-time use cases with less complexity than WebSockets.
FAQ
What is the difference between WebSockets and Server-Sent Events?
WebSockets provide full-duplex communication — both client and server can send messages at any time over a single persistent connection. SSE is one-way only: the server pushes data to the client, but the client cannot send messages back on that same connection. WebSockets require a protocol upgrade from HTTP (the ws:// or wss:// scheme). SSE is plain HTTP — a long-lived response with Content-Type: text/event-stream. For most notification and feed use cases, SSE is simpler and sufficient.
When should I use WebSockets vs SSE?
Use SSE for: live dashboards, notification feeds, progress bars, log streaming, live scores, and streaming AI responses (ChatGPT and Claude both use SSE). Use WebSockets for: chat applications, collaborative editing (Google Docs-style), multiplayer games, trading platforms, and any use case where the client also sends real-time data. If you are unsure, start with SSE — it uses plain HTTP (works through all proxies), auto-reconnects natively, and is simpler to implement.
How do WebSockets work behind a load balancer?
WebSocket connections are stateful and long-lived — once established, the client is connected to one specific server. Load balancers need to use sticky sessions (session affinity) to ensure subsequent WebSocket frames route to the same server. AWS ALB and Nginx both support this. For horizontal scale across multiple WebSocket servers, use a shared pub/sub layer (Redis Pub/Sub, Kafka) so all servers receive every message and can forward it to their local connections.
How do I scale WebSocket connections horizontally?
Add a pub/sub broker (Redis is the simplest) between your WebSocket servers. Each server subscribes to relevant Redis channels. When any client sends a message, publish it to Redis — every server receives it and delivers to its connected clients. For very large scale (millions of connections), dedicated WebSocket services like Pusher, Ably, or AWS API Gateway WebSocket API handle the infrastructure so you only pay per message rather than managing connection servers.
What happens when a WebSocket connection drops?
The server gets a disconnect event and cleans up the connection. The client gets an onclose event. Unlike SSE, the browser does not reconnect automatically — you must implement reconnection logic yourself using exponential backoff with jitter to avoid thundering-herd reconnect storms.
Is long polling obsolete?
Not completely. Long polling works through every proxy, firewall, and corporate network — even those that block persistent connections. For update frequencies below once per minute, the overhead is negligible. Modern use cases where long polling still makes sense: legacy browser support, environments with restrictive network policies, and simple webhook-style notifications where connection efficiency doesn't matter. For any new development where SSE or WebSockets can be used, they should be preferred.
Can I stream AI responses with SSE?
Yes — SSE is the standard protocol for streaming LLM responses. OpenAI, Anthropic, and most AI APIs use SSE to stream tokens as they are generated, rather than waiting for the full response. The API sends a stream of data: events, each containing a chunk of the generated text. The client appends each chunk to the UI as it arrives, giving the "typing" effect. This is significantly better UX than waiting 10–30 seconds for a full response and displaying it all at once.
Related reading: Message Queues Explained · Rate Limiting Your API · API Gateway Pattern
Enjoyed this article?
Get weekly insights on backend architecture, system design, and Go programming.
Related Posts
Continue reading with these related posts
DNS (Domain Name System) Explained: How It Works, Record Types, Security, and Cloud
Complete guide to how DNS works: resolution process, record types, TTL, DNSSEC, DNS over HTTPS, Route 53, Cloudflare, troubleshooting with dig, and common DNS mistakes engineers make.
GraphQL vs REST: Which API Style Should You Use?
GraphQL lets clients request exactly the data they need from a single endpoint, while REST exposes fixed resources over multiple URLs with standard HTTP verbs. Learn when each wins — with real N+1 examples, schema design, performance tradeoffs, tooling, and a decision matrix.
TCP, UDP, HTTP, gRPC, WebSockets: When to Use Each
Learn the difference between TCP, UDP, HTTP, gRPC, and WebSockets. Practical guide on picking the right protocol for your backend system.