JavaScriptWebSockets

WebSockets

HTTP is request/response: the client asks, the server answers, the connection closes. For chat, multiplayer games, live trading data, collaborative editing — anywhere the server needs to push to the client without being asked — that round-trip model is wrong. WebSockets keep a single TCP connection open and let both sides send messages whenever they like. The browser API for using them is small.

Opening a connection

You connect with new WebSocket(url). The URL uses the ws:// (plaintext) or wss:// (TLS) scheme. Use wss in production for the same reasons you use https.

JS
const socket = new WebSocket("wss://example.com/chat");

The constructor returns immediately. The connection is established asynchronously — wait for the open event before sending.

The four events
  • open — the handshake completed; you can call send().

  • message — a frame arrived. Read event.data (string or Blob / ArrayBuffer).

  • close — connection ended. event.code, event.reason, event.wasClean.

  • error — something went wrong. Followed by a close.

chat.js

JS
const socket = new WebSocket("wss://example.com/chat");

socket.addEventListener("open", () => {
  console.log("connected");
  socket.send(JSON.stringify({ type: "hello", user: "alice" }));
});

socket.addEventListener("message", (event) => {
  const msg = JSON.parse(event.data);
  appendChat(msg);
});

socket.addEventListener("close", (event) => {
  console.log("closed:", event.code, event.reason);
});

socket.addEventListener("error", () => {
  console.warn("websocket error");
});
Sending data

socket.send takes a string, ArrayBuffer, typed array, or Blob. Most apps stick to JSON strings.

JS
socket.send("hello");
socket.send(JSON.stringify({ type: "move", x: 12, y: 4 }));
socket.send(new Uint8Array([1, 2, 3])); // binary frame
Check readyState before send
Calling `send` before `open` (or after `close`) throws. Check `socket.readyState === WebSocket.OPEN` if you're not sure.
readyState values
  • WebSocket.CONNECTING (0) — handshake in progress.

  • WebSocket.OPEN (1) — ready to send and receive.

  • WebSocket.CLOSING (2) — close() has been called, frames still flushing.

  • WebSocket.CLOSED (3) — connection done.

Receiving binary frames

By default event.data for binary frames is a Blob. Set binaryType to get ArrayBuffer instead — usually nicer for protocol code.

JS
socket.binaryType = "arraybuffer";

socket.addEventListener("message", (e) => {
  if (typeof e.data === "string") {
    handleText(e.data);
  } else {
    handleBinary(new Uint8Array(e.data));
  }
});
Closing the connection

JS
// graceful close
socket.close(1000, "user logged out");

// any code in the 4000-4999 range is reserved for application use:
socket.close(4001, "session-expired");

Close codes are documented in RFC 6455 and on MDN. 1000 is the standard "normal closure".

Ping-pong and keepalive

The protocol has built-in ping/pong frames, but the browser API does not expose them. If you need to keep an idle connection alive through proxies (which often time out after 30-60 seconds), send your own small message periodically.

JS
setInterval(() => {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify({ type: "ping" }));
  }
}, 25_000);
Reconnection

Network connections drop. Production WebSocket clients almost always include a reconnect loop with exponential backoff so a server outage does not become a thundering-herd retry storm.

reconnect.js

JS
function connect(url) {
  let socket;
  let attempt = 0;

  function open() {
    socket = new WebSocket(url);

    socket.addEventListener("open", () => { attempt = 0; });
    socket.addEventListener("message", onMessage);
    socket.addEventListener("close", () => {
      const delay = Math.min(30_000, 500 * 2 ** attempt++);
      setTimeout(open, delay);
    });
  }

  open();
  return () => socket.close();
}

Real implementations also stop retrying after a maximum number of attempts, expose connection state to the UI, and re-emit any queued messages once the new socket opens.

Subprotocols

A second argument to the constructor negotiates a subprotocol — useful when a server can speak more than one wire format.

JS
const socket = new WebSocket(url, ["graphql-transport-ws", "graphql-ws"]);
socket.addEventListener("open", () => {
  console.log("server picked:", socket.protocol);
});
When NOT to use WebSockets
If the server only needs to push to the client (no client → server messages) and the data is plain text, **Server-Sent Events** (`EventSource`) are simpler, automatically reconnect, and travel over plain HTTP. Save WebSockets for true two-way streams.