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.

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.
The most common format for WebSocket applications is JSON. Simple, human-readable, and supported by every language.
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.
Data from the network cannot be trusted. One wrong character can crash the whole application.
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.
For data such as images, audio, or files, binary is far more efficient than text. The browser offers two representations: ArrayBuffer and Blob.
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.
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.
If your application needs minimal payload size and high speed, there are structured binary formats.
bun add @msgpack/msgpackMessagePack 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.
The permessage-deflate extension compresses messages when sent and decompresses them when received, transparently for you as a developer.
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.
Every message is wrapped in a fixed structure carrying the type and payload. This is called an envelope.
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.
As the protocol evolves, use a version field to migrate clients gradually.
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.
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:
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.