Connection Lifecycle & Upgrade Handshake
A WebSocket connection does not start as a raw TCP socket out of nowhere—it begins its life as a standard HTTP request.
Understanding the upgrade handshake, connection states, close codes, and heartbeat mechanisms is essential for debugging dropped connections in production.
1. The HTTP Upgrade Handshake
To establish a WebSocket connection, the client sends an HTTP GET request with special Upgrade headers. If the server agrees, it responds with HTTP 101 Switching Protocols:
Handshake Headers Explained
| Header | Purpose |
|---|---|
Upgrade: websocket | Tells the server the client wants to transition from HTTP to WebSocket. |
Connection: Upgrade | Instructs intermediate proxies not to close the connection after the response. |
Sec-WebSocket-Key | A random 16-byte base64 string generated by the client for security validation. |
Sec-WebSocket-Accept | The server's proof that it speaks the WebSocket protocol. |
Sec-WebSocket-Version: 13 | Declares the protocol version (13 is the standard). |
2. How Sec-WebSocket-Accept is Computed
To prevent accidental upgrades by proxy servers or non-WebSocket HTTP endpoints, the server concatenates the client's Sec-WebSocket-Key with a fixed GUID defined in RFC 6455 (258EAFA5-E914-47DA-95CA-C5AB0DC85B11), hashes it with SHA-1, and base64-encodes the result:
import crypto from "crypto";
function computeAcceptKey(clientKey) {
const MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
return crypto
.createHash("sha1")
.update(clientKey + MAGIC_GUID)
.digest("base64");
}
// Example:
// Client sends: Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
// Server output: Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=3. The 4 Connection States (readyState)
A WebSocket connection transitions through four distinct numeric states:
const ws = new WebSocket("wss://example.com/socket");
console.log(ws.readyState); // 0 (CONNECTING)
ws.onopen = () => {
console.log(ws.readyState); // 1 (OPEN) - Safe to send data
};
ws.onclose = () => {
console.log(ws.readyState); // 3 (CLOSED)
};
// Always check readiness before sending
function safeSend(message) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
} else {
console.warn("Socket is not open. Message queued or dropped.");
}
}4. Lifecycle Events
const ws = new WebSocket("wss://example.com/chat");
// 1. Connection established
ws.addEventListener("open", (event) => {
console.log("Connected to server!");
ws.send("Hello from client!");
});
// 2. Message received from server
ws.addEventListener("message", (event) => {
console.log("Data received:", event.data);
});
// 3. Error occurred
ws.addEventListener("error", (error) => {
console.error("WebSocket encountered an error:", error);
});
// 4. Connection closed
ws.addEventListener("close", (event) => {
console.log(`Disconnected. Code: ${event.code}, Reason: ${event.reason}`);
console.log(`Was clean close: ${event.wasClean}`);
});5. WebSocket Close Codes
When a connection terminates, the close event passes a numeric status code explaining why:
| Code | Name | Description |
|---|---|---|
1000 | Normal Closure | Clean disconnect initiated intentionally (e.g., user logged out). |
1001 | Going Away | Server is shutting down or browser navigated away to another page. |
1006 | Abnormal Closure | Connection lost abruptly without a close frame (e.g., WiFi dropped, process crash). |
1008 | Policy Violation | Message violated server rules (e.g., unauthenticated or rate-limited). |
1011 | Server Error | Server crashed or encountered an unhandled exception while processing. |
6. Client Reconnection with Exponential Backoff
In production, network connections will drop. You should always implement an automatic reconnection strategy with exponential backoff + jitter so clients do not overwhelm your server when it recovers:
export class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.reconnectDelay = 1000; // Start at 1s
this.maxReconnectDelay = 30000; // Cap at 30s
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log("🟢 WebSocket Connected");
this.reconnectDelay = 1000; // Reset delay on successful connection
};
this.ws.onclose = (event) => {
if (event.code !== 1000) {
console.warn(`đź”´ Socket disconnected. Retrying in ${this.reconnectDelay / 1000}s...`);
setTimeout(() => this.connect(), this.reconnectDelay);
// Exponential backoff: 1s -> 2s -> 4s -> 8s -> max 30s
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
}
}
this.ws.onerror = (err) => {
console.error("Socket error:", err);
};
}
send(data) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(typeof data === "string" ? data : JSON.stringify(data));
}
}
}7. Heartbeats & Ping-Pong Liveness Detection
If a user unplugs their ethernet cable or walks into an elevator, neither the client nor the server will receive a TCP FIN packet. The connection appears "open" until an active ping fails.
Server-Side Ping-Pong with ws in Node.js
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws) => {
ws.isAlive = true;
// When client replies with pong, mark socket as healthy
ws.on("pong", () => {
ws.isAlive = true;
});
});
// Periodically check all connected clients every 30 seconds
const heartbeatInterval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
console.log("đź’€ Dead client connection detected. Terminating.");
return ws.terminate();
}
ws.isAlive = false;
ws.ping(); // Expect pong response from client
});
}, 30000);
wss.on("close", () => clearInterval(heartbeatInterval));👉 Next Step: Build complete chat rooms, structured message protocols, and authentication in Client & Server Implementation.