Skip to content

WebSocket Frame Format — Understanding Opcodes, Masking, and Payload Length

DodaTech Updated 2026-06-28 4 min read

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

WebSocket frames are binary structures with a 2-byte minimum header containing FIN, opcode, mask, and payload length fields, followed by optional masking key and payload data.

What You'll Learn

  • The structure of a WebSocket frame header
  • How payload length is encoded (7-bit, 16-bit, 64-bit)
  • How masking works in client-to-server frames

Why It Matters

To implement custom WebSocket clients, build debugging tools, or understand wire-level protocol behavior, you must understand the frame format. Many performance and compatibility issues trace back to incorrect frame construction.

def create_websocket_frame(payload, opcode=1, masked=False):
    """Create a WebSocket frame from payload data"""
    frame = bytearray()

    # FIN + opcode
    fin_and_opcode = 0x80 | (opcode & 0x0F)
    frame.append(fin_and_opcode)

    # Mask + payload length
    payload_len = len(payload)
    if masked:
        mask_bit = 0x80
    else:
        mask_bit = 0x00

    if payload_len < 126:
        frame.append(mask_bit | payload_len)
    elif payload_len < 65536:
        frame.append(mask_bit | 126)
        frame.extend(payload_len.to_bytes(2, 'big'))
    else:
        frame.append(mask_bit | 127)
        frame.extend(payload_len.to_bytes(8, 'big'))

    # Masking key (if masked)
    if masked:
        import random
        masking_key = bytes(random.randint(0, 255) for _ in range(4))
        frame.extend(masking_key)
        payload = bytes(payload[i] ^ masking_key[i % 4] for i in range(len(payload)))

    frame.extend(payload)
    return bytes(frame)

Frame Parser

def parse_frame(data):
    """Parse a WebSocket frame and return its components"""
    if len(data) < 2:
        raise ValueError("Frame too short")

    first_byte = data[0]
    second_byte = data[1]

    frame = {
        'fin': (first_byte >> 7) & 1,
        'rsv1': (first_byte >> 6) & 1,
        'rsv2': (first_byte >> 5) & 1,
        'rsv3': (first_byte >> 4) & 1,
        'opcode': first_byte & 0x0F,
        'masked': (second_byte >> 7) & 1,
        'payload_length': second_byte & 0x7F,
    }

    offset = 2

    if frame['payload_length'] == 126:
        frame['payload_length'] = int.from_bytes(data[2:4], 'big')
        offset = 4
    elif frame['payload_length'] == 127:
        frame['payload_length'] = int.from_bytes(data[2:10], 'big')
        offset = 10

    if frame['masked']:
        frame['masking_key'] = data[offset:offset + 4]
        offset += 4

    raw_payload = data[offset:offset + frame['payload_length']]

    if frame['masked']:
        frame['payload'] = bytes(
            raw_payload[i] ^ frame['masking_key'][i % 4]
            for i in range(len(raw_payload))
        )
    else:
        frame['payload'] = raw_payload

    return frame

Opcodes

OPCODES = {
    0x0: 'continuation',
    0x1: 'text',
    0x2: 'binary',
    0x8: 'close',
    0x9: 'ping',
    0xA: 'pong'
}

def handle_frame(frame):
    """Handle a WebSocket frame based on its opcode"""
    opcode_name = OPCODES.get(frame['opcode'], f'unknown ({frame["opcode"]})')

    if frame['opcode'] == 0x1:  # Text
        text = frame['payload'].decode('utf-8')
        print(f"Text message: {text}")

    elif frame['opcode'] == 0x2:  # Binary
        print(f"Binary message: {len(frame['payload'])} bytes")

    elif frame['opcode'] == 0x8:  # Close
        if len(frame['payload']) >= 2:
            close_code = int.from_bytes(frame['payload'][:2], 'big')
            reason = frame['payload'][2:].decode('utf-8', errors='replace')
            print(f"Close: code={close_code}, reason='{reason}'")
        else:
            print("Close: no code")

    elif frame['opcode'] == 0x9:  # Ping
        print(f"Ping: {len(frame['payload'])} bytes")

    elif frame['opcode'] == 0xA:  # Pong
        print(f"Pong: {len(frame['payload'])} bytes")

Common Mistakes

1. Forgetting to Mask Client Frames

Client frames must have the mask bit set and a 4-byte masking key. Unmasked client frames are rejected by the server.

2. Masking Server Frames

Server frames must NOT be masked. Masked server frames confuse clients.

3. Incorrect Payload Length Encoding

For payloads over 125 bytes, use 2-byte (126) or 8-byte (127) extended length. Using the wrong encoding breaks frame Parsing.

4. Not Setting the FIN Bit

Without FIN=1, the receiver waits for more fragments that never arrive. Always set FIN=1 for complete messages.

5. Sending Invalid UTF-8 in Text Frames

Text frames (opcode 1) must contain valid UTF-8. Invalid UTF-8 causes the receiver to close the connection.

Practice Questions

  1. What field indicates the last frame of a message?
  2. How is payload length encoded for messages over 125 bytes?
  3. What is the masking key used for?
  4. What opcode indicates a binary frame?
  5. How many bytes does the basic frame header use?

Answers

  1. FIN bit (1 = last frame). 2. 2 bytes for messages 126-65535, 8 bytes for larger. 3. XOR mask to prevent cache poisoning. 4. 0x2. 5. 2 bytes minimum.

Challenge

Build a WebSocket frame inspector that: accepts raw hex frame data, parses and displays all frame fields (FIN, opcode, mask, payload length, masking key, payload), validates the frame structure, and identifies errors.

FAQ

What is a WebSocket frame?

The basic unit of WebSocket communication, consisting of a header and optional payload.

How big can a single WebSocket frame be?

Up to 2^63 bytes (extremely large), but most implementations have practical limits.

What is the FIN bit?

A bit indicating whether this is the final frame of a message (1) or more fragments follow (0).

What opcode is used for close frames?

0x8 (close).

How is masking applied to the payload?

XOR each payload byte with the corresponding masking key byte (key[payload_index % 4]).

Mini Project

Build a WebSocket frame construction and parsing library that: creates frames for all opcodes with correct FIN, mask, and length encoding, parses frames and returns structured frame objects, validates frame correctness, and includes test cases for edge cases (empty payload, max length, fragmented messages).

What's Next

  • Learn about WebSocket close codes and their meanings
  • Explore WebSocket extensions like permessage-deflate
  • Continue to WebSocket implementation with Express and ws library

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro