ws libraryA protocol providing full-duplex communication channels over a single TCP connection. Designed to work over HTTP ports 80 and 443, and to support HTTP proxies and intermediaries.
ws:// — unencrypted (port 80)wss:// — TLS encrypted (port 443)| Aspect | HTTP/1.1 | WebSocket |
|---|---|---|
| Communication | Request-response (half-duplex) | Full-duplex, bidirectional |
| Connection | New connection per request (or keep-alive) | Single persistent TCP connection |
| Header Overhead | ~800 bytes per request/response | 2–14 bytes per frame |
| Latency | TCP + TLS handshake + request time | Near-zero after initial handshake |
| Server Push | Not natively (requires polling/SSE) | Native — server pushes any time |
| Data Format | Text (body can be binary) | Text frames + binary frames |
| Caching | Built-in (ETags, Cache-Control) | No caching (real-time stream) |
| Proxies | Universally supported | Some proxies require configuration |
| Statelessness | Stateless by design | Stateful connection |
The connection begins as a standard HTTP/1.1 request with an Upgrade header. The server responds with 101 Switching Protocols.
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate
Origin: http://example.com
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Random 16-byte base64 value. Server concatenates with GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, SHA-1 hashes, and base64 encodes the result.
Proves the server understands WebSocket. Computed as base64(SHA1(key + GUID)). Prevents cross-protocol attacks.
Client proposes one or more subprotocols (e.g., graphql-ws, mqtt). Server selects one. Defines the message format on the connection.
0x0 — Continuation frame0x1 — Text frame (UTF-8)0x2 — Binary frame0x8 — Connection close0x9 — Ping0xA — Pongpayload[i] ^= mask[i % 4]// Create connection
const ws = new WebSocket('wss://api.example.com/chat');
// Connection opened
ws.addEventListener('open', (event) => {
console.log('Connected!');
ws.send(JSON.stringify({ type: 'join', room: 'general' }));
});
// Listen for messages
ws.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
});
// Handle errors
ws.addEventListener('error', (event) => {
console.error('WebSocket error:', event);
});
// Connection closed
ws.addEventListener('close', (event) => {
console.log(`Closed: ${event.code} ${event.reason}`);
if (!event.wasClean) {
console.log('Connection lost, reconnecting...');
}
});
// Send data
ws.send('Hello, server!');
ws.send(new Blob(['binary data']));
// Close gracefully
ws.close(1000, 'Done');
0 — CONNECTING1 — OPEN2 — CLOSING3 — CLOSEDws.url — resolved URLws.protocol — selected subprotocolws.bufferedAmount — bytes queuedws.binaryType — 'blob' or 'arraybuffer'ws.extensions — negotiated extensions| Event | When |
|---|---|
open | Connection established |
message | Data received |
error | Error occurred |
close | Connection closed |
WebSockets natively support binary frames alongside text. Set ws.binaryType to control how incoming binary data is delivered.
// ArrayBuffer
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setFloat64(0, Math.PI);
ws.send(buffer);
// Typed Array
const pixels = new Uint8Array([255, 0, 128, 255]);
ws.send(pixels.buffer);
// Blob
const blob = new Blob(['Hello'], { type: 'text/plain' });
ws.send(blob);
// File from input
fileInput.onchange = (e) => {
const file = e.target.files[0];
ws.send(file); // sends as binary
};
// Set binary type
ws.binaryType = 'arraybuffer'; // or 'blob'
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data);
const value = view.getFloat64(0);
console.log('Received float:', value);
}
};
// Blob approach (for large files)
ws.binaryType = 'blob';
ws.onmessage = async (event) => {
if (event.data instanceof Blob) {
const arrayBuf = await event.data.arrayBuffer();
const bytes = new Uint8Array(arrayBuf);
renderImage(bytes);
}
};
Fixed-length raw binary buffer. Best for structured data with known layout. Zero-copy transfer.
Immutable raw data. Better for large files — can be streamed from disk. Use URL.createObjectURL() for display.
Uint8Array, Float32Array, Int16Array, etc. Provide views into an ArrayBuffer with typed access.
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws, req) => {
const ip = req.socket.remoteAddress;
console.log(`Client connected from ${ip}`);
ws.on('message', (data, isBinary) => {
const msg = isBinary ? data : data.toString();
console.log('Received:', msg);
// Echo to all clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(msg);
}
});
});
ws.on('close', (code, reason) => {
console.log(`Disconnected: ${code}`);
});
ws.send('Welcome to the server!');
});
import express from 'express';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });
// REST endpoints
app.get('/api/status', (req, res) => {
res.json({
clients: wss.clients.size,
uptime: process.uptime()
});
});
// WebSocket connections
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
ws.on('message', (msg) => {
ws.send(`Echo: ${msg}`);
});
});
server.listen(3000, () => {
console.log('HTTP + WS on :3000');
});
npm install ws — fastest WS lib for Node.js. Zero dependencies. ~300 KB.
Handles 50K+ concurrent connections. Use perMessageDeflate: false to reduce CPU usage on high-throughput servers.
uWebSockets.js for extreme perf (C++ core). Bun.serve() has built-in WS. Deno has native Deno.serve().
import { Server } from 'socket.io';
const io = new Server(3000, {
cors: { origin: 'http://localhost:5173' }
});
// Namespaces
const chat = io.of('/chat');
chat.on('connection', (socket) => {
// Join a room
socket.join('room-42');
// Listen for events
socket.on('chat:message', (msg, callback) => {
// Broadcast to room
socket.to('room-42').emit('chat:message', {
user: socket.data.username,
text: msg,
ts: Date.now()
});
callback({ status: 'ok' }); // acknowledgement
});
socket.on('disconnect', (reason) => {
console.log(`Left: ${reason}`);
});
});
import { io } from 'socket.io-client';
const socket = io('http://localhost:3000/chat', {
auth: { token: 'jwt-token-here' },
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5
});
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
// Send with acknowledgement
socket.emit('chat:message', 'Hello!', (res) => {
console.log('Server ack:', res.status);
});
// Receive events
socket.on('chat:message', (data) => {
renderMessage(data);
});
socket.on('connect_error', (err) => {
console.log('Auth error:', err.message);
});
// Client
const ws = new WebSocket(
`wss://api.example.com/ws?token=${jwt}`
);
// Server (Node.js ws)
wss.on('connection', (ws, req) => {
const url = new URL(req.url, 'http://localhost');
const token = url.searchParams.get('token');
try {
const user = jwt.verify(token, SECRET);
ws.userId = user.id;
} catch (e) {
ws.close(4001, 'Invalid token');
}
});
ws.on('connection', (ws) => {
ws.isAuthenticated = false;
ws.on('message', (data) => {
if (!ws.isAuthenticated) {
const { token } = JSON.parse(data);
if (verifyToken(token)) {
ws.isAuthenticated = true;
ws.send(JSON.stringify({ type: 'auth_ok' }));
} else {
ws.close(4001, 'Unauthorized');
}
return;
}
handleMessage(ws, data);
});
});
// Cookies are sent during handshake
// Server reads them from upgrade request
import cookie from 'cookie';
wss.on('connection', (ws, req) => {
const cookies = cookie.parse(
req.headers.cookie || ''
);
const sessionId = cookies['session_id'];
const session = await sessionStore
.get(sessionId);
if (!session) {
ws.close(4001, 'No valid session');
return;
}
ws.user = session.user;
});
// Server heartbeat interval
const HEARTBEAT_MS = 30_000;
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) {
console.log('Terminating dead connection');
return ws.terminate();
}
ws.isAlive = false;
ws.ping(); // client auto-responds pong
});
}, HEARTBEAT_MS);
wss.on('close', () => clearInterval(interval));
class ReconnectingWebSocket {
#url; #ws; #retries = 0;
#maxRetries = 10;
#baseDelay = 1000;
constructor(url) {
this.#url = url;
this.#connect();
}
#connect() {
this.#ws = new WebSocket(this.#url);
this.#ws.onopen = () => {
this.#retries = 0; // reset on success
};
this.#ws.onclose = (e) => {
if (e.code !== 1000) this.#reconnect();
};
}
#reconnect() {
if (this.#retries >= this.#maxRetries) return;
const delay = this.#baseDelay
* Math.pow(2, this.#retries)
+ Math.random() * 1000; // jitter
this.#retries++;
setTimeout(() => this.#connect(), delay);
}
send(data) { this.#ws.send(data); }
}
NATs, proxies, and firewalls silently drop idle TCP connections (often after 60–120s). Ping/pong keeps the connection alive.
Delays: 1s, 2s, 4s, 8s, 16s... plus random jitter. Prevents thundering herd when server restarts.
Browser WS API cannot send protocol pings. Use application-level {"type":"ping"} messages as a fallback.
| Code | Name | Meaning |
|---|---|---|
1000 | Normal | Graceful close |
1001 | Going Away | Server shutdown / page nav |
1002 | Protocol Error | Malformed frame |
1003 | Unsupported | Unexpected data type |
1006 | Abnormal | No close frame (network drop) |
1008 | Policy Violation | Message violates policy |
1009 | Too Large | Message exceeds size limit |
1011 | Server Error | Unexpected server condition |
4000–4999 | App-defined | Custom codes for your protocol |
ws.onerror = (err) => {
// error event has no useful info
// in browsers; close follows
console.error('WS error');
};
ws.onclose = (event) => {
switch (event.code) {
case 1000:
break; // normal
case 1006:
scheduleReconnect(); // lost
break;
case 4001:
refreshToken(); // auth failed
break;
default:
logAndReconnect(event);
}
};
onerror event gives no details1006 = connection lost, not a clean closereadyState before send()bufferedAmount to prevent memory leaksimport Redis from 'ioredis';
const pub = new Redis();
const sub = new Redis();
sub.subscribe('chat');
sub.on('message', (ch, msg) => {
// broadcast to local clients
localClients.forEach(ws =>
ws.send(msg));
});
// When a client sends a msg:
pub.publish('chat', data);
ulimit -n > 100Knet.core.somaxconncluster or PM2 for multi-coreupstream websocket_backend {
# Use ip_hash for sticky sessions
ip_hash;
server 10.0.1.1:8080;
server 10.0.1.2:8080;
server 10.0.1.3:8080;
}
server {
listen 443 ssl;
server_name ws.example.com;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
location /ws {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
# Critical: forward Upgrade headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# Timeouts for idle connections
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# Forward real IP
proxy_set_header X-Real-IP
$remote_addr;
proxy_set_header X-Forwarded-For
$proxy_add_x_forwarded_for;
}
}
proxy_read_timeout is 60s — kills idle WS connectionsproxy_http_version 1.1 blocks upgradesConnection "upgrade" header = 400 errorsfrontend ws_front
bind *:443 ssl crt /etc/ssl/cert.pem
acl is_ws hdr(Upgrade) -i websocket
use_backend ws_back if is_ws
backend ws_back
balance source # sticky
timeout tunnel 86400s
server ws1 10.0.1.1:8080 check
server ws2 10.0.1.2:8080 check
wss.on('headers', (headers, req) => {
const origin = req.headers.origin;
const allowed = [
'https://example.com',
'https://app.example.com'
];
if (!allowed.includes(origin)) {
req.destroy(); // reject connection
}
});
const rateMap = new Map();
ws.on('message', (data) => {
const now = Date.now();
const history = rateMap.get(ws) || [];
// Remove msgs older than 1 minute
const recent = history.filter(
t => now - t < 60_000
);
recent.push(now);
rateMap.set(ws, recent);
if (recent.length > 100) { // 100/min
ws.close(4008, 'Rate limit exceeded');
return;
}
handleMessage(ws, data);
});
Origin header on servermaxPayload on server (ws: default 100 MB!)connect-src directiveimport { WebSocketServer } from 'ws';
import WebSocket from 'ws';
describe('Chat Server', () => {
let wss, serverUrl;
beforeAll((done) => {
wss = new WebSocketServer({ port: 0 });
setupHandlers(wss);
const port = wss.address().port;
serverUrl = `ws://localhost:${port}`;
done();
});
afterAll(() => wss.close());
test('echoes messages', (done) => {
const ws = new WebSocket(serverUrl);
ws.on('open', () => {
ws.send('hello');
});
ws.on('message', (data) => {
expect(data.toString())
.toBe('Echo: hello');
ws.close();
done();
});
});
test('rejects bad auth', (done) => {
const ws = new WebSocket(
`${serverUrl}?token=invalid`
);
ws.on('close', (code) => {
expect(code).toBe(4001);
done();
});
});
});
// k6 script: ws_load.js
import ws from 'k6/ws';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 1000 },
{ duration: '1m', target: 1000 },
{ duration: '10s', target: 0 },
],
};
export default function () {
const url = 'wss://api.example.com/ws';
const res = ws.connect(url, {}, (socket) => {
socket.on('open', () => {
socket.send(JSON.stringify({
type: 'ping'
}));
});
socket.on('message', (msg) => {
check(msg, {
'is pong': (m) =>
JSON.parse(m).type === 'pong',
});
});
socket.setTimeout(() => {
socket.close();
}, 5000);
});
check(res, { 'status 101': (r) =>
r && r.status === 101 });
}
| Technology | Direction | Transport | Binary | Reconnect | Best For |
|---|---|---|---|---|---|
| WebSocket | Full-duplex | TCP | Yes | Manual | Chat, gaming, collaboration |
| SSE | Server → Client | HTTP/1.1 | No (text) | Built-in | Live feeds, notifications |
| Long Polling | Simulated push | HTTP | Yes | Built-in | Legacy fallback |
| HTTP/2 Push | Server → Client | HTTP/2 | Yes | N/A | Resource preloading (deprecated) |
| WebTransport | Full-duplex | QUIC/HTTP3 | Yes | Built-in | Low-latency, unreliable streams |
// Server (Express)
app.get('/events', (req, res) => {
res.setHeader('Content-Type',
'text/event-stream');
res.setHeader('Cache-Control',
'no-cache');
const id = setInterval(() => {
res.write(
`data: ${JSON.stringify({
time: Date.now()
})}\n\n`
);
}, 1000);
req.on('close', () =>
clearInterval(id));
});
const transport =
new WebTransport(
'https://example.com/wt'
);
await transport.ready;
// Bidirectional stream
const stream = await transport
.createBidirectionalStream();
const writer = stream.writable
.getWriter();
await writer.write(
new Uint8Array([1, 2, 3])
);
open, message, close, errorws or Socket.IO