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.
Text versus binary frames
Section titled “Text versus binary frames”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().
Sending binary
Section titled “Sending binary”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.
Receiving binary: binaryType
Section titled “Receiving binary: binaryType”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// orws.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.
Watching bufferedAmount
Section titled “Watching bufferedAmount”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"] Echo an ArrayBuffer, live
Section titled “Echo an ArrayBuffer, live”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.
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.