Web Platform APIs · 8 / 10
lesson 8

Real-time: WebSockets

Two-way messaging, lifecycle, and reconnect patterns.

~ 15 min read·lesson 8 of 10
0 / 10

You've built a comments page. New comments appear when someone refreshes — which is fine for a blog and terrible for a chat. The problem is that HTTP only works one way: the client asks, the server answers. There's no way for the server to push a new comment without the client asking first. WebSockets is the standard way around that. Once a WebSocket is open, both sides can send messages whenever they have something to say.

A WebSocket is a single, long-lived TCP connection that upgrades from an HTTP request. After the upgrade, both ends can send text or binary messages until somebody hangs up. The mental model is closer to a phone call than a pile of letters.

Opening a connection

new WebSocket(url) opens a connection. The URL uses ws:// or wss:// (WebSocket Secure) — the secure version is what you'll use in production, exactly like https.

open.js
const socket = new WebSocket("wss://chat.example.com/room/42");

socket.addEventListener("open", () => {
console.log("connected");
socket.send("hello");
});

Construction begins immediately. The browser does an HTTP request that asks the server "want to upgrade to WebSocket?", and if the server agrees, the connection switches protocols. The open event fires once that handshake succeeds.

Until open fires, calling socket.send() throws — the channel isn't ready. Wait for the event, then send.

Tip

Don't write code that calls send immediately after new WebSocket(...). Wait for the open event, or queue messages and flush them inside the handler. The handshake takes a round-trip.

Sending and receiving

socket.send(data) accepts a string, an ArrayBuffer, or a Blob. Strings are sent as text frames; the others as binary. There's no automatic JSON serialization — if you want to send an object, JSON.stringify it yourself.

json-messages.js
function sendMessage(socket, payload) {
socket.send(JSON.stringify(payload));   /* always serialize */
}

socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);  /* always parse */
console.log("received", message);
});

event.data is whatever the server sent — a string for text frames, a Blob (or ArrayBuffer if you set socket.binaryType = "arraybuffer") for binary. JSON-over-WebSocket is the most common shape: each message is a small JSON object with a type field so both sides know how to interpret it.

typed-messages.js
socket.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
  case "comment":   addComment(msg.comment);  break;
  case "typing":    showTyping(msg.user);     break;
  case "presence":  updatePresence(msg.list); break;
}
});

Each message handler is short. The type field doesn't exist in the protocol — it's a convention you and the server agree on. Both ends speak the same little vocabulary.

check your understanding
Your code does const socket = new WebSocket(url); socket.send("hi"); immediately. The console shows "InvalidStateError: still in CONNECTING state". What's the fix?

The four lifecycle events

A WebSocket goes through a small state machine. Four events tell you where you are:

  • open — handshake succeeded, connection is alive. Send-able now.
  • message — the server sent something. Fires once per message.
  • error — something went wrong. Fired before close when the disconnect was abnormal. The event itself doesn't carry useful detail; treat it as "expect a close next".
  • close — the connection ended, cleanly or otherwise. The event has event.code (a number) and event.reason (a string).
lifecycle.js
socket.addEventListener("open",    () => setStatus("connected"));
socket.addEventListener("message", (e) => onMessage(e.data));
socket.addEventListener("error",   () => setStatus("error"));
socket.addEventListener("close",   (e) => {
setStatus(`closed: ${e.code} ${e.reason}`);
scheduleReconnect();
});

A few common close codes worth recognizing:

  • 1000 — normal closure. Both sides agreed.
  • 1001 — going away (e.g. the server is restarting, or the user navigated away).
  • 1006 — abnormal closure (the connection dropped without a proper close). You'll see this on flaky networks.

You don't usually act on the code itself in app code — close is your signal to retry, regardless.

CONNECTINGOPENCLOSEDopenclosehandshake failed
A WebSocket's lifecycle: connecting, open, then either a clean close or an abnormal one — both end the connection.

Reconnect with backoff

Connections drop. The user goes through a tunnel, the server restarts, the laptop closes its lid. A real WebSocket client reconnects on close, but it doesn't reconnect immediately — that would hammer the server when it's already in trouble.

The standard pattern is exponential backoff with jitter: each retry waits longer, and a random offset prevents every client reconnecting at the same instant.

reconnect.js
function connect(url, onMessage) {
let attempts = 0;
let socket;

function open() {
  socket = new WebSocket(url);
  socket.addEventListener("open", () => { attempts = 0; });
  socket.addEventListener("message", (e) => onMessage(JSON.parse(e.data)));
  socket.addEventListener("close", () => {
    attempts++;
    const delay = Math.min(30000, 2 ** attempts * 500);
    const jitter = Math.random() * 250;
    setTimeout(open, delay + jitter);
  });
}

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

Two things to notice:

  • attempts = 0 resets when a new connection succeeds — so a brief flake doesn't poison future retries.
  • Math.min(30000, ...) caps the delay. Without a cap, the wait would grow until it's effectively forever.

For most apps, this 12-line pattern is enough. Production-grade clients add buffered messages (queue while disconnected, flush on reconnect), heartbeat pings (ping/pong every 30 seconds to detect zombie connections), and authentication on reconnect — but the bones are this loop.

Watch out

Don't reconnect inside the error handler. The close event always follows an error. If you reconnect on both, you'll open two sockets.

check your understanding
Your WebSocket app reconnects immediately on every close. The server crashes once and within a minute, every connected client is hammering it with reconnect attempts. What's the standard fix?

Try it yourself

check your understanding
Both your client and your server are sending JSON-shaped messages with a type field. Why type and not just plain JSON?
check your understanding
You want a chat UI to feel "live" — comments others type appear without refresh. WebSocket vs polling every second?
check your understanding
Inside your error listener, you call connectAgain(). Inside close you also call connectAgain(). What happens?
← prevnext lesson →
KeepLearningcertificate
for completing
Web Platform APIs
0 of 10 read