WebSockets Explained: Real-Time Communication for Web Apps
HTTP is a request-response protocol: the client asks, the server answers. But what if the server needs to push data to the client without waiting for a request? That's where WebSockets come in.
The Problem with HTTP Polling
Before WebSockets, developers used polling to simulate real-time updates:
// Short polling — inefficient
setInterval(async () => {
const res = await fetch('/api/status');
const data = await res.json();
updateUI(data);
}, 2000);Every 2 seconds, the client makes a new HTTP request — most of which return no new data. This wastes bandwidth and server resources at scale.
Long polling keeps the request open until the server has data, then closes it and the client immediately reconnects. Better, but still inefficient.
What WebSockets Are
WebSocket is a full-duplex, persistent connection protocol that runs over a single TCP connection. After an initial HTTP handshake, the connection is "upgraded" and both sides can send messages at any time.
Client → HTTP GET /ws (Upgrade: websocket) → Server
Client ← 101 Switching Protocols ← Server
Client ↔ WebSocket frames (bidirectional) ↔ ServerThe connection stays open until either side closes it. No repeated handshakes. No polling.
The WebSocket Handshake
The browser initiates a WebSocket connection with a special HTTP request:
GET /ws HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13The server responds with 101 Switching Protocols, and the connection switches from HTTP to the WebSocket protocol.
WebSocket in the Browser
const socket = new WebSocket('wss://api.example.com/ws');
socket.addEventListener('open', () => {
console.log('Connected');
socket.send(JSON.stringify({ type: 'subscribe', channel: 'deployments' }));
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
updateDeploymentStatus(data);
});
socket.addEventListener('close', (event) => {
console.log('Disconnected:', event.code, event.reason);
// Implement reconnection logic
});
socket.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});Note: always use wss:// (WebSocket Secure) in production — it runs over TLS.
Server-Side WebSocket: Node.js
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws, req) => {
console.log('Client connected from', req.socket.remoteAddress);
ws.on('message', (data) => {
const message = JSON.parse(data);
console.log('Received:', message);
// Broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === ws.OPEN) {
client.send(JSON.stringify({ type: 'update', payload: message }));
}
});
});
ws.on('close', () => {
console.log('Client disconnected');
});
// Send a welcome message
ws.send(JSON.stringify({ type: 'connected', message: 'Welcome!' }));
});Reconnection Logic
Network connections drop. Always implement reconnection with exponential backoff:
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.delay = 1000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.addEventListener('open', () => { this.delay = 1000; });
this.ws.addEventListener('close', () => this.scheduleReconnect());
this.ws.addEventListener('message', (e) => this.onMessage(e));
}
scheduleReconnect() {
setTimeout(() => this.connect(), this.delay);
this.delay = Math.min(this.delay * 2, 30000); // cap at 30s
}
onMessage(event) {
// Override in subclass
}
}WebSockets vs. Server-Sent Events (SSE)
| Feature | WebSockets | SSE |
|---|---|---|
| Direction | Bidirectional | Server → Client only |
| Protocol | Custom (over TCP) | HTTP |
| Reconnect | Manual | Automatic |
| Proxy support | Sometimes tricky | Generally easier |
| Use case | Chat, gaming, collab | Live feeds, notifications |
For one-way data streams (live logs, notifications, price feeds), SSE is simpler to implement. For true two-way communication, use WebSockets.
Scaling WebSockets
WebSocket connections are stateful and persistent. When you scale horizontally, a client connected to Server A can't receive messages published to Server B.
The standard solution: use a pub/sub broker (like Redis Pub/Sub or a message queue) to fan out messages across all server instances.
Client A → Server 1 → Redis Pub/Sub → Server 1, Server 2, Server 3
→ Client A, Client B, Client CWebSockets on PandaStack
PandaStack's backend uses WebSockets to push real-time deployment progress updates to the dashboard at [dashboard.pandastack.io](https://dashboard.pandastack.io). When you trigger a container deployment, log streaming and status updates flow over a persistent WebSocket connection — no polling needed. Deploy your own WebSocket-powered app with Docker containers on PandaStack. See [docs.pandastack.io](https://docs.pandastack.io) for more.
Conclusion
WebSockets enable true real-time, bidirectional communication between clients and servers. They're the right tool for chat, live dashboards, collaborative editing, and deployment log streaming. Implement reconnection logic, use Redis for multi-server scaling, and always use wss:// in production.