Skip to content

WebSocket Frames — Complete Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about WebSocket Frames. We cover key concepts, practical examples, and best practices to help you master this topic.

WebSocket frames are the basic unit of data transmission. Each frame contains control information and payload data. Understanding frames is essential for implementing WebSocket protocol handling and debugging low-level issues.

What You'll Learn

  • WebSocket frame structure
  • Opcodes and their meanings
  • Masking for client-to-server frames
  • Fragmentation of large messages
  • Control frames (ping, pong, close)

Why It Matters

Frame-level understanding helps debug protocol issues, optimize message sizes, and implement custom WebSocket handlers when libraries are insufficient.

Real-World Use

A real-time gaming platform optimizes WebSocket frame sizes by using binary frames with protocol buffers instead of text JSON frames, reducing per-message overhead from 200 bytes to 20 bytes.

Flow Chart

flowchart LR
    A[Frame] --> B[Header]
    A --> C[Payload]
    B --> D[FIN: Final Frame?]
    B --> E[RSV: Reserved Bits]
    B --> F[Opcode: Type]
    B --> G[Mask: Client?]
    B --> H[Length]
    H --> I[7-bit / 16-bit / 64-bit]
    F --> J[Text / Binary / Close / Ping / Pong]

Code Examples

Example 1: Parsing WebSocket Frames in Node.js

function parseFrame(buffer) {
  const firstByte = buffer[0];
  const fin = (firstByte & 0x80) !== 0;
  const opcode = firstByte & 0x0F;

  const secondByte = buffer[1];
  const masked = (secondByte & 0x80) !== 0;
  let payloadLength = secondByte & 0x7F;

  let offset = 2;
  if (payloadLength === 126) {
    payloadLength = buffer.readUInt16BE(offset);
    offset += 2;
  } else if (payloadLength === 127) {
    payloadLength = buffer.readBigUInt64BE(offset);
    offset += 8;
  }

  let maskKey = null;
  if (masked) {
    maskKey = buffer.slice(offset, offset + 4);
    offset += 4;
  }

  let payload = buffer.slice(offset, offset + payloadLength);
  if (masked) {
    for (let i = 0; i < payload.length; i++) {
      payload[i] ^= maskKey[i % 4];
    }
  }

  return {
    fin,
    opcode,
    masked,
    payloadLength: Number(payloadLength),
    payload,
    isControl: opcode >= 0x08,
  };
}

// Usage
const buffer = Buffer.from([0x81, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F]);
const frame = parseFrame(buffer);
console.log('Unmasked payload:', frame.payload.toString());
// Output: Unmasked payload: Hello

Expected output: Parsed frame showing text opcode, unmasked payload containing "Hello".

Example 2: Building WebSocket Frames

function buildFrame(data, opcode = 0x01) {
  const payload = Buffer.from(data);
  const length = payload.length;

  let header;
  if (length < 126) {
    header = Buffer.alloc(2);
    header[1] = length;
  } else if (length < 65536) {
    header = Buffer.alloc(4);
    header[1] = 126;
    header.writeUInt16BE(length, 2);
  } else {
    header = Buffer.alloc(10);
    header[1] = 127;
    header.writeBigUInt64BE(BigInt(length), 2);
  }

  header[0] = 0x80 | opcode; // FIN + opcode

  return Buffer.concat([header, payload]);
}

const frame = buildFrame('Hello Server');
console.log('Frame bytes:', frame.toString('hex'));
// Output: Frame bytes: 810548656c6c6f20536572766572

Expected output: A properly constructed WebSocket frame with FIN bit set and text opcode.

Example 3: Handling Control Frames

const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 8080 });

server.on('connection', (ws) => {
  console.log('Client connected');

  // Send ping every 30 seconds
  const pingInterval = setInterval(() => {
    ws.ping();
  }, 30000);

  ws.on('pong', () => {
    console.log('Received pong from client');
  });

  ws.on('close', (code, reason) => {
    clearInterval(pingInterval);
    console.log(`Client disconnected: ${code} - ${reason}`);
  });
});

// Client with ping/pong handling
const client = new WebSocket('ws://localhost:8080');

client.on('open', () => {
  console.log('Connected');
});

client.on('ping', () => {
  console.log('Received ping, sending pong');
  client.pong();
});

Expected output: Server sends periodic pings, client responds with pongs, ensuring the connection remains alive.

Common Mistakes

Mistake Explanation
Forgetting to mask client frames All client-to-server frames must be masked per the WebSocket specification
Ignoring fragmentation Large messages are fragmented; always reassemble fragments using the FIN bit
Confusing opcodes Use correct opcode: 0x1 for text, 0x2 for binary, 0x8 for close, 0x9 for ping, 0xA for pong
Not handling control frames in fragments Control frames can appear between fragments; Process them without breaking reassembly
Misreading extended length Payload length encoding varies: 7-bit, 16-bit, or 64-bit depending on the first length byte

Practice Questions

  1. What is the structure of a WebSocket frame?
  2. Why are client-to-server frames masked?
  3. How does WebSocket handle messages larger than the maximum frame size?
  4. What opcodes are used for control frames?
  5. How do you send binary data vs text data over WebSocket?

Challenge

Implement a WebSocket frame analyzer that captures raw frames, decodes each field, and displays the frame structure in a human-readable format. Include support for fragmented messages and control frame handling.

FAQ

What is the maximum WebSocket frame size?

The frame length field supports up to 2^63 bytes, but practical limits are much lower. Most libraries default to 256KB or 1MB max frame size.

Can I send frames without the FIN bit?

Yes, set FIN to 0 to indicate a fragment. The final fragment must have FIN set to 1. Control frames must not be fragmented.

What happens if I receive an unknown opcode?

The receiver should close the connection with a protocol error. Unknown opcodes indicate a protocol violation.

How is masking key generated?

The client generates a random 32-bit masking key for each frame. The key is XORed with the payload data.

Can I mix text and binary frames in one connection?

Yes, but you cannot mix opcodes within a fragmented message. Each fragment of a message must use the same opcode.

What are RSV bits used for?

RSV bits are reserved for extensions. If the server does not negotiate an extension that uses RSV bits, it must reject frames with non-zero RSV.

Mini Project

Build a WebSocket frame visualizer that captures WebSocket traffic, decodes each frame, and displays a real-time timeline showing frame types, sizes, and payload contents. Include the ability to reconstruct fragmented messages.

What's Next

Learn the difference between ws:// and wss://

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro