WebSockets vs Polling: Real-Time Communication for Modern Applications
Real-time communication is at the heart of modern web applications, from chat applications and live dashboards to collaborative editing tools and multiplayer games. Choosing the right real-time communication strategy significantly impacts application performance, scalability, and user experience. This guide provides a comprehensive comparison of WebSockets, long polling, short polling, and Server-Sent Events (SSE), with practical examples and scaling strategies.
Short Polling
Short polling is the simplest approach to getting updates from a server. The client repeatedly sends HTTP requests at fixed intervals to check if new data is available. This is the "are we there yet?" approach to real-time communication.
How Short Polling Works
The client sets up a timer (e.g., every 5 seconds) and sends a regular HTTP request to the server. The server responds immediately with the current state, regardless of whether anything has changed. If new data is available, it is included in the response. If not, the server returns an empty or unchanged response.
// Short Polling Implementation
class ShortPoller {
constructor(url, interval = 5000) {
this.url = url;
this.interval = interval;
this.timer = null;
this.lastETag = null;
}
start(callback) {
this.timer = setInterval(async () => {
try {
const headers = {};
if (this.lastETag) {
headers['If-None-Match'] = this.lastETag;
}
const response = await fetch(this.url, { headers });
if (response.status === 304) {
return; // No changes
}
this.lastETag = response.headers.get('ETag');
const data = await response.json();
callback(data);
} catch (error) {
console.error('Polling error:', error);
}
}, this.interval);
}
stop() {
clearInterval(this.timer);
}
}
// Usage
const poller = new ShortPoller('/api/notifications');
poller.start((data) => {
console.log('New notifications:', data);
});
Pros: Extremely simple to implement, works with any HTTP server, no special infrastructure needed.
Cons: Wasteful (most requests return no new data), adds latency (up to the polling interval), puts unnecessary load on the server, does not scale well.
Long Polling
Long polling improves on short polling by holding the connection open until the server has new data to send. Instead of responding immediately, the server waits until data is available or a timeout occurs, then sends the response. The client immediately sends a new request after receiving a response.
How Long Polling Works
The client sends an HTTP request. The server holds this request open (does not respond immediately). When new data becomes available, the server sends the response. The client processes the response and immediately sends a new request to maintain the listening connection. If no data is available within a timeout period, the server sends an empty response, and the client reconnects.
// Long Polling Client
async function longPoll(url, onMessage) {
let lastEventId = 0;
while (true) {
try {
const response = await fetch(`${url}?since=${lastEventId}`, {
signal: AbortSignal.timeout(60000) // 60s timeout
});
if (response.ok) {
const data = await response.json();
if (data.events && data.events.length > 0) {
data.events.forEach(event => onMessage(event));
lastEventId = data.lastEventId;
}
}
} catch (error) {
if (error.name === 'TimeoutError') {
// Normal timeout, reconnect immediately
continue;
}
console.error('Long polling error:', error);
// Back off on errors
await new Promise(r => setTimeout(r, 3000));
}
}
}
// Server-side (Node.js/Express)
const express = require('express');
const app = express();
const waitingClients = [];
app.get('/api/events', (req, res) => {
const since = parseInt(req.query.since) || 0;
// Check if there are already new events
const newEvents = getEventsSince(since);
if (newEvents.length > 0) {
return res.json({ events: newEvents, lastEventId: newEvents.at(-1).id });
}
// Hold the connection open
const timeout = setTimeout(() => {
const idx = waitingClients.indexOf(client);
if (idx !== -1) waitingClients.splice(idx, 1);
res.json({ events: [], lastEventId: since });
}, 30000);
const client = { res, since, timeout };
waitingClients.push(client);
req.on('close', () => {
clearTimeout(timeout);
const idx = waitingClients.indexOf(client);
if (idx !== -1) waitingClients.splice(idx, 1);
});
});
function notifyClients(event) {
waitingClients.forEach(client => {
clearTimeout(client.timeout);
client.res.json({ events: [event], lastEventId: event.id });
});
waitingClients.length = 0;
}
Pros: Near real-time updates, more efficient than short polling (fewer empty responses), works through firewalls and proxies.
Cons: Holds server connections open (resource intensive), can cause connection limits issues at scale, more complex than short polling, header overhead on each reconnection.
Server-Sent Events (SSE)
Server-Sent Events provide a standardized way for servers to push updates to clients over a single HTTP connection. SSE uses a simple text-based protocol built on top of HTTP, making it firewall-friendly and easy to implement.
How SSE Works
The client opens a persistent HTTP connection using the EventSource API. The server sends events as they occur, formatted as text/event-stream. The connection stays open, and the server can send multiple events over time. The browser handles automatic reconnection if the connection drops.
// SSE Client (Browser)
const eventSource = new EventSource('/api/stream');
eventSource.onopen = () => {
console.log('SSE connection established');
};
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('New event:', data);
};
eventSource.addEventListener('notification', (event) => {
const notification = JSON.parse(event.data);
showNotification(notification);
});
eventSource.addEventListener('price-update', (event) => {
const price = JSON.parse(event.data);
updatePriceDisplay(price);
});
eventSource.onerror = (error) => {
console.error('SSE error:', error);
// Browser automatically reconnects
};
// SSE Server (Node.js/Express)
app.get('/api/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // Disable Nginx buffering
});
// Send initial data
res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
// Send heartbeat every 30 seconds
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
// Send events as they occur
const onEvent = (event) => {
res.write(`event: ${event.type}\n`);
res.write(`data: ${JSON.stringify(event.data)}\n`);
res.write(`id: ${event.id}\n\n`);
};
eventEmitter.on('event', onEvent);
req.on('close', () => {
clearInterval(heartbeat);
eventEmitter.off('event', onEvent);
});
});
Pros: Simple API (browser EventSource), automatic reconnection with last-event-ID, text-based and firewall-friendly, built on standard HTTP, lightweight.
Cons: Unidirectional only (server to client), limited to text data (no binary), maximum 6 connections per domain in HTTP/1.1, no support in IE (polyfills available).
WebSockets
WebSockets provide full-duplex, bidirectional communication over a single TCP connection. After an initial HTTP handshake (upgrade request), the connection switches to the WebSocket protocol, allowing both client and server to send messages at any time without the overhead of HTTP headers.
The WebSocket Handshake
A WebSocket connection begins with an HTTP Upgrade request. If the server supports WebSockets, it responds with a 101 Switching Protocols status, and the connection is upgraded from HTTP to the WebSocket protocol.
// WebSocket Client
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
this.handlers = new Map();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectDelay = 1000; // Reset delay on success
this.send('auth', { token: 'your-jwt-token' });
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
const handler = this.handlers.get(message.type);
if (handler) handler(message.payload);
};
this.ws.onclose = (event) => {
console.log(`WebSocket closed: ${event.code} ${event.reason}`);
this.reconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
send(type, payload) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type, payload }));
}
}
on(type, handler) {
this.handlers.set(type, handler);
}
reconnect() {
setTimeout(() => {
console.log(`Reconnecting in ${this.reconnectDelay}ms...`);
this.connect();
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
}, this.reconnectDelay);
}
}
// Usage
const ws = new WebSocketClient('wss://api.swehelper.com/ws');
ws.on('chat-message', (msg) => displayMessage(msg));
ws.on('user-joined', (user) => updateUserList(user));
ws.connect();
// WebSocket Server (Node.js with ws library)
const WebSocket = require('ws');
const http = require('http');
const server = http.createServer();
const wss = new WebSocket.Server({ server });
const rooms = new Map(); // room -> Set of clients
wss.on('connection', (ws, req) => {
console.log('New WebSocket connection');
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
ws.on('message', (data) => {
const message = JSON.parse(data);
switch (message.type) {
case 'join-room':
joinRoom(ws, message.payload.room);
break;
case 'chat-message':
broadcastToRoom(
message.payload.room,
'chat-message',
message.payload,
ws
);
break;
}
});
ws.on('close', () => {
removeFromAllRooms(ws);
});
});
function broadcastToRoom(room, type, payload, excludeClient) {
const clients = rooms.get(room);
if (!clients) return;
const message = JSON.stringify({ type, payload });
clients.forEach(client => {
if (client !== excludeClient && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// Heartbeat to detect dead connections
const heartbeatInterval = setInterval(() => {
wss.clients.forEach(ws => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
server.listen(8080);
Comparison Table
| Feature | Short Polling | Long Polling | SSE | WebSockets |
|---|---|---|---|---|
| Direction | Client → Server | Client → Server | Server → Client | Bidirectional |
| Latency | Up to polling interval | Near real-time | Real-time | Real-time |
| Protocol | HTTP | HTTP | HTTP | WS/WSS |
| Connection Overhead | High (new request each time) | Medium | Low (single connection) | Low (single connection) |
| Server Resources | Low per request | High (held connections) | Medium | Medium-High |
| Browser Support | Universal | Universal | All modern (no IE) | All modern |
| Binary Data | Yes | Yes | No (text only) | Yes |
| Auto Reconnect | Built-in (next poll) | Built-in (next request) | Built-in (EventSource) | Manual implementation |
Scaling WebSockets
Scaling WebSocket connections is more challenging than scaling stateless HTTP because each connection maintains persistent state on a specific server.
Horizontal Scaling with a Message Broker
When running multiple WebSocket server instances behind a load balancer, clients connected to different servers need a way to communicate. Use a pub/sub message broker (Redis Pub/Sub, Kafka, or RabbitMQ) to relay messages between server instances.
Sticky Sessions
WebSocket connections must remain on the same server for their entire lifetime. Configure your load balancer to use sticky sessions (IP hash or cookie-based) to ensure WebSocket upgrade requests go to the same server as the initial HTTP handshake.
Connection Limits
Each WebSocket connection consumes a file descriptor and memory. A single server can typically handle 10,000 to 100,000 concurrent connections depending on hardware and message frequency. Monitor connection counts and implement backpressure mechanisms.
When to Use What
Short Polling: Low-frequency updates where simplicity matters (checking for software updates, infrequent status checks).
Long Polling: When you need near real-time updates but cannot use WebSockets (firewall restrictions, legacy infrastructure). Good fallback for WebSocket failures.
SSE: Server-to-client streaming like live news feeds, stock tickers, log tailing, and notification systems. Perfect when you only need server-to-client communication.
WebSockets: Bidirectional real-time communication like chat, gaming, collaborative editing, and live trading platforms. Essential when the client needs to send frequent messages to the server.
Understanding real-time communication patterns is essential when building modern applications. These techniques integrate with API design, load balancing, and reverse proxy configurations. Test your WebSocket and HTTP connections using our API and Network Tools.
Frequently Asked Questions
Do WebSockets work behind corporate firewalls?
WebSockets can sometimes be blocked by corporate firewalls and proxy servers that do not understand the WebSocket protocol. Using WSS (WebSocket Secure over TLS on port 443) significantly improves compatibility because firewalls typically allow HTTPS traffic. If WebSockets are blocked, implement a fallback to long polling. Libraries like Socket.IO handle this automatic fallback transparently.
How many concurrent WebSocket connections can a server handle?
A well-optimized server can handle 100,000+ concurrent WebSocket connections. The main limiting factors are file descriptors (each connection uses one), memory (each connection consumes some memory for buffers and state), and CPU (for message processing). Tools like Node.js and Go excel at handling many concurrent connections due to their event-driven and goroutine-based architectures respectively.
When should I use SSE instead of WebSockets?
Use SSE when you only need server-to-client communication (live feeds, notifications, dashboards). SSE is simpler to implement, works over standard HTTP (no protocol upgrade), has built-in reconnection, and works naturally with HTTP/2 multiplexing. SSE also works better with CDNs and reverse proxies since it uses standard HTTP.
How does Socket.IO relate to WebSockets?
Socket.IO is a library that provides a WebSocket-like API but adds features like automatic reconnection, fallback to long polling, room-based broadcasting, and acknowledgements. It uses its own protocol on top of WebSocket (or long polling as fallback). While convenient, Socket.IO is not compatible with standard WebSocket clients. If you need standard WebSocket compatibility, use the native WebSocket API or a library like ws (Node.js).
How do I handle authentication with WebSockets?
There are two common approaches. First, authenticate during the initial HTTP handshake by passing a JWT token as a query parameter or cookie in the upgrade request. Second, authenticate after the connection is established by sending an authentication message as the first WebSocket message. The first approach is simpler but exposes the token in server logs. The second approach is more secure but requires handling unauthenticated connections briefly. Always use WSS (TLS) for security. For more on API security, see our Security and Crypto Tools.