HTTP is a request-response protocol — the client asks, the server answers, the connection closes. That model works for loading pages and fetching data, but it breaks down when your application needs to push data to the client in real time. WebSockets solve this by establishing a persistent, bidirectional connection between browser and server — enabling live feeds, collaborative editing, chat, gaming, financial dashboards, and any other feature where both sides need to talk continuously.
How WebSockets Work
A WebSocket connection begins as an HTTP request — the client sends an "upgrade" header requesting a protocol switch. If the server supports WebSockets, it agrees to the upgrade and the connection is promoted from HTTP to the WebSocket protocol (ws:// or wss:// for secure). From this point, the connection stays open and both sides can send messages at any time without the overhead of re-establishing a connection.
The Handshake
// Client initiates
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
// Server responds
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
After this exchange, the TCP connection remains open for the duration of the session. Messages flow freely in both directions with minimal overhead — each frame has as little as 2 bytes of header compared to hundreds of bytes for an HTTP request.
WebSockets vs. Server-Sent Events vs. Long Polling
WebSockets aren't the only real-time option. Choose the right tool for your use case:
Server-Sent Events (SSE)
One-way: server to client only. Simpler than WebSockets, built on standard HTTP (no protocol upgrade needed), and works well with HTTP/2 multiplexing. Perfect for live dashboards, notification feeds, and streaming AI responses. If your client doesn't need to send data continuously, SSE is often the better choice — less complexity, automatic reconnection built in.
Long Polling
The client sends a request, the server holds it open until new data is available, then responds and the client immediately re-polls. Simulates real-time over HTTP. Higher latency and server resource usage than WebSockets or SSE, but works in environments where WebSockets are blocked (some proxies and firewalls strip WebSocket upgrade headers).
WebSockets
Bidirectional, low-latency, full-duplex. The right choice when both sides need to send messages continuously — collaborative editors, multiplayer games, trading platforms, live chat with typing indicators.
Implementing WebSockets in Node.js
The ws library is the standard for Node.js WebSocket servers:
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws, req) => {
const clientId = req.headers['x-client-id'];
console.log(`Client connected: ${clientId}`);
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
// Handle message
broadcast(message, ws); // Send to all other clients
});
ws.on('close', () => {
console.log(`Client disconnected: ${clientId}`);
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
});
function broadcast(message, sender) {
wss.clients.forEach(client => {
if (client !== sender && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
}
Client-Side WebSocket Implementation
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectDelay = 1000; // Reset delay on successful connection
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
this.ws.onclose = () => {
console.log('Disconnected — reconnecting...');
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
};
}
send(message) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
}
The exponential backoff reconnection pattern is essential — it prevents thundering herd problems when a server restarts and all clients try to reconnect simultaneously.
Scaling WebSocket Applications
WebSockets are stateful — each connection is tied to a specific server instance. This creates scaling challenges that don't exist with stateless HTTP APIs.
The Sticky Sessions Problem
In a multi-server deployment, a load balancer must route all messages from a given client to the same server instance (sticky sessions). Otherwise, broadcast messages sent to one server won't reach clients connected to other servers.
Pub/Sub with Redis
The standard solution: use Redis Pub/Sub as a message broker between server instances. When any server receives a message to broadcast, it publishes to Redis. All servers subscribe to Redis and forward messages to their connected clients.
import { createClient } from 'redis';
const publisher = createClient();
const subscriber = createClient();
await publisher.connect();
await subscriber.connect();
// When receiving a message to broadcast
async function broadcastToAll(message) {
await publisher.publish('ws-messages', JSON.stringify(message));
}
// Each server instance subscribes
await subscriber.subscribe('ws-messages', (message) => {
const data = JSON.parse(message);
// Forward to all locally connected clients
localClients.forEach(client => client.send(message));
});
Managed WebSocket Services
For most applications, managed services eliminate the scaling complexity entirely: Ably, Pusher, and Supabase Realtime handle connection management, scaling, and delivery guarantees. Supabase Realtime is particularly compelling for projects already using Supabase — it exposes database changes as WebSocket events with row-level security applied.
Security Considerations
- Always use wss:// (WebSocket Secure) in production — plain ws:// transmits data unencrypted
- Authenticate on connection — pass a JWT or session token in the handshake; validate before establishing the connection
- Validate all incoming messages — never trust client-sent data; parse and validate every message schema
- Rate limit messages — prevent abuse by limiting messages per connection per time window
- Set connection timeouts — close idle connections to prevent resource exhaustion
Frequently Asked Questions
Do WebSockets work through firewalls and proxies?
Most modern firewalls allow WebSockets. Some corporate proxies block the Upgrade header, in which case you can fall back to long polling. Socket.io handles this automatically by detecting the available transport and selecting the best option.
How many concurrent WebSocket connections can a server handle?
A well-tuned Node.js server can handle 10,000–100,000+ concurrent connections depending on available memory and connection activity level. Each idle WebSocket connection uses roughly 50–100KB of memory. Plan infrastructure accordingly and use horizontal scaling for high-concurrency requirements.
Should I use Socket.io or native WebSockets?
Socket.io adds useful features (rooms, namespaces, automatic fallback, reconnection) but adds ~200KB to your bundle and abstracts over native WebSockets. For simple use cases, native WebSockets are fine. For complex real-time features with multiple channels, Socket.io's abstractions save significant development time.
How do I test WebSocket connections?
Use websocat (command-line WebSocket client) for quick testing. For integration tests, the ws library's WebSocket client works in Node.js test environments. Postman also supports WebSocket testing through its UI.
Related Reading
- Web Development: The Complete Guide
- API Gateway Design Patterns
- Progressive Disclosure in UX Design
- User Onboarding Design Guide
Need real-time features in your web application?
We build WebSocket-powered applications — from live dashboards to collaborative tools to real-time notification systems. Let's talk about your project.
Let's Build Something Real-Time