WebSocket Fundamentals
Learn how WebSocket protocol works, its lifecycle, and when to use it over HTTP
What is WebSocket?
WebSocket is a communication protocol that provides full-duplex (two-way) communication channels over a single, long-lived TCP connection. Unlike HTTP's request-response model, WebSocket allows both client and server to send messages independently at any time.
WebSocket was standardized in 2011 (RFC 6455) and is now supported by all modern browsers. It's the foundation for real-time features in chat apps, games, trading platforms, and collaborative tools.
🔄 HTTP vs WebSocket
HTTP (Request-Response)
Client: GET /api/messages
Server: 200 OK [messages]
(connection closed)
Client: POST /api/message
Server: 201 Created
(connection closed)
# New connection for each request!
WebSocket (Persistent)
Client: Upgrade to WebSocket
Server: 101 Switching Protocols
(connection stays open)
Client: {"type": "message", ...}
Server: {"type": "message", ...}
Server: {"type": "notification", ...}
Client: {"type": "typing", ...}
# Same connection, messages flow both ways!
The WebSocket Handshake
WebSocket connections start with an HTTP "upgrade" request. This allows WebSocket to work through HTTP infrastructure (proxies, load balancers) before switching protocols.
// Client sends HTTP upgrade request:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com
// Server responds with 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
// Now the connection speaks WebSocket protocol, not HTTP!
Understanding the Handshake Headers
// Key handshake components:
// 1. Upgrade headers - Required for protocol switch
{
"Upgrade": "websocket", // Tell server we want WebSocket
"Connection": "Upgrade" // This is an upgrade request
}
// 2. Security key - Prevents cache poisoning attacks
{
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==" // Random base64
}
// Server must hash this with magic GUID and return:
// Sec-WebSocket-Accept = Base64(SHA1(Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
// 3. Version - Protocol version (always 13 for RFC 6455)
{
"Sec-WebSocket-Version": "13"
}
// 4. Optional: Subprotocols and extensions
{
"Sec-WebSocket-Protocol": "chat, superchat", // App-level protocols
"Sec-WebSocket-Extensions": "permessage-deflate" // Compression
}
WebSocket Frame Structure
After the handshake, data is transmitted in "frames". Understanding frames helps debug issues and optimize performance.
// WebSocket Frame Format (simplified):
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-------+-+-------------+-------------------------------+
// |F|R|R|R| opcode|M| Payload len | Extended payload length |
// |I|S|S|S| (4) |A| (7) | (16/64) |
// |N|V|V|V| |S| | (if payload len==126/127) |
// | |1|2|3| |K| | |
// +-+-+-+-+-------+-+-------------+-------------------------------+
// | Extended payload length continued, if payload len == 127 |
// +-------------------------------+-------------------------------+
// | |Masking-key, if MASK set to 1 |
// +-------------------------------+-------------------------------+
// | Masking-key (continued) | Payload Data |
// +-------------------------------+-------------------------------+
// | Payload Data continued ... |
// +---------------------------------------------------------------+
// Frame types (opcodes):
const OPCODES = {
0x0: 'Continuation', // Fragment of previous message
0x1: 'Text', // UTF-8 text data
0x2: 'Binary', // Binary data
0x8: 'Close', // Close connection
0x9: 'Ping', // Keepalive ping
0xA: 'Pong' // Keepalive pong
};
// Key points:
// - FIN bit indicates if this is the final fragment
// - Client-to-server frames MUST be masked
// - Server-to-client frames must NOT be masked
// - Payload length can be 7, 16, or 64 bits
WebSocket Connection Lifecycle
// Complete lifecycle of a WebSocket connection
// 1. CONNECTING - Initial state when WebSocket is created
const socket = new WebSocket('wss://api.example.com/ws');
console.log(socket.readyState); // 0 = CONNECTING
// 2. OPEN - Handshake complete, ready for communication
socket.onopen = (event) => {
console.log(socket.readyState); // 1 = OPEN
console.log('Connection established!');
// Now safe to send messages
socket.send(JSON.stringify({ type: 'hello' }));
};
// 3. MESSAGE - Receiving data
socket.onmessage = (event) => {
// event.data can be string, Blob, or ArrayBuffer
if (typeof event.data === 'string') {
const message = JSON.parse(event.data);
handleMessage(message);
} else {
// Binary data
handleBinaryData(event.data);
}
};
// 4. CLOSING - Connection is being closed
// Triggered after close() is called or server initiates close
// 5. CLOSED - Connection is fully closed
socket.onclose = (event) => {
console.log(socket.readyState); // 3 = CLOSED
console.log('Close code:', event.code);
console.log('Close reason:', event.reason);
console.log('Was clean close:', event.wasClean);
};
// ERROR - Something went wrong
socket.onerror = (error) => {
console.error('WebSocket error:', error);
// Note: error event is always followed by close event
};
// ReadyState constants
WebSocket.CONNECTING = 0;
WebSocket.OPEN = 1;
WebSocket.CLOSING = 2;
WebSocket.CLOSED = 3;
Sending Different Data Types
// WebSocket supports text and binary data
// 1. Text (UTF-8 strings)
socket.send('Hello, World!');
socket.send(JSON.stringify({
type: 'message',
content: 'Hello!'
}));
// 2. ArrayBuffer (raw binary)
const buffer = new ArrayBuffer(16);
const view = new Uint8Array(buffer);
view[0] = 0x48; // 'H'
view[1] = 0x69; // 'i'
socket.send(buffer);
// 3. Blob (binary with MIME type)
const blob = new Blob(['Hello'], { type: 'text/plain' });
socket.send(blob);
// 4. TypedArray (views into ArrayBuffer)
const floats = new Float32Array([1.0, 2.5, 3.14]);
socket.send(floats);
// Setting binary type for receiving
socket.binaryType = 'arraybuffer'; // or 'blob' (default)
socket.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
// Handle binary data
const view = new DataView(event.data);
console.log(view.getFloat32(0));
}
};
Closing Connections
// Graceful close with code and reason
socket.close(1000, 'User logged out');
// Common close codes:
const CLOSE_CODES = {
1000: 'Normal closure', // Clean close
1001: 'Going away', // Page navigation, server shutdown
1002: 'Protocol error', // Invalid frame received
1003: 'Unsupported data', // Received data type not supported
1006: 'Abnormal closure', // No close frame received (network issue)
1007: 'Invalid frame payload', // Non-UTF8 in text frame
1008: 'Policy violation', // Generic policy error
1009: 'Message too big', // Frame exceeds size limit
1010: 'Extension required', // Client expected extension
1011: 'Internal error', // Unexpected server error
1015: 'TLS handshake failure' // Failed to establish TLS
};
// Handling close event
socket.onclose = (event) => {
if (event.code === 1000) {
console.log('Normal close');
} else if (event.code === 1006) {
console.log('Connection lost - attempting reconnect');
reconnect();
} else {
console.log(`Closed with code ${event.code}: ${event.reason}`);
}
};
Ping/Pong Keepalive
// WebSocket has built-in ping/pong frames for keepalive
// Note: Browser API doesn't expose ping/pong directly!
// Server sends ping, browser automatically responds with pong
// This happens at the protocol level
// For application-level keepalive (visible in JS):
class HeartbeatWebSocket {
constructor(url) {
this.url = url;
this.heartbeatInterval = 30000; // 30 seconds
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => this.startHeartbeat();
this.ws.onclose = () => this.stopHeartbeat();
this.ws.onmessage = (e) => this.handleMessage(e);
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
// Expect pong within 5 seconds
this.pongTimeout = setTimeout(() => {
console.log('Pong timeout - connection dead');
this.ws.close();
}, 5000);
}
}, this.heartbeatInterval);
}
handleMessage(event) {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
clearTimeout(this.pongTimeout);
return;
}
// Handle other messages...
}
stopHeartbeat() {
clearInterval(this.heartbeatTimer);
clearTimeout(this.pongTimeout);
}
}
WebSocket URL Schemes
// WebSocket URLs use ws:// or wss:// (secure)
// Unsecured (like HTTP)
const ws = new WebSocket('ws://example.com/socket');
// Secured with TLS (like HTTPS) - ALWAYS use this in production!
const wss = new WebSocket('wss://example.com/socket');
// With path and query parameters
const wsWithParams = new WebSocket(
'wss://api.example.com/ws/chat?room=general&token=abc123'
);
// Port specification
const wsWithPort = new WebSocket('wss://api.example.com:8080/ws');
// Common patterns
const patterns = {
// Dedicated WebSocket server
dedicated: 'wss://ws.example.com/connect',
// Same server, different path
samePath: 'wss://example.com/api/websocket',
// Versioned endpoint
versioned: 'wss://api.example.com/v2/realtime',
// Room/channel in path
withRoom: 'wss://chat.example.com/rooms/general',
// Auth token in query (common but not ideal)
withToken: `wss://api.example.com/ws?token=${authToken}`
};
⚠️ WebSocket Security Considerations
- ✗ Always use WSS in production - WS is unencrypted!
- ✗ Don't pass tokens in URLs - They appear in logs. Use first message or cookies instead.
- ✗ Validate Origin header - Prevent cross-site WebSocket hijacking.
- ✗ Validate all incoming data - Treat WebSocket messages like any user input.
- ✗ Implement rate limiting - Prevent message flooding attacks.
- ✗ Set message size limits - Prevent memory exhaustion.
Browser Support and Polyfills
// WebSocket is supported in all modern browsers
// No polyfills needed for basic functionality
// Feature detection
if ('WebSocket' in window) {
// WebSocket supported
const ws = new WebSocket(url);
} else {
// Fallback to polling (very rare case)
useLongPolling();
}
// For older environments, Socket.io provides fallbacks:
// 1. WebSocket (preferred)
// 2. HTTP long-polling
// 3. HTTP polling
// Connection limits to be aware of:
// - Browsers limit connections per domain (typically 6 for HTTP, varies for WS)
// - Use a single WebSocket connection when possible
// - Multiplex channels over one connection
💡 Key Takeaways
- ✓ WebSocket is a protocol - Not just an API, it's a true bidirectional protocol over TCP
- ✓ Starts as HTTP - The handshake uses HTTP, making it proxy-friendly
- ✓ Persistent connection - One connection for all messages (no overhead per message)
- ✓ Full duplex - Both sides can send simultaneously
- ✓ Text and binary - Supports UTF-8 strings and raw binary data
- ✓ Built-in keepalive - Protocol-level ping/pong frames