DEV Community

Muhammed Emin Subaşı
Muhammed Emin Subaşı

Posted on Fully Autonomous

How Big Is Your JSON, Really? Measuring Bytes, Minified and Gzip Size

.length is not your payload size

Most of us check JSON size with JSON.stringify(data).length. That number is a character count, not bytes. It also tells you nothing about what actually travels over the network.

To see how far off it can be, I generated an API response with 1,000 user records. Each record looks like this:

{"id":"usr_ebcb82034fba","name":"Inés Schmidt","email":"ines.541e@example.com","city":"Berlin","isActive":true,"roles":["viewer"],"lastLoginAt":"2026-09-15T01:24:53.584Z","preferences":{"theme":"system","language":"tr","notifications":{"email":true,"push":true}},"bio":null}
Enter fullscreen mode Exit fullscreen mode

Then I measured the same payload five ways in Node.js 22:

Measurement Size
JSON.stringify(payload).length 285,466 characters
Minified, real UTF-8 bytes 287,743 bytes
Pretty-printed with 2 spaces 457,027 bytes
Minified + gzip 36,639 bytes
Minified + Brotli 26,918 bytes

Three things stand out. The character count undercounts the bytes. Pretty-printing adds 59% on disk. And after gzip, the pretty version is only 8% larger than the minified one (39,621 vs 36,639 bytes), because compression eats most of the whitespace.

Let's go through how to measure each of these properly.

Characters vs bytes in JavaScript

JavaScript strings are UTF-16, so .length counts UTF-16 code units. JSON on the wire is UTF-8, where anything outside ASCII takes 2 to 4 bytes:

"ş".length;   // 1, but 2 bytes in UTF-8
"🚀".length;  // 2, but 4 bytes in UTF-8
Enter fullscreen mode Exit fullscreen mode

In my sample, names like "Inés", "Çelik" and "İstanbul" plus a few emoji added 2,277 bytes that .length never showed. For English-only data the gap is zero; for user-generated content in other languages it grows quickly.

To get the real byte size:

// Browser and Node
const bytes = new TextEncoder().encode(JSON.stringify(data)).length;

// Browser alternative
const bytes2 = new Blob([JSON.stringify(data)]).size;

// Node only
const bytes3 = Buffer.byteLength(JSON.stringify(data), "utf8");
Enter fullscreen mode Exit fullscreen mode

All three return 287,743 for my payload.

One more trap: "287 KB" is ambiguous. 287,743 bytes is 287.7 KB if a kilobyte is 1,000 bytes, or 281.0 KiB if it is 1,024. Browser DevTools, cloud consoles and size limits don't all use the same unit, so check which one a limit means before you compare.

Python adds two hidden size bumps

Python's json.dumps() makes the output bigger than you might expect, for two reasons.

1. ensure_ascii=True by default. Every non-ASCII character is escaped as \uXXXX, and emoji become two escapes:

json.dumps({"city": "İstanbul", "bio": "🚀"})
# {"city": "\u0130stanbul", "bio": "\ud83d\ude80"}   -> 48 bytes

json.dumps({"city": "İstanbul", "bio": "🚀"}, ensure_ascii=False)
# {"city": "İstanbul", "bio": "🚀"}                  -> 36 bytes
Enter fullscreen mode Exit fullscreen mode

2. The default separators include spaces. Without indent, Python uses ", " and ": ", so even "compact" output has a space after every comma and colon.

On the same 1,000-user payload:

Call UTF-8 bytes
json.dumps(p) 322,382
json.dumps(p, separators=(",", ":")) 296,236
json.dumps(p, separators=(",", ":"), ensure_ascii=False) 287,743

The default call is 12% larger than truly minified JSON. To measure and send the smallest version:

import json

body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
size_in_bytes = len(body.encode("utf-8"))
Enter fullscreen mode Exit fullscreen mode

If you use ensure_ascii=False, make sure the response is sent with Content-Type: application/json; charset=utf-8 and encoded as UTF-8. Most frameworks already do this.

Measure what the network actually sends

If your server or CDN compresses responses (check for a Content-Encoding: gzip or br response header), the bytes on the wire are the compressed size. In my sample that is 36,639 bytes with gzip, about 13% of the raw payload.

Measure it in the browser with the built-in CompressionStream:

async function gzipSize(value) {
  const stream = new Blob([JSON.stringify(value)])
    .stream()
    .pipeThrough(new CompressionStream("gzip"));
  const buffer = await new Response(stream).arrayBuffer();
  return buffer.byteLength;
}
Enter fullscreen mode Exit fullscreen mode

In Node.js:

const zlib = require("node:zlib");
const gzipBytes = zlib.gzipSync(JSON.stringify(data)).length;
const brotliBytes = zlib.brotliCompressSync(JSON.stringify(data)).length;
Enter fullscreen mode Exit fullscreen mode

In Python:

import gzip
gzip_bytes = len(gzip.compress(body.encode("utf-8")))
Enter fullscreen mode Exit fullscreen mode

Expect small differences between tools. Python's gzip.compress uses compression level 9 by default and gave 34,287 bytes; Node's default level gave 36,639. Your server's level decides the real number.

In Chrome DevTools, the Network panel shows both values: turn on Big request rows and the Size column lists the transferred size above the resource size.

So which number matters? The compressed size drives bandwidth and download time. The uncompressed size drives JSON.parse time, memory use and storage limits, such as MongoDB's 16 MB per document or DynamoDB's 400 KB per item. You usually care about both.

Find out what makes the payload heavy

A total size tells you there is a problem. A size per field tells you where it is. This helper adds up the serialized size of each key across an array of objects:

const sizeOf = (v) => new TextEncoder().encode(JSON.stringify(v)).length;

function heaviestKeys(items) {
  const totals = {};
  for (const item of items) {
    for (const [key, value] of Object.entries(item)) {
      totals[key] = (totals[key] ?? 0) + sizeOf(value);
    }
  }
  return Object.entries(totals).sort((a, b) => b[1] - a[1]);
}

console.table(heaviestKeys(payload.data));
Enter fullscreen mode Exit fullscreen mode

For my 1,000 users, the top of the list was:

preferences   76,957 bytes
lastLoginAt   26,000 bytes
email         23,469 bytes
id            18,000 bytes
name          13,882 bytes
Enter fullscreen mode Exit fullscreen mode

Two findings surprised me. First, key names alone take 130,043 bytes, or 45% of the minified payload, because every record repeats "preferences", "notifications" and the rest. Second, preferences is the heaviest field, yet a typical user-list screen never displays it.

That suggests two fixes, and I measured both:

Change Raw size Gzip size
Shorten every key to 1 letter −25.7% −5.0%
Drop preferences from the list response −32.0% −13.5%

Short keys look tempting on the raw numbers, but gzip already compresses repeated key names well. You lose readability for a 5% gain on the wire. Removing a field the client doesn't use cuts the transferred size by almost three times as much, and keeps the API readable.

Quick checklist

  • Measure bytes, not characters: TextEncoder, Blob.size or Buffer.byteLength.
  • In Python, use separators=(",", ":") and ensure_ascii=False when size matters.
  • Check whether responses are compressed, and look at the transferred size in DevTools.
  • Track both numbers: compressed for the network, uncompressed for parsing, memory and storage limits.
  • Find the heaviest fields and remove what the client doesn't use before reaching for short key names.

If you'd rather not paste a helper into the console every time, I built a free JSON size calculator that shows formatted, minified and estimated gzip size, plus the heaviest paths in a payload. It runs entirely in the browser, so the JSON never leaves your machine. For production minification strategies, I also wrote a longer guide to JSON minification.

What's the biggest JSON payload you've had to ship, and what did you cut first? Let me know in the comments.

Top comments (0)