Connection Lifecycle & Upgrade
WebSocket connections don't just appear - they start life as HTTP requests. Understanding this handshake process is key to debugging connection issues.
The Upgrade Handshake
WebSocket uses HTTP to establish the connection, then upgrades to the WebSocket protocol.
Key Headers Explained
| Header | Purpose |
|---|---|
Upgrade: websocket |
Requests protocol upgrade to WebSocket |
Connection: Upgrade |
Indicates connection should be upgraded |
Sec-WebSocket-Key |
Random base64 value for security handshake |
Sec-WebSocket-Accept |
Server's proof it understands WebSocket |
Sec-WebSocket-Version |
Protocol version (13 is current standard) |
The Security Handshake
The Sec-WebSocket-Key and Sec-WebSocket-Accept headers prevent accidental upgrades:
// How the server computes Sec-WebSocket-Accept
const crypto = require('crypto');
function computeAcceptKey(clientKey) {
const MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
return crypto
.createHash('sha1')
.update(clientKey + MAGIC_STRING)
.digest('base64');
}
// Example
// Client sends: Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
// Server responds: Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
This handshake prevents a misconfigured HTTP server from accidentally accepting WebSocket connections. The magic string is a fixed GUID in the WebSocket spec.
Connection States
A WebSocket connection goes through these states:
const ws = new WebSocket('wss://example.com/chat');
console.log(ws.readyState); // 0 = CONNECTING
ws.onopen = () => {
console.log(ws.readyState); // 1 = OPEN
};
ws.onclose = () => {
console.log(ws.readyState); // 3 = CLOSED
};
// Check before sending
if (ws.readyState === WebSocket.OPEN) {
ws.send('Hello!');
}
Lifecycle Events
onopen
Connection established, safe to send messages
onmessage
Message received from server
onerror
Error occurred (usually followed by close)
onclose
Connection closed (graceful or error)
const ws = new WebSocket('wss://example.com/chat');
ws.onopen = (event) => {
console.log('Connected!');
ws.send('Hello server!');
};
ws.onmessage = (event) => {
console.log('Received:', event.data);
};
ws.onerror = (event) => {
console.error('WebSocket error:', event);
};
ws.onclose = (event) => {
console.log('Disconnected:', event.code, event.reason);
// event.code: numeric close code
// event.reason: string explanation
// event.wasClean: boolean, true if closed gracefully
};
Close Codes
When a connection closes, it includes a code explaining why:
| Code | Name | Meaning |
|---|---|---|
| 1000 | Normal Closure | Connection closed cleanly |
| 1001 | Going Away | Server shutting down or browser navigating away |
| 1006 | Abnormal Closure | Connection lost unexpectedly (network issue) |
| 1008 | Policy Violation | Message violates server policy |
| 1011 | Internal Error | Server encountered unexpected error |
Graceful Closing
Either side can initiate a close:
// Client-side graceful close
ws.close(1000, 'User logged out');
// Server-side (Node.js ws library)
ws.close(1000, 'Session expired');
// The close handshake:
// 1. Initiator sends close frame with code + reason
// 2. Receiver acknowledges with close frame
// 3. TCP connection closes
Reconnection Strategy
Connections will drop. Always implement reconnection:
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.reconnectDelay = 1000;
this.maxDelay = 30000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectDelay = 1000; // Reset delay on success
};
this.ws.onclose = (event) => {
if (event.code !== 1000) {
// Abnormal close - reconnect
console.log(`Reconnecting in ${this.reconnectDelay}ms...`);
setTimeout(() => this.connect(), this.reconnectDelay);
// Exponential backoff with max
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxDelay
);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
console.warn('WebSocket not open, queuing message');
// Could queue messages here
}
}
}
Start with 1s delay, double each failure (1s → 2s → 4s → 8s...), cap at a maximum (30s). This prevents hammering the server when it's down.
Heartbeats / Ping-Pong
Keep connections alive and detect dead ones:
// Server side (Node.js with 'ws' library)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true; // Client responded
});
});
// Ping all clients every 30 seconds
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
console.log('Client dead, terminating');
return ws.terminate();
}
ws.isAlive = false;
ws.ping(); // Client should respond with pong
});
}, 30000);
// Client side - respond to pings (automatic in browsers)
// But you can also send application-level heartbeats:
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
Key Takeaways
- Starts as HTTP - Upgrade request with special headers
- 101 Switching Protocols - Server accepts the upgrade
- Four states - CONNECTING → OPEN → CLOSING → CLOSED
- Close codes matter - 1000 = normal, 1006 = connection lost
- Always reconnect - Use exponential backoff
Next up: Implementation - Build a real WebSocket client and server in Node.js.