JavaScript WebSocket Table
| Piece | What it does | Field note |
|---|---|---|
new WebSocket(url) | Open a socket | ws:// or wss:// (secure) - HTTP cannot upgrade to plain ws from HTTPS pages |
onopen / onclose | Lifecycle | close event fires on server drops AND errors - reconnect logic lives there |
send(data) | Push either way | Strings or ArrayBuffers - full-duplex, no request/response pairing |
onmessage | Receive pushes | Server can initiate - the property fetch lacks |
binaryType = 'arraybuffer' | Binary frames | Default blob; arraybuffer for typed-array parsing |
readyState | 0-3 states | CONNECTING before onopen - sending early throws |
heartbeat ping/pong | Keep-alive detection | Dead TCP connections need app-level pings - sockets lie silently |
reconnect with backoff | The missing built-in | No auto-reconnect - exponential backoff is YOUR code |
A WebSocket is a full-duplex socket reached through one HTTP upgrade handshake: after onopen, either side pushes at any time with no request/response pairing. That single property - server-initiated pushes - is what realtime apps need and what fetch structurally cannot do.
Bottom line: the API is deliberately thin (open, message, close, send), so the real engineering lives in what it omits: send before onopen throws (readyState discipline), dead TCP connections LOOK alive until a write fails (heartbeat pings), and there is NO auto-reconnect - every production socket app hand-writes exponential backoff.
The honest part: a WebSocket's lifecycle outlives your UI state. The reconnect that succeeds hands you a socket with no memory - server-side session state, missed messages, and client-side UI all need an explicit re-sync step, or the app shows a live connection over stale data.
How to use
- Gate every send: wrap socket.send in a readyState check or queue outgoing messages until onopen - CONNECTING sockets throw, and users click fast.
- Heartbeat to detect silent deaths: a 30-second ping from the client; two missed pongs = force close and reconnect - NAT gateways kill idle sockets without warning.
- Reconnect with exponential backoff and jitter: 1s, 2s, 4s, capped at 30s plus random jitter - thundering-herd reconnections after a server restart are a self-inflicted outage.
Frequently asked questions
Why do I need WebSockets when fetch and polling exist?
Direction and latency. Polling asks 'anything new?' on a timer - the updates arrive up to one interval late while paying a full HTTP request each tick. Long-polling improves latency but holds request slots. A WebSocket is a standing pipe: the server pushes the instant data exists, with one handshake of overhead and frame-sized messages after. The decision line: request/response data (forms, page loads) stays fetch; state that changes server-side and matters within seconds (chat, collaborative edits, live prices, game state) wants a socket. SSE (server-sent events) is the middle option when pushes are one-directional - simpler, but the client cannot talk back on the same pipe.
Why does my WebSocket connection die silently after a few idle minutes?
Intermediate infrastructure. NAT gateways, load balancers and firewalls hold connection state with idle timeouts - an open socket with no traffic looks dead to them and gets dropped WITHOUT notifying either end, while the TCP connection on both sides still appears established. The fix is application-level heartbeats: the client pings every 30 seconds (any tiny frame works), treats two missed pongs as a dead socket, and force-closes to trigger reconnect. The lesson generalized: absence of error is not proof of liveness on long-lived connections - liveness is something you probe for.
What belongs in my reconnect logic?
Exponential backoff with jitter, a re-sync step, and state gating. Backoff: retry at 1s, 2s, 4s... capped around 30s, with random jitter so a thousand clients do not stampede the recovering server on the same tick. Re-sync: after onopen, treat it as a NEW session - request missed data (last event id, current state snapshot) before trusting the UI, because messages sent while you were disconnected are gone unless the server queues them. State gating: show a reconnecting indicator and freeze optimistic UI until re-sync completes - a live-looking connection over stale data is worse than an honest offline badge. And cap it: after N failures, back off to manual ('reconnect' button) territory.
How do binary frames work, and when do they beat JSON strings?
The socket transports strings or bytes; binaryType decides how onmessage hands you the bytes ('blob' by default, 'arraybuffer' for typed-array parsing). Binary wins when payloads are dense and structured: game state at 60Hz, sensor streams, audio chunks - a Float32Array frame is smaller to send and faster to parse than the equivalent JSON (no stringify/parse, no key repetition). The costs: no human readability in devtools without tooling, and you own the schema (field order, endianness - DataView territory). The pragmatic split: JSON for control messages and low-frequency data, binary for the high-frequency stream that pays for the complexity.