Real-Time Web: WebSockets, SSE, and When You Need Them
- Contributor
- Aug 30, 2025
- 5 min read
Normal web communication is request-response. The client asks, the server answers. If the client wants new data, it asks again. The server never reaches out unprompted.
Real-time communication flips this. The server pushes data to the client when something changes, without the client asking. Chat messages appear instantly. Stock prices update live. Notifications pop up the moment something happens. Collaborative documents show other users' changes as they type.
There are three approaches to real-time on the web, each with different tradeoffs. Choosing the right one depends on what you're building — and often, the right choice is the simplest one.
Approach 1: Polling (The Simplest)
The client asks "anything new?" on a regular interval.
// Check for new messages every 5 seconds
setInterval(async () => {
const messages = await fetch('/api/messages?since=' + lastTimestamp);
if (messages.length > 0) {
displayMessages(messages);
lastTimestamp = messages[messages.length - 1].timestamp;
}
}, 5000);
How it works: The client sends an HTTP request every N seconds. The server responds with any new data since the last check. Standard HTTP. No special infrastructure.
Strengths:
Dead simple to implement
Works through any proxy, firewall, or load balancer
Stateless on the server — each request is independent
Easy to scale (it's just HTTP)
Weaknesses:
Not truly real-time — there's up to N seconds of delay
Wasteful when nothing's changed (most polls return empty responses)
Higher server load at scale (thousands of clients polling every 5 seconds)
When to use it: When "near-real-time" (5-30 second delay) is acceptable and the implementation simplicity matters. Dashboards that refresh periodically. Notification badges that update regularly. Any situation where a slight delay is fine.
The honest truth: Polling solves most "real-time" requirements. Before reaching for WebSockets, ask: "Would a 5-second delay actually matter?" Often, it doesn't.
Approach 2: Server-Sent Events (SSE)
The server pushes data to the client over a single, long-lived HTTP connection.
// Client
const events = new EventSource('/api/events');
events.onmessage = (event) => {
const data = JSON.parse(event.data);
displayUpdate(data);
};
# Server (Flask example)
@app.route('/api/events')
def events():
def generate():
while True:
data = get_new_events() # blocks until new data
if data:
yield f"data: {json.dumps(data)}\n\n"
return Response(generate(), mimetype='text/event-stream')
How it works: The client opens an HTTP connection that stays open. The server sends data down this connection whenever there's something new. The connection is one-directional — server to client only.
Strengths:
True push — data arrives the instant it's available
Built on standard HTTP — works through most proxies and load balancers
Automatic reconnection — the browser reconnects if the connection drops
Simple API — EventSource is a browser built-in, no library needed
Lightweight on the server compared to WebSockets
Weaknesses:
One-directional (server → client only). The client can't send data back over the same connection
Limited to text data (no binary)
Connection limit per domain (browsers allow ~6 concurrent HTTP connections per domain)
Not supported in older browsers (though support is now broad)
When to use it: Live feeds, notifications, real-time dashboards, progress updates — any scenario where the server needs to push data to the client but the client doesn't need to send data back over the same channel. The client can still use regular HTTP requests to send data to the server.
Approach 3: WebSockets
A full-duplex, bidirectional communication channel between client and server.
// Client
const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleMessage(data);
};
ws.send(JSON.stringify({ type: 'chat', text: 'Hello!' }));
How it works: The connection starts as an HTTP request (the "upgrade handshake"), then switches to the WebSocket protocol. Both sides can send data at any time without waiting for a response. The connection stays open until explicitly closed.
Strengths:
Full duplex — both sides send and receive simultaneously
Low latency — no HTTP overhead per message
Efficient for high-frequency updates (gaming, collaboration, trading)
Supports binary data
Weaknesses:
More complex to implement and operate than HTTP-based approaches
Stateful connections — the server maintains a connection per client, which affects scaling
Load balancers and proxies need specific configuration for WebSocket support
No automatic reconnection (you implement this yourself or use a library)
Connection management (heartbeats, timeouts, cleanup) is your responsibility
When to use it: Chat applications, collaborative editing (Google Docs style), multiplayer games, live trading platforms — any scenario requiring bidirectional, low-latency, high-frequency communication.
The Decision Framework
| Requirement | Polling | SSE | WebSocket | |-------------|---------|-----|-----------| | Delay tolerance | 5-30 sec OK | Real-time | Real-time | | Direction | Client → Server (request) | Server → Client (push) | Both directions | | Frequency | Low-medium | Medium-high | High | | Complexity | Lowest | Low | Highest | | Infrastructure | Standard HTTP | Standard HTTP | Special config | | Scaling | Easiest | Easy | Hardest |
The progression: Start with polling. Move to SSE when you need true push and the communication is one-directional. Move to WebSockets when you need bidirectional real-time communication.
Most applications never need WebSockets. Many "real-time" features are perfectly served by polling or SSE with much less complexity.
Implementation Considerations
Connection Management
SSE and WebSocket connections are stateful — the server maintains an open connection per client. With 10,000 connected clients, that's 10,000 open connections.
Most web servers handle this fine with modern runtimes (Node.js, Go, .NET). Thread-per-connection servers (traditional Java servlets, PHP) may struggle.
Scaling With Multiple Servers
If you have multiple server instances behind a load balancer, a client's WebSocket/SSE connection goes to one specific server. When another server has data to push, it needs to reach the connected clients on other servers.
Solution: A pub/sub system (Redis, RabbitMQ, or a managed service) that broadcasts events to all server instances, which then push to their connected clients.
Reconnection and Reliability
Connections drop. Mobile users lose signal. Laptops go to sleep. Networks switch.
SSE handles this automatically — the EventSource API reconnects and includes a Last-Event-Id header so the server can resume from where it left off.
WebSockets do not. You implement reconnection logic yourself: detect disconnection, wait with exponential backoff, reconnect, and replay missed messages. Libraries like Socket.IO handle this for you.
Authentication
Polling uses standard HTTP auth (cookies, tokens) on every request.
SSE uses standard HTTP auth on the initial connection request.
WebSockets authenticate during the handshake, but the connection itself doesn't carry HTTP headers after that. Token-based authentication (passing the token in the connection URL or as the first message) is the common approach.
Key Takeaway
Not everything needs to be real-time. Polling handles most "live update" requirements with minimal complexity. SSE provides true server-push over standard HTTP for one-directional updates — live feeds, notifications, dashboards. WebSockets provide bidirectional, low-latency communication for chat, collaboration, and gaming. Start with the simplest approach that meets your latency requirements and move to more complex solutions only when measurement proves it's needed.


