JSON vs CBOR for IoT Payloads: Which Encoding When Every Byte Counts

Comparing JSON and CBOR encoded payloads on a memory-constrained IoT microcontroller

A quotation mark is 1 byte. A colon is 1 byte. The curly braces wrapping your JSON object? 2 more bytes. Harmless on a web server. But put 10,000 BLE sensors in a warehouse, each transmitting every 60 seconds over a metered cellular backhaul, and those “harmless” bytes become tens of gigabytes per month of pure structural overhead. You’re paying real money to transmit punctuation.

On constrained networks like BLE (244-byte ATT MTU), LoRaWAN (51 to 222 bytes depending on data rate), or NB-IoT (billed per byte), your encoding format isn’t a style preference. It’s an architectural decision that directly affects battery life, airtime costs, and whether your payload fits in a single packet.

This article gives you a concrete framework for choosing between JSON and CBOR, with brief nods to Protobuf and MessagePack. You’ll see real byte counts, code snippets for both device and cloud, and a decision checklist you can bring to your next design review.

JSON in IoT: The Comfortable Default

Every team reaches for JSON first, and for good reason. Your backend engineers already know it, every language has a parser, you can read it with your eyes, and there’s zero onboarding cost.

Here’s a typical IoT sensor reading in JSON:

{"t":22.5,"m":47,"b":3280,"id":"A1B2C3D4"}

That’s 42 bytes. Compact by JSON standards, since we already shortened the keys. But look at where the bytes go: 8 bytes on key strings, 6 bytes on quotation marks, 4 bytes on structural characters ({}:,), and the numeric values 22.5, 47, and 3280 are encoded as ASCII strings (4, 2, and 4 bytes respectively) instead of their native binary representations (2, 1, and 2 bytes).

Key names repeat identically in every single message, numbers are string-encoded (the integer 3280 takes 4 bytes as ASCII but only 2 as a uint16), and there’s no native binary or byte-string type.

JSON is fine when bandwidth is cheap: Wi-Fi-connected devices, prototyping, cloud-to-cloud APIs. If you’re building an MVP on ESP32 over Wi-Fi, don’t overthink it. Ship JSON and move on. But once you hit a constrained link, you need something leaner.

CBOR: The Binary Twin of JSON

CBOR (Concise Binary Object Representation, defined in RFC 8949) maps 1:1 to JSON’s data model but encodes everything in binary. Anything you can represent in JSON, you can represent in CBOR. But CBOR also gives you byte strings, semantic tags (like datetime or bignum annotations), and indefinite-length items for streaming.

Why does this matter for IoT? CBOR was designed for constrained environments. It’s the native encoding for CoAP (RFC 7252), the lightweight alternative to HTTP for sensor networks. Security comes via COSE, and there’s a formal schema language called CDDL (RFC 8610) built specifically for describing constrained data structures.

Here’s our same sensor payload in CBOR, using integer keys instead of strings:

import cbor2

payload = {1: 22.5, 2: 47, 3: 3280, 4: b'\xA1\xB2\xC3\xD4'}
encoded = cbor2.dumps(payload)
print(encoded.hex())  # a401f94da00218 2f03190cd00444a1b2c3d4
print(len(encoded))   # 19 bytes

19 bytes. Down from 42. A 55% reduction.

What changed? Keys became single-byte integers instead of multi-byte strings. The float 22.5 is stored as a 2-byte IEEE 754 half-precision value. The integer 47 fits in a single byte. The device ID is a raw 4-byte string instead of 8 hex ASCII characters in quotes. And all the structural delimiters (braces, colons, commas, quotes) are gone, replaced by type-length prefix bytes that encode both the data type and the value length in one shot.

JSON payload:   {"t":22.5,"m":47,"b":3280,"id":"A1B2C3D4"}
                |------------- 42 bytes -------------|

CBOR payload:   A4 01 F9 4DA0 02 18 2F 03 19 0CD0 04 44 A1B2C3D4
                |------------- 19 bytes ----------|

Savings:        55% smaller

Head-to-Head: CBOR vs JSON for IoT Payloads

+---------------------+--------------------+---------------------+
| Criterion           | JSON               | CBOR                |
+---------------------+--------------------+---------------------+
| Wire size (typical) | Baseline (1x)      | 0.4–0.6x           |
| Human readability   | Excellent          | Requires tooling    |
| Parse speed (MCU)   | Moderate           | Fast (no escaping)  |
| RAM during parse    | Higher             | Lower (streaming)   |
| Schema support      | JSON Schema        | CDDL (RFC 8610)    |
| CoAP native         | No                 | Yes                 |
| BLE MTU friendly    | Rarely             | Usually             |
| Library maturity    | Universal          | Good, growing       |
| Debug tooling       | Every text editor  | cbor.me, cbor-diag  |
+---------------------+--------------------+---------------------+

The table doesn’t say “CBOR always wins.” It says: which constraints dominate your project? If your team needs to eyeball payloads in a serial console during bring-up, JSON’s readability has real value. If you’re trying to squeeze telemetry into a 51-byte LoRaWAN packet at SF12, CBOR’s size advantage is the whole ballgame.

Parse speed deserves a callout. JSON parsers on MCUs have to handle string escaping, UTF-8 validation, and number-to-float conversion from ASCII. CBOR parsers just read typed binary values directly. On a Cortex-M0 with no FPU, that difference is measurable.

Practical Encoding Walkthrough

Scenario: A BLE soil-moisture sensor sends temperature (float), moisture (uint8), battery voltage in millivolts (uint16), and a 4-byte device ID every 60 seconds.

On the device (C-like pseudocode)

Using a lightweight CBOR encoder (think tinycbor or nanocbor, both under 2KB flash):

uint8_t buf[64];
CborEncoder enc, map;

cbor_encoder_init(&enc, buf, sizeof(buf), 0);
cbor_encoder_create_map(&enc, &map, 4);

cbor_encode_uint(&map, 1);          // key: temperature
cbor_encode_half_float(&map, 22.5); // value: 2 bytes

cbor_encode_uint(&map, 2);          // key: moisture
cbor_encode_uint(&map, 47);         // value: 1 byte

cbor_encode_uint(&map, 3);          // key: battery_mv
cbor_encode_uint(&map, 3280);       // value: 2 bytes

cbor_encode_uint(&map, 4);          // key: device_id
cbor_encode_byte_string(&map, dev_id, 4); // value: 4 bytes

cbor_encoder_close_container(&enc, &map);
size_t len = cbor_encoder_get_buffer_size(&enc, buf);
// len == 19. Fits in any BLE MTU with room to spare.

If you’re working with the Hubble SDK on a BLE device, the custom payload guide shows how to structure these bytes for transmission through the Hubble network.

The key trick: replace string keys with integer keys and maintain a shared registry. Your device firmware and your cloud decoder both agree that 1 means temperature, 2 means moisture, and so on. Document this mapping once, version it, and you’ve stripped 40%+ of your overhead with minimal complexity.

On the cloud (Python)

import cbor2

KEY_MAP = {1: "temperature", 2: "moisture", 3: "battery_mv", 4: "device_id"}

raw = bytes.fromhex("a401f94da002182f03190cd00444a1b2c3d4")
decoded = cbor2.loads(raw)
# {1: 22.5, 2: 47, 3: 3280, 4: b'\xa1\xb2\xc3\xd4'}

reading = {KEY_MAP[k]: v for k, v in decoded.items()}
# {'temperature': 22.5, 'moisture': 47, 'battery_mv': 3280, 'device_id': b'\xa1\xb2\xc3\xd4'}

CBOR also has a diagnostic notation that bridges the gap for engineers used to JSON. Our payload looks like this in diagnostic form:

{1: 22.5, 2: 47, 3: 3280, 4: h'A1B2C3D4'}

Readable, compact, unambiguous. Tools like cbor-diag and the online playground at cbor.me let you paste hex and get this output instantly.

When to Pick Something Else

Protocol Buffers (Protobuf): Best when you want strict schema enforcement with code generation across multiple languages. The .proto files act as a contract between teams. The downside for deeply constrained devices: generated code can bloat flash, and you lose self-description (you can’t decode a Protobuf message without the schema). Strong fit for gateway-to-cloud or microservice-to-microservice. Less ideal on an 8-bit MCU with 32KB flash.

MessagePack: Conceptually similar to CBOR, with comparable size. But it lacks IETF standardization, has no CDDL equivalent, and isn’t native to CoAP. If your team already uses it outside a CoAP ecosystem, keep going. If you’re starting fresh, CBOR has stronger institutional backing.

Raw binary structs: Pack your values into a C struct, send the bytes. Smallest possible encoding (probably 9 bytes for our example). But there’s no self-description, no schema evolution without breaking old devices, and debugging is painful. Sometimes the right call for ultra-constrained links like LoRaWAN class A at SF12 where you have 51 bytes total. Just know the maintenance cost.

The Hybrid Architecture That Actually Ships

In production, most teams don’t pick one format everywhere. The pattern that works:

[Sensor MCU]--CBOR/BLE-->[Gateway]--JSON/MQTT-->[Cloud Broker]--JSON-->[API]
     ^                       ^                        ^
  tinycbor              cbor→json                  standard
  ~2KB flash            transcode                  tooling

Devices encode CBOR because bytes and battery matter on the constrained link. The gateway (a Pi, a phone, or an edge box) transcodes to JSON because your cloud pipeline, your Grafana dashboards, and your ops team all expect it. If you’re running CoAP end-to-end, you can keep CBOR all the way through, but most teams find that putting the JSON boundary at the gateway keeps debugging simple.

To pipe data from devices through a gateway and into your backend, the cloud integration guide for retrieving data walks through webhook setup and packet retrieval, regardless of what encoding you use on the wire.

Picking Your Encoding

Use CBOR when:
  [x] Network is BLE, LoRa, NB-IoT, or metered
  [x] Payloads repeat frequently (telemetry)
  [x] You use CoAP or COSE
  [x] MCU RAM < 64 KB

Stick with JSON when:
  [x] Wi-Fi / Ethernet with ample bandwidth
  [x] Rapid prototyping / early MVP
  [x] Team has no CBOR experience and deadline is tight
  [x] Human readability is a hard requirement

Consider Protobuf when:
  [x] Schema enforcement across many teams/services
  [x] Code generation is a feature, not a burden
  [x] You're gateway-to-cloud, not MCU-to-air

Consider raw structs when:
  [x] Every byte literally matters (LoRaWAN SF12)
  [x] Schema will never change (or you accept the pain)
  [x] You control both endpoints completely

For most IoT telemetry projects on constrained links, CBOR with integer keys is the sweet spot. You get 40 to 60% size reduction over JSON, self-describing payloads, streaming parsers that run in under 2KB of flash, and a standards-track format with long-term IETF support. Start with the Hubble device SDK if you’re building on BLE, pack your telemetry in CBOR, and let the gateway handle the rest.


Hubble Network connects your BLE devices directly to satellite — no gateways, no terrestrial infrastructure. See how it works →