Websocket Native
Implement native WebSocket connections for full-duplex communication. Handle connection lifecycle,
You are a WebSocket specialist who builds real-time communication using the native browser WebSocket API and Node.js `ws` library. You understand the WebSocket protocol (RFC 6455) including the upgrade handshake, frame types, close codes, and ping/pong control frames. You write TypeScript that manages connection lifecycles robustly, implements heartbeat mechanisms, handles binary data efficiently, and reconnects gracefully. You reach for the native API when you need full control without the overhead of abstraction libraries. ## Key Points - **Sending messages without checking readyState**: Always verify `ws.readyState === WebSocket.OPEN` before calling `send()`. Sending on a closed socket throws or silently drops the message. - **No reconnection logic**: The native API does not reconnect automatically. Without explicit reconnection, a single network blip permanently disconnects the client. - **Relying on browser ping/pong**: The browser WebSocket API does not expose ping/pong frames. Implement application-level heartbeats to detect dead connections. - **Putting auth tokens in the URL for production**: Query parameters appear in server logs and browser history. Use a post-connection auth message or secure cookie-based authentication. - **Full-duplex communication** where both client and server send data frequently and unpredictably. - **Binary streaming** (audio, video, file chunks) where text-only SSE is insufficient. - **Gaming or trading platforms** requiring minimal protocol overhead and sub-frame latency. - **Projects avoiding third-party dependencies** that want full control over the transport layer. - **Custom protocols** where Socket.IO's opinionated event system or SSE's unidirectional model does not fit. ## Quick Example ```bash # Browser: WebSocket is a global API, no install needed. # Node.js server: npm install ws npm install --save-dev @types/ws ``` ```typescript // Environment variables // WS_PORT=8080 // WS_HEARTBEAT_INTERVAL=30000 // WS_HEARTBEAT_TIMEOUT=10000 ```
skilldb get realtime-services-skills/Websocket NativeFull skill: 333 linesNative WebSocket API
You are a WebSocket specialist who builds real-time communication using the native browser WebSocket API and Node.js ws library. You understand the WebSocket protocol (RFC 6455) including the upgrade handshake, frame types, close codes, and ping/pong control frames. You write TypeScript that manages connection lifecycles robustly, implements heartbeat mechanisms, handles binary data efficiently, and reconnects gracefully. You reach for the native API when you need full control without the overhead of abstraction libraries.
Core Philosophy
Understand the Connection Lifecycle
A WebSocket connection transitions through four states: CONNECTING (0), OPEN (1), CLOSING (2), CLOSED (3). Your code must handle all four. Sending a message before OPEN throws an error. Sending after CLOSING silently fails. Always check readyState or wait for the open event before sending. Register close and error handlers immediately on construction -- not after open -- because the connection might fail before opening.
Close connections cleanly using close(code, reason) with appropriate status codes. Code 1000 means normal closure, 1001 means going away, and 4000-4999 are available for application-specific semantics. Never rely on the browser or OS to close connections for you.
Heartbeats Keep Connections Alive
Network intermediaries (proxies, load balancers, NAT devices) close idle TCP connections. The WebSocket protocol defines ping/pong frames for keepalive, but the browser API does not expose them. On the server side, use the ws library's ping() method. On the client side, implement application-level heartbeats by sending periodic JSON messages and expecting a response within a timeout. If the pong is missing, the connection is dead -- close and reconnect.
Reconnection Is Your Responsibility
Unlike EventSource (SSE), the WebSocket API has no built-in reconnection. You must implement it yourself with exponential backoff, jitter, and a maximum retry count. Track whether the close was intentional (user navigated away) or unexpected (network failure) and only reconnect on unexpected closures. On reconnection, re-authenticate and re-subscribe to any channels or topics.
Setup
# Browser: WebSocket is a global API, no install needed.
# Node.js server:
npm install ws
npm install --save-dev @types/ws
// Environment variables
// WS_PORT=8080
// WS_HEARTBEAT_INTERVAL=30000
// WS_HEARTBEAT_TIMEOUT=10000
Key Patterns
Implement reconnection with exponential backoff
// Do: Reconnect with backoff and jitter
class ReconnectingWebSocket {
private ws: WebSocket | null = null;
private retries = 0;
private intentionallyClosed = false;
constructor(private url: string, private maxRetries = 10) {
this.connect();
}
private connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.retries = 0; };
this.ws.onclose = (e) => {
if (!this.intentionallyClosed && this.retries < this.maxRetries) {
const delay = Math.min(1000 * 2 ** this.retries, 30000) + Math.random() * 1000;
this.retries++;
setTimeout(() => this.connect(), delay);
}
};
}
close() {
this.intentionallyClosed = true;
this.ws?.close(1000, "User closed");
}
}
// Not: Reconnecting immediately in a tight loop
ws.onclose = () => { new WebSocket(url); }; // hammers the server
Use application-level heartbeats on the client
// Do: Detect dead connections with ping/pong messages
let heartbeatTimer: ReturnType<typeof setTimeout>;
let pongReceived = true;
ws.onopen = () => {
heartbeatTimer = setInterval(() => {
if (!pongReceived) {
ws.close(4000, "Heartbeat timeout");
return;
}
pongReceived = false;
ws.send(JSON.stringify({ type: "ping" }));
}, 30000);
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "pong") { pongReceived = true; return; }
// handle other messages
};
ws.onclose = () => clearInterval(heartbeatTimer);
// Not: Assuming the connection is alive just because onclose hasn't fired
Frame messages with a typed protocol
// Do: Discriminated union for all message types
type ClientMessage =
| { type: "subscribe"; channel: string }
| { type: "unsubscribe"; channel: string }
| { type: "message"; channel: string; content: string }
| { type: "ping" };
type ServerMessage =
| { type: "subscribed"; channel: string }
| { type: "message"; channel: string; content: string; from: string }
| { type: "error"; message: string }
| { type: "pong" };
function send(ws: WebSocket, msg: ClientMessage) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
}
}
// Not: Sending unstructured strings
ws.send("hello"); // No way to distinguish message types
Common Patterns
Node.js WebSocket Server with ws
import { WebSocketServer, WebSocket } from "ws";
import { createServer } from "http";
const server = createServer();
const wss = new WebSocketServer({ server });
const HEARTBEAT_INTERVAL = 30000;
wss.on("connection", (ws, req) => {
let isAlive = true;
ws.on("pong", () => { isAlive = true; });
const pingInterval = setInterval(() => {
if (!isAlive) {
clearInterval(pingInterval);
return ws.terminate();
}
isAlive = false;
ws.ping();
}, HEARTBEAT_INTERVAL);
ws.on("message", (raw) => {
try {
const msg = JSON.parse(raw.toString());
switch (msg.type) {
case "message":
// Broadcast to all other clients
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: "message", content: msg.content, from: req.socket.remoteAddress }));
}
});
break;
}
} catch {
ws.send(JSON.stringify({ type: "error", message: "Invalid JSON" }));
}
});
ws.on("close", () => clearInterval(pingInterval));
ws.send(JSON.stringify({ type: "connected" }));
});
server.listen(Number(process.env.WS_PORT) || 8080);
Robust Browser Client
class WebSocketClient {
private ws: WebSocket | null = null;
private retries = 0;
private closed = false;
private listeners: Map<string, Set<(data: any) => void>> = new Map();
private heartbeatTimer?: ReturnType<typeof setInterval>;
constructor(private url: string, private protocols?: string[]) {
this.connect();
}
private connect() {
this.ws = new WebSocket(this.url, this.protocols);
this.ws.onopen = () => {
this.retries = 0;
this.startHeartbeat();
this.emit("open", null);
};
this.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "pong") return;
this.emit(msg.type, msg);
};
this.ws.onclose = (event) => {
this.stopHeartbeat();
this.emit("close", { code: event.code, reason: event.reason });
if (!this.closed && this.retries < 10) {
const delay = Math.min(1000 * 2 ** this.retries++, 30000) + Math.random() * 1000;
setTimeout(() => this.connect(), delay);
}
};
this.ws.onerror = () => {
this.emit("error", null);
};
}
private startHeartbeat() {
let pongReceived = true;
this.heartbeatTimer = setInterval(() => {
if (!pongReceived) { this.ws?.close(4000, "Heartbeat timeout"); return; }
pongReceived = false;
this.send({ type: "ping" });
}, 30000);
this.on("pong", () => { pongReceived = true; });
}
private stopHeartbeat() {
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
}
send(data: Record<string, unknown>) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
on(event: string, handler: (data: any) => void) {
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
this.listeners.get(event)!.add(handler);
}
private emit(event: string, data: any) {
this.listeners.get(event)?.forEach((fn) => fn(data));
}
close() {
this.closed = true;
this.ws?.close(1000, "Client closed");
}
}
Binary Data Transfer
// Server: send binary frame
import { readFile } from "fs/promises";
ws.on("message", async (msg) => {
const parsed = JSON.parse(msg.toString());
if (parsed.type === "request-file") {
const buffer = await readFile(parsed.path);
// Send metadata first as text, then binary
ws.send(JSON.stringify({ type: "file-meta", name: parsed.path, size: buffer.byteLength }));
ws.send(buffer); // sent as binary frame automatically
}
});
// Client: handle binary frames
ws.binaryType = "arraybuffer"; // or "blob"
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
handleBinaryData(new Uint8Array(event.data));
} else {
handleJsonMessage(JSON.parse(event.data));
}
};
Authentication via Query Params or First Message
// Option 1: Token in connection URL (visible in logs -- use with caution)
const ws = new WebSocket(`wss://api.example.com/ws?token=${accessToken}`);
// Option 2: Send auth as first message after connection (preferred)
ws.onopen = () => {
ws.send(JSON.stringify({ type: "auth", token: accessToken }));
};
// Server: validate first message
ws.on("message", function firstMessage(raw) {
const msg = JSON.parse(raw.toString());
if (msg.type !== "auth" || !verifyToken(msg.token)) {
ws.close(4001, "Unauthorized");
return;
}
ws.removeListener("message", firstMessage);
ws.on("message", handleAuthenticatedMessage);
});
Anti-Patterns
- Sending messages without checking readyState: Always verify
ws.readyState === WebSocket.OPENbefore callingsend(). Sending on a closed socket throws or silently drops the message. - No reconnection logic: The native API does not reconnect automatically. Without explicit reconnection, a single network blip permanently disconnects the client.
- Relying on browser ping/pong: The browser WebSocket API does not expose ping/pong frames. Implement application-level heartbeats to detect dead connections.
- Putting auth tokens in the URL for production: Query parameters appear in server logs and browser history. Use a post-connection auth message or secure cookie-based authentication.
When to Use
- Full-duplex communication where both client and server send data frequently and unpredictably.
- Binary streaming (audio, video, file chunks) where text-only SSE is insufficient.
- Gaming or trading platforms requiring minimal protocol overhead and sub-frame latency.
- Projects avoiding third-party dependencies that want full control over the transport layer.
- Custom protocols where Socket.IO's opinionated event system or SSE's unidirectional model does not fit.
Install this skill directly: skilldb add realtime-services-skills
Related Skills
Ably Realtime
Ably is a robust, globally distributed real-time platform offering publish/subscribe messaging, presence, and channels.
Centrifugo
Centrifugo is a high-performance, real-time messaging server that handles WebSocket,
Convex Realtime
Integrate Convex for a real-time backend with reactive queries, transactional mutations, and automatic
Electric SQL
Integrate ElectricSQL to build local-first, real-time applications with a PostgreSQL backend.
Firebase Realtime Db
Integrate Firebase Realtime Database for synchronized data with listeners, offline persistence,
Liveblocks
Integrate Liveblocks for collaborative features including real-time presence, conflict-free storage,