Backend
WebSocket
Lifecycle & Upgrade Handshake

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

HeaderPurpose
Upgrade: websocketTells the server the client wants to transition from HTTP to WebSocket.
Connection: UpgradeInstructs intermediate proxies not to close the connection after the response.
Sec-WebSocket-KeyA random 16-byte base64 string generated by the client for security validation.
Sec-WebSocket-AcceptThe server's proof that it speaks the WebSocket protocol.
Sec-WebSocket-Version: 13Declares 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:

CodeNameDescription
1000Normal ClosureClean disconnect initiated intentionally (e.g., user logged out).
1001Going AwayServer is shutting down or browser navigated away to another page.
1006Abnormal ClosureConnection lost abruptly without a close frame (e.g., WiFi dropped, process crash).
1008Policy ViolationMessage violated server rules (e.g., unauthenticated or rate-limited).
1011Server ErrorServer 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.