AlgoInsight

Free lesson · System Design Interviews

Realtime Updates - Choosing Between Long Polling, SSE, and WebSockets

Medium11 min readsystem-designinterview-prep

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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?

Back to System Design Interviews