Real-Time Communication Patterns
How does a chat app instantly show new messages? How do stock tickers update in real-time? A normal short HTTP response ends, while a streaming response or upgraded connection can stay open for updates. This article explores four techniques for achieving real-time updates.
The Problem: Server Can't Initiate
Traditional HTTP is client-initiated:
Client: "GET /messages" → Server: "Here are 10 messages" Client: "GET /messages" → Server: "Here are 10 messages (same ones)"
Problem: Server has new data but can't notify client!
Scope of the snippets
These fragments compare transport behavior, not complete authenticated chat services. They require the named libraries, user identity, database cursors, UI functions and application setup. Server examples bind to loopback where applicable. Before deployment, add authorization, origin checks, message validation, queue limits, heartbeat/idle timeouts and cleanup. A transport that delivers a frame does not prove the application committed the associated operation.
Pattern 1: Short Polling
Client repeatedly asks server for updates at regular intervals.
Implementation
// Poll again only after the previous request finishes. updateUI is supplied by the app.
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function shortPolling(signal) {
while (!signal.aborted) {
try {
const response = await fetch('/api/messages', { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
updateUI(await response.json());
} catch (error) {
if (signal.aborted) break;
console.error('Polling failed', error);
}
await delay(5000);
}
}
// Start with an AbortController; abort it when the view is disposed.
# Server-side (Flask)
@app.route('/api/messages')
def get_messages():
messages = db.get_new_messages(user_id)
return jsonify(messages)
Pros & Cons
✅ Pros:
- Extremely simple to implement
- Works with any HTTP server
- No special protocols needed
❌ Cons:
- Wasteful: Many requests return no new data when updates are rare
- High latency: Up to polling interval (5s delay)
- Server load: N clients × polling frequency
- Bandwidth waste: HTTP headers on every request
Best For
- Very simple apps with few users
- Updates can tolerate 5-10 second delays
Pattern 2: Long Polling (Comet)
Client sends request, server holds it open until new data arrives.
Implementation
// Client-side fragment; stop/cancellation belongs to the surrounding view.
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function longPolling() {
while (true) {
try {
const response = await fetch('/api/messages/poll', {
// Long timeout
signal: AbortSignal.timeout(60000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const messages = await response.json();
updateUI(messages);
// Immediately reconnect
} catch (error) {
// Timeout or error, reconnect after brief delay
await sleep(1000);
}
}
}
longPolling();
# Server-side (with timeout)
@app.route('/api/messages/poll')
def poll_messages():
timeout = 30 # Hold connection for max 30s
start_time = time.time()
while time.time() - start_time < timeout:
messages = db.get_new_messages(user_id)
if messages:
return jsonify(messages)
time.sleep(0.5) # Check every 500ms
return jsonify([]) # Timeout, return empty
Pros & Cons
✅ Pros:
- Lower latency than short polling (near-instant)
- Fewer requests than short polling
- Works with standard HTTP
❌ Cons:
- Requires another HTTP request after each response; the underlying connection can be reused
- HTTP request/response header overhead on every polling cycle
- Connection timeout management complex
- Scales poorly (holds server resources per client)
Best For
- Applications needing lower latency than short polling
- When WebSocket infrastructure not available
Pattern 3: Server-Sent Events (SSE)
One-way channel from server to client over HTTP.
Implementation
// Client-side
const evtSource = new EventSource('/api/events');
evtSource.onmessage = (event) => {
const data = JSON.parse(event.data);
updateUI(data);
};
evtSource.onerror = () => {
console.log('Connection lost, reconnecting...');
};
# Server-side (Flask)
@app.route('/api/events')
def stream():
def generate():
while True:
# Check for new data
messages = db.get_new_messages(user_id)
if messages:
for msg in messages:
yield f"data: {json.dumps(msg)}\n\n"
time.sleep(1)
return Response(generate(), mimetype='text/event-stream')
# Server-side (FastAPI)
from fastapi.responses import StreamingResponse
@app.get("/events")
async def events():
async def event_generator():
while True:
messages = await get_new_messages(user_id)
if messages:
for msg in messages:
yield f"data: {json.dumps(msg)}\n\n"
await asyncio.sleep(1)
return StreamingResponse(event_generator(), media_type="text/event-stream")
Pros & Cons
✅ Pros:
- Built into browsers (EventSource API)
- Automatic reconnection
- Efficient (single long-lived connection)
- Works over HTTP/1.1 and HTTP/2
❌ Cons:
- One-way only (server → client)
- Limited browser support for custom headers
- Connection limit per domain (6 in HTTP/1.1)
Best For
- Stock tickers, live scores, dashboards
- Notifications, progress updates
- Any server-to-client streaming
Pattern 4: WebSockets
Bidirectional, full-duplex communication over single TCP connection.
Upgrade Handshake
Client → GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 Server → HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= # Connection now uses WebSocket protocol
Implementation
// Client-side
const ws = new WebSocket('wss://example.com/chat');
ws.onopen = () => {
console.log('Connected!');
ws.send(JSON.stringify({ type: 'join', room: 'general' }));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
updateUI(message);
};
ws.onclose = () => {
console.log('Disconnected, reconnecting...');
// Application reconnect logic needs backoff, jitter and a resume cursor.
};
// Send message
function sendMessage(text) {
if (ws.readyState !== WebSocket.OPEN) return false;
ws.send(JSON.stringify({ type: 'message', text }));
return true;
}
# Server-side (Python with websockets library)
import asyncio
from websockets.asyncio.server import serve
import json
connected_clients = set()
async def chat_handler(websocket):
connected_clients.add(websocket)
try:
async for message in websocket:
data = json.loads(message)
# Broadcast to all connected clients
await asyncio.gather(
*[client.send(message) for client in tuple(connected_clients)],
return_exceptions=True
)
finally:
connected_clients.discard(websocket)
async def main():
async with serve(chat_handler, "127.0.0.1", 8765, max_size=65536):
await asyncio.Future() # run forever
asyncio.run(main())
// Server-side (Node.js with ws library)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ host: '127.0.0.1', port: 8080, maxPayload: 65536 });
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (message) => {
let data;
try { data = JSON.parse(message.toString()); }
catch { ws.close(1007, 'Invalid JSON'); return; }
// Broadcast to all clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
});
});
ws.on('close', () => {
console.log('Client disconnected');
});
});
Pros & Cons
✅ Pros:
- True bidirectional communication
- Low latency (no HTTP overhead after handshake)
- Efficient: Single connection, minimal overhead
- Full-duplex: Both sides send simultaneously
❌ Cons:
- More complex than HTTP
- Load balancer/proxy support needed
- Connection state management required
- Not RESTful (different mental model)
Best For
- Chat applications
- Multiplayer games
- Collaborative editing (Google Docs)
- Real-time dashboards
- Trading platforms
Comparison Table
| Feature | Short Polling | Long Polling | SSE | WebSockets |
|---|---|---|---|---|
| Latency | Up to polling interval | Depends on server delivery | Depends on buffering and delivery | Depends on delivery and processing |
| Server Load | Request-rate driven | Pending-request driven | Connection and event-rate driven | Connection and message-rate driven |
| Bandwidth | Repeated request headers | Repeated request headers | Text event framing | Binary or text frames |
| Bidirectional | ❌ No | ❌ No | ❌ No | ✅ Yes |
| Complexity | Very Simple | Simple | Simple | Moderate |
| Browser Support | ✅ All | ✅ All | ✅ Modern | ✅ Modern |
| Firewall/Proxy | ✅ Works | ✅ Works | ✅ Works | ⚠️ May need config |
| Best For | Simple apps | Medium apps | Notifications | Chat, games |
Real-World Examples
Hypothetical chat application (WebSockets)
// Illustrative chat client; endpoint and UI helpers are placeholders
const socket = new WebSocket('wss://chat.example.com');
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'message') {
displayMessage(msg);
} else if (msg.type === 'typing') {
showTypingIndicator(msg.user);
} else if (msg.type === 'read') {
markAsRead(msg.messageId);
}
};
// Send typing indicator
inputField.addEventListener('input', () => {
socket.send(JSON.stringify({ type: 'typing', user: currentUser }));
});
Stock Market Dashboard (SSE)
# Server streams stock prices
@app.route('/stock-stream')
def stock_stream():
def generate():
while True:
prices = get_current_stock_prices(['AAPL', 'GOOGL', 'MSFT'])
yield f"data: {json.dumps(prices)}\n\n"
time.sleep(1) # Update every second
return Response(generate(), mimetype='text/event-stream')
// Client displays live prices
const eventSource = new EventSource('/stock-stream');
eventSource.onmessage = (event) => {
const prices = JSON.parse(event.data);
updateStockPrices(prices);
};
A client with a polling fallback
A hypothetical client can implement polling when a WebSocket connection fails. Feature detection alone is insufficient: a browser may support WebSocket while the network blocks its connection.
class RealtimeConnection {
connect() {
if ('WebSocket' in window) {
this.ws = new WebSocket('wss://chat.example.com/events');
this.ws.onmessage = this.handleMessage;
} else {
// Fallback to long polling
this.longPoll();
}
}
longPoll() {
fetch('/api/events/poll')
.then(res => res.json())
.then(data => {
this.handleMessage(data);
this.longPoll(); // Reconnect
});
}
}
Scaling Considerations
WebSocket Scaling Challenges
100,000 connected WebSocket clients = 100,000 open TCP connections = Significant server resources
Solutions:
- Load balancing persistent connections
upstream websocket_backend {
ip_hash; # Same client → same server
server ws1.example.com;
server ws2.example.com;
}
server {
location /ws {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
An established connection already stays on one backend. Affinity mainly affects reconnects or application state; it is not required for every WebSocket deployment. A shared durable event log or recoverable state is still needed if clients must resume after a backend failure.
- Message Broker for Cross-Server Communication
# Server 1 receives message, publishes to Redis
await redis.publish('chat:room:general', json.dumps(message))
# All servers subscribe to broadcast channel
async def broadcast_listener():
pubsub = redis.pubsub()
await pubsub.subscribe('chat:room:general')
async for message in pubsub.listen():
# Send to all local WebSocket connections
await broadcast_to_local_clients(message)
Interview Tips 💡
When discussing real-time communication in system design interviews:
- Start with requirements: "Do we need bidirectional communication or just server-to-client updates?"
- Compare options: "For notifications, SSE works well. For chat, we'd need WebSockets..."
- Discuss scaling: "With 1M concurrent connections, we'd need multiple WebSocket servers with Redis pub/sub..."
- Mention fallbacks: "We can implement and test a polling fallback if the requirements justify it..."
- Consider alternatives: "For simple use cases, short polling might be good enough..."
- Real examples: "A chat design can use WebSockets for bidirectional events..."
Worked failure: a connection resumes with a missing message
A dashboard disconnects after event 120 and reconnects after event 125. Reconnecting the socket does not recover events 121–125. Include a durable event ID and a resume request, or reload an authoritative snapshot. SSE’s Last-Event-ID mechanism helps identify the last event, but the server must retain and replay the relevant history. Redis pub/sub alone does not retain messages for an offline subscriber.
Test the same loss case for each transport. Short polling can use a cursor, long polling can request events after that cursor, and a WebSocket protocol can carry it in a resume message. The important requirement is recovery semantics, not the shape of the connection.
Protocol framing is defined by RFC 6455. The websockets project’s routing tutorial demonstrates per-connection state and cleanup, while MDN’s SSE guide covers event framing and reconnection.
Related Concepts
- HTTP/2 and HTTP/3 — Improved protocols for web communication
- Load Balancing — Distributing WebSocket connections
- Message Queues — Pub/sub for cross-server WebSocket messaging
- CDN — Edge connection proxying; live WebSocket frames are not ordinary cached assets
- WebRTC — Peer-to-peer real-time communication
Keep reading
WebRTC (Peer-to-Peer Communication)
Browser APIs and protocols for real-time audio, video and data, with signaling, ICE connectivity checks, TURN relay fallback and multiparty media-server tradeoffs.
BitTorrent: Pieces, Peer Discovery and Swarm Limits
Understand verified piece exchange, v1 versus v2 metadata, peer discovery, and the capacity limits of cooperative distribution.
Content Delivery Networks: Cache Keys, Freshness and Origin Load
Choose safe cache policies, calculate origin offload, and test personalized responses, invalidation and cold-cache recovery.
DNS Architecture
The phonebook of the internet. How Domain Name System works, the DNS hierarchy, and recursive vs iterative resolution strategies.
GraphQL vs REST vs gRPC
Comprehensive comparison of three major API paradigms: REST (resource-based), GraphQL (query-based), and gRPC (RPC-based), covering performance, use cases, and implementation trade-offs for modern distributed systems.
HTTP Evolution (H1 to H3)
From text-based HTTP/1.1 to binary HTTP/2 and UDP-based HTTP/3 (QUIC). Why we needed upgrades and how they solve Head-of-Line Blocking.