tests.ws

JavaScript WebSocket API Guide

javascript websocket real-time browser api

The browser WebSocket API lets you open persistent connections to a server and exchange messages in real time. It is built into every modern browser and requires no libraries. The browser WebSocket implementation is consistent across all major platforms. This guide covers everything you need to use it in production.

Creating a Connection

To open a WebSocket connection, create a new WebSocket instance and pass it a URL.

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

The URL must use the ws:// or wss:// scheme. The wss:// scheme sends traffic over TLS, just like HTTPS. Always use wss:// in production. The ws:// scheme is unencrypted and should only be used during local development.

You can also pass an optional second argument for subprotocols, which is covered later in this guide.

readyState Values

Every WebSocket instance exposes a readyState property that tells you the current state of the connection.

ValueConstantMeaning
0WebSocket.CONNECTINGConnection is being established
1WebSocket.OPENConnection is open and ready
2WebSocket.CLOSINGConnection is closing
3WebSocket.CLOSEDConnection is closed

Check readyState before sending messages or performing any operation that requires an active connection.

Connection Events

The WebSocket API fires four events on a connection: open, message, close, and error. You can listen to them with addEventListener or by assigning handler properties like ws.onopen. This guide uses addEventListener because it allows multiple listeners per event and follows the same pattern as DOM events.

open

The open event fires when the WebSocket handshake completes and the connection is ready.

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

ws.addEventListener("open", (event) => {
  console.log("Connection established");
});

At this point, ws.readyState is 1 (OPEN) and you can start sending data.

message

The message event fires every time the server sends a frame to the client. The payload is available on event.data.

ws.addEventListener("message", (event) => {
  console.log("Received:", event.data);
});

The type of event.data depends on what the server sent and the binaryType property of your WebSocket. Text frames arrive as strings. Binary frames arrive as Blob by default, but you can change that to ArrayBuffer. See the Receiving Messages section for details.

close

The close event fires when the connection shuts down, whether initiated by the client, the server, or a network failure. The event includes a numeric code and an optional reason string.

ws.addEventListener("close", (event) => {
  console.log("Connection closed:", event.code, event.reason);
  console.log("Clean close:", event.wasClean);
});

The event.wasClean boolean tells you whether the closing handshake completed normally. A value of false usually indicates a network interruption. You can find a full list of close codes in the reference.

error

The error event fires when something goes wrong with the connection. It always fires before a close event.

ws.addEventListener("error", (event) => {
  console.error("WebSocket error:", event);
});

The browser does not expose detailed error information for security reasons. If you need to debug connection issues, check the browser developer tools Network tab for the WebSocket frames.

Sending Messages

Use the send() method to transmit data to the server.

ws.send("Hello, server!");

The send() method accepts four data types:

  • String for text messages
  • ArrayBuffer for raw binary data
  • Blob for file-like binary data
  • ArrayBufferView (such as Uint8Array) for typed array data

Always verify the connection is open before calling send(). If you call it while readyState is not OPEN, the browser throws an InvalidStateError.

function safeSend(ws, data) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(data);
  } else {
    console.warn("WebSocket is not open. readyState:", ws.readyState);
  }
}

bufferedAmount

The bufferedAmount property tells you how many bytes of data are queued and waiting to be transmitted. This is useful for flow control when you send large payloads.

function sendWithBackpressure(ws, data, callback) {
  ws.send(data);

  const check = setInterval(() => {
    if (ws.bufferedAmount === 0) {
      clearInterval(check);
      callback();
    }
  }, 100);
}

If bufferedAmount keeps growing, you are sending faster than the network can transmit. Slow down or buffer messages on your side.

Receiving Messages

When a message event fires, event.data contains the payload. For text frames, this is always a string.

ws.addEventListener("message", (event) => {
  if (typeof event.data === "string") {
    const parsed = JSON.parse(event.data);
    console.log("Text message:", parsed);
  }
});

binaryType

For binary frames, the type of event.data depends on the binaryType property. It defaults to "blob" but you can set it to "arraybuffer".

ws.binaryType = "arraybuffer";

ws.addEventListener("message", (event) => {
  if (event.data instanceof ArrayBuffer) {
    const view = new Uint8Array(event.data);
    console.log("Binary message, byte length:", view.byteLength);
  }
});

Set binaryType to "arraybuffer" when you need to process bytes directly. Use "blob" when you want to pass the data to APIs that accept Blobs, such as URL.createObjectURL().

Closing the Connection

Call close() to shut down a WebSocket connection. You can pass an optional status code and a reason string.

ws.close(1000, "Normal closure");

The code must be either 1000 (normal closure) or a value in the range 3000-4999. The reason string must be no longer than 123 bytes when encoded as UTF-8. After calling close(), the readyState transitions to CLOSING and then to CLOSED once the server acknowledges the shutdown.

ws.addEventListener("close", (event) => {
  if (event.code === 1000) {
    console.log("Clean shutdown");
  } else {
    console.log("Unexpected close, code:", event.code);
  }
});

For a full reference of status codes like 1001 (going away), 1006 (abnormal closure), and 1011 (server error), see the close codes guide.

Reconnection Logic

WebSocket connections drop. Servers restart, networks switch, and mobile devices move between cell towers. The browser WebSocket API does not reconnect automatically, so you need to build that yourself.

The standard approach is exponential backoff: wait a short time after the first disconnect, then double the wait with each consecutive failure, up to a maximum delay.

function createReconnectingWebSocket(url) {
  let ws;
  let reconnectAttempts = 0;
  const maxReconnectDelay = 30000;
  const baseDelay = 1000;
  const listeners = { open: [], message: [], close: [], error: [] };

  function connect() {
    ws = new WebSocket(url);

    ws.addEventListener("open", (event) => {
      console.log("Connected");
      reconnectAttempts = 0;
      listeners.open.forEach((fn) => fn(event));
    });

    ws.addEventListener("message", (event) => {
      listeners.message.forEach((fn) => fn(event));
    });

    ws.addEventListener("close", (event) => {
      listeners.close.forEach((fn) => fn(event));

      if (event.code !== 1000) {
        scheduleReconnect();
      }
    });

    ws.addEventListener("error", (event) => {
      listeners.error.forEach((fn) => fn(event));
    });
  }

  function scheduleReconnect() {
    const delay = Math.min(
      baseDelay * Math.pow(2, reconnectAttempts),
      maxReconnectDelay
    );
    console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts + 1})`);
    reconnectAttempts++;
    setTimeout(connect, delay);
  }

  connect();

  return {
    on(eventName, callback) {
      if (listeners[eventName]) {
        listeners[eventName].push(callback);
      }
    },
    send(data) {
      if (ws && ws.readyState === WebSocket.OPEN) {
        ws.send(data);
      } else {
        console.warn("Cannot send, socket is not open");
      }
    },
    close() {
      if (ws) {
        ws.close(1000, "Client closed");
      }
    },
  };
}

Usage:

const socket = createReconnectingWebSocket("wss://example.com/socket");

socket.on("message", (event) => {
  console.log("Received:", event.data);
});

socket.on("open", () => {
  socket.send("Hello!");
});

This wrapper reconnects on any abnormal close, resets the attempt counter on a successful connection, and caps the delay at 30 seconds.

Sending JSON Messages

Most WebSocket applications exchange JSON. Since the WebSocket API only sends strings or binary data, you need to serialize and deserialize manually.

function sendJSON(ws, type, payload) {
  const message = JSON.stringify({ type, payload });
  ws.send(message);
}

function handleMessage(event) {
  const message = JSON.parse(event.data);

  switch (message.type) {
    case "chat":
      displayChat(message.payload);
      break;
    case "notification":
      showNotification(message.payload);
      break;
    case "error":
      handleError(message.payload);
      break;
    default:
      console.warn("Unknown message type:", message.type);
  }
}

ws.addEventListener("message", handleMessage);

Using a type field in every message creates a simple protocol. Each side knows what to expect, and you can route messages to different handlers based on the type. Wrap JSON.parse in a try/catch in production to handle malformed messages.

ws.addEventListener("message", (event) => {
  let message;
  try {
    message = JSON.parse(event.data);
  } catch (err) {
    console.error("Invalid JSON received:", event.data);
    return;
  }

  handleMessage(message);
});

Binary Data

The WebSocket API supports sending and receiving binary data through ArrayBuffer and Blob. This is useful for transferring files, images, audio, or any non-text content.

Sending Binary Data

// Send raw bytes
const buffer = new ArrayBuffer(4);
const view = new Uint8Array(buffer);
view[0] = 0x48; // H
view[1] = 0x65; // e
view[2] = 0x6c; // l
view[3] = 0x6c; // l
ws.send(buffer);

Sending a File Chunk

A practical use case is uploading a file in chunks over a WebSocket connection.

async function sendFile(ws, file, chunkSize = 64 * 1024) {
  const totalChunks = Math.ceil(file.size / chunkSize);

  // Send metadata as JSON first
  ws.send(JSON.stringify({
    type: "file-start",
    name: file.name,
    size: file.size,
    totalChunks,
  }));

  for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    const arrayBuffer = await chunk.arrayBuffer();

    ws.send(arrayBuffer);
  }

  ws.send(JSON.stringify({ type: "file-end", name: file.name }));
}

Receiving Binary Data

Set binaryType to "arraybuffer" and check the data type in your message handler.

ws.binaryType = "arraybuffer";

ws.addEventListener("message", (event) => {
  if (typeof event.data === "string") {
    const json = JSON.parse(event.data);
    console.log("Control message:", json);
  } else if (event.data instanceof ArrayBuffer) {
    const bytes = new Uint8Array(event.data);
    console.log("Binary chunk received, size:", bytes.byteLength);
  }
});

Subprotocols

The WebSocket constructor accepts an optional second argument: a string or array of strings representing subprotocols.

const ws = new WebSocket("wss://example.com/socket", ["graphql-ws", "json"]);

During the handshake, the browser sends these values in the Sec-WebSocket-Protocol request header. The server picks one and includes it in the response header. After the connection opens, you can read which protocol was selected.

ws.addEventListener("open", () => {
  console.log("Negotiated protocol:", ws.protocol);
});

If the server does not support any of the requested protocols, it may reject the connection or accept it without a protocol. Common subprotocols include graphql-ws for GraphQL subscriptions and mqtt for IoT messaging.

Error Handling

The error event fires when the connection encounters a problem. Common triggers include:

  • The server is unreachable or refuses the connection
  • A TLS certificate error on wss:// connections
  • The server closes the connection unexpectedly during the handshake
  • A network interruption breaks an active connection

The browser intentionally limits the information available in the error event. You will not get HTTP status codes or error messages. This is a security measure to prevent scripts from probing internal networks.

ws.addEventListener("error", (event) => {
  console.error("WebSocket error occurred");

  // Check the readyState to understand context
  switch (ws.readyState) {
    case WebSocket.CONNECTING:
      console.error("Failed to establish connection");
      break;
    case WebSocket.OPEN:
      console.error("Error on open connection");
      break;
    case WebSocket.CLOSING:
      console.error("Error while closing");
      break;
    case WebSocket.CLOSED:
      console.error("Error on closed connection");
      break;
  }
});

An error event is always followed by a close event. Place your reconnection logic in the close handler, not in the error handler.

Full Example: Simple Chat Client

Here is a complete working example that connects to a WebSocket server, sends chat messages, and displays received messages. You can test it against any echo server or the WebSocket tester.

const chatLog = document.getElementById("chat-log");
const messageInput = document.getElementById("message-input");
const sendButton = document.getElementById("send-button");
const statusDisplay = document.getElementById("status");

let ws;
let reconnectAttempts = 0;

function connect() {
  ws = new WebSocket("wss://example.com/chat");

  ws.addEventListener("open", () => {
    statusDisplay.textContent = "Connected";
    reconnectAttempts = 0;
  });

  ws.addEventListener("message", (event) => {
    let message;
    try {
      message = JSON.parse(event.data);
    } catch {
      message = { user: "unknown", text: event.data };
    }

    const entry = document.createElement("div");
    entry.className = "chat-entry";
    entry.textContent = `${message.user}: ${message.text}`;
    chatLog.appendChild(entry);
    chatLog.scrollTop = chatLog.scrollHeight;
  });

  ws.addEventListener("close", (event) => {
    statusDisplay.textContent = "Disconnected";

    if (event.code !== 1000) {
      const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
      reconnectAttempts++;
      statusDisplay.textContent = `Reconnecting in ${delay / 1000}s...`;
      setTimeout(connect, delay);
    }
  });

  ws.addEventListener("error", () => {
    statusDisplay.textContent = "Connection error";
  });
}

function sendMessage() {
  const text = messageInput.value.trim();
  if (!text) return;

  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "chat", text }));
    messageInput.value = "";
  } else {
    statusDisplay.textContent = "Cannot send, not connected";
  }
}

sendButton.addEventListener("click", sendMessage);

messageInput.addEventListener("keydown", (event) => {
  if (event.key === "Enter") {
    sendMessage();
  }
});

connect();

This example includes reconnection with exponential backoff, JSON message parsing with error handling, and readyState checks before sending.

Browser Support

The WebSocket API is available in all modern browsers. It has been supported since:

  • Chrome 16+
  • Firefox 11+
  • Safari 7+
  • Edge 12+
  • Internet Explorer 10+
  • Opera 12.1+

No polyfill is needed. If you need to support very old browsers, you can fall back to long polling, but this is rarely necessary today. For more background on how WebSocket works under the hood, read What is WebSocket?.