WebSockets
Most HTTP communication is a one-way street: the browser asks, the server answers, the connection closes. That model breaks down when you need live data — a chat message arriving, a stock price ticking, a multiplayer game state changing. WebSockets solve this by opening a single, long-lived TCP connection over which both the client and server can push messages at any time, with almost no per-message overhead. This page covers the browser WebSocket API end to end: opening connections, handling events, sending data, and reconnecting gracefully.
How WebSockets work
A WebSocket connection starts life as an ordinary HTTP request that asks to be upgraded. The browser sends an Upgrade: websocket header; if the server agrees, the same TCP socket is reused for full-duplex messaging. From that point the protocol switches from http:// to ws:// (or wss:// for the encrypted variant over TLS).
| Feature | HTTP request | WebSocket |
|---|---|---|
| Direction | Client → server (request/response) | Bidirectional, any time |
| Connection | Opened per request | One persistent connection |
| Server push | Not native (needs polling/SSE) | Built in |
| Overhead per message | Full headers | A few bytes of framing |
| URL scheme | http / https | ws / wss |
Always prefer
wss://in production. Likehttps, it encrypts traffic and avoids mixed-content errors when your page is served over HTTPS.
Opening a connection
You create a connection by constructing a WebSocket with a URL. The connection is established asynchronously, so you attach event handlers rather than waiting on a return value.
const socket = new WebSocket("wss://echo.websocket.org");
socket.addEventListener("open", () => {
console.log("Connected, readyState:", socket.readyState); // 1 (OPEN)
socket.send("Hello, server!");
});
The readyState property reports where the connection is in its lifecycle:
| Constant | Value | Meaning |
|---|---|---|
WebSocket.CONNECTING | 0 | Handshake in progress |
WebSocket.OPEN | 1 | Ready to send and receive |
WebSocket.CLOSING | 2 | Close handshake started |
WebSocket.CLOSED | 3 | Connection closed or failed |
The four core events
A WebSocket exposes four events. You can assign them via on* properties or, preferably, addEventListener (which lets you attach multiple listeners).
const socket = new WebSocket("wss://echo.websocket.org");
socket.onopen = () => console.log("open");
socket.onmessage = (event) => console.log("received:", event.data);
socket.onclose = (event) => console.log(`closed: ${event.code} ${event.reason}`);
socket.onerror = () => console.log("error — connection problem");
open— fires once the handshake succeeds. Send your first messages here, never before.message— fires for every inbound message. The payload is onevent.data(a string by default, or aBlob/ArrayBufferfor binary).close— fires when either side closes.event.code(e.g.1000for normal) andevent.reasonexplain why.error— fires on failure; acloseevent usually follows. The event itself carries no detail by design.
Sending and receiving messages
send() accepts a string, Blob, ArrayBuffer, or typed array. To exchange structured data, serialize with JSON on the way out and parse on the way in.
function sendChat(socket, text) {
if (socket.readyState !== WebSocket.OPEN) return;
socket.send(JSON.stringify({ type: "chat", text, at: Date.now() }));
}
socket.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "chat") {
console.log(`${new Date(msg.at).toLocaleTimeString()}: ${msg.text}`);
}
});
Output:
2:14:09 PM: Hello, server!
A working echo demo
This self-contained example connects to a public echo server (which sends back whatever you send it), wiring up a tiny chat-style UI.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
body { font: 16px system-ui; padding: 1rem; }
#log { border: 1px solid #ccc; height: 140px; overflow: auto; padding: .5rem; margin-bottom: .5rem; }
.me { color: #2563eb; } .srv { color: #16a34a; }
</style>
</head>
<body>
<div id="log"></div>
<input id="text" placeholder="Type a message…" />
<button id="send">Send</button>
<script>
const log = document.getElementById("log");
const add = (cls, who, txt) => {
const p = document.createElement("p");
p.className = cls;
p.textContent = `${who}: ${txt}`;
log.appendChild(p);
log.scrollTop = log.scrollHeight;
};
const socket = new WebSocket("wss://echo.websocket.org");
socket.addEventListener("open", () => add("srv", "system", "connected"));
socket.addEventListener("message", (e) => add("srv", "server", e.data));
socket.addEventListener("close", () => add("srv", "system", "disconnected"));
const input = document.getElementById("text");
document.getElementById("send").addEventListener("click", () => {
if (socket.readyState === WebSocket.OPEN && input.value) {
socket.send(input.value);
add("me", "you", input.value);
input.value = "";
}
});
</script>
</body>
</html>
Closing cleanly
Call close() to end a connection. You may pass a code and a short reason, which the other side reads from the close event. Code 1000 means a normal closure.
socket.close(1000, "user left the page");
Closing is asynchronous:
readyStatebecomesCLOSINGimmediately andCLOSEDonly after thecloseevent fires. Don’t callsend()in between — it will throw.
Reconnection basics
Networks drop. A robust client should detect a closed socket and reconnect, ideally with exponential backoff so a struggling server isn’t hammered by a stampede of retries.
function createResilientSocket(url, onMessage) {
let delay = 1000; // start at 1s
const maxDelay = 30000; // cap at 30s
let socket;
function connect() {
socket = new WebSocket(url);
socket.addEventListener("open", () => {
console.log("connected");
delay = 1000; // reset backoff after a success
});
socket.addEventListener("message", (event) => onMessage(event.data));
socket.addEventListener("close", () => {
console.log(`reconnecting in ${delay}ms`);
setTimeout(connect, delay);
delay = Math.min(delay * 2, maxDelay); // back off
});
socket.addEventListener("error", () => socket.close());
}
connect();
return () => socket.close(1000, "shutdown"); // cleanup function
}
const stop = createResilientSocket("wss://echo.websocket.org", (data) =>
console.log("got:", data)
);
For long-lived connections, also send periodic heartbeat messages (a “ping”) so idle proxies don’t silently drop the socket, and so you can detect a half-open connection that hasn’t fired close.
// A minimal heartbeat: ping every 20s, expect a pong back.
function startHeartbeat(socket, intervalMs = 20000) {
const id = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "ping" }));
}
}, intervalMs);
socket.addEventListener("close", () => clearInterval(id));
}
const socket = new WebSocket("wss://echo.websocket.org");
socket.addEventListener("open", () => startHeartbeat(socket));
console.log("heartbeat scheduled");
Common use cases
- Chat and messaging — instant delivery without polling.
- Live dashboards — stock tickers, sports scores, IoT sensor feeds.
- Collaborative editing — shared documents and whiteboards.
- Multiplayer games — low-latency state synchronization.
- Notifications — push alerts the moment something happens.
If you only need server-to-client updates (no client sending), consider Server-Sent Events (EventSource) — they’re simpler and auto-reconnect. Use WebSockets when you need true bidirectional traffic.
Best Practices
- Use
wss://everywhere in production to encrypt traffic and avoid mixed-content blocks. - Only call
send()whenreadyState === WebSocket.OPEN, and queue messages until theopenevent fires. - Send structured data as JSON with a
typefield so a single handler can route different message kinds. - Implement reconnection with exponential backoff and jitter; reset the delay after a successful connection.
- Add heartbeats to keep idle connections alive and to detect half-open sockets that never emit
close. - Clean up — call
close()and clear timers when the component or page unmounts to prevent leaks. - Authenticate after connecting (e.g. send a token as your first message) rather than putting secrets in the URL, since URLs can be logged.