WebSocket Real-Time Communication Patterns
Quick Guide: Use native WebSocket API for real-time bidirectional communication. Implement exponential backoff with jitter for reconnection. Use discriminated unions for type-safe message handling. Queue messages during disconnection for delivery on reconnect. Close connections on
pagehideto allow bfcache.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST implement exponential backoff with jitter for ALL reconnection logic)
(You MUST use discriminated unions with a type field for ALL WebSocket message types)
(You MUST queue messages during disconnection and flush on reconnect)
(You MUST implement heartbeat/ping-pong to detect dead connections)
(You MUST set binaryType to 'arraybuffer' when handling binary data)
(You MUST use wss:// for secure origins - browsers block ws:// on HTTPS pages except localhost)
(You MUST handle bfcache with pagehide/pageshow events)
</critical_requirements>
Auto-detection: WebSocket, ws://, wss://, onmessage, onopen, onclose, onerror, reconnect, heartbeat, ping, pong, real-time, bidirectional
When to use:
- Building real-time features (chat, notifications, live updates)
- Implementing bidirectional communication between client and server
- Creating live dashboards or collaborative editing features
- Streaming data updates with low latency requirements
When NOT to use:
- One-way server-to-client streaming only (use SSE instead)
- Simple request-response patterns (use HTTP/REST instead)
- When library abstractions are required (use a WebSocket wrapper library)
- When automatic backpressure handling is critical (consider WebSocketStream when widely supported)
Key patterns covered:
- WebSocket connection lifecycle management
- Reconnection with exponential backoff and jitter
- Heartbeat/ping-pong for connection health
- Message queuing during disconnection
- Type-safe message handling with discriminated unions
- Binary data handling (ArrayBuffer, Blob)
- Custom React hooks (useWebSocket)
- Authentication patterns
- Room/channel subscriptions
- bfcache compatibility
Detailed Resources:
- examples/core.md - Connection lifecycle, reconnection, heartbeat, queuing, auth, rooms, hooks
- examples/state-machine.md - Connection state machine pattern
- examples/binary.md - Binary data and file upload
- examples/presence.md - User presence detection
- reference.md - Decision frameworks, close codes, anti-patterns
<philosophy>
Philosophy
WebSockets provide full-duplex communication channels over a single TCP connection, enabling real-time bidirectional data flow between client and server. Unlike HTTP, WebSocket connections remain open, eliminating the overhead of repeated handshakes.
The native WebSocket API is simple but requires careful handling:
-
Connection Resilience: Networks are unreliable. Always implement reconnection with exponential backoff and jitter to prevent thundering herd problems.
-
Connection Health: Intermediate proxies and firewalls can silently drop idle connections. Heartbeats detect dead connections and keep connections alive.
-
Message Integrity: Messages sent during disconnection are lost. Queue them and flush on reconnect for reliable delivery.
-
Type Safety: WebSocket messages are untyped strings. Use discriminated unions with a shared
typefield for compile-time safety. -
bfcache Compatibility: Open WebSocket connections prevent pages from using the browser's back/forward cache, degrading navigation performance. Close connections on
pagehideand reconnect onpageshowwhenevent.persisted.
Connection Lifecycle:
CONNECTING -> OPEN <-> (messages) -> CLOSING -> CLOSED
| |
(error) <- reconnect <- (close)
</philosophy>
<patterns>
Core Patterns
Pattern 1: Basic WebSocket Connection
The native WebSocket API provides four lifecycle events: onopen, onmessage, onerror, and onclose. Always handle all four.
const WS_URL = "wss://api.example.com/ws";
const socket = new WebSocket(WS_URL);
socket.onopen = () => {
/* connection ready - safe to send */
};
socket.onmessage = (event: MessageEvent) => {
/* JSON.parse(event.data) */
};
socket.onerror = (event: Event) => {
/* always followed by onclose */
};
socket.onclose = (event: CloseEvent) => {
/* reconnect here */
};
Why good: All four lifecycle events handled, typed event parameters, named constant for URL
Full implementation: examples/core.md Pattern 1
Pattern 2: Exponential Backoff with Jitter
Reconnection attempts must use exponential backoff with jitter to prevent all clients from reconnecting simultaneously (thundering herd problem). Cap delay at a maximum and limit total retry attempts.
const INITIAL_BACKOFF_MS = 1000;
const MAX_BACKOFF_MS = 30000;
const BACKOFF_MULTIPLIER = 2;
const JITTER_FACTOR = 0.5;
function calculateBackoff(attempt: number): number {
const exponential = Math.min(
INITIAL_BACKOFF_MS * Math.pow(BACKOFF_MULTIPLIER, attempt),
MAX_BACKOFF_MS,
);
const jitter = exponential * JITTER_FACTOR * (Math.random() * 2 - 1);
return Math.floor(exponential + jitter);
}
Why good: Jitter prevents thundering herd, capped maximum delay, retry limit prevents infinite loops
Full reconnecting class: examples/core.md Pattern 2
Pattern 3: Heartbeat/Ping-Pong
Heartbeats detect dead connections and prevent intermediate infrastructure from closing idle connections. Send a ping on an interval; if pong is not received within a timeout, consider the connection dead.
const HEARTBEAT_INTERVAL_MS = 30000;
const HEARTBEAT_TIMEOUT_MS = 10000;
// Send ping -> start timeout -> if pong received, clear timeout
// If timeout fires without pong -> connection is dead, close and reconnect
When to use: All WebSocket connections, especially those that may be idle for extended periods or pass through NATs/proxies.
Full implementation: examples/core.md Pattern 3
Pattern 4: Message Queuing During Disconnection
Messages sent during disconnection are lost. Queue them and flush when connection is restored. Limit queue size to prevent unbounded memory growth.
const MAX_QUEUE_SIZE = 100;
public send(data: unknown): void {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
} else {
this.queueMessage(data); // Queue with size limit
}
}
Why good: Queue has size limit, oldest messages dropped when full, flush on reconnect, readyState check before sending
Full implementation: examples/core.md Pattern 4
Pattern 5: Type-Safe Messages with Discriminated Unions
Use discriminated unions with a shared type field for compile-time type safety and exhaustive handling. Define separate types for client-to-server and server-to-client messages.
type ServerMessage =
| { type: "subscribed"; channel: string; members: string[] }
| { type: "message"; channel: string; content: string; sender: string }
| { type: "error"; code: number; message: string };
function handleServerMessage(message: ServerMessage): void {
switch (message.type) {
case "subscribed":
/* ... */ break;
case "message":
/* ... */ break;
case "error":
/* ... */ break;
default:
const exhaustiveCheck: never = message; // Compile error if case missing
}
}
Why good: Discriminated union enables type narrowing, exhaustiveness check catche