Networking & Communication
Real-time communication patterns for chat, notifications, and live updates. Compare WebSockets, SSE, and long polling trade-offs.
Most of the web runs on request-response: the client asks, the server answers. But some features need the server to push data to the client — chat messages, live notifications, stock tickers, collaborative editing, real-time dashboards. For these use cases, you need a different communication model. Understanding the spectrum of real-time options and their trade-offs is essential for system design interviews.
There are four main techniques for server-to-client communication, each with different characteristics:
Approach Connection Direction Overhead Complexity
────────────────────────────────────────────────────────────────────────
Short Polling Repeated Client → Srv Very High Low
Long Polling Held open Client → Srv Medium Medium
SSE Persistent Server → Clt Low Low
WebSockets Persistent Bidirectional Very Low HighThe simplest approach: the client repeatedly asks the server for updates at fixed intervals.
Client Server
│ │
│──── GET /messages ──────────►│
│◄─── 200 [] (no new msgs) ───│
│ │
│ (wait 5 seconds) │
│ │
│──── GET /messages ──────────►│
│◄─── 200 [] (no new msgs) ───│
│ │
│ (wait 5 seconds) │
│ │
│──── GET /messages ──────────►│
│◄─── 200 [{msg: "hello"}] ───│
│ │# Client-side short polling
import time
import requests
def poll_for_messages(last_seen_id=0):
while True:
response = requests.get(
f"/api/messages?since={last_seen_id}"
)
messages = response.json()
for msg in messages:
display(msg)
last_seen_id = max(last_seen_id, msg["id"])
time.sleep(5) # Fixed intervalShort polling is acceptable for dashboards that refresh every 30-60 seconds, but not for real-time features.
Long polling improves on short polling by having the server hold the request open until there is new data (or a timeout occurs). The client immediately sends a new request when it receives a response.
Client Server
│ │
│──── GET /messages ──────────►│
│ (server holds │
│ request open... │
│ waiting for data) │
│ │ ◄── new message arrives
│◄─── 200 [{msg: "hello"}] ───│
│ │
│──── GET /messages ──────────►│ (immediately reconnect)
│ (server holds │
│ request open again) │
│ │# Server-side long polling
import asyncio
class LongPollingHandler:
def __init__(self):
self.waiters = {} # user_id -> list of futures
async def get_messages(self, user_id, timeout=30):
future = asyncio.get_event_loop().create_future()
self.waiters.setdefault(user_id, []).append(future)
try:
# Wait for new data or timeout
result = await asyncio.wait_for(future, timeout=timeout)
return {"messages": result}
except asyncio.TimeoutError:
return {"messages": []} # Return empty, client reconnects
finally:
self.waiters[user_id].remove(future)
async def publish_message(self, user_id, message):
for future in self.waiters.get(user_id, []):
if not future.done():
future.set_result([message])Long polling is still used when WebSocket support is not available. Facebook's original chat used long polling. It is also a common fallback mechanism — many WebSocket libraries (like Socket.IO) fall back to long polling when WebSocket connections fail.
SSE provides a standardized, unidirectional push channel from server to client over a single HTTP connection. The server sends events as a text stream, and the browser's built-in `EventSource` API handles reconnection automatically.
Client Server
│ │
│──── GET /events ────────────►│
│◄─── HTTP 200 │
│◄─── Content-Type: text/ │
│ event-stream │
│ │
│◄─── data: {"msg":"hi"}\n\n ──│
│ │
│ (connection stays open) │
│ │
│◄─── data: {"msg":"hey"}\n\n ─│
│ │# Server-side SSE endpoint
from flask import Flask, Response
import json
import time
app = Flask(__name__)
def event_stream(user_id):
pubsub = redis.pubsub()
pubsub.subscribe(f"events:{user_id}")
# Send initial connection event
yield f"event: connected\ndata: {{}}\n\n"
for message in pubsub.listen():
if message["type"] == "message":
data = json.loads(message["data"])
yield f"id: {data['id']}\n"
yield f"event: {data['type']}\n"
yield f"data: {json.dumps(data['payload'])}\n\n"
@app.route("/events/<user_id>")
def stream(user_id):
return Response(
event_stream(user_id),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive"
}
)`Last-Event-ID` header, and the server can replay missed events.`event: notification\n`), letting the client listen for specific event types.SSE is ideal when you only need server-to-client push: notifications, live feeds, real-time dashboards, stock tickers. It is simpler than WebSockets, works over standard HTTP (easier to proxy, cache, and load balance), and has built-in reconnection.
WebSockets provide full-duplex, bidirectional communication over a single TCP connection. After an initial HTTP handshake that "upgrades" the connection, both client and server can send messages at any time without HTTP overhead.
Client Server
│ │
│──── GET /chat │
│ Upgrade: websocket ─────►│
│◄─── 101 Switching Protocols ─│
│ │
│ ════ WebSocket connection ════│
│ │
│──── {"type":"msg","text":"hi"}│
│ │
│◄─── {"type":"msg","text":"hey"}│
│ │
│──── {"type":"typing"} ───────│
│ │
│◄─── {"type":"msg","text":"ok"}│
│ │# WebSocket server with heartbeat
import asyncio
import websockets
import json
connected_clients = {} # user_id -> websocket
async def handler(websocket, path):
user_id = authenticate(websocket)
connected_clients[user_id] = websocket
try:
# Start heartbeat task
heartbeat_task = asyncio.create_task(
send_heartbeats(websocket)
)
async for raw_message in websocket:
message = json.loads(raw_message)
await route_message(user_id, message)
except websockets.ConnectionClosed:
pass
finally:
heartbeat_task.cancel()
del connected_clients[user_id]
async def send_heartbeats(websocket, interval=30):
"""Send periodic pings to detect dead connections."""
while True:
try:
await websocket.ping()
await asyncio.sleep(interval)
except websockets.ConnectionClosed:
break
async def send_to_user(user_id, message):
ws = connected_clients.get(user_id)
if ws:
await ws.send(json.dumps(message))
else:
# User is not connected — queue for later or use push notification
await message_queue.enqueue(user_id, message)WebSocket connections are long-lived and stateful, which introduces several challenges:
Heartbeats: Network equipment (firewalls, load balancers, NAT devices) often kills idle connections after 30-60 seconds. Periodic ping/pong frames keep the connection alive and detect dead clients quickly.
Reconnection: Clients must handle disconnections gracefully. A good reconnection strategy uses exponential backoff with jitter:
# Client-side reconnection with exponential backoff
class WebSocketClient:
def __init__(self, url):
self.url = url
self.base_delay = 1 # Start with 1 second
self.max_delay = 30 # Cap at 30 seconds
self.attempt = 0
def connect(self):
try:
self.ws = websocket.connect(self.url)
self.attempt = 0 # Reset on success
except ConnectionError:
self.attempt += 1
delay = min(
self.base_delay * (2 ** self.attempt),
self.max_delay
)
jitter = random.uniform(0, delay * 0.3)
time.sleep(delay + jitter)
self.connect() # RetryAuthentication: WebSocket handshakes do not easily support custom headers in browser environments. Common approaches include:
`ws://host/chat?token=abc`Scaling WebSockets is fundamentally harder than scaling stateless HTTP services because each connection is tied to a specific server.
The problem: User A is connected to Server 1. User B is connected to Server 2. When A sends a message to B, Server 1 needs to know that B is on Server 2.
┌─────────────┐
│ Load Balancer│
│ (sticky/L4) │
└──────┬───────┘
┌────┴────┐
│ │
┌─────┴──┐ ┌───┴────┐
│Server 1│ │Server 2│
│ User A │ │ User B │
└────┬───┘ └───┬────┘
│ │
┌────┴──────────┴────┐
│ Redis Pub/Sub │
│ (message broker) │
└────────────────────┘Solutions:
┌─────────────────┬───────────┬─────────────┬──────────────┬──────────────┐
│ │ Short │ Long │ SSE │ WebSockets │
│ │ Polling │ Polling │ │ │
├─────────────────┼───────────┼─────────────┼──────────────┼──────────────┤
│ Direction │ Client→ │ Client→ │ Server→ │ Bidirectional│
│ Latency │ High │ Low-Medium │ Low │ Very Low │
│ Server load │ Very High │ Medium │ Low │ Low │
│ Complexity │ Very Low │ Medium │ Low │ High │
│ Browser support │ Universal │ Universal │ Modern │ Modern │
│ Proxy friendly │ Yes │ Mostly │ Yes (HTTP) │ Sometimes │
│ Auto reconnect │ N/A │ Manual │ Built-in │ Manual │
│ Scalability │ Easy │ Medium │ Medium │ Hard │
│ Binary data │ Yes (req) │ Yes (req) │ No (text) │ Yes │
│ HTTP/2 support │ Yes │ Yes │ Yes │ Separate │
└─────────────────┴───────────┴─────────────┴──────────────┴──────────────┘Slack: Uses WebSockets for real-time messaging. Falls back to long polling in restricted network environments. The connection is multiplexed — all channels share a single WebSocket connection per client.
Twitter/X: Uses a combination of SSE for the streaming API and long polling for some features. The firehose API streams tweets in real time over persistent HTTP connections.
Google Docs: Uses WebSockets for collaborative editing with Operational Transformation (OT). Each keystroke is sent as a small message, and the server broadcasts operations to all connected editors.
Stock trading platforms: WebSockets for price feeds and order updates. The low latency of WebSockets is critical when prices change multiple times per second.