Skip to main content
← All Articles Β· Β· 14 min read

WebSocket vs SSE vs Long Polling: Complete Guide 2026

Definitive comparison of WebSocket, Server-Sent Events (SSE), and Long Polling for real-time web apps. Performance benchmarks, code examples in JavaScript and Python, decision matrix, and migration paths.

websocketsseserver-sent-eventslong-pollingreal-timejavascriptbackendcomparison

Real-time communication is no longer optional. Users expect live notifications, collaborative editing, live dashboards, and instant chat β€” all without hitting refresh. The question isn’t whether to go real-time, but which protocol to use.

Three technologies dominate: WebSocket, Server-Sent Events (SSE), and Long Polling. Each solves the same problem differently, and picking the wrong one will haunt your architecture for years.

This guide cuts through the confusion with honest comparisons, working code examples, and a decision matrix you can use today.


How Each Protocol Works

Understanding the internals makes the tradeoffs obvious.

Long Polling

Long polling is HTTP β€” no new protocol, no browser APIs, just a clever use of existing infrastructure.

Client β†’ Server: "Any updates?"
Server: [holds connection open, waiting...]
Server β†’ Client: "Here's new data!" (after 30s)
Client β†’ Server: "Any updates?" (immediately re-requests)

The client sends a request. The server holds it open until new data arrives (or a timeout hits). The client gets a response, processes it, and immediately fires another request. From the outside it looks real-time. Under the hood it’s a polling loop with patience.

Connection lifecycle:

  1. Client opens HTTP request
  2. Server blocks response until data is ready (or timeout ~30–60s)
  3. Server sends response
  4. Client processes data
  5. Client immediately opens new request
  6. Repeat forever

Server-Sent Events (SSE)

SSE is a W3C standard built on HTTP. One persistent connection, server pushes data to client whenever it wants. The client cannot send messages back through this channel.

Client β†’ Server: GET /events (one-time)
Server β†’ Client: data: {"type":"notification","msg":"Hello"}
Server β†’ Client: data: {"type":"update","count":42}
Server β†’ Client: [keeps connection open indefinitely]

The browser’s EventSource API handles reconnection automatically. Events are text-based (UTF-8). The protocol adds data:, event:, id:, and retry: fields.

SSE event format:

event: notification
data: {"message": "New order received", "orderId": "12345"}
id: 42
retry: 3000

(Note the blank line β€” it terminates each event)

WebSocket

WebSocket starts as an HTTP request (the handshake), then upgrades to a persistent, full-duplex TCP connection. Both sides can send data at any time, independently.

Client β†’ Server: HTTP Upgrade request
Server β†’ Client: 101 Switching Protocols
--- WebSocket connection established ---
Client ↔ Server: bidirectional messages (any time, any direction)

Messages can be text or binary. There’s no built-in format β€” you define your own message schema. The connection stays alive until either side closes it.


Performance Comparison

Numbers matter. Here’s an honest breakdown.

Latency

ProtocolFirst Message LatencySubsequent Message Latency
Long PollingHigh (HTTP overhead per message)High (new HTTP request each time)
SSELow (after initial connection)Very low (persistent connection)
WebSocketLow (after handshake)Very low (persistent connection)

Long polling adds 10–100ms of HTTP overhead per message cycle. SSE and WebSocket deliver subsequent messages at near-wire speed.

Throughput

WebSocket wins for high-frequency bidirectional traffic. SSE is efficient for server-to-client streams. Long polling degrades badly under load.

Rough throughput comparison (single server, 10k concurrent users):

ProtocolMessages/secServer MemoryConnections
Long Polling~5,000High (thread per connection)Many short-lived
SSE~50,000Medium10k persistent
WebSocket~100,000+Low (with async server)10k persistent

Overhead per Message

Long polling HTTP headers add 500–800 bytes per message. WebSocket frames add 2–14 bytes. SSE adds ~5 bytes (data: prefix + newlines).

For a chat app sending 100 messages/second per user, WebSocket saves ~50KB/s in overhead per user compared to long polling.

Scalability

All three can scale horizontally, but the mechanism differs:

  • Long Polling: Stateless HTTP β€” scales easily with any load balancer. No sticky sessions needed.
  • SSE: Persistent connections require some stickiness or a pub/sub backend (Redis, etc.) to route messages to the right server.
  • WebSocket: Same as SSE β€” stateful connections need sticky sessions or a message broker.

Code Examples

Long Polling in JavaScript + Node.js

Server (Node.js/Express):

const express = require('express');
const app = express();

let waitingClients = [];
let messages = [];

app.get('/poll', (req, res) => {
  const lastId = parseInt(req.query.lastId) || 0;
  const newMessages = messages.filter(m => m.id > lastId);

  if (newMessages.length > 0) {
    // Data available immediately
    res.json({ messages: newMessages });
    return;
  }

  // No data β€” hold the connection
  const timeout = setTimeout(() => {
    const idx = waitingClients.indexOf(res);
    if (idx !== -1) waitingClients.splice(idx, 1);
    res.json({ messages: [] }); // timeout, empty response
  }, 30000);

  waitingClients.push({ res, timeout });
});

app.post('/send', express.json(), (req, res) => {
  const message = { id: messages.length + 1, text: req.body.text };
  messages.push(message);

  // Notify all waiting clients
  waitingClients.forEach(({ res: clientRes, timeout }) => {
    clearTimeout(timeout);
    clientRes.json({ messages: [message] });
  });
  waitingClients = [];
  res.sendStatus(200);
});

Client:

let lastId = 0;

async function poll() {
  try {
    const res = await fetch(`/poll?lastId=${lastId}`);
    const data = await res.json();

    data.messages.forEach(msg => {
      lastId = Math.max(lastId, msg.id);
      console.log('Received:', msg.text);
    });
  } catch (err) {
    console.error('Poll failed:', err);
    await new Promise(r => setTimeout(r, 1000)); // backoff on error
  }

  poll(); // immediately re-poll
}

poll(); // start the loop

Server-Sent Events in JavaScript + Python

Server (Python/FastAPI):

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import json

app = FastAPI()
subscribers = set()

async def event_stream(queue: asyncio.Queue):
    try:
        while True:
            data = await queue.get()
            yield f"data: {json.dumps(data)}\n\n"
    except asyncio.CancelledError:
        pass

@app.get("/events")
async def sse_endpoint():
    queue = asyncio.Queue()
    subscribers.add(queue)

    async def cleanup():
        subscribers.discard(queue)

    return StreamingResponse(
        event_stream(queue),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",  # disable nginx buffering
        }
    )

@app.post("/broadcast")
async def broadcast(message: dict):
    for queue in subscribers.copy():
        await queue.put(message)
    return {"sent_to": len(subscribers)}

Client:

const source = new EventSource('/events');

source.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('SSE received:', data);
};

source.onerror = (err) => {
  console.error('SSE error:', err);
  // EventSource auto-reconnects β€” no manual handling needed
};

// Listen to named events
source.addEventListener('notification', (event) => {
  const notification = JSON.parse(event.data);
  showToast(notification.message);
});

// Cleanup when done
function disconnect() {
  source.close();
}

WebSocket in JavaScript + Node.js

Server (Node.js with ws):

const WebSocket = require('ws');
const http = require('http');

const server = http.createServer();
const wss = new WebSocket.Server({ server });

const clients = new Map();

wss.on('connection', (ws, req) => {
  const clientId = Date.now().toString();
  clients.set(clientId, ws);

  console.log(`Client ${clientId} connected`);

  ws.on('message', (rawData) => {
    const message = JSON.parse(rawData);

    switch (message.type) {
      case 'chat':
        // Broadcast to all clients
        broadcast({ type: 'chat', from: clientId, text: message.text });
        break;

      case 'ping':
        ws.send(JSON.stringify({ type: 'pong' }));
        break;
    }
  });

  ws.on('close', () => {
    clients.delete(clientId);
    console.log(`Client ${clientId} disconnected`);
  });

  ws.on('error', (err) => {
    console.error(`Client ${clientId} error:`, err);
    clients.delete(clientId);
  });

  // Send welcome message
  ws.send(JSON.stringify({ type: 'connected', clientId }));
});

function broadcast(data, excludeId = null) {
  const message = JSON.stringify(data);
  clients.forEach((client, id) => {
    if (id !== excludeId && client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

server.listen(3000);

Client:

class ReliableWebSocket {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.reconnectDelay = 1000;
    this.maxDelay = 30000;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('WebSocket connected');
      this.reconnectDelay = 1000; // reset backoff
    };

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      this.onMessage(data);
    };

    this.ws.onclose = () => {
      console.log(`Disconnected. Reconnecting in ${this.reconnectDelay}ms`);
      setTimeout(() => this.connect(), this.reconnectDelay);
      this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxDelay);
    };

    this.ws.onerror = (err) => {
      console.error('WebSocket error:', err);
    };
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    }
  }

  onMessage(data) {
    // Override this
    console.log('Received:', data);
  }
}

const ws = new ReliableWebSocket('ws://localhost:3000');
ws.send({ type: 'chat', text: 'Hello!' });

Real-World Use Cases

Chat Applications

Best choice: WebSocket

Chat is bidirectional by nature. Users send messages, receive messages from others, see typing indicators, and get read receipts β€” all flowing both directions simultaneously.

SSE would require a separate HTTP endpoint for sending messages, which works but adds complexity. Long polling is viable for low-volume chat (Slack used it early on) but struggles at scale.

Live Notifications

Best choice: SSE

Notifications are inherently one-directional: server pushes to client. SSE is the simplest, most browser-native solution. The EventSource API auto-reconnects, handles Last-Event-ID for missed events, and integrates cleanly with HTTP infrastructure (CDNs, proxies, load balancers).

No WebSocket upgrade handshake, no binary framing β€” just HTTP streaming. Your existing CORS, authentication middleware, and load balancers work unchanged.

Live Dashboards

Best choice: SSE (for data display) or WebSocket (for interactive dashboards)

If the dashboard only displays server data (metrics, logs, analytics), SSE is perfect. One connection per client, server pushes updates.

If users can interact β€” filter data, change time ranges, trigger actions β€” WebSocket makes the bidirectional communication cleaner.

Collaborative Editing (Google Docs-style)

Best choice: WebSocket

Operational Transformation and CRDT algorithms require rapid, bidirectional state synchronization. A user’s edit must propagate to other users in milliseconds. WebSocket’s low overhead and persistent connection handle this well.

Financial Data / Trading

Best choice: WebSocket

High-frequency price feeds, order book updates, and trade execution all benefit from WebSocket’s minimal overhead and low latency. Many trading platforms use binary WebSocket frames (not JSON text) to squeeze out additional performance.

Legacy Browser Support

Best choice: Long Polling

If you must support IE 9 or environments where WebSocket is blocked by corporate proxies, long polling is the safe fallback. Libraries like Socket.io transparently fall back to long polling when WebSocket is unavailable.


Decision Matrix

ScenarioLong PollingSSEWebSocket
Bidirectional communicationβœ—βœ—βœ“
Server-to-client onlyβœ“βœ“ (best)βœ“ (overkill)
Simple HTTP infrastructureβœ“βœ“βœ—
CDN / proxy friendlyβœ“βœ“βœ—
High message frequency (>10/sec)βœ—βœ“βœ“
Binary data transferβœ—βœ—βœ“
Auto-reconnect built inβœ—βœ“βœ—
Low server memory per connectionβœ—βœ“βœ“ (async)
Works behind corporate proxiesβœ“βœ“Sometimes
Browser supportUniversalAll modern + IE polyfillAll modern
Mobile battery efficiencyPoorGoodGood

Quick decision flowchart:

Does the client need to send messages?
β”œβ”€β”€ Yes β†’ WebSocket
└── No β†’ Is the message rate > 5/sec?
    β”œβ”€β”€ Yes β†’ SSE
    └── No β†’ Is proxy compatibility required?
        β”œβ”€β”€ Yes β†’ Long Polling
        └── No β†’ SSE (simpler than WebSocket, good enough)

Migration Guide

Long Polling β†’ SSE

SSE is a drop-in improvement for any server-to-client polling pattern.

Before (Long Polling):

// Client: repeated fetch loop
async function poll() {
  const res = await fetch('/updates?since=' + lastEventId);
  const data = await res.json();
  processUpdates(data);
  poll();
}

After (SSE):

// Client: single EventSource, no loop needed
const source = new EventSource('/stream');
source.onmessage = (e) => processUpdates(JSON.parse(e.data));

The server endpoint changes from returning JSON to streaming text/event-stream. The client loop disappears entirely.

Migration risk: Low. SSE works over HTTP/1.1 and HTTP/2. No new infrastructure needed. Roll out server-side first, then update client.

SSE β†’ WebSocket

Necessary when you add client-to-server messaging.

Key changes:

  1. Replace EventSource with WebSocket
  2. Add message type routing on both client and server
  3. Implement reconnection logic (EventSource does this automatically; WebSocket does not)
  4. Update load balancer to support WebSocket upgrade (Upgrade: websocket header)

Migration risk: Medium. WebSocket requires infrastructure changes (proxies, load balancers must be configured). Plan for sticky sessions or a pub/sub message broker.

WebSocket β†’ SSE (downgrade)

Sometimes justified when simplifying architecture. If you realize your WebSocket usage is 95% server-to-client with rare client messages, SSE + a REST endpoint for client actions is simpler and easier to scale.

Migration risk: Low for the communication layer. Audit client-sent messages first β€” they become POST requests.


Common Pitfalls

WebSocket Behind Nginx

Without the right config, Nginx drops WebSocket connections. Add this:

location /ws {
  proxy_pass http://backend;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_read_timeout 86400; # 24h β€” don't timeout idle WS connections
}

SSE and HTTP/2 Multiplexing

HTTP/2 multiplexes multiple streams over one TCP connection. Browsers limit HTTP/1.1 to 6 connections per domain β€” SSE consumes one of those slots. Under HTTP/2, this limit disappears. If you’re on HTTP/2, SSE scales much better than on HTTP/1.1.

Long Polling Timeout Tuning

Set server timeout at 25–30 seconds, not 60+. Many proxies and load balancers kill idle connections at 60 seconds. A 25-second server timeout ensures a clean response before the proxy cuts the connection.

WebSocket Heartbeats

Idle WebSocket connections can be killed by middleboxes. Implement a ping/pong heartbeat:

// Server-side heartbeat
setInterval(() => {
  clients.forEach((ws) => {
    if (ws.isAlive === false) { ws.terminate(); return; }
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
});

Summary

Long PollingSSEWebSocket
Best forLegacy support, simple notificationsLive feeds, notifications, dashboardsChat, gaming, collaborative apps
ComplexityLowLowMedium
InfrastructureStandard HTTPStandard HTTPWebSocket-aware proxy
PerformancePoor at scaleGoodBest
DirectionServer β†’ Client (via polling)Server β†’ ClientBidirectional

The default recommendation for new projects:

  • Start with SSE if you only need server-to-client data flow
  • Upgrade to WebSocket when you need bidirectional communication
  • Use Long Polling only as a fallback for legacy environments or as a quick prototype

Avoid over-engineering. Most notification systems, live dashboards, and status feeds work perfectly with SSE and never need WebSocket. The extra complexity of bidirectional connections is only worth it when users actively send data back.


Explore related tools on DevPlaybook: REST vs GraphQL Comparison | API Testing Best Practices | Node.js Backend Tools

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.