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.
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 callsend().message— a frame arrived. Readevent.data(string orBlob/ArrayBuffer).close— connection ended.event.code,event.reason,event.wasClean.error— something went wrong. Followed by aclose.
chat.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.
socket.send("hello");
socket.send(JSON.stringify({ type: "move", x: 12, y: 4 }));
socket.send(new Uint8Array([1, 2, 3])); // binary framereadyState 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.
socket.binaryType = "arraybuffer";
socket.addEventListener("message", (e) => {
if (typeof e.data === "string") {
handleText(e.data);
} else {
handleBinary(new Uint8Array(e.data));
}
});Closing the connection
// 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.
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
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.
const socket = new WebSocket(url, ["graphql-transport-ws", "graphql-ws"]);
socket.addEventListener("open", () => {
console.log("server picked:", socket.protocol);
});