WebSocket Patterns

WebSockets provide a bidirectional, full-duplex communication channel between client and server. Once established, both sides can send messages independently. The protocol upgrades from HTTP to a long-lived TCP connection.

This page is about the patterns that make WebSocket applications work in production — not the protocol details, but the operational concerns.

When WebSocket is right

- Bidirectional real-time communication (chat, collaborative editing)

- Server-initiated push that's part of an interactive flow

- Low-latency messaging where polling is too slow

- Applications where SSE's one-way constraint is limiting

When WebSocket is wrong

- One-way server-to-client streams: use [SSE](ServerSentEventsPatterns); simpler.

- Request-response patterns: use HTTP; better tooling.

- Workloads dominated by request-response with occasional pushes: HTTP plus a fallback push mechanism is often simpler.

Connection lifecycle

```

Client → HTTP GET with Upgrade headers → Server

Server → HTTP 101 Switching Protocols → Client

Connection upgraded to WebSocket

... message exchange ...

Either side → close frame → Other side

```

The HTTP-to-WebSocket upgrade requires:

- `Upgrade: websocket`

- `Connection: Upgrade`

- `Sec-WebSocket-Key` (handshake security)

Most clients/servers handle this automatically.

Authentication

WebSocket authentication is done during the upgrade. Several patterns:

Cookie-based

If your app already uses cookies for auth, they apply to the upgrade request. Simple.

Token in subprotocol

```javascript

new WebSocket('wss://api/ws', ['token.your-jwt-here'])

```

The token is in the `Sec-WebSocket-Protocol` header. Cleaner than URL parameters.

Token in URL

```

wss://api/ws?token=...

```

Works but logged everywhere; not great for sensitive tokens.

Post-connect authentication

Connect first, then send an auth message:

```

{ "type": "auth", "token": "..." }

```

Server keeps the connection open only after successful auth. Useful for protocols where the upgrade itself is unauthenticated.

Backpressure

What happens when the server sends faster than the client consumes? Or vice versa?

The TCP layer provides some backpressure (the OS buffers fill; sends block). Above that:

- **Drop on overflow**: discard messages when buffer is full. Useful for high-frequency state updates.

- **Slow down**: send rate limit; server stops sending until ACK received.

- **Disconnect on overflow**: aggressive but simple.

For real-time but loss-tolerant streams (cursor positions, presence), drop on overflow. For streams where every message matters, slow down or disconnect.

Heartbeats

Long-idle WebSocket connections may be closed by NAT, proxies, or network equipment. Heartbeats keep the connection alive and detect failures.

```

Client → ping (every 30s) → Server

Server → pong → Client

```

If a pong is missed, reconnect. Specific timeouts depend on your infrastructure.

WebSocket has built-in ping/pong frames; some libraries handle this automatically.

Reconnection

Always assume disconnects happen. Reconnection logic:

1. Detect disconnect (heartbeat failure, close frame, error)

2. Wait with exponential backoff (start at 1s, max ~30s)

3. Reconnect; re-authenticate

4. Resync state (request "what did I miss")

The "resync state" step is application-specific. Some patterns:

- **Sequence numbers**: server assigns IDs; client sends "give me everything after ID X"

- **Snapshot on reconnect**: server sends a state snapshot; client replaces local state

- **No resync**: client missed events; tolerate or reload

Fan-out

Broadcasting to many connected clients. The challenge: each connection holds a TCP socket; servers can hold thousands but not millions.

For very high fan-out:

- Pub/sub backend (Redis, Kafka): server processes publish to topics; per-connection processes subscribe

- Specialized infrastructure: services like Pusher, Ably, AWS API Gateway WebSocket

- Server sharding by topic or user

Library and framework support

- **Node.js**: ws, Socket.IO

- **Java/Spring**: Spring WebSocket, native Jakarta WebSocket

- **Python**: websockets, FastAPI's websocket support

- **Go**: gorilla/websocket

- **Browsers**: native WebSocket API

Socket.IO adds features (auto-reconnect, fallbacks to long-polling, rooms) at the cost of being its own protocol on top of WebSocket. Use vanilla WebSocket unless Socket.IO's features are specifically needed.

Common operational issues

Sticky sessions

If clients connect to one server and you have multiple servers, the connection is server-pinned. Load balancers need sticky sessions or the connection breaks.

For stateless WebSocket usage (using a pub/sub backend for shared state), any server can handle any connection — sticky sessions optional.

Memory pressure

Each connection consumes memory. Estimate ~10-50 KB per idle connection plus per-message buffers. 100K concurrent connections = 1-5 GB just in baseline memory.

Connection limits

Each TCP connection consumes a file descriptor. Tune `ulimit -n` and OS limits accordingly.

Common failure patterns

- **No reconnection logic.** Disconnects mean lost functionality.

- **No heartbeats.** Stale connections accumulate.

- **Unbounded buffers.** Slow client or fast server fills memory.

- **Missing authentication on upgrade.** Connections without auth.

- **No connection limits per user.** Single user opens 1000 connections; DoS.

- **Treating WebSocket as request-response.** WebSocket is best for streams; for RPC-style, HTTP is usually clearer.

Further Reading

- [ServerSentEventsPatterns](ServerSentEventsPatterns) — Simpler one-way alternative

- [ApiProtocolComparison](ApiProtocolComparison) — When WebSocket fits

- [WebhookPatterns](WebhookPatterns) — Server-to-server alternative for callbacks

- [HttpTwoAndHttpThree](HttpTwoAndHttpThree) — HTTP/3 + QUIC have their own streaming model

- [WebServicesAndApis Hub](WebServicesAndApisHub) — Cluster index