backend

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.

By Akash Sharma·6 min read
#websockets
#sse
#real-time
#backend
#system design
#long polling
#web

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).

python
# 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()
javascript
// 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.

python
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
        }
    )
javascript
// 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.

python
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"})
javascript
// 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:

python
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 PollingSSEWebSockets
DirectionClient→Server→ClientServer→ClientBoth
ConnectionReopened per responsePersistentPersistent
ProtocolHTTPHTTPWS (upgraded HTTP)
Browser supportAllAll modernAll modern
ReconnectManualAutomaticManual
Proxy friendlyYesYesSometimes
ScalingEasyEasyNeeds pub/sub
Best forSimple notificationsLive feedsChat, 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.

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.