Real-time communication is essential for modern applications - chat, notifications, live updates, and collaborative features. Two primary technologies enable this: WebSockets and Server-Sent Events (SSE).
WebSockets and Server-Sent Events serve different real-time communication needs. Understanding their trade-offs helps you pick the right tool for your use case.
Understanding Real-Time Communication
The Problem
┌─────────────────────────────────────────────────────────────┐
│ Traditional Request-Response │
│ │
│ Client ───────────────────────────► Server │
│ ◄─────────────────────────── │
│ (Response) │
│ │
│ Problem: Server can't push data to client! │
│ Solution: Use WebSockets or SSE │
└─────────────────────────────────────────────────────────────┘
Technology Comparison
real_time_options = {
"polling": {
"description": "Client repeatedly asks for updates",
"pros": ["Simple", "Works everywhere"],
"cons": ["Wastes bandwidth", "Delayed updates"],
"use_when": "Simple use cases only"
},
"long_polling": {
"description": "Server holds request until data available",
"pros": ["Near real-time", "More efficient"],
"cons": ["Complex", "Still not true push"],
"use_when": "Temporary solution"
},
"webhooks": {
"description": "Server calls client URL",
"pros": ["True push", "Simple"],
"cons": ["Requires public URL", "Complex"],
"use_when": "Machine-to-machine"
},
"sse": {
"description": "Server pushes to client over HTTP",
"pros": ["True push", "Simple", "HTTP/2"],
"cons": ["One-way only", "Browser limits"],
"use_when": "Server to client only"
},
"websockets": {
"description": "Full-duplex communication",
"pros": ["Bi-directional", "Low latency", "Binary"],
"cons": ["Requires upgrade", "More complex"],
"use_when": "Chat, games, bi-directional"
}
}
WebSockets
How WebSockets Work
┌─────────────────────────────────────────────────────────────┐
│ WebSocket Handshake │
│ │
│ Client ──────────────────────────────────────────────► │
│ GET /ws HTTP/1.1 │
│ Host: example.com │
│ Upgrade: websocket │
│ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== │
│ │
│ Server ◄──────────────────────────────────────────── │
│ HTTP/1.1 101 Switching Protocols │
│ Upgrade: websocket │
│ Sec-WebSocket-Accept: s3pPL... │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ NOW: Full-duplex TCP connection! │
│ │
│ Client ◄───────────────────► Server │
│ (messages flow freely in both directions) │
│ │
└─────────────────────────────────────────────────────────────┘
WebSocket Protocol
# WebSocket frame format
frame_structure = {
"FIN": "1 bit - Last frame in message",
"RSV1-3": "3 bits - Reserved for extensions",
"OPCODE": "4 bits - Message type",
"MASK": "1 bit - Client→Server is masked",
"PAYLOAD_LENGTH": "7 bits (+ extended)",
"MASKING_KEY": "0/4 bytes",
"PAYLOAD_DATA": "Data"
}
opcodes = {
"0x0": "Continuation frame",
"0x1": "Text frame",
"0x2": "Binary frame",
"0x8": "Connection close",
"0x9": "Ping",
"0xA": "Pong"
}
Python WebSocket Server
# Using websockets library
import asyncio
import websockets
import json
from datetime import datetime
# Store connected clients
connected_clients = set()
async def chat_handler(websocket):
"""Handle WebSocket connections"""
client_id = id(websocket)
connected_clients.add(websocket)
try:
# Send welcome message
await websocket.send(json.dumps({
"type": "welcome",
"message": "Connected to chat",
"client_id": client_id
}))
# Handle incoming messages
async for message in websocket:
data = json.loads(message)
if data["type"] == "chat":
# Broadcast to all clients
broadcast = {
"type": "message",
"client_id": client_id,
"message": data["message"],
"timestamp": datetime.utcnow().isoformat()
}
# Send to all connected clients
await asyncio.gather(
*[client.send(json.dumps(broadcast))
for client in connected_clients]
)
elif data["type"] == "ping":
await websocket.send(json.dumps({"type": "pong"}))
except websockets.exceptions.ConnectionClosed:
pass
finally:
connected_clients.remove(websocket)
# Run server
async def main():
async with websockets.serve(chat_handler, "localhost", 8765):
await asyncio.Future() # Run forever
asyncio.run(main())
WebSocket Client
// Browser WebSocket client
const ws = new WebSocket('wss://example.com/ws');
// Connection opened
ws.onopen = () => {
console.log('Connected to WebSocket');
ws.send(JSON.stringify({ type: 'hello' }));
};
// Handle messages
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
addMessage(data.message);
}
};
// Handle errors
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Handle disconnection
ws.onclose = () => {
console.log('Disconnected');
// Reconnect after 5 seconds
setTimeout(connect, 5000);
};
// Send message
function sendMessage(text) {
ws.send(JSON.stringify({
type: 'chat',
message: text
}));
}
Server-Sent Events (SSE)
How SSE Works
┌─────────────────────────────────────────────────────────────┐
│ Server-Sent Events │
│ │
│ Client ──────────────────────────────────────────────► │
│ GET /events HTTP/1.1 │
│ Accept: text/event-stream │
│ │
│ Server ◄──────────────────────────────────────────── │
│ HTTP/1.1 200 OK │
│ Content-Type: text/event-stream │
│ │
│ data: {"type": "update", "value": 1} │
│ data: {"type": "update", "value": 2} │
│ data: {"type": "update", "value": 3} │
│ ... │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ One-way: Server pushes to client │
│ Uses standard HTTP (no special protocol) │
│ │
└─────────────────────────────────────────────────────────────┘
SSE Format
# SSE message format
message_parts:
- "data: The message payload"
- "id: Optional event ID (for reconnection)"
- "event: Optional event type"
- "retry: Optional retry interval (ms)"
# Example messages
examples:
simple_data:
content: |
data: Hello
json_data:
content: |
data: {"message": "Hello", "count": 42}
with_id:
content: |
id: 123
data: Update
with_retry:
content: |
retry: 5000
data: Auto-reconnect in 5 seconds
Python SSE Server
# Using Flask for SSE
from flask import Flask, Response, stream_with_context
import json
import time
app = Flask(__name__)
def generate_events():
"""Generate SSE events"""
count = 0
while True:
# Create event data
data = json.dumps({
"count": count,
"timestamp": time.time()
})
# Yield SSE-formatted message
yield f"data: {data}\n\n"
count += 1
time.sleep(1)
@app.route('/events')
def events():
"""SSE endpoint"""
return Response(
stream_with_context(generate_events()),
mimetype='text/event-stream'
)
# With event types
def generate_notifications():
"""Generate typed events"""
for notification in get_notifications():
yield f"event: notification\ndata: {json.dumps(notification)}\n\n"
@app.route('/notifications')
def notifications():
return Response(
stream_with_context(generate_notifications()),
mimetype='text/event-stream'
)
SSE Client
// Browser SSE client
const eventSource = new EventSource('/events');
// Handle default messages
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Message:', data);
};
// Handle custom event types
eventSource.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
showNotification(notification);
});
// Connection opened
eventSource.onopen = () => {
console.log('Connected to SSE');
};
// Handle errors
eventSource.onerror = (error) => {
console.error('SSE error:', error);
// EventSource automatically reconnects!
};
// Close connection when done
function disconnect() {
eventSource.close();
}
Choosing Between WebSockets and SSE
Decision Matrix
| Factor | WebSockets | SSE |
|---|---|---|
| Direction | Bi-directional | Server→Client |
| Protocol | WebSocket (upgrade) | HTTP |
| Browser Support | All modern | All modern |
| Connections | Single TCP | New HTTP each time |
| Binary Data | Yes | No (text only) |
| Firewalls | Sometimes blocked | Usually fine |
| Complexity | Higher | Lower |
| Auto-reconnect | Manual | Built-in |
When to Use WebSockets
websocket_use_cases:
- "Chat applications"
- "Multiplayer games"
- "Collaborative editing"
- "Financial tickers"
- "Real-time dashboards with user input"
- "Bi-directional communication needed"
When to Use SSE
sse_use_cases:
- "Live feeds (Twitter, news)"
- "Notifications"
- "Progress updates"
- "Status monitoring"
- "Server push only"
- "Behind restrictive firewalls"
- "Simpler implementation needed"
Scaling Considerations
WebSocket Scaling
# WebSocket scaling strategies
scaling_strategies = {
"sticky_sessions": {
"description": "Route client to same server",
"implementation": "Load balancer with session affinity"
},
"redis_pubsub": {
"description": "Share messages across servers",
"implementation": "Redis pub/sub for message distribution"
},
"message_queue": {
"description": "Use queue for async processing",
"implementation": "RabbitMQ or Kafka"
}
}
# Example: Redis pub/sub with Socket.io
const io = require('socket.io')(server);
io.on('connection', (socket) => {
socket.on('message', (msg) => {
// Publish to Redis
redisClient.publish('messages', JSON.stringify(msg));
});
});
// Multiple servers subscribe to Redis
redisClient.subscribe('messages', (err) => {
redisClient.on('message', (channel, message) => {
io.emit('message', JSON.parse(message));
});
});
SSE Scaling
# SSE scaling considerations
scaling:
browser_limits:
- "Max 6 connections per domain (HTTP/1.1)"
- "No limit with HTTP/2"
server_limits:
- "Keep-alive connections consume memory"
- "Set appropriate timeouts"
solutions:
- "Use HTTP/2 for more connections"
- "Implement connection pooling"
- "Use CDN for large scale"
Security
WebSocket Security
# WebSocket security
security_considerations = {
"authentication": {
"recommendation": "Authenticate on connection, validate per message",
"example": "JWT in connection query or first message"
},
"authorization": {
"recommendation": "Check permissions for each message type"
},
"input_validation": {
"recommendation": "Validate all incoming data"
},
"rate_limiting": {
"recommendation": "Prevent message flooding"
},
"wss": {
"recommendation": "Always use WSS (WebSocket Secure)"
}
}
# Secure WebSocket with authentication
async def secure_handler(websocket, path):
# Get token from query string
token = parse_qs(path).get('token', [None])[0]
# Validate token
user = await validate_token(token)
if not user:
await websocket.close()
return
# Continue with authenticated session
await chat_handler(websocket, user)
SSE Security
# SSE security
security:
- "Use HTTPS"
- "Implement authentication (cookies/tokens)"
- "Validate origin header"
- "Set appropriate CORS headers"
- "Implement rate limiting"
gRPC Streaming
gRPC extends Remote Procedure Call with streaming capabilities. It uses Protocol Buffers for efficient binary serialization and HTTP/2 for multiplexed connections.
Protocol Buffers Definition
syntax = "proto3";
service DataService {
rpc GetData (DataRequest) returns (DataResponse);
rpc StreamData (DataRequest) returns (stream DataResponse);
rpc BidirectionalStream (stream DataRequest) returns (stream DataResponse);
}
message DataRequest { string id = 1; }
message DataResponse { string data = 1; int64 timestamp = 2; }
Server Streaming
import grpc
class DataServiceServicer(data_pb2_grpc.DataServiceServicer):
def StreamData(self, request, context):
for i in range(10):
yield data_pb2.DataResponse(
data=f"Message {i}",
timestamp=time.time()
)
time.sleep(1)
Bidirectional Streaming
class ChatServiceServicer(data_pb2_grpc.ChatServiceServicer):
async def ChatStream(self, request_iterator, context):
async for message in request_iterator:
await context.write(data_pb2.ChatResponse(
text=f"Echo: {message.text}"
))
Performance Comparison
| Feature | WebSocket | SSE | gRPC Streaming |
|---|---|---|---|
| Direction | Bidirectional | Server→Client | Bidirectional |
| Protocol | WS/WSS | HTTP | HTTP/2 |
| Browser Support | Excellent | Excellent | Limited |
| Complexity | Medium | Low | High |
| Performance | High | Medium | Very High |
| Binary Data | Yes | No | Yes (Protobuf) |
| Use Case | Chat, Games | Feeds, Updates | Microservices |
Error Handling and Resilience
Connection Management
All real-time protocols require robust connection management. Implement heartbeat mechanisms to detect failed connections. Use exponential backoff for reconnection attempts to prevent overwhelming servers during outages. Queue messages during disconnection and send when reconnection succeeds.
State Synchronization
Handle state synchronization carefully in real-time applications. Use timestamps or vector clocks to order events. Consider operational transformation or CRDTs for collaborative editing scenarios.
Backpressure Handling
Handle situations where message production exceeds consumption. Flow control mechanisms prevent memory exhaustion. Implement message dropping strategies or buffering limits appropriate to your application.
Conclusion
Choose based on your needs:
- WebSockets: Full bi-directional communication, chat, games
- SSE: Server→Client push, simpler, better HTTP compatibility
Both are essential tools in your real-time communication toolkit.
Comments