WebSockets: Persistent Connections
WebSockets provide a persistent, full-duplex communication channel between client and server. Unlike traditional HTTP requests where the client always initiates communication, WebSockets allow either side to send messages at any time — making them ideal for chat applications, live dashboards, collaborative tools, and gaming.
How WebSockets Differ from HTTP
HTTP follows a request-response pattern. The client asks, the server answers, and the connection closes. Each request requires a new TCP handshake, adding latency for repeated communication:
// HTTP: New connection for each request
async function fetchUpdates() {
const response = await fetch("/api/notifications");
const data = await response.json();
return data;
}
WebSockets maintain an open TCP connection after the initial handshake. Both client and server can push data without waiting for a request:
// WebSocket: Persistent connection, bidirectional
const ws = new WebSocket("wss://example.com/socket");
ws.onopen = () => {
console.log("Connected to server");
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Received:", data);
};
ws.send(JSON.stringify({ type: "hello", message: "From client" }));
The wss:// protocol indicates a WebSocket connection over TLS — always use this in production.
The WebSocket Lifecycle
A WebSocket connection goes through distinct states:
- Connecting: The initial handshake is in progress
- Open: The connection is established and ready for communication
- Closing: The connection is being closed
- Closed: The connection has been closed
You can check the current state via the readyState property:
const ws = new WebSocket("wss://example.com/socket");
console.log(ws.readyState); // 0 = CONNECTING
ws.onopen = () => {
console.log(ws.readyState); // 1 = OPEN
};
ws.onclose = () => {
console.log(ws.readyState); // 3 = CLOSED
};
// Constants for readability
const State = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3
};
Client-Side Implementation
Creating a Connection
function connectWebSocket(url) {
const ws = new WebSocket(url);
ws.onopen = function() {
console.log("WebSocket open");
};
ws.onmessage = function(event) {
// Handle incoming message
const message = JSON.parse(event.data);
handleMessage(message);
};
ws.onerror = function(error) {
console.error("WebSocket error:", error);
};
ws.onclose = function(event) {
console.log("WebSocket closed:", event.code, event.reason);
// Optionally attempt reconnection
};
return ws;
}
const socket = connectWebSocket("wss://api.example.com/ws");
Sending Messages
You can send text or binary data:
// Send text
socket.send(JSON.stringify({ action: "subscribe", channel: "updates" }));
// Send binary (ArrayBuffer)
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setFloat64(0, 3.14159);
socket.send(buffer);
// Send binary (Blob)
const blob = new Blob([JSON.stringify({ type: "file", data: "..." })],
{ type: "application/json" });
socket.send(blob);
Handling Reconnection
Network issues cause WebSocket connections to close. Implement reconnection logic:
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
this.reconnectDelay = options.reconnectDelay || 1000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectAttempts = 0;
if (this.onopen) this.onopen();
};
this.ws.onmessage = (event) => {
if (this.onmessage) this.onmessage(event);
};
this.ws.onclose = (event) => {
if (this.onclose) this.onclose(event);
this.attemptReconnect();
};
this.ws.onerror = (error) => {
if (this.onerror) this.onerror(error);
};
}
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
setTimeout(() => this.connect(), delay);
}
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
}
}
}
Server-Side Considerations
Protocol Differences
The WebSocket handshake begins as an HTTP request with an Upgrade header. The server must respond with a 101 status code to complete the upgrade:
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Node.js WebSocket Server
Using the ws library:
const { WebSocketServer } = require("ws");
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws, req) => {
const clientIp = req.socket.remoteAddress;
console.log("Client connected:", clientIp);
ws.on("message", (message) => {
// Parse and handle incoming message
const data = JSON.parse(message);
if (data.type === "ping") {
ws.send(JSON.stringify({ type: "pong", timestamp: Date.now() }));
}
});
ws.on("close", () => {
console.log("Client disconnected");
});
// Send periodic updates
const interval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: "update",
time: new Date().toISOString()
}));
}
}, 5000);
ws.on("close", () => clearInterval(interval));
});
Broadcasting to All Clients
wss.on("connection", (ws) => {
ws.on("message", (message) => {
// Broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
});
Security Considerations
Validating Origins
Always validate the origin header on the server to prevent cross-site WebSocket hijacking:
const ALLOWED_ORIGINS = ["https://app.example.com", "https://admin.example.com"];
wss.on("connection", (ws, req) => {
const origin = req.headers.origin;
if (!ALLOWED_ORIGINS.includes(origin)) {
ws.close(1008, "Origin not allowed");
return;
}
// Proceed with connection
});
Authentication After Connect
WebSockets do not automatically carry cookies or authentication headers. Authenticate after the connection opens:
// Client: Send token after connect
ws.onopen = () => {
ws.send(JSON.stringify({
type: "auth",
token: localStorage.getItem("authToken")
}));
};
// Server: Validate on each message
ws.on("message", (message) => {
const data = JSON.parse(message);
if (data.type === "auth") {
const user = validateToken(data.token);
if (!user) {
ws.close(4001, "Invalid token");
return;
}
ws.user = user;
}
});
Rate Limiting
Prevent abuse by implementing message rate limits:
wss.on("connection", (ws) => {
let messageCount = 0;
const resetTime = Date.now() + 1000;
ws.on("message", (message) => {
const now = Date.now();
if (now > resetTime) {
messageCount = 0;
}
messageCount++;
if (messageCount > 100) {
ws.close(4000, "Rate limit exceeded");
}
});
});
Common Patterns
Heartbeat / Ping-Pong
Keep connections alive and detect dead connections:
// Client: Send ping every 30 seconds
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ping" }));
}
}, 30000);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "pong") {
console.log("Server responsive");
}
};
// Server: Respond to pings
ws.on("message", (message) => {
const data = JSON.parse(message);
if (data.type === "ping") {
ws.send(JSON.stringify({ type: "pong" }));
}
});
Optimistic Updates
Update the UI immediately, then reconcile with server response:
function sendMessage(text) {
// Optimistically add to UI
addMessageToUI({ text, status: "sending" });
// Send to server
ws.send(JSON.stringify({ type: "message", text }));
// Server confirms or corrects
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "message-ack") {
updateMessageStatus(data.id, "sent");
} else if (data.type === "message-error") {
updateMessageStatus(data.id, "error", data.reason);
}
};
}
When to Use WebSockets
WebSockets excel for:
- Real-time chat applications
- Live notifications and feeds
- Collaborative editing (like Google Docs)
- Multiplayer games
- Live dashboards with frequent updates
Consider HTTP long-polling or Server-Sent Events when:
- Communication is mostly one-directional (server → client)
- Updates are infrequent
- You need maximum compatibility with proxies and firewalls
- Simple request-response workflows suffice
See Also
- Fetch and XHR — Learn about traditional HTTP request patterns
- Service Workers and Caching — Understand offline-first architectures
- Promises in Depth — Master asynchronous JavaScript patterns