Free lesson · System Design Interviews
Realtime Updates - Choosing Between Long Polling, SSE, and WebSockets
There's a question I've heard in dozens of variations: "How would you design a real-time notification system?" Or a chat app. Or a live scoreboard. Or collaborative editing. The surface question changes, but the underlying pattern is always the same: how do you push data from the server to the client the moment something happens?
This is the realtime update pattern, and I've watched candidates handle it in two very different ways. The weak candidate says "I'd use WebSockets" and moves on - as if that's a complete answer. The strong candidate walks through the three main approaches, explains why each exists, picks the right one for the specific scenario, and discusses how to scale it. That difference is the gap between a mid-level answer and a senior one.
Let me teach you all three approaches so you can make that choice deliberately in every interview.
The Core Problem - HTTP Wasn't Built for Push
HTTP was designed as a request-response protocol. The client asks, the server answers. This works beautifully for loading web pages, fetching API data, or submitting forms. But it fails for a simple scenario: the server has new data and wants to tell the client about it without the client asking.
How do you build real-time features on top of a protocol that doesn't support push?
Three solutions have emerged, each with different tradeoffs.
Approach 1: Long Polling - The Simplest Path
Long polling is regular HTTP polling, but smarter. Instead of the client asking "got anything?" every second (which wastes bandwidth and server resources), the client sends a request and the server holds it open until there's something to send.
How It Works
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET /notifications (request 1)
Note over S: Holds connection open...<br/>waiting for new data
Note over S: 30 seconds pass...<br/>new notification arrives!
S-->>C: 200 OK - here's your notification
C->>S: GET /notifications (request 2)
Note over S: Holds connection open...
Note over S: 60 seconds pass...<br/>timeout, no new data
S-->>C: 204 No Content - nothing new
C->>S: GET /notifications (request 3)
Note over S: Holds connection open...
Note over S: 5 seconds pass...<br/>new notification!
S-->>C: 200 OK - here's your notification
Long polling sequence diagram. The client sends a GET request. The server holds the connection open without responding. When new data arrives - whether after 5 seconds or 30 - the server sends the response. The client immediately makes a new request. If no data arrives before a timeout period, the server sends an empty response and the client reconnects. This creates a near-real-time push effect using standard HTTP.
How to Implement It
The client side is surprisingly simple - it's just a recursive fetch:
async function longPoll() {
try {
const response = await fetch('/api/notifications?since=' + lastEventId);
if (response.status === 200) {
const data = await response.json();
handleNotifications(data.notifications);
lastEventId = data.lastEventId;
}
} catch (error) {
// Connection lost - wait before retrying
await sleep(3000);
}
// Immediately start next long poll
longPoll();
}
The server side holds the request open:
async def long_poll(request):
since = request.query.get('since', 0)
# Wait up to 30 seconds for new data
for _ in range(300): # 300 * 100ms = 30 seconds
notifications = await db.get_notifications(user_id, since=since)
if notifications:
return json_response({
'notifications': notifications,
'lastEventId': notifications[-1].id
})
await asyncio.sleep(0.1) # Check every 100ms
# Timeout - no new data
return json_response({'notifications': [], 'lastEventId': since}, status=204)
Pros and Cons
| Aspect | Detail |
|---|---|
| Works everywhere | Standard HTTP - no special infrastructure, works through all proxies, load balancers, and firewalls |
| Simple deployment | No WebSocket servers needed, no sticky sessions required |
| Connection overhead | Each poll cycle creates a new HTTP connection (mitigated by HTTP/2 and keep-alive) |
| Latency | Near-instant when data arrives while connection is open, but new connection needed after each response |
| Server load | Each waiting client holds a server thread/connection - needs async framework to scale |
Interview: When to pick long polling: "I'd use long polling for the notification system. Notifications arrive irregularly - maybe a few per hour per user. There's no need for persistent bidirectional connections. Long polling works through all existing infrastructure without any special proxy configuration, and for infrequent updates, the overhead of reconnecting after each notification is negligible."
Approach 2: Server-Sent Events (SSE) - The One-Way Stream
SSE is a standardized browser API that opens a persistent one-way connection from the server to the client. The server pushes events down this connection whenever it wants. The client listens.
How It Works
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET /events (Accept: text/event-stream)
Note over C,S: Persistent connection established
S-->>C: event: notification<br/>data: {"type": "like", "count": 5}
Note over C: Process notification
S-->>C: event: notification<br/>data: {"type": "comment", "text": "Great post!"}
Note over C: Process notification
Note over S: 15 seconds of silence
S-->>C: : heartbeat (keep-alive)
S-->>C: event: notification<br/>data: {"type": "follow", "user": "jane"}
Note over C: Process notification
Note over C,S: Connection stays open indefinitely
Server-sent events sequence diagram. The client opens a persistent connection with Accept text event stream header. The server keeps this connection open and pushes events whenever new data is available. Between events, the server sends periodic heartbeat comments to keep the connection alive. The connection remains open indefinitely, and if it drops, the browser automatically reconnects.
The Wire Format
SSE uses a dead-simple text format:
event: notification
data: {"type": "like", "postId": "abc123", "count": 42}
id: 1001
event: notification
data: {"type": "comment", "postId": "abc123"}
data: {"text": "This is a great article!"}
id: 1002
: this is a comment (heartbeat)
event: system
data: {"type": "maintenance", "message": "Server restart in 5 minutes"}
id: 1003
Each event has an optional event type, one or more data lines, and an optional id. The browser automatically tracks the last id and sends it on reconnection via the Last-Event-ID header - giving you automatic resume.
Client Implementation
const eventSource = new EventSource('/api/events');
eventSource.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
displayNotification(data);
});
eventSource.addEventListener('system', (event) => {
const data = JSON.parse(event.data);
showSystemMessage(data.message);
});
eventSource.onerror = () => {
// Browser automatically reconnects with Last-Event-ID header
console.log('Connection lost - browser will auto-reconnect');
};
Pros and Cons
| Aspect | Detail |
|---|---|
| Auto-reconnect | Browser handles reconnection automatically, including sending the last event ID |
| Simple server | Just write text to an HTTP response stream - no special protocol |
| One-way only | Server → client only. Client can't send messages back on the same connection |
| HTTP/2 multiplexing | Multiple SSE connections share a single TCP connection with HTTP/2 |
| Connection limits | Browsers limit concurrent connections per domain (6 for HTTP/1.1, effectively unlimited for HTTP/2) |
| No binary data | Text-only format - binary data must be base64 encoded |
Interview: When to pick SSE: "For the live dashboard that shows real-time metrics - server CPU, request rates, error counts - I'd use Server-Sent Events. The data flows only one direction: server to client. SSE gives us automatic reconnection with event ID tracking, so if a dashboard tab loses connection briefly, it catches up from where it left off without any client-side reconnection logic. There's no need for the dashboard to send data back to the server, so WebSocket's bidirectional capability would be unnecessary complexity."
Approach 3: WebSockets - The Full-Duplex Channel
WebSockets provide a persistent, bidirectional connection between client and server. Either side can send messages at any time. This is the most powerful approach - and the most complex to operate.
How It Works
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET /chat (Upgrade: websocket)
S-->>C: 101 Switching Protocols
Note over C,S: WebSocket connection established<br/>Full-duplex communication
C->>S: {"type": "message", "text": "Hello!"}
S-->>C: {"type": "message", "from": "alice", "text": "Hello!"}
S-->>C: {"type": "typing", "user": "bob"}
C->>S: {"type": "message", "text": "Hi Bob!"}
S-->>C: {"type": "message", "from": "you", "text": "Hi Bob!"}
S-->>C: {"type": "message", "from": "bob", "text": "Hey!"}
Note over C,S: Either side can send at any time
C->>S: {"type": "ping"}
S-->>C: {"type": "pong"}
WebSocket connection sequence. The client sends an HTTP upgrade request. The server responds with 101 Switching Protocols and the connection switches to the WebSocket protocol. After this, both sides can send messages at any time - the client sends chat messages, the server broadcasts messages from other users and typing indicators. Either side can ping/pong for health checks. The connection stays open for the duration of the session.
Scaling WebSockets - The Hard Part
Here's what candidates often miss: scaling WebSockets is fundamentally harder than scaling HTTP.
With regular HTTP, any server can handle any request because each request is independent. Load balancers just round-robin. With WebSockets, the connection is stateful - user Alice's WebSocket is connected to Server 3. If Bob sends Alice a message, that message needs to reach Server 3 specifically.
flowchart TB
subgraph CLIENTS["Clients"]
ALICE["Alice"]
BOB["Bob"]
CAROL["Carol"]
end
LB["Load Balancer"]
subgraph SERVERS["WebSocket Servers"]
S1["Server 1<br/>Bob's connection"]
S2["Server 2<br/>Carol's connection"]
S3["Server 3<br/>Alice's connection"]
end
PUB["Pub/Sub Layer<br/>(Redis Pub/Sub or Kafka)"]
ALICE --> LB
BOB --> LB
CAROL --> LB
LB --> S1
LB --> S2
LB --> S3
S1 <--> PUB
S2 <--> PUB
S3 <--> PUB
WebSocket scaling architecture. Clients connect through a load balancer to one of several WebSocket servers. Each server holds persistent connections for its clients. A pub/sub layer using Redis Pub/Sub or Kafka connects all WebSocket servers. When Bob on Server 1 sends a message to Alice on Server 3, Server 1 publishes to the pub/sub layer, and Server 3 receives it and pushes it to Alice's WebSocket connection.
The solution: a pub/sub backbone. Each WebSocket server subscribes to a message bus (Redis Pub/Sub, Kafka, or a custom solution). When Server 1 receives a message from Bob intended for Alice, it publishes to the bus. Server 3 (which holds Alice's connection) receives it from the bus and pushes it to Alice.
Pros and Cons
| Aspect | Detail |
|---|---|
| Full duplex | Both sides can send at any time - essential for chat, gaming, collaborative editing |
| Low latency | No HTTP overhead per message - just raw frames on an open connection |
| Binary support | Can send binary data directly - useful for audio, video, binary protocols |
| Scaling complexity | Needs sticky sessions or pub/sub backbone, connection state management |
| Infrastructure | Many proxies, load balancers, and firewalls need configuration for WebSocket support |
| Reconnection | No auto-reconnect or event ID tracking - must implement yourself |
Warning: The most common WebSocket mistake in interviews: Saying "I'll use WebSockets" without addressing scaling. If you have 1M concurrent users, each with a WebSocket connection, you need to explain how messages route between servers. The pub/sub backbone is the answer. Skip this and the interviewer will question whether you've ever operated WebSockets at scale.
The Decision Framework - Which Approach When
flowchart TD
START["Need realtime<br/>updates"] --> Q1{"Does the client need<br/>to send data back?"}
Q1 -->|"No, server → client only"| Q2{"How frequent<br/>are updates?"}
Q1 -->|"Yes, bidirectional"| WS["WebSocket"]
Q2 -->|"Infrequent<br/>(seconds to minutes)"| LP["Long Polling"]
Q2 -->|"Frequent<br/>(sub-second)"| SSE["Server-Sent Events"]
WS --> WS_USE["Chat, gaming, collaborative editing,<br/>live trading, multiplayer"]
SSE --> SSE_USE["Live dashboards, notifications,<br/>live scores, stock tickers"]
LP --> LP_USE["Email notifications, social media<br/>updates, infrequent alerts"]
Decision framework for real-time update approaches. First ask: does the client need to send data back? If yes, use WebSockets. If no, ask how frequent the updates are. Infrequent updates like email notifications use long polling. Frequent sub-second updates like live dashboards use Server-Sent Events.
Summary Comparison
| Feature | Long Polling | SSE | WebSocket |
|---|---|---|---|
| Direction | Client → Server → Client | Server → Client | Bidirectional |
| Protocol | HTTP | HTTP | WebSocket (ws://) |
| Auto-reconnect | Manual | Built-in | Manual |
| Binary data | No (JSON/text) | No (text) | Yes |
| HTTP/2 multiplexing | N/A | Yes | No (separate connection) |
| Infrastructure complexity | Low | Low | High |
| Scaling complexity | Low-Medium | Low-Medium | High (needs pub/sub) |
| Latency | Medium (reconnection gap) | Low | Lowest |
| Best for | Infrequent, irregular updates | Continuous server pushes | Interactive, bidirectional |
Interview: The multi-protocol answer that earns senior points: "I'd use different protocols for different parts of the system. Chat messages use WebSockets because users need to send and receive messages with minimal latency - it's inherently bidirectional. The notification bell uses SSE because it's server-to-client only and SSE gives us automatic reconnection with event ID tracking. For email notifications about offline activity, the client long-polls when the user opens their inbox - no need for a persistent connection when they're checking email a few times a day."
Scaling All Three - Connection Management at Scale
Regardless of which approach you choose, scaling real-time connections requires addressing some common challenges.
Connection Limits
A single server can typically handle 10,000-100,000 concurrent connections, depending on memory and the work done per connection. At 1M users, you need 10-100 servers just for connection handling.
Health Checks and Heartbeats
Connections die silently. Mobile networks switch between WiFi and cellular. Laptops go to sleep. You need regular heartbeat messages to detect dead connections and clean up resources.
Graceful Degradation
When the real-time system is overloaded, fall back gracefully. Increase heartbeat intervals. Switch from WebSocket to SSE. Switch from SSE to polling. The user experience degrades slightly but doesn't break entirely.
Tip: In an interview, mention connection limits and graceful degradation proactively. "Each WebSocket server handles about 50K concurrent connections. At our scale of 2M concurrent users, that's 40 servers in the WebSocket tier. If the WebSocket infrastructure goes down, clients fall back to long polling through the regular API servers - the experience degrades from instant to near-instant, but doesn't break."
Key Takeaways
-
Long polling is the simplest approach. It works through all existing HTTP infrastructure with no special configuration. Choose it for infrequent, irregular updates where the reconnection gap after each response is acceptable.
-
SSE is the sweet spot for server-to-client streaming. Auto-reconnection, event ID tracking, and HTTP/2 multiplexing make it operationally simpler than WebSockets. Choose it for live dashboards, notification feeds, and any one-directional stream.
-
WebSockets are for bidirectional, interactive communication. Chat, gaming, collaborative editing. They're the most powerful but also the most complex to scale - always explain the pub/sub backbone for cross-server message routing.
-
Use different protocols for different parts of the same system. Chat over WebSockets, notifications over SSE, offline updates over long polling. This is a senior-level answer that shows you understand each tool's strengths.
-
Scaling real-time connections is its own problem. Connection limits, heartbeat management, sticky sessions, pub/sub routing, and graceful degradation all need to be addressed.
-
Always address reconnection. Mobile networks are unreliable. Clients need to handle connection drops, reconnect with backoff, and catch up on missed events. SSE handles this natively; for WebSockets and long polling, you implement it yourself.
Exercise: Exercise 1: You're designing a collaborative document editor (like Google Docs). Multiple users can edit the same document simultaneously and see each other's changes in real time, including cursor positions. Which real-time approach would you use? Design the message format for communicating edits between users. How do you handle two users editing the same paragraph at the same time?
Exercise 2: A sports app shows live scores for 50 concurrent games, each updating every 5-15 seconds. The app has 500,000 concurrent users. Design the real-time update architecture. Which approach (long polling, SSE, WebSocket) would you use and why? How many servers do you need for the connection tier? How do score updates from the data provider reach all connected clients?
Exercise 3: Compare the server resource usage of long polling vs SSE vs WebSocket for a notification system with 100,000 connected users receiving an average of 2 notifications per hour per user. Calculate the number of HTTP requests per second for the long polling approach (assuming a 30-second timeout). How does this compare to SSE's resource usage?