What is WebSocket?
WebSocket is a computer communications protocol that provides full-duplex communication channels over a single TCP connection. Unlike HTTP’s request-response model, WebSocket enables bidirectional, real-time communication between client and server, making it ideal for applications requiring instant updates like chat, live feeds, and gaming.
WebSocket vs HTTP
Key Differences
interface ProtocolComparison {
http: {
pattern: 'Request-Response';
connection: 'Short-lived';
direction: 'Client → Server';
overhead: 'High (headers with each request)';
realtime: 'Poor (polling required)';
useCases: ['REST APIs', 'Web pages', 'File downloads'];
};
webSocket: {
pattern: 'Bidirectional streaming';
connection: 'Long-lived, persistent';
direction: 'Client ↔ Server';
overhead: 'Low (initial handshake only)';
realtime: 'Excellent (push-based)';
useCases: ['Chat apps', 'Live feeds', 'Gaming', 'Collaboration tools'];
};
}
// HTTP - Multiple requests
// Client → Request 1 → Server
// Client ← Response 1 ← Server
// Client → Request 2 → Server
// Client ← Response 2 ← Server
// WebSocket - Single connection
// Client ↔ Handshake ↔ Server
// Client ↔ Message 1 ↔ Server
// Client ↔ Message 2 ↔ Server
// Client ↔ Message 3 ↔ Server
WebSocket Handshake
Connection Upgrade
// Client initiates upgrade from HTTP to WebSocket
const request = `
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
`;
// Server accepts upgrade
const response = `
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
`;
// After handshake, connection upgraded to WebSocket
// Now both can send/receive messages freely
Basic WebSocket Usage
Client-Side (Browser)
class WebSocketClient {
private ws: WebSocket | null = null;
private reconnectAttempts: number = 0;
private maxReconnectAttempts: number = 5;
connect(url: string) {
this.ws = new WebSocket(url);
this.ws.onopen = (event) => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
// Send initial message
this.send({ type: 'auth', token: 'user-token' });
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
this.handleMessage(data);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
// Attempt reconnection
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => this.connect(url), delay);
}
};
}
send(data: any) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
console.error('WebSocket not connected');
}
}
private handleMessage(data: any) {
switch (data.type) {
case 'message':
console.log('Chat message:', data.content);
break;
case 'notification':
console.log('Notification:', data.message);
break;
case 'update':
console.log('Update:', data.payload);
break;
}
}
disconnect() {
if (this.ws) {
this.ws.close(1000, 'Client disconnecting');
this.ws = null;
}
}
getState(): string {
if (!this.ws) return 'DISCONNECTED';
const states = {
[WebSocket.CONNECTING]: 'CONNECTING',
[WebSocket.OPEN]: 'OPEN',
[WebSocket.CLOSING]: 'CLOSING',
[WebSocket.CLOSED]: 'CLOSED'
};
return states[this.ws.readyState];
}
}
// Usage
const client = new WebSocketClient();
client.connect('wss://api.example.com/ws');
// Send messages
client.send({ type: 'message', content: 'Hello!' });
// Disconnect
client.disconnect();
Server-Side (Node.js with ws)
import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';
class WSServer {
private wss: WebSocketServer;
private clients: Map<string, WebSocket> = new Map();
constructor(port: number) {
const server = http.createServer();
this.wss = new WebSocketServer({ server });
this.wss.on('connection', (ws, req) => {
const clientId = this.generateClientId();
this.clients.set(clientId, ws);
console.log(`Client connected: ${clientId}`);
console.log(`Total clients: ${this.clients.size}`);
ws.on('message', (data) => {
this.handleMessage(clientId, data.toString());
});
ws.on('close', () => {
console.log(`Client disconnected: ${clientId}`);
this.clients.delete(clientId);
});
ws.on('error', (error) => {
console.error(`Client error ${clientId}:`, error);
});
// Send welcome message
ws.send(JSON.stringify({
type: 'welcome',
clientId,
message: 'Connected to server'
}));
});
server.listen(port, () => {
console.log(`WebSocket server listening on port ${port}`);
});
}
private handleMessage(clientId: string, message: string) {
try {
const data = JSON.parse(message);
console.log(`Message from ${clientId}:`, data);
switch (data.type) {
case 'message':
this.broadcast(data, clientId);
break;
case 'private':
this.sendToClient(data.targetId, data);
break;
default:
console.log('Unknown message type:', data.type);
}
} catch (error) {
console.error('Invalid message format:', error);
}
}
private broadcast(data: any, exclude?: string) {
const message = JSON.stringify(data);
this.clients.forEach((ws, clientId) => {
if (clientId !== exclude && ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
});
}
private sendToClient(clientId: string, data: any) {
const ws = this.clients.get(clientId);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
}
private generateClientId(): string {
return `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
getClientsCount(): number {
return this.clients.size;
}
}
// Usage
const server = new WSServer(8080);
Real-World Applications
Chat Application
class ChatApp {
private ws: WebSocket;
private username: string;
constructor(url: string, username: string) {
this.username = username;
this.ws = new WebSocket(url);
this.ws.onopen = () => {
// Join chat room
this.send({
type: 'join',
username: this.username,
room: 'general'
});
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'message':
this.displayMessage(data.username, data.content);
break;
case 'userJoined':
this.displaySystemMessage(`${data.username} joined the chat`);
break;
case 'userLeft':
this.displaySystemMessage(`${data.username} left the chat`);
break;
}
};
}
sendMessage(content: string) {
this.send({
type: 'message',
content,
timestamp: Date.now()
});
}
private send(data: any) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
private displayMessage(username: string, content: string) {
console.log(`${username}: ${content}`);
// Update UI
}
private displaySystemMessage(message: string) {
console.log(`[System] ${message}`);
// Update UI
}
}
// Usage
const chat = new ChatApp('wss://chat.example.com', 'Alice');
chat.sendMessage('Hello everyone!');
Live Dashboard
class LiveDashboard {
private ws: WebSocket;
private metrics: Map<string, any> = new Map();
constructor(url: string) {
this.ws = new WebSocket(url);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'metrics') {
this.updateMetrics(data.metrics);
}
};
}
private updateMetrics(metrics: Record<string, any>) {
Object.entries(metrics).forEach(([key, value]) => {
const previous = this.metrics.get(key);
this.metrics.set(key, value);
// Calculate change
if (previous !== undefined) {
const change = value - previous;
const percentage = (change / previous) * 100;
console.log(`${key}: ${value} (${percentage.toFixed(2)}%)`);
}
// Update UI charts/graphs
this.updateChart(key, value);
});
}
private updateChart(metric: string, value: any) {
// Update dashboard UI
console.log(`Update chart: ${metric} = ${value}`);
}
subscribe(metrics: string[]) {
this.ws.send(JSON.stringify({
type: 'subscribe',
metrics
}));
}
}
// Usage
const dashboard = new LiveDashboard('wss://dashboard.example.com');
dashboard.subscribe(['cpu', 'memory', 'requests']);
WebSocket with Proxies
Proxying WebSocket Connections
# Nginx reverse proxy for WebSocket
location /ws/ {
proxy_pass http://backend:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket timeout settings
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
Node.js WebSocket Proxy
import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';
class WebSocketProxy {
constructor(listenPort: number, targetUrl: string) {
const server = http.createServer();
const wss = new WebSocketServer({ server });
wss.on('connection', (clientWs) => {
console.log('Client connected to proxy');
// Connect to target server
const targetWs = new WebSocket(targetUrl);
targetWs.on('open', () => {
console.log('Connected to target server');
// Pipe messages between client and target
clientWs.on('message', (data) => {
if (targetWs.readyState === WebSocket.OPEN) {
targetWs.send(data);
}
});
targetWs.on('message', (data) => {
if (clientWs.readyState === WebSocket.OPEN) {
clientWs.send(data);
}
});
});
targetWs.on('close', () => {
clientWs.close();
});
clientWs.on('close', () => {
targetWs.close();
});
});
server.listen(listenPort, () => {
console.log(`WebSocket proxy listening on port ${listenPort}`);
});
}
}
// Usage - Proxy WebSocket traffic
new WebSocketProxy(8080, 'wss://target-server.com/ws');
Security
Secure WebSocket (WSS)
// Always use wss:// in production
const secureWs = new WebSocket('wss://api.example.com/ws');
// vs insecure ws://
const insecureWs = new WebSocket('ws://api.example.com/ws'); // Don't use
// WSS provides:
// - Encrypted communication (TLS/SSL)
// - Authentication
// - Data integrity
// - Protection from man-in-the-middle attacks
Authentication
class AuthenticatedWebSocket {
private ws: WebSocket | null = null;
private token: string;
constructor(url: string, token: string) {
this.token = token;
// Method 1: Token in URL query parameter
this.ws = new WebSocket(`${url}?token=${token}`);
// Method 2: Token in first message
this.ws.onopen = () => {
this.ws?.send(JSON.stringify({
type: 'auth',
token: this.token
}));
};
// Method 3: Token in subprotocol header
this.ws = new WebSocket(url, [`auth.${token}`]);
}
}
// Server-side validation
class SecureWSServer {
private wss: WebSocketServer;
constructor(port: number) {
this.wss = new WebSocketServer({
port,
verifyClient: (info, callback) => {
// Verify token
const token = new URL(info.req.url!, 'ws://base').searchParams.get('token');
if (this.isValidToken(token)) {
callback(true);
} else {
callback(false, 401, 'Unauthorized');
}
}
});
}
private isValidToken(token: string | null): boolean {
// Validate JWT or session token
return token !== null && token.length > 0;
}
}
Rate Limiting
class RateLimitedWSServer {
private clients: Map<string, {
ws: WebSocket;
messageCount: number;
lastReset: number;
}> = new Map();
private readonly MAX_MESSAGES_PER_MINUTE = 60;
handleMessage(clientId: string, message: string) {
const client = this.clients.get(clientId);
if (!client) return;
const now = Date.now();
// Reset counter every minute
if (now - client.lastReset > 60000) {
client.messageCount = 0;
client.lastReset = now;
}
// Check rate limit
if (client.messageCount >= this.MAX_MESSAGES_PER_MINUTE) {
client.ws.send(JSON.stringify({
type: 'error',
message: 'Rate limit exceeded'
}));
return;
}
client.messageCount++;
// Process message
this.processMessage(message);
}
private processMessage(message: string) {
// Handle message
}
}
Heartbeat / Ping-Pong
Keep-Alive Implementation
class WebSocketWithHeartbeat {
private ws: WebSocket;
private pingInterval: NodeJS.Timeout | null = null;
private pongTimeout: NodeJS.Timeout | null = null;
constructor(url: string) {
this.ws = new WebSocket(url);
this.ws.onopen = () => {
this.startHeartbeat();
};
this.ws.onclose = () => {
this.stopHeartbeat();
};
// Listen for pong
this.ws.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
this.receivedPong();
}
});
}
private startHeartbeat() {
// Send ping every 30 seconds
this.pingInterval = setInterval(() => {
this.sendPing();
}, 30000);
}
private stopHeartbeat() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
if (this.pongTimeout) {
clearTimeout(this.pongTimeout);
}
}
private sendPing() {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
// Expect pong within 5 seconds
this.pongTimeout = setTimeout(() => {
console.log('No pong received - connection lost');
this.ws.close();
}, 5000);
}
}
private receivedPong() {
if (this.pongTimeout) {
clearTimeout(this.pongTimeout);
}
}
}
Best Practices
For Clients
- Always use WSS (secure WebSocket) in production
- Implement reconnection logic with exponential backoff
- Handle connection state changes properly
- Validate all incoming messages
- Implement heartbeat/ping-pong
For Servers
- Authenticate connections
- Implement rate limiting
- Set appropriate timeouts
- Handle client disconnections gracefully
- Use message validation
For Performance
- Minimize message size (use binary when possible)
- Batch multiple small messages
- Implement message compression
- Use connection pooling on server
- Monitor active connections