Skip to content

Binary Data

So far every message has been a string. But a WebSocket carries bytes just as happily — images, audio, protocol buffers, game state packed into a tight binary format. This final lesson covers the two flavors of frame, how the browser hands binary data back to you, and how to keep an eye on how much is waiting to go out.

At the protocol level, every WebSocket message is either a text frame or a binary frame:

  • A text frame carries UTF-8 encoded text. You produce one by passing a string to send(), and you receive it as a string.
  • A binary frame carries raw bytes with no encoding assumptions. You produce one by passing binary data to send().

The receiving side is told which kind a frame is, so there is never ambiguity between “the four-character string 1234” and “four raw bytes”. You choose the flavor simply by what you pass to send().

send() accepts three binary inputs in addition to strings:

// An ArrayBuffer — a fixed-length block of raw bytes.
const buffer = new ArrayBuffer(8);
ws.send(buffer);
// A TypedArray (or any ArrayBufferView) — a typed view over bytes.
const bytes = new Uint8Array([1, 2, 3, 4]);
ws.send(bytes);
// A Blob — file-like binary data, e.g. straight from an <input type="file">.
const blob = new Blob(['raw content']);
ws.send(blob);

All three are sent as a single binary frame. ArrayBuffer and TypedArray are the usual choices when you are building a binary message in code; Blob shines when the data already exists as a file or came from a media API.

Here is the one setting that matters for binary. When a binary frame arrives, what should event.data be — a Blob or an ArrayBuffer? You decide with ws.binaryType:

ws.binaryType = 'arraybuffer'; // event.data will be an ArrayBuffer
// or
ws.binaryType = 'blob'; // event.data will be a Blob (the default)

The default is 'blob'. Set it to 'arraybuffer' when you want to read the bytes immediately and synchronously — for example to wrap them in a DataView or Uint8Array and pull out fields. Reading a Blob is asynchronous, so for low-latency binary protocols 'arraybuffer' is usually the better fit.

ws.binaryType = 'arraybuffer';
ws.onmessage = (event) => {
if (typeof event.data === 'string') {
console.log('text frame:', event.data);
} else {
const view = new Uint8Array(event.data); // event.data is an ArrayBuffer
console.log('binary frame, first byte:', view[0]);
}
};

Note how the same onmessage handler must branch on the type of event.data: a string for text frames, your chosen binary type for binary frames.

send() returns immediately, but the bytes are not always on the wire yet — if you send faster than the network drains, they pile up in an internal buffer. The read-only ws.bufferedAmount tells you how many bytes are still queued:

if (ws.bufferedAmount < 1_000_000) {
ws.send(nextChunk); // only push more if the buffer is not backing up
}

This is your backpressure signal. When streaming large or frequent binary data, check bufferedAmount before sending more; if it keeps climbing, you are producing faster than the connection can carry, and blindly calling send() will bloat memory.

flowchart TD
  A["You call ws.send(payload)"] --> B{"What did you pass?"}
  B -- "string" --> T["Text frame (UTF-8)"]
  B -- "ArrayBuffer / TypedArray / Blob" --> N["Binary frame (raw bytes)"]
  N --> C{"ws.binaryType on the receiver"}
  C -- "'arraybuffer'" --> AB["event.data is an ArrayBuffer"]
  C -- "'blob'" --> BL["event.data is a Blob"]
Choosing a frame type when you send

The demo uses an in-page echo socket with the real WebSocket API, so it runs anywhere with no server. It sets binaryType to 'arraybuffer', sends a small Uint8Array, and reads the echoed bytes straight back out. In a real app only the first line — new WebSocket('wss://your-server') — would change.

JavaScript

The four bytes go out as a binary frame and come back as an ArrayBuffer — because we set binaryType = 'arraybuffer' — which we then read through a Uint8Array. Had we left binaryType at its 'blob' default, event.data would have been a Blob to read asynchronously instead.

Which value of ws.binaryType makes event.data an ArrayBuffer?
Which of these can you NOT pass to ws.send()?
What does ws.bufferedAmount tell you?