Back to Blog
Real-time2026-04-21

WebSocket Real-Time Apps: Production Patterns

Build resilient WebSocket clients that survive disconnects, network changes, and backpressure.

WebSockets are simple to start and tricky to keep alive in production. Mobile networks, proxies, and laptop sleep all break naive implementations.

Reconnect with Backoff

``js class ReliableSocket { constructor(url) { this.url = url; this.attempt = 0; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { this.attempt = 0; }; this.ws.onclose = () => { const delay = Math.min(30000, 1000 * 2 ** this.attempt++); setTimeout(() => this.connect(), delay + Math.random() * 1000); }; } } `

The jitter avoids thundering herds when a server restarts.

Heartbeats

Idle connections silently die behind NATs. Send a ping every 25-30 seconds:

`js setInterval(() => ws.readyState === WebSocket.OPEN && ws.send('"ping"'), 25000); `

The server replies with pong; if no pong arrives in 10 seconds, consider the connection dead and reconnect.

Resume State

After reconnect, replay missed events. Patterns:

- Server-assigned message IDs; client requests "since X" - CRDT or sync-engine like Yjs handles this transparently

Backpressure

WebSocket has bufferedAmount. Pause large pushes when it grows:

`js if (ws.bufferedAmount > 1_000_000) await waitForBuffer(ws); `

Use Sec-WebSocket-Protocol

The subprotocol field versions your wire format. Bumping from chat-v1 to chat-v2` lets servers handle both during deploy.

Server-Sent Events as Alternative

For server-to-client only streams, SSE auto-reconnects and works through proxies more reliably than WebSocket. Worth considering for notifications and live updates.

For HTTP retry patterns see [modern fetch API patterns](/blog/modern-fetch-api-patterns).