JSON Serialization Performance: Parsing, Streaming, and Large Documents
Parsing and serializing JSON is routine enough that most of us never measure it — until a large export freezes an event loop, a log pipeline falls behind, or a p99 budget shatters. This article covers where the cost goes: DOM versus streaming parsers, memory blowup on big files, NDJSON for record streams, rough numbers across languages, and when binary formats start paying for themselves.
Why parse cost matters
JSON is text, and turning text into objects is real CPU work: tokenizing, validating escapes and number syntax, allocating strings, building hash tables and arrays. On a typical CRUD service the payloads are small and the cost is noise. It stops being noise when documents are large, traffic is high, or parsing sits on a latency-sensitive path — and serialization costs roughly the same again.
Start with a ratio: parse time versus total request time. If a handler spends 3 ms of a 10 ms budget deserializing the body, you have a JSON problem — the rest of this article is your options.
DOM-style and streaming parsers
The parsers you use daily — JSON.parse in JavaScript, json.loads in Python, encoding/json in Go — are DOM-style: they consume the whole document and return a complete in-memory tree. That's the right default. It's simple, safe, and lets you read any field in any order.
Streaming parsers work at the token level instead. Pull parsers such as Python's ijson or Go's json.Decoder hand you one element at a time and you drive the loop; push (SAX-style) parsers fire callbacks as tokens pass and you assemble what you care about. Both let you stop early and skip what you don't need. The win is memory, not raw speed — every byte still gets tokenized, but you never hold the whole tree.
Memory blowup on large documents
A parsed JSON document is much bigger than the same bytes on the wire. Every string becomes a heap allocation with its own overhead; every object becomes a hash table with buckets to spare. A working rule of thumb across runtimes: the in-memory object graph occupies three to ten times the size of the JSON text.
By that math a 100 MB file is plausibly half a gigabyte resident while parsed — and JSON.parse holds the main thread for the entire parse, freezing everything else in a browser or Node. Multiply by concurrent requests and the garbage-collector pressure alone shows up in your metrics.
The fixes, in order of effort: stream the parse and keep only what you need, split the document into records, or stop treating it as one giant JSON value. The next section covers the standard way to do the last two.
NDJSON for logs and event streams
Newline-delimited JSON — one JSON value per line, no outer array — is the standard answer for record-shaped data. Each line parses independently, so you can process a 10 GB export in constant memory, resume from any byte offset, split the file across workers, and append without rewriting anything.
{"ts":"2025-07-22T09:14:02Z","level":"info","event":"order.created","order_id":98421,"total":59.9}
{"ts":"2025-07-22T09:14:03Z","level":"warn","event":"payment.retry","order_id":98422,"attempt":2}
{"ts":"2025-07-22T09:14:05Z","level":"error","event":"inventory.short","sku":"SKU-3312","wanted":3,"available":1}Contrast that with the same events wrapped in one JSON array: nothing is readable until the closing bracket arrives, and the writer can't emit an event until it's willing to terminate the file. That difference is why NDJSON dominates logging, queues, and bulk exports.
The trade-off is that NDJSON is not one document: there's no root envelope, no cross-line references, and a corrupted line damages only that line. For event streams, that's exactly the failure mode you want.
Rough numbers: JavaScript, Python, Go
Order-of-magnitude throughput for stock parsers on one modern core: V8's JSON.parse handles roughly 400–800 MB/s and is consistently the fastest mainstream-runtime parser. Go's encoding/json sits around 100–200 MB/s, with jsoniter and sonic well beyond. CPython's stdlib json manages 50–100 MB/s, while orjson and msgspec clear a gigabyte per second.
Treat these as orientation, not benchmarks; real numbers depend on document shape — many small objects cost more per byte than long strings — and the integer-to-float mix. The gap that matters is structural: a 300 MB log stream is a sub-second job in Node and a minute-plus in naive Python — the difference between a default parser and a fast one, not tuning.
On the wire, gzip or brotli typically shrinks JSON five to ten times, so compress — but compression reduces bytes, not parse CPU: a compressed payload must still be fully decompressed and tokenized before the first field is usable.
When to move to a binary format
MessagePack, CBOR, Protocol Buffers, and Avro encode the same data in roughly 30–70 percent of the bytes and decode several times faster, because lengths and types are explicit instead of discovered by scanning text. The cost is tooling: schemas, code generation, and files you can no longer just open and read.
The sensible boundary is edges versus interiors: JSON at the edges — public APIs, configuration, anything a human might open — and binary formats on internal high-volume paths: service fan-out, caches, event buses, anywhere payloads run large at high rates. If you already validate against schemas, Protobuf and Avro hand you the schema and the speed in one move.
FAQ
- How big is too big for JSON.parse?
- There's no hard limit, only symptoms. A few megabytes parses in single-digit milliseconds; tens of megabytes block the main thread noticeably in browsers; hundreds of megabytes mean the document should be streamed, split, or converted. The binding constraint is usually memory — a parsed tree can be several times the text size — not parse time.
- Does minified JSON parse faster than pretty-printed JSON?
- Slightly, but the real win is bandwidth. Whitespace tokens are cheap for a parser to skip, so minifying improves parse speed by a small percentage while cutting transfer size by 30–60 percent on typical documents. The practical rule: minify for the wire, format for humans in the editor.
- Is YAML or XML faster to parse than JSON?
- Generally no. YAML's grammar is far more complex and most parsers are slower than their JSON equivalents; XML DOM parsing is comparable at best and more verbose on the wire. If you're switching formats for performance, the meaningful jump is to a binary encoding, not another text format.
- When is NDJSON the wrong choice?
- When the data is genuinely one document rather than a sequence of records — deeply cross-referenced, needing atomic writes, or consumed by tools that expect a single JSON value. NDJSON buys constant-memory processing and resumability at the price of no root envelope and no whole-file validity guarantee. For logs, queues, and exports that trade is almost always right; for a config file it isn't.
Try these tools
Related articles
What is JSON? A Complete Beginner's Guide →
JSON (JavaScript Object Notation) is the lightweight data-interchange format behind nearly every API, config file, and NoSQL database. Learn the syntax, the six data types, how parsing behaves in code, and the mistakes everyone makes hand-writing it.
The History of JSON: From 2000 to Industry Standard →
Trace JSON from Douglas Crockford's idea in 2001, through Yahoo's adoption, to becoming the ECMA-404 standard that powers 90% of modern APIs.
The Complete JSON Schema Guide (Draft 7) →
JSON Schema is the standard for describing and validating JSON structure. Learn the core keywords, build a real API schema, compose and reuse schemas with $ref, and run validation in code with Ajv.