The problem with `JSON.parse` at scale
JSON.parse (and its equivalent in every language) is a DOM-style parser: it reads the entire document and builds the whole object tree in memory before you can touch a single value. That's perfect for a 5KB API response and a disaster for a 5GB export. Parse a file bigger than available RAM and the process simply dies — often with a cryptic out-of-memory crash rather than a clean error.
When JSON gets big, you stop asking "how do I parse this?" and start asking "how do I process this *without holding it all at once?*" That's what streaming parsers are for. If you've hit JSON size limits or wrestled with large-file parse and stringify, this is the toolbox.
Streaming / SAX-style parsing
A streaming parser reads the input as a flow of bytes and emits events as it recognizes structure — "start of object," "key name," "value Ada," "end of array" — without ever building the full tree. You subscribe to the events for the parts you care about and let the rest pass through. Memory stays roughly constant regardless of file size, because at any moment the parser only holds the current path, not the whole document.
Popular options by ecosystem:
- Node.js: stream-json, clarinet, or oboe.js (which lets you register JSONPath-like patterns and get callbacks as matching nodes arrive).
- Python: ijson yields items lazily as it walks the file.
- Java/Jackson: the streaming JsonParser API pulls tokens one at a time.
The trade-off: streaming code is more work to write. You're reacting to a token flow instead of indexing into an object, so you maintain a little state machine. For files that fit in memory, plain JSON.parse is simpler and usually the right call.
NDJSON: make streaming trivial by design
The easiest streaming problem is the one you avoid. If *you* control the format, emit [NDJSON](/blog/ndjson-json-lines) (newline-delimited JSON) instead of one giant array — one complete JSON object per line:
{"id":1,"country":"US","amount":42.50}
{"id":2,"country":"US","amount":17.00}
{"id":3,"country":"NO","amount":88.25}Now streaming is just "read a line, parse the line, process, discard, repeat" — constant memory with a trivial loop and no special parser. NDJSON is why log pipelines, JSONL AI datasets, and data exports overwhelmingly use line-delimited records instead of a single top-level array. Whenever you generate large JSON output, prefer NDJSON; you'll never fight a streaming parser again.
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
const rl = createInterface({ input: createReadStream("events.ndjson") });
let total = 0;
for await (const line of rl) {
if (!line) continue;
total += JSON.parse(line).amount; // one small object at a time
}simdjson: when you must parse fast *and* in full
Sometimes you genuinely need the whole document parsed, just *much* faster. simdjson parses JSON at gigabytes per second by using SIMD CPU instructions to validate and index the structure in bulk, and offers an On-Demand API that lazily materializes only the values you actually access. Its bindings (C++, plus wrappers for Python, Node, Rust, and more) are the go-to when parse throughput is the bottleneck and the data still fits in memory.
Choosing the right approach
| Situation | Use |
|---|---|
| Small/medium payload, fits in RAM | JSON.parse — don't overthink it |
| Huge file, you only need some fields | Streaming parser (ijson, stream-json, oboe) |
| You control the output format | Emit NDJSON, loop line by line |
| Whole doc needed, parse speed is the wall | simdjson (On-Demand) |
| Columnar analytics over the data | Load into Apache Arrow once |
The decision tree is basically: can you avoid holding it all at once? Prefer NDJSON. If you're stuck with a giant array, stream it. If you must have the full tree, make the parse fast. Reach for the heavy machinery only when the simple path actually runs out of memory.
To inspect chunks while building a pipeline, format a sample record and check payload size. For the storage-format angle see Apache Arrow vs JSON and JSON in databases.