Skip to content
JavaScript js browser 6 min read

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).

FeatureHTTP requestWebSocket
DirectionClient → server (request/response)Bidirectional, any time
ConnectionOpened per requestOne persistent connection
Server pushNot native (needs polling/SSE)Built in
Overhead per messageFull headersA few bytes of framing
URL schemehttp / httpsws / wss

Always prefer wss:// in production. Like https, 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:

ConstantValueMeaning
WebSocket.CONNECTING0Handshake in progress
WebSocket.OPEN1Ready to send and receive
WebSocket.CLOSING2Close handshake started
WebSocket.CLOSED3Connection 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 on event.data (a string by default, or a Blob/ArrayBuffer for binary).
  • close — fires when either side closes. event.code (e.g. 1000 for normal) and event.reason explain why.
  • error — fires on failure; a close event 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: readyState becomes CLOSING immediately and CLOSED only after the close event fires. Don’t call send() 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() when readyState === WebSocket.OPEN, and queue messages until the open event fires.
  • Send structured data as JSON with a type field 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.
Last updated June 1, 2026
Was this helpful?