WebSockets Cheat Sheet
Quick reference for opening, sending, and closing full-duplex WebSocket connections between browser and server, including lifecycle events and reconnection.
Client-Side WebSocket API
Opening a connection and handling core events.
// Create connectionconst socket = new WebSocket('wss://example.com/socket');socket.addEventListener('open', () => { console.log('Connected'); socket.send('Hello Server!');});socket.addEventListener('message', (event) => { console.log('Received:', event.data);});socket.addEventListener('close', (event) => { console.log('Closed:', event.code, event.reason);});socket.addEventListener('error', (event) => { console.error('WebSocket error:', event);});// Send JSONsocket.send(JSON.stringify({ type: 'chat', text: 'hi' }));// Close gracefullysocket.close(1000, 'Done');
Node.js WebSocket Server (ws)
Accepting connections and broadcasting messages.
const { WebSocketServer } = require('ws');const wss = new WebSocketServer({ port: 8080 });wss.on('connection', (ws, req) => { console.log('Client connected:', req.socket.remoteAddress); ws.on('message', (data) => { // Broadcast to all connected clients wss.clients.forEach((client) => { if (client.readyState === ws.OPEN) { client.send(data.toString()); } }); }); ws.on('close', () => console.log('Client disconnected')); ws.ping(); // heartbeat frame});
readyState Values
The four states a WebSocket connection moves through.
- CONNECTING (0)- Socket created, connection not yet open
- OPEN (1)- Connection is open and ready to send/receive
- CLOSING (2)- Connection handshake for closing is in progress
- CLOSED (3)- Connection is closed or failed to open
- socket.readyState- Property on the WebSocket instance exposing the current state as a number
Common Close Codes
Status codes sent in the close frame.
- 1000- Normal closure, purpose fulfilled
- 1001- Going away (e.g. page navigating away, server shutting down)
- 1006- Abnormal closure; no close frame was received (connection dropped)
- 1008- Policy violation (generic rejection)
- 1011- Server encountered an unexpected internal error
- 4000-4999- Reserved range for application-defined custom codes
Heartbeat Keepalive (ws server)
Detecting and terminating dead connections that never sent a close frame.
const { WebSocketServer } = require('ws');const wss = new WebSocketServer({ port: 8080 });function heartbeat() { this.isAlive = true;}wss.on('connection', (ws) => { ws.isAlive = true; ws.on('pong', heartbeat); // client replied to our ping});// Every 30s, ping everyone; terminate anyone that didn't pong last roundconst interval = setInterval(() => { wss.clients.forEach((ws) => { if (ws.isAlive === false) return ws.terminate(); ws.isAlive = false; ws.ping(); });}, 30000);wss.on('close', () => clearInterval(interval));
Client Reconnect with Exponential Backoff
Resilient client wrapper that retries with capped, jittered exponential backoff.
class ResilientSocket { constructor(url) { this.url = url; this.attempt = 0; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.addEventListener('open', () => { this.attempt = 0; }); this.ws.addEventListener('close', (e) => { if (e.code === 1000) return; // normal closure, don't retry const base = Math.min(30000, 500 * 2 ** this.attempt); const jitter = Math.random() * base * 0.3; const delay = base + jitter; this.attempt += 1; setTimeout(() => this.connect(), delay); }); } send(data) { if (this.ws.readyState === WebSocket.OPEN) this.ws.send(data); }}const socket = new ResilientSocket('wss://example.com/socket');
Sending & Receiving Binary Frames
Switching binaryType and handling ArrayBuffer/Blob payloads for non-text protocols.
const socket = new WebSocket('wss://example.com/stream');socket.binaryType = 'arraybuffer'; // default is 'blob'socket.addEventListener('message', (event) => { if (event.data instanceof ArrayBuffer) { const view = new DataView(event.data); const messageType = view.getUint8(0); const payload = event.data.slice(1); handleBinaryMessage(messageType, payload); }});// Sending a typed binary frame: 1-byte header + Float32 payloadfunction sendSample(value) { const buf = new ArrayBuffer(5); const view = new DataView(buf); view.setUint8(0, 0x02); // message type view.setFloat32(1, value); socket.send(buf);}
Authenticating a WebSocket Connection
Verifying a token during the HTTP upgrade instead of trusting an unauthenticated first message.
const { WebSocketServer } = require('ws');const http = require('http');const jwt = require('jsonwebtoken');const server = http.createServer();const wss = new WebSocketServer({ noServer: true });server.on('upgrade', (req, socket, head) => { const url = new URL(req.url, 'http://localhost'); const token = url.searchParams.get('token'); try { const user = jwt.verify(token, process.env.JWT_SECRET); wss.handleUpgrade(req, socket, head, (ws) => { ws.user = user; wss.emit('connection', ws, req); }); } catch { socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); }});wss.on('connection', (ws) => { console.log('Authenticated as', ws.user.sub);});
Scaling WebSockets in Production
Concerns that only surface once you run more than one server process.
- Sticky sessions- load balancer must route a client to the same backend instance for the life of the connection
- Pub/sub fan-out- use Redis Pub/Sub or a message broker so a message from one instance reaches clients connected to another
- bufferedAmount- bytes queued but not yet sent; check it before send() to detect a slow client and apply backpressure
- permessage-deflate- per-message WebSocket compression extension; saves bandwidth but costs CPU on high-throughput servers
- Subprotocols- negotiated via Sec-WebSocket-Protocol to version a wire format between client and server
- Connection limits- each open socket holds a file descriptor and memory; tune ulimits and worker count for expected concurrency
Implement heartbeat ping/pong frames and exponential-backoff reconnect logic on the client — WebSocket connections silently die behind proxies and load balancers without periodic keepalive traffic.