---
title: "MessagePack vs JSON: Smaller Payloads for Real-Time Apps"
description: MessagePack produces smaller payloads than JSON, with no Base64 overhead for binary data. We measured how much on real message shapes, and explain when binary serialization matters for real-time apps and how NoLag uses it.
excerpt: "JSON is human readable. MessagePack is wire efficient. We encoded the same messages in both to see how much bandwidth you actually save, and where the savings come from."
date: '2026-04-14'
readTime: 7 min read
category: Performance
---

JSON is everywhere. It is human readable, trivially debuggable, and natively understood by every language runtime that matters. For most HTTP APIs it is the obvious default, and the right one. But for real-time systems moving hundreds of thousands of small messages per second, the overhead of text serialization starts to show up in your bandwidth bill.

MessagePack is the alternative. It is a binary serialization format that encodes the same data structure as JSON but in a more compact binary representation. Same types, same structure, fewer bytes. Here is how they compare and when the difference actually matters.

## What MessagePack Actually Is

MessagePack stores data using a compact type-tagged binary layout. Each value is prefixed with a single byte that encodes its type and sometimes part of its value. Small integers, for example, are stored in a single byte. Short strings store the length in the same byte as the type tag. This is fundamentally different from JSON, which must represent every value as a sequence of ASCII characters regardless of the underlying data type.

The format supports the same logical types as JSON: null, boolean, integer, float, string, binary (bytes), array, and map (object). The key difference is that the representation is binary and therefore not human-readable. You need a decoder to inspect MessagePack data, which is a real tradeoff to acknowledge. Debugging raw bytes is harder than reading a JSON string in your terminal.

## Side-by-Side: The Same Message in Both Formats

Take a minimal chat message payload with a few fields:

```json [JSON]
{
  "id": "01906b2e-4f3c-7a1d-8e2b-9f3c4d5e6a7b",
  "room": "general",
  "text": "Hello, world!",
  "ts": 1745827200000,
  "uid": "u_8x2k9"
}
```

Encoded compactly (no whitespace, which is how every serializer sends it) and with the reference JavaScript MessagePack encoder, the two look like this:

```text [Encodings]
JSON encoding (compact):
{"id":"01906b2e-4f3c-7a1d-8e2b-9f3c4d5e6a7b","room":"general","text":"Hello, world!","ts":1745827200000,"uid":"u_8x2k9"}
Bytes: 120

MessagePack encoding (hex):
85 a2 69 64 d9 24 30 31 39 30 36 62 32 65 2d 34
66 33 63 2d 37 61 31 64 2d 38 65 32 62 2d 39 66
33 63 34 64 35 65 36 61 37 62 a4 72 6f 6f 6d a7
67 65 6e 65 72 61 6c a4 74 65 78 74 ad 48 65 6c
6c 6f 2c 20 77 6f 72 6c 64 21 a2 74 73 cf 00 00
01 96 7b 68 fc 00 a3 75 69 64 a7 75 5f 38 78 32
6b 39
Bytes: 98
```

That is an 18% reduction for one message. Read the hex and you can see why it is not more: the message is mostly string content (`85` opens a five-entry map, `d9 24` announces a 36-byte string, and then the 36 bytes of the UUID follow unchanged). Strings cost the same in both formats; what MessagePack removes is the quoting and punctuation around them, and the ASCII expansion of numbers. Scale even that to 10,000 messages per second across a high-traffic chat server, or 100 telemetry readings per second from a fleet of 5,000 devices, and the savings are not theoretical.

## Where the Savings Come From

The gains come from a few structural differences:

- **No quoting overhead.** JSON must quote every string key and string value. MessagePack encodes string length as a prefix byte and stores the string bytes directly. For a map with five keys, that eliminates 10 quotation characters before you count the values.
- **No key/value separator characters.** JSON uses colons and commas as structural delimiters. MessagePack uses a fixed-width map header that encodes the count of entries. No colons, no commas, no whitespace.
- **Compact integer encoding.** A 64-bit integer in JSON takes up to 20 ASCII characters. In MessagePack it takes at most 9 bytes (1 type byte + 8 data bytes), and as few as 1 byte for small positive integers. Floats are the exception: a double is 9 bytes in MessagePack, which is about what `-33.8688` costs as text, so float-heavy payloads save less than you might expect.
- **Native binary support.** This is the one that matters most for IoT and media. JSON has no binary type. To embed binary data in JSON you must Base64-encode it, which inflates the size by approximately 33%. MessagePack has a native binary type. You write raw bytes directly into the payload. No encoding overhead.

## Measured Reduction Numbers

Rather than quote an industry range, here is what we measured for four representative payloads, encoded with `JSON.stringify` and `@msgpack/msgpack` 3.1.2 in Node. These are NoLag's own measurements and the script below reproduces them.

| Payload | JSON (bytes) | MessagePack (bytes) | Reduction |
| --- | --- | --- | --- |
| Chat message (the example above) | 120 | 98 | 18% |
| GPS telemetry (10 fields, mixed ints and floats) | 151 | 121 | 20% |
| Sensor sample with a 256-byte binary blob | 396 | 295 | 26% |
| Presence event (two short strings, nested map) | 85 | 70 | 18% |

```typescript [measure.ts]
import { encode } from '@msgpack/msgpack'

const telemetry = {
  vehicleId: 'v_abc123', lat: -33.8688, lng: 151.2093, speed: 62, heading: 287,
  altitude: 41, satellites: 9, hdop: 0.8, battery: 87, ts: 1745827200000,
}

const json = new TextEncoder().encode(JSON.stringify(telemetry)).length
const msgpack = encode(telemetry).length
console.log({ json, msgpack, reduction: Math.round((1 - msgpack / json) * 100) + '%' })
```

The pattern is consistent: payloads made of short keys, small integers, and binary blobs see the largest reductions, and the figures you see quoted elsewhere in the 30 to 50% range come from payloads shaped like that. Payloads that are mostly long string values see smaller reductions because string content itself does not shrink. If your messages are chat text, expect the lower end; if they are sensor readings with integer fields and raw bytes, expect the upper end.

## When This Actually Matters

For a low-traffic app sending a few messages per second, the difference between JSON and MessagePack is noise. Choose JSON for the debuggability. The tradeoff changes in a few specific situations.

### High-Frequency IoT

A GPS tracker sending position updates 10 times per second is generating 600 messages per minute per device. At 10,000 devices that is 6 million messages per minute. A 20% payload reduction translates directly into server costs, egress fees, and mobile data usage for devices on cellular connections, and shaping the payload for the encoder (integers instead of floats where the precision allows, raw bytes instead of Base64) pushes it higher.

### Chat at Scale

A busy chat platform might see 50,000 messages per minute across all rooms. Each message is fanned out to multiple subscribers. The total bytes transferred is the message size multiplied by the number of recipients. At scale, a smaller message size has a multiplier effect on your infrastructure cost.

### Mobile Clients on Limited Data

Mobile apps on 3G or metered data plans benefit directly from smaller payloads. Do not count on a CPU win as well: encode and decode speed depends on the runtime, and V8's native JSON parser is heavily optimised, so a JavaScript MessagePack implementation is often no faster than `JSON.parse`. On mobile the win is bytes on the radio, which is also where the battery goes.

## The Debuggability Tradeoff

The biggest practical downside of MessagePack is that you cannot read a raw message in a network inspector without a decoder. This is a real cost during development. The mitigation is to build a small decoder step into your debugging tooling, or to keep a JSON path open for development. Most serious real-time systems end up doing this anyway because their debugging tooling ends up outliving any specific wire format decision.

## How NoLag Uses MessagePack

The NoLag SDKs encode every frame they send as MessagePack: every message published to a topic, every presence update, every acknowledgment travels as a MessagePack-encoded binary WebSocket frame, and the broker decodes it on arrival. The broker also accepts JSON text frames carrying the same message shapes, so a client on a platform without a MessagePack library, or a developer poking at the protocol with a plain WebSocket tool, can still talk to it.

The SDK handles encoding and decoding transparently. You pass a plain JavaScript object to `room.emit()` and receive a plain JavaScript object in your `room.on()` handler. The binary serialization is invisible to application code.

```typescript [TypeScript]
import { NoLag } from '@nolag/js-sdk'

const client = NoLag(TOKEN)
await client.connect()
const room = client.setApp(APP_SLUG).setRoom('fleet-zone')

room.subscribe('positions')
room.on('positions', (position, meta) => {
  // `position` is already a decoded object; `meta.timestamp` came with the frame
  console.log(position, meta.timestamp)
})

// Encoded as MessagePack on the way out
room.emit('positions', { vehicleId: 'v_abc123', lat: -33.8688, lng: 151.2093, ts: Date.now() })
```

The combination of WebSocket framing (which has a 2 to 14 byte overhead per message against hundreds of bytes for HTTP headers) and MessagePack encoding gives NoLag a per-message overhead that is close to the minimum for a framed binary transport. For IoT workloads and high-frequency dashboards migrating from JSON-over-HTTP polling, the bytes saved per message are the smaller part of the story; dropping the per-request headers and the polling itself is the larger one.
