What are WebSockets?
WebSockets provide full-duplex communication channels over a single TCP connection. Unlike HTTP, WebSockets allow the server to push data to the client without the client requesting it first.
🔌 WebSocket vs HTTP
// HTTP (Request-Response)
// Client must initiate every interaction
// 1. Client → Server: Request
// 2. Server → Client: Response
// Overhead: Headers sent with every request
fetch('/api/messages') // Client must poll repeatedly
// WebSocket (Full-Duplex)
// Persistent connection, both can send anytime
// 1. Client ↔ Server: Handshake (upgrade from HTTP)
// 2. Client ↔ Server: Bidirectional messages
// Low overhead: Only data frames after connection
let ws = new WebSocket('ws://localhost:3000');
ws.onmessage = e => console.log(e.data); // Server can push
// Use cases for WebSockets:
// ✅ Chat applications
// ✅ Live notifications
// ✅ Real-time dashboards
// ✅ Collaborative editing
// ✅ Gaming
// ✅ Live sports scores
// ✅ Stock tickers
// Use HTTP when:
// ❌ One-way data flow
// ❌ Infrequent updates
// ❌ RESTful operations
// ❌ Large file transfers
🚀 Creating a WebSocket Connection
// Create connection
let ws = new WebSocket('ws://localhost:3000');
// Secure WebSocket (WSS) - like HTTPS
let wss = new WebSocket('wss://example.com/socket');
// With subprotocols
let ws = new WebSocket('ws://localhost:3000', 'chat');
let ws = new WebSocket('ws://localhost:3000', ['chat', 'json']);
// Connection states
console.log(ws.readyState);
// WebSocket.CONNECTING (0) - connecting
// WebSocket.OPEN (1) - connection open
// WebSocket.CLOSING (2) - connection closing
// WebSocket.CLOSED (3) - connection closed
// Check if open
if (ws.readyState === WebSocket.OPEN) {
ws.send('Hello');
}
// Connection lifecycle
let ws = new WebSocket('ws://localhost:3000');
ws.onopen = event => {
console.log('Connected');
ws.send('Hello Server!');
};
ws.onmessage = event => {
console.log('Received:', event.data);
};
ws.onerror = error => {
console.error('WebSocket error:', error);
};
ws.onclose = event => {
console.log('Disconnected');
console.log('Code:', event.code);
console.log('Reason:', event.reason);
console.log('Clean:', event.wasClean);
};
📤 Sending Messages
// Send text
ws.send('Hello Server');
// Send JSON
let data = { type: 'message', content: 'Hello' };
ws.send(JSON.stringify(data));
// Send Blob
let blob = new Blob(['Hello'], { type: 'text/plain' });
ws.send(blob);
// Send ArrayBuffer
let buffer = new ArrayBuffer(8);
let view = new Uint8Array(buffer);
view[0] = 72; // 'H'
view[1] = 105; // 'i'
ws.send(buffer);
// Check buffer amount before sending
console.log(ws.bufferedAmount); // Bytes queued but not sent
// Wait for buffer to clear
function sendWhenReady(ws, data) {
if (ws.bufferedAmount === 0) {
ws.send(data);
} else {
setTimeout(() => sendWhenReady(ws, data), 100);
}
}
// Safe send function
function safeSend(ws, data) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data);
return true;
}
return false;
}
📥 Receiving Messages
// Receive text
ws.onmessage = event => {
console.log('Message:', event.data);
};
// Receive JSON
ws.onmessage = event => {
try {
let data = JSON.parse(event.data);
console.log('Type:', data.type);
console.log('Content:', data.content);
} catch (error) {
console.error('Invalid JSON:', event.data);
}
};
// Handle different message types
ws.onmessage = event => {
let message = JSON.parse(event.data);
switch (message.type) {
case 'chat':
displayChatMessage(message.content);
break;
case 'notification':
showNotification(message.content);
break;
case 'update':
updateData(message.content);
break;
default:
console.warn('Unknown message type:', message.type);
}
};
// Receive Blob
ws.onmessage = event => {
if (event.data instanceof Blob) {
event.data.text().then(text => {
console.log('Blob content:', text);
});
}
};
// Receive ArrayBuffer
ws.binaryType = 'arraybuffer'; // Default is 'blob'
ws.onmessage = event => {
if (event.data instanceof ArrayBuffer) {
let view = new Uint8Array(event.data);
console.log('Binary data:', view);
}
};
🔒 Closing Connection
// Close connection
ws.close();
// Close with code and reason
ws.close(1000, 'Normal closure');
// Close event codes
// 1000 - Normal closure
// 1001 - Going away (page unload)
// 1002 - Protocol error
// 1003 - Unsupported data
// 1006 - Abnormal closure (no close frame)
// 1007 - Invalid data
// 1008 - Policy violation
// 1009 - Message too big
// 1011 - Server error
// 3000-3999 - Application-specific
// 4000-4999 - Private use
ws.onclose = event => {
console.log('Close code:', event.code);
console.log('Reason:', event.reason);
console.log('Was clean:', event.wasClean);
if (event.code === 1000) {
console.log('Normal close');
} else if (event.code === 1006) {
console.log('Connection lost');
reconnect();
}
};
// Close on page unload
window.addEventListener('beforeunload', () => {
ws.close(1001, 'Page closing');
});
🔄 Automatic Reconnection
// Simple reconnect
function connect(url) {
let ws = new WebSocket(url);
ws.onopen = () => {
console.log('Connected');
};
ws.onclose = event => {
console.log('Disconnected, reconnecting...');
setTimeout(() => connect(url), 1000);
};
return ws;
}
let ws = connect('ws://localhost:3000');
// Exponential backoff
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.attempts = 0;
this.maxAttempts = 10;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.attempts = 0;
this.onopen && this.onopen();
};
this.ws.onmessage = event => {
this.onmessage && this.onmessage(event);
};
this.ws.onerror = error => {
this.onerror && this.onerror(error);
};
this.ws.onclose = event => {
this.onclose && this.onclose(event);
if (this.attempts < this.maxAttempts) {
let delay = Math.min(1000 * Math.pow(2, this.attempts), 30000);
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => {
this.attempts++;
this.connect();
}, delay);
} else {
console.error('Max reconnection attempts reached');
}
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
console.warn('WebSocket not open');
}
}
close() {
this.attempts = this.maxAttempts; // Prevent reconnect
this.ws.close();
}
}
// Usage
let ws = new ReconnectingWebSocket('ws://localhost:3000');
ws.onopen = () => console.log('Connected');
ws.onmessage = event => console.log('Message:', event.data);
ws.send('Hello');
💬 Chat Application Example
class ChatClient {
constructor(url, username) {
this.url = url;
this.username = username;
this.ws = null;
this.messageHandlers = [];
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected to chat');
this.send({
type: 'join',
username: this.username
});
};
this.ws.onmessage = event => {
let message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onerror = error => {
console.error('Chat error:', error);
};
this.ws.onclose = () => {
console.log('Disconnected from chat');
setTimeout(() => this.connect(), 3000);
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
sendMessage(text) {
this.send({
type: 'message',
username: this.username,
text: text,
timestamp: Date.now()
});
}
sendTyping() {
this.send({
type: 'typing',
username: this.username
});
}
handleMessage(message) {
switch (message.type) {
case 'message':
this.notifyHandlers('message', message);
break;
case 'user-joined':
this.notifyHandlers('userJoined', message);
break;
case 'user-left':
this.notifyHandlers('userLeft', message);
break;
case 'typing':
this.notifyHandlers('typing', message);
break;
}
}
on(event, handler) {
this.messageHandlers.push({ event, handler });
}
notifyHandlers(event, data) {
this.messageHandlers
.filter(h => h.event === event)
.forEach(h => h.handler(data));
}
disconnect() {
this.send({ type: 'leave' });
this.ws.close();
}
}
// Usage
let chat = new ChatClient('ws://localhost:3000/chat', 'John');
chat.on('message', message => {
console.log(`${message.username}: ${message.text}`);
displayMessage(message);
});
chat.on('userJoined', data => {
console.log(`${data.username} joined`);
});
chat.on('typing', data => {
showTypingIndicator(data.username);
});
// Send message
let input = document.querySelector('#messageInput');
let sendBtn = document.querySelector('#sendBtn');
sendBtn.addEventListener('click', () => {
chat.sendMessage(input.value);
input.value = '';
});
// Typing indicator
let typingTimeout;
input.addEventListener('input', () => {
clearTimeout(typingTimeout);
chat.sendTyping();
typingTimeout = setTimeout(() => {
// Stop typing indicator
}, 3000);
});
📊 Real-Time Dashboard Example
class Dashboard {
constructor(url) {
this.ws = new WebSocket(url);
this.subscribers = new Map();
this.connect();
}
connect() {
this.ws.onopen = () => {
console.log('Dashboard connected');
// Subscribe to data streams
this.subscribe(['users', 'sales', 'traffic']);
};
this.ws.onmessage = event => {
let data = JSON.parse(event.data);
this.handleUpdate(data);
};
this.ws.onclose = () => {
console.log('Dashboard disconnected');
setTimeout(() => this.connect(), 5000);
};
}
subscribe(topics) {
this.ws.send(JSON.stringify({
action: 'subscribe',
topics: topics
}));
}
handleUpdate(data) {
let { topic, value } = data;
// Notify subscribers
if (this.subscribers.has(topic)) {
this.subscribers.get(topic).forEach(callback => {
callback(value);
});
}
}
on(topic, callback) {
if (!this.subscribers.has(topic)) {
this.subscribers.set(topic, []);
}
this.subscribers.get(topic).push(callback);
}
}
// Usage
let dashboard = new Dashboard('ws://localhost:3000/dashboard');
dashboard.on('users', count => {
document.querySelector('#userCount').textContent = count;
});
dashboard.on('sales', data => {
updateSalesChart(data);
});
dashboard.on('traffic', data => {
updateTrafficGraph(data);
});
🎮 Multiplayer Game Example
class GameClient {
constructor(url, playerId) {
this.url = url;
this.playerId = playerId;
this.ws = new WebSocket(url);
this.players = new Map();
this.setup();
}
setup() {
this.ws.onopen = () => {
console.log('Connected to game');
this.send({
type: 'join',
playerId: this.playerId
});
};
this.ws.onmessage = event => {
let message = JSON.parse(event.data);
this.handleMessage(message);
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
handleMessage(message) {
switch (message.type) {
case 'player-joined':
this.players.set(message.playerId, message.player);
this.onPlayerJoined(message.player);
break;
case 'player-left':
this.players.delete(message.playerId);
this.onPlayerLeft(message.playerId);
break;
case 'player-moved':
let player = this.players.get(message.playerId);
if (player) {
player.x = message.x;
player.y = message.y;
this.onPlayerMoved(message.playerId, message.x, message.y);
}
break;
case 'game-state':
this.onGameState(message.state);
break;
}
}
move(x, y) {
this.send({
type: 'move',
playerId: this.playerId,
x: x,
y: y,
timestamp: Date.now()
});
}
// Throttle position updates
startSendingPosition() {
setInterval(() => {
let { x, y } = this.getPlayerPosition();
this.move(x, y);
}, 50); // 20 updates per second
}
// Override these methods
onPlayerJoined(player) {}
onPlayerLeft(playerId) {}
onPlayerMoved(playerId, x, y) {}
onGameState(state) {}
getPlayerPosition() { return { x: 0, y: 0 }; }
}
// Usage
let game = new GameClient('ws://localhost:3000/game', 'player-123');
game.onPlayerJoined = player => {
console.log('Player joined:', player);
addPlayerToScene(player);
};
game.onPlayerMoved = (playerId, x, y) => {
updatePlayerPosition(playerId, x, y);
};
game.startSendingPosition();
💡 Best Practices
// 1. Always handle reconnection
// Network can be unstable
// 2. Heartbeat/Ping-Pong
// Keep connection alive, detect dead connections
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // Every 30 seconds
ws.onmessage = event => {
let message = JSON.parse(event.data);
if (message.type === 'pong') {
lastPong = Date.now();
}
};
// 3. Message queue for offline messages
class QueuedWebSocket {
constructor(url) {
this.url = url;
this.queue = [];
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
// Send queued messages
while (this.queue.length > 0) {
this.ws.send(this.queue.shift());
}
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
this.queue.push(data);
}
}
}
// 4. Compression for large messages
// Use gzip or deflate if supported
// 5. Authentication
// Send token after connection
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'auth',
token: localStorage.getItem('token')
}));
};
// 6. Error handling
ws.onerror = error => {
console.error('WebSocket error:', error);
notifyUser('Connection error');
};
// 7. Close gracefully
window.addEventListener('beforeunload', () => {
ws.close(1000, 'Page closing');
});
// 8. Rate limiting
// Don't spam server with messages
let lastSend = 0;
const MIN_INTERVAL = 100; // ms
function rateLimitedSend(data) {
let now = Date.now();
if (now - lastSend >= MIN_INTERVAL) {
ws.send(data);
lastSend = now;
}
}
🔧 Debugging WebSockets
// Browser DevTools
// 1. Network tab → WS filter
// 2. Shows all WebSocket connections
// 3. Click connection to see messages
// 4. Can see frames sent/received
// Log all events
let ws = new WebSocket('ws://localhost:3000');
ws.addEventListener('open', event => {
console.log('OPEN:', event);
});
ws.addEventListener('message', event => {
console.log('MESSAGE:', event.data);
});
ws.addEventListener('error', event => {
console.log('ERROR:', event);
});
ws.addEventListener('close', event => {
console.log('CLOSE:', event.code, event.reason);
});
// Wrapper for logging
class LoggedWebSocket extends WebSocket {
send(data) {
console.log('→ SEND:', data);
super.send(data);
}
}
// Monitor connection quality
let messagesSent = 0;
let messagesReceived = 0;
let latency = 0;
ws.send(JSON.stringify({
type: 'ping',
timestamp: Date.now()
}));
ws.onmessage = event => {
let message = JSON.parse(event.data);
if (message.type === 'pong') {
latency = Date.now() - message.timestamp;
console.log('Latency:', latency, 'ms');
}
};
🎯 Key Takeaways
- Full-Duplex: Both client and server can send messages anytime
- Persistent Connection: Stays open, low overhead compared to HTTP polling
- Real-Time: Ideal for chat, notifications, live updates, games
- Reconnection: Always implement automatic reconnection with backoff
- Heartbeat: Send ping/pong messages to keep connection alive
- WSS: Use secure WebSockets (wss://) in production
- Message Format: JSON is common, but can send binary (Blob/ArrayBuffer)
- Ready State: Check readyState before sending to avoid errors