Frames vs. Messages
A WebSocket message is a logical unit of data an application sends, but on the wire it is split into one or more frames. Each frame has a small header describing an opcode, whether it is the final fragment (the FIN bit), a masking flag, and a payload length, followed by the payload itself. A single short chat message might fit in one frame with FIN set to 1, while a large file transfer can be split across many frames, with the first frame carrying the real opcode (0x1 for text or 0x2 for binary) and subsequent continuation frames using opcode 0x0 until the final one sets FIN to 1.
Cricket analogy: It's like a partnership between two batters being described as a single stand of runs, even though it's actually built delivery by delivery, each ball its own discrete event, with the final wicket or declaration marking the stand as complete.
Text Frames and UTF-8 Validation
Opcode 0x1 marks a text frame, and the payload must be valid UTF-8 — this is not optional. If an endpoint receives a text frame (or a fragmented text message once reassembled) that isn't valid UTF-8, it is required by the specification to fail the connection, typically closing with code 1007 (invalid payload data). This strictness exists because text frames are meant for human-readable and JSON-style application data where character encoding correctness matters, so implementations like browsers' WebSocket API expose text frame payloads directly as JavaScript strings without giving the developer a chance to inspect raw bytes first.
Cricket analogy: It's like the DRS ball-tracking system rejecting any review where the tracking data is incomplete or corrupted rather than guessing — an invalid input simply cannot be processed as a valid decision.
const socket = new WebSocket('wss://example.com/stream');
socket.binaryType = 'arraybuffer';
socket.addEventListener('message', (event) => {
if (typeof event.data === 'string') {
// Text frame: opcode 0x1, guaranteed valid UTF-8
const payload = JSON.parse(event.data);
console.log('Text message:', payload);
} else {
// Binary frame: opcode 0x2, delivered as ArrayBuffer
const view = new DataView(event.data);
const messageType = view.getUint8(0);
console.log('Binary message, type byte:', messageType);
}
});
// Sending a binary frame
const buffer = new Uint8Array([0x01, 0xff, 0x10, 0x22]);
socket.send(buffer.buffer);Choosing Binary Frames
Opcode 0x2 marks a binary frame, whose payload is an opaque byte sequence with no encoding constraints — it can carry Protocol Buffers, MessagePack, compressed audio chunks, or any custom byte layout the application defines. Binary frames are the right choice whenever payload size matters, since encodings like Protobuf or MessagePack are typically far more compact than an equivalent JSON string, and whenever the data is inherently binary already, like image tiles or audio samples, where converting to base64 text would inflate the payload by roughly a third and add unnecessary encode/decode overhead on both ends.
Cricket analogy: It's like a broadcaster sending the raw high-definition camera feed to the production truck rather than a written play-by-play description — the raw feed is bulkier per second but preserves everything without the lossy overhead of converting motion into text.
The extension permessage-deflate can compress both text and binary frame payloads transparently at the protocol layer, negotiated during the handshake via the Sec-WebSocket-Extensions header — useful for repetitive JSON text frames, though it adds CPU overhead and is sometimes disabled for already-compressed binary payloads like images or video.
- A WebSocket message can be split across multiple frames; the FIN bit marks the final frame of a message.
- Opcode 0x1 marks a text frame; opcode 0x2 marks a binary frame; opcode 0x0 marks a continuation frame.
- Text frame payloads must be valid UTF-8, or the connection must be failed with close code 1007.
- Binary frames carry opaque bytes with no encoding requirement, ideal for Protobuf, MessagePack, images, or audio.
- Browsers expose text frames as JavaScript strings and binary frames as Blob or ArrayBuffer depending on binaryType.
- Binary encodings are typically more compact than JSON text, avoiding base64 inflation for inherently binary data.
- permessage-deflate can compress frame payloads at the protocol level when negotiated during the handshake.
Practice what you learned
1. What opcode identifies a continuation frame in a fragmented WebSocket message?
2. What must happen if a server receives a text frame whose payload is not valid UTF-8?
3. Why might an application prefer binary frames over text frames for high-frequency sensor data?
4. What does the FIN bit in a WebSocket frame header indicate?
5. Which extension, negotiated during the handshake, can transparently compress frame payloads?
Was this page helpful?
You May Also Like
Opening and Closing Connections
How a WebSocket connection is established through the HTTP upgrade handshake and gracefully terminated using the closing handshake.
Subprotocols Explained
How the Sec-WebSocket-Protocol header lets client and server agree on an application-level messaging convention over a raw WebSocket connection.
Ping/Pong and Heartbeats
How WebSocket ping and pong control frames keep connections alive and let both sides detect dead peers.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics