devtools
guide
07 Oct 2026
Seven JSON parsers on one 621 MiB file, ranked by peak memory
Same file, same sum, seven parsers. Peak memory ran from 3 MiB to 5 GiB, and the one that gave up had 27 GiB of RAM free when it did.
By Purvansh Parmar
Checked 2026-10-21
Read 8 min
Entries 7
You have a 621 MiB JSON export and one number to get out of it. Seven tools can do that. The spread between them is not the twenty percent a benchmark table would suggest, it is three orders of magnitude of memory, and one of the seven cannot do it at all on a machine with 27 GiB free. The thing that decides the answer is not how fast a parser scans bytes. It is whether teh parser builds the whole document in memory before you are allowed to look at any of it.
How this was checkedGenerated two JSON arrays of the same shape, 164 MiB and 621 MiB, and made seven parsers compute the same sum over one float field. Measured wall clock as the median of three runs and peak RSS with /usr/bin/time on an 8 thread i5-1135G7 with 31 GiB of RAM. Node 22 could not read the larger file at all. orjson, which advertises lower memory use when fed bytes, peaked 1.45 GiB higher than the standard library on the same input.
What I compared on
- peak resident memory on the 621 MiB file, which is what decides whether the job fits in a container
- wall clock, median of three runs
- whether it finishes at all
The same jq binary told to emit path and value events instead of building the document.
Wins
The job fits anywhere. A 128 MiB container runs this against a file five times its own memory limit, and the same command works unchanged when the file doubles.
Loses
It is 3.6 times slower than the jq you already know, and the filter is genuinely hard to write: selecting one field means matching on path arrays, which is unreadable next to map(.amount).
3 MiB peak57.4 s3.6x slower than jq
Python event driven JSON parser, with a C backend over yajl2 when one is available.
Wins
The only entry here with both constant memory and a readable API: ijson.items(f, 'item.amount') is one line and does what it looks like. The C backend is doing real work, and it is the default when the wheel installs cleanly.
Loses
It is a dependency, and the wheel matters: the pure Python fallback backend is several times slower, and nothing warns you which one you got unless you check ijson.backend yourself.
12 MiB peak7.3 sconstant across a 3.8x file size increase
JavaScript runtime using JavaScriptCore rather than V8, running the same readFileSync and JSON.parse.
Wins
Nothing in the script changes. The same four lines that fail on Node 22 run on Bun, which makes it the cheapest fix available when you hit the ceiling at two in the morning.
Loses
Peak memory is still 2.6 times the file, so this buys you one doubling of input and no more. A 1.5 GiB working set to add up a column is a lot to ask of a build agent.
3.21 s1596 MiB peak2.6x the file size
The standard library parser, json.load straight onto a file object, no dependency at all.
Wins
It is already installed, it is the baseline every other Python number gets quoted against, and for files in the low hundreds of megabytes the difference between this and the fast alternative is twelve percent.
Loses
5.9 times the file in peak memory, and it is the slowest of the in-memory Python options. On anything above about 2 GiB of input you are sizing the machine around the parser.
13.5 s3643 MiB peak5.9x the file size
The default mode of the JSON tool that ships with Ubuntu 22.04, building the whole document first.
Wins
map(.amount)|add is the shortest correct expression anyone wrote all day, and it needs no language runtime, no dependency and no script file.
Loses
It scales its memory exactly with the input and gives no warning before it does. It is also the slowest in-memory option measured here, slower than Python on the same file.
15.8 s3825 MiB peak6.2x the file size
Rust JSON library for Python, the usual recommendation when the standard library is too slow.
Wins
It is genuinely faster, and if your payloads are small and numerous, which is the shape its benchmarks use, the per call overhead it removes is the thing that was costing you.
Loses
On one large document it is the worst choice on the board. The README recommends passing bytes for lower memory use, which is exactly what this test did, and it still peaked 8.2 times the file size.
5096 MiB peak1.13x faster parse8.2x the file size
V8's JSON.parse behind readFileSync, the shape of every quick script written in the last decade.
Wins
On files under the ceiling it is second only to Bun, and 515 MiB for a 164 MiB document is the second tightest of the in-memory options.
Loses
The failure has nothing to do with memory and cannot be configured away: 27 GiB were free, and the limit is V8's maximum string length, so it fails identically on a machine with a terabyte.
512 MiB ceiling536,870,888 characters27 GiB free when it failed
Side by side
| 621 MiB file | Peak RSS | Multiple of file |
| jq --stream | 57.4 s | 3 MiB | 0.005x |
| ijson | 7.3 s | 12 MiB | 0.02x |
| Bun 1.3.14 | 3.2 s | 1596 MiB | 2.6x |
| Python json | 13.5 s | 3643 MiB | 5.9x |
| jq 1.6 | 15.8 s | 3825 MiB | 6.2x |
| orjson 3.13.0 | 11.8 s | 5096 MiB | 8.2x |
| Node 22 | failed | n/a | n/a |
Verdict
Use ijson. It held 12 MiB flat across a 3.8x increase in file size, finished the big file in 7.3 seconds, and the call that does it is one line you can read. The in-memory parsers are not wrong, they are just priced per byte of input, and that price is invisible until the day the export doubles and your job gets killed in a container you sized last year. If you cannot add a dependency, jq --stream does the same trick for 3 MiB and a much worse filter.
Sources