Skip to content
Back to all concepts
NetworkingReal-timeAPI Design

WebSockets vs Polling

Comprehensive comparison of real-time communication techniques including short polling, long polling, Server-Sent Events (SSE), and WebSockets for building responsive web applications.

By ScaleWikiUpdated 5 min readIntermediate

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

javascript
// 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.
python
# 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

javascript
// 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();
python
# 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

javascript
// 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...');
};
python
# 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')
python
# 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

javascript
// 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;
}
python
# 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())
javascript
// 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

FeatureShort PollingLong PollingSSEWebSockets
LatencyUp to polling intervalDepends on server deliveryDepends on buffering and deliveryDepends on delivery and processing
Server LoadRequest-rate drivenPending-request drivenConnection and event-rate drivenConnection and message-rate driven
BandwidthRepeated request headersRepeated request headersText event framingBinary or text frames
Bidirectional❌ No❌ No❌ No✅ Yes
ComplexityVery SimpleSimpleSimpleModerate
Browser Support✅ All✅ All✅ Modern✅ Modern
Firewall/Proxy✅ Works✅ Works✅ Works⚠️ May need config
Best ForSimple appsMedium appsNotificationsChat, games

Real-World Examples

Hypothetical chat application (WebSockets)

javascript
// 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)

python
# 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')
javascript
// 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.

javascript
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:

  1. Load balancing persistent connections
nginx
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.

  1. Message Broker for Cross-Server Communication
python
# 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:

  1. Start with requirements: "Do we need bidirectional communication or just server-to-client updates?"
  2. Compare options: "For notifications, SSE works well. For chat, we'd need WebSockets..."
  3. Discuss scaling: "With 1M concurrent connections, we'd need multiple WebSocket servers with Redis pub/sub..."
  4. Mention fallbacks: "We can implement and test a polling fallback if the requirements justify it..."
  5. Consider alternatives: "For simple use cases, short polling might be good enough..."
  6. 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.

  • 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