Understanding WebSockets: A Comprehensive Guide
WebSockets enable real-time, full-duplex communication between a client and server over a single persistent TCP connection. Unlike HTTP, where the client initiates every request, WebSockets allow the server to push data to the client at any time — no polling required.
WebSockets vs. HTTP: The Core Difference
HTTP is a request-response protocol. Every interaction requires the client to open a connection, send a request, wait for a response, and close the connection. This works for static page loads but is inefficient for real-time features.
| HTTP (REST) | WebSocket | |
|---|---|---|
| Connection | New connection per request | Single persistent connection |
| Direction | Client → Server only | Bidirectional |
| Latency | New TCP + TLS handshake each time | Established once |
| Overhead | Full HTTP headers every request | Minimal framing after handshake |
| Server push | Not supported (without SSE/polling) | Native |
| Best for | CRUD APIs, document retrieval | Chat, live feeds, multiplayer, IoT |
How WebSockets Work
Step 1: The Handshake
A WebSocket connection starts as an HTTP request. The client sends an Upgrade header:
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Step 2: Server Accepts
If the server supports WebSockets, it responds with HTTP 101:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
From this point, the TCP connection is no longer HTTP. Both sides can send frames at any time.
Step 3: Framing
WebSocket messages are sent as frames. A frame contains:
- An opcode (text, binary, ping, pong, close)
- A payload length
- An optional masking key (clients must mask frames; servers must not)
- The payload data
This framing is lightweight: a small text frame adds only 2–6 bytes of overhead.
Step 4: Closure
Either side can send a close frame. The receiving side must respond with a close frame, then both sides tear down the TCP connection.
Use Cases
Real-time applications that need WebSockets:
- Chat: Instant message delivery without polling
- Collaborative editing: Google Docs-style concurrent document changes
- Live dashboards: Stock tickers, system metrics, analytics in real time
- Multiplayer games: Low-latency game state synchronization
- Financial trading: Order book updates, price feeds
- IoT: Sensor data streams, device control
Applications that do NOT need WebSockets:
- Standard CRUD APIs — use REST or GraphQL
- Infrequent updates (once per minute) — use HTTP polling or SSE
- File uploads/downloads — standard HTTP handles these better
Server-Side Implementation
Node.js with ws
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
server.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (message) => {
console.log('Received:', message.toString());
// Broadcast to all connected clients
server.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message.toString());
}
});
});
ws.on('close', () => console.log('Client disconnected'));
ws.send('Connected to server');
});
Browser Client
const socket = new WebSocket('wss://example.com/ws');
socket.addEventListener('open', () => {
console.log('Connected');
socket.send(JSON.stringify({ type: 'hello', text: 'Hi server!' }));
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Message from server:', data);
});
socket.addEventListener('close', (event) => {
console.log(`Closed: code=${event.code}, reason=${event.reason}`);
// Reconnect logic here
});
socket.addEventListener('error', (err) => {
console.error('WebSocket error:', err);
});
Security
Always Use wss://
ws:// sends data in plaintext — equivalent to http://. Use wss:// (WebSocket Secure = WebSocket over TLS) in all production environments.
Validate the Origin Header
Browsers send an Origin header in the WebSocket handshake. If your server ignores it, any malicious website can open a WebSocket connection to your server using the user's session cookies (cross-site WebSocket hijacking).
Always check that the origin matches your known domains before upgrading the connection.
Authenticate Before Upgrading
The WebSocket handshake is an HTTP request — you can read cookies, query parameters, or Authorization headers:
// Option 1: token in query string
// wss://example.com/ws?token=abc123
const token = new URL(request.url, 'http://base').searchParams.get('token');
// Option 2: cookie-based (automatic in browsers)
const sessionCookie = parseCookie(request.headers.cookie);
Reject the upgrade (return HTTP 401) if authentication fails — once upgraded to WebSocket, you have less control over the connection.
Message Validation
Never trust incoming WebSocket messages. Validate message types, sizes, and content before processing — treat WebSocket input with the same caution as any user input.
Scaling WebSockets
A WebSocket connection holds a file descriptor on the server for its lifetime. A single Node.js process can typically handle ~50,000–100,000 concurrent connections before hitting limits.
Horizontal scaling with Redis Pub/Sub:
When you have multiple server instances, a message received by instance A needs to reach clients connected to instances B and C. Use a message broker:
Client A → Server Instance 1 → Redis Pub/Sub → Server Instance 2 → Client B
Each server instance subscribes to a Redis channel. When it receives a WebSocket message, it publishes to Redis. All instances receive the publication and forward it to their local connected clients.
WebSockets vs. Alternatives
| WebSockets | Server-Sent Events (SSE) | Long Polling | |
|---|---|---|---|
| Direction | Bidirectional | Server → Client only | Bidirectional |
| Protocol | Custom over TCP | HTTP | HTTP |
| Browser support | Excellent | Good (no IE) | Universal |
| Auto-reconnect | Manual | Built-in | Manual |
| Overhead | Very low | Low | High (new request per message) |
| Best for | Chat, games, bidirectional feeds | News feeds, notifications | Legacy fallback |
Common Pitfalls
- No reconnection logic: Networks are unreliable. Implement exponential backoff reconnection on the client.
- Storing connection state in memory only: If the server restarts, all connections drop. Persist important state to a database.
- Forgetting ping/pong: Idle connections can be silently dropped by proxies and NAT gateways. Implement heartbeats.
- Unbounded message queues: If a slow client can't consume messages fast enough, the queue grows without bound. Set limits and drop or disconnect lagging clients.
