Learn WebSocket - Message Serialization & Data Formats
Episode 10 of 34

Learn WebSocket - Message Serialization & Data Formats

This episode covers WebSocket message formats: text and JSON, binary ArrayBuffer and Blob, structured formats such as MessagePack and protobuf, compression, and the envelope pattern for messages that stay stable through changes.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

So far you have been sending messages over WebSocket as text. But the WebSocket protocol does not care about message content — what is sent can be text, binary, or a mix of both. The decision of how to package the data actually determines the speed, size, and resilience of your application.

Episode 10 covers serialization: turning objects into bytes that can be sent, and data formats: the structure used to wrap messages. From simple JSON to compact protobuf, you will know when to choose which.

Text Messages and JSON

JSON as the Base Format

The most common format for WebSocket applications is JSON. Simple, human-readable, and supported by every language.

JSEncoding and decoding JSON
const pesan = {
  type: "chat",
  room: "nodejs",
  teks: "Halo semuanya!",
  waktu: Date.now(),
};
 
ws.send(JSON.stringify(pesan));
 
ws.on("message", (data) => {
  const terima = JSON.parse(data.toString());
  console.log(terima.type, terima.teks);
});

JSON.stringify(pesan) turns the object into a string before sending, and JSON.parse(...) turns it back into an object on the receiving side. It must be wrapped in a try-catch because parsing can fail when receiving corrupted data.

Handling Corrupted JSON

Data from the network cannot be trusted. One wrong character can crash the whole application.

JSSafe parsing with try-catch
ws.on("message", (data) => {
  try {
    const pesan = JSON.parse(data.toString());
    prosesPesan(pesan);
  } catch {
    ws.send(JSON.stringify({ error: "format pesan tidak valid" }));
  }
});

The try-catch pattern above prevents one corrupted message from killing the server process. Send an error response so the client can correct itself, then continue processing the next messages.

Binary Messages

ArrayBuffer and Blob

For data such as images, audio, or files, binary is far more efficient than text. The browser offers two representations: ArrayBuffer and Blob.

JSReceiving and sending binary
ws.binaryType = "arraybuffer";
 
ws.onmessage = (event) => {
  if (typeof event.data === "string") {
    console.log("pesan teks:", event.data);
  } else {
    const buffer = new Uint8Array(event.data);
    console.log("pesan binary:", buffer.length, "byte");
  }
};
 
const bytes = new Uint8Array([1, 2, 3, 4]);
ws.send(bytes.buffer);

ws.binaryType = "arraybuffer" sets how the browser presents incoming binary data. Sending binary uses ws.send(bytes.buffer) — an ArrayBuffer or Blob is sent directly without serialization.

When to Use Binary

Binary saves bandwidth and is processed faster, but it cannot be read by humans. The rule of thumb: use JSON for metadata and small messages, binary for large data or data already structured as bytes.

Structured Data Formats

MessagePack and Protobuf

If your application needs minimal payload size and high speed, there are structured binary formats.

Install a serialization library
bun add @msgpack/msgpack

MessagePack turns objects into binary that is much smaller than JSON, while Protocol Buffers (protobuf) requires a .proto schema and produces the most compact encoding. For small applications this difference is rarely noticeable; for millions of messages a day, it matters a lot.

Per-Message Compression

The permessage-deflate extension compresses messages when sent and decompresses them when received, transparently for you as a developer.

JSEnabling compression on the ws server
const { WebSocketServer } = require("ws");
 
const wss = new WebSocketServer({
  port: 8080,
  perMessageDeflate: true,
});

perMessageDeflate: true enables compression on the ws server. Note the trade-off: compression adds CPU latency and memory, so for small non-repeating messages, disabling it is actually faster.

The Envelope Pattern

A Uniform Message Structure

Every message is wrapped in a fixed structure carrying the type and payload. This is called an envelope.

JSEnvelope structure
const envelope = {
  v: 2,
  type: "message:send",
  id: "msg_7f3a",
  data: {
    to: "user:12",
    teks: "Halo!",
  },
};

The type field determines how the receiver processes the payload, id for tracking and deduplication, and v for the protocol version. With an envelope, you can add new events without breaking old clients.

Message Versioning

As the protocol evolves, use a version field to migrate clients gradually.

JSHandling different versions
function prosesEnvelope(envelope) {
  if (envelope.v !== 2) {
    return { error: "versi protokol tidak didukung" };
  }
  return prosesBerdasarkanTipe(envelope);
}

Checking the version at the start lets the server reject old clients with a clear message, or run a transformation for backward compatibility.

Closing

Episode 10 made you realize that WebSocket message content is a design decision, not something determined by the protocol. JSON for convenience, binary for efficiency, structured formats for large scale, and an envelope for protocol evolution.

Key takeaways:

  • JSON is the default format, easiest to use and debug.
  • JSON parsing is always wrapped in try-catch because network data cannot be trusted.
  • Binary ArrayBuffer and Blob are efficient for files and large data.
  • MessagePack and protobuf save bandwidth at scale.
  • An envelope with type, id, and version fields keeps the protocol maintainable.

In the next episode we cover presence & heartbeat mechanisms: ping-pong frames, dead connection detection, user online and offline status, and heartbeat interval settings in Socket.IO.

Learn WebSocket - Message Serialization & Data Formats | Learn WebSocket