JavaScript WebSocket API Guide
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.
| Value | Constant | Meaning |
|---|---|---|
| 0 | WebSocket.CONNECTING | Connection is being established |
| 1 | WebSocket.OPEN | Connection is open and ready |
| 2 | WebSocket.CLOSING | Connection is closing |
| 3 | WebSocket.CLOSED | Connection 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?.
What to Read Next
- What is WebSocket? covers the protocol fundamentals and how it differs from HTTP.
- The WebSocket Handshake explains the upgrade process from HTTP to WebSocket in detail.
- WebSocket Close Codes is a reference for every status code you might encounter.
- Node.js ws Library Guide shows how to build the server side of a WebSocket application.
- WebSocket Tester lets you connect to any WebSocket server and send messages from your browser.