Skip to content

Performance and Benchmarks

Daniël Smith edited this page Oct 14, 2026 · 16 revisions

Performance and Benchmarks

Every figure on this page was measured, and every benchmark can be re-run from the repository. Unless a table says otherwise: an Intel i7-12700H laptop (20 logical cores) under Windows 11, each figure the best of 3 runs after a warm-up run applied to every contender alike.

Contents: Headline numbers · Wide tables · One large file, section by section · Queries · Shared files · LINQ · Memory and priority · Conversion · Re-run them · Measure your own data

Headline numbers

200,000 customer records with 8 fields.

.NET 10 (vs Newtonsoft.Json 13.0.3 and System.Text.Json)

Measure Newtonsoft System.Text.Json JAZMIN
File size 30,529 KB not measured 1,297 KB (deflate) / 838 KB (brotli)
File size, gzipped JSON for comparison 3,324 KB
File size with 3 indexes + AES-256 encryption 2,235 KB
Find one record by id (open the file → result) 265 ms 104 ms 1.4 ms (1.3 ms encrypted)
Filter Country == "NA" && Age > 80 276 ms 13 ms
Memory allocated for one lookup 196 MB 1.1 MB
Deserialize every record 211 ms 91 ms 32 ms
Serialize every record 103 ms 57 ms 42 ms (94 ms with 3 indexes)

Node.js 24 (vs native JSON)

Measure JSON JAZMIN
File size 31,307 KB (gzip: 3,338 KB) 1,292 KB (deflate) / 838 KB (brotli)
Find one record by id 109 ms 1.7 ms (1.7 ms encrypted)
Read every record 109 ms 45 ms
Write every record 277 ms 169 ms (309 ms with 3 indexes)
Broad filter matching 1 row in 8, spread through the file 116 ms 13 ms
Text search (contains) 112 ms 21 ms
Memory to answer a lookup (--expose-gc) 42 MB under 0.1 MB

On Node 26 (same day), JAZMIN takes 1.8 ms per lookup and 58 ms to read every record; JSON.parse is about 10% faster there than on Node 24.

What the numbers mean:

  • Size: about 24× smaller than JSON and 61% smaller than gzipped JSON with deflate; with Brotli, 75% smaller than gzipped JSON. This is regular benchmark data; expect less on very varied data.
  • Finding records: about 75–190× faster than parsing JSON in .NET (System.Text.Json and Newtonsoft), about 64× in Node.
  • Reading a whole file: faster than JSON.parse and System.Text.Json.
  • Writing: without indexes, faster than JSON.stringify and both .NET serializers, while also compressing. With 3 indexes: a little faster than Newtonsoft, about 1.6× System.Text.Json, and about 1.1× JSON.stringify.

History: measured 5 October 2026 and re-run for 1.1.0 on 6 October, every figure within 10%. Since 1.0.0, query planning took lookups from 2.7 to 1.7 ms in Node and from 3.7 to 1.9 ms in .NET, and the filter from 56 to 13 ms and from 40 to 13 ms. In 1.2.0, .NET readers of files with the same columns share their compiled row code, which took the .NET lookup from about 2.0 to 1.4 ms. Checked again for 1.2.0 on 7 October against 1.1.0 side by side (runs alternating, on a busier machine): Node unchanged within noise; .NET lookups 2.6–2.7 ms (1.1.0) against 1.9 ms (1.2.0); file sizes identical.

Wide tables: 1,000,000 rows × 300 columns

300 million values: a table nobody can hold in memory as JSON. Written and read as streams on both sides, each test in its own process; JSON read with each platform's streaming parser. Memory is the peak working set of the process.

Measure JSON (Node) JAZMIN (Node) JSON (.NET 10, System.Text.Json) JAZMIN (.NET 10)
File size 6,346 MB (gzip 1,393 MB) 643 MB 6,212 MB 649 MB
Open the file — 0.01 s, 63 MB — 0.03 s, 34 MB
Look up one id 52.0 s, 291 MB 0.03 s, 76 MB 24.1 s, 47 MB 0.08 s, 45 MB
Sum 3 of the 300 columns 70.6 s, 293 MB 3.4 s, 121 MB 35.5 s, 47 MB 1.1 s, 67 MB
Filter on 2 columns 75.4 s, 293 MB 3.4 s, 134 MB 33.5 s, 49 MB 1.2 s, 67 MB
Read every row 100.2 s, 292 MB 15.9 s, 204 MB (98 MB¹) 38.2 s, 47 MB 10.4 s, 64 MB
Write 81.8 s, 251 MB 69 s, 338 MB² 14.8 s, 48 MB 11.3 s, 144 MB

Times include generating the rows, done the same way on both sides. Repeated runs vary by up to 20%. ¹ With node --max-semi-space-size=8. ² With 2 worker threads (the default); on the main thread only, 90 s, 319 MB.

  • Reads decode only the columns a query uses, and chunk statistics load per column on demand, so opening costs the same however many columns there are.
  • .NET keeps values in typed arrays until you read them, decodes chunks ahead on worker threads within a memory budget, and encodes, compresses and encrypts on worker threads when writing.
  • Node keeps about 11 MB alive during a full scan; most of the rest is garbage V8 hasn't collected yet.

One large file, section by section

A 271 MB JSON file of 1.37 million statement lines in 5,000 sections (about 274 lines each), one PDF per section:

Reload the JSON per section JAZMIN
Time to get one section 1.3 s 0.3–0.4 ms (p99: 3.4 ms)
All 5,000 sections ≈ 1.8 hours 1.5–2 s
Peak memory of the process 1,659 MB 73–85 MB (an idle Node process is ~50 MB)
One-off conversion — 9 s, 71 MB peak, file 23.6 MB

Against streaming JSON readers (Newtonsoft JsonTextReader, System.Text.Json DeserializeAsyncEnumerable, a Node streaming parser), which fix JSON's memory problem but not its access problem:

Same 271 MB file Streaming JSON JAZMIN
Section near the start 2–48 ms 0.3–1.8 ms
Section in the middle 0.7–1.3 s 0.3–1.8 ms
Section at the end 1.3–2.6 s 0.3–1.8 ms
All 5,000 sections, one at a time ≈ 1.2–1.7 hours 1.4–2.3 s
Memory allocated per section (.NET) up to 416 MB (STJ), 747 MB (Newtonsoft) ≈ 3 MB
Full sequential read, Node 3.2 s 1.3 s
Full sequential read, .NET, raw rows STJ 1.2 s, Newtonsoft 1.7 s 0.9 s
Full sequential read, .NET, into C# objects STJ 1.2 s, Newtonsoft 1.7 s 1.4 s

The only case where streaming JSON leads is reading everything once into C# objects with System.Text.Json, by about 15%.

Queries: paging, counting and index choice

Selective filters returning whole rows (1.3.0). 2.5M order lines, every column returned, best of 3. The columns a query returns are decoded only for matching rows:

Query Node 1.2 Node 1.3 .NET 1.2 .NET 1.3
order_id in 2,000 ids (4,971 rows) 774 ms 347 ms 344 ms, 375 MB allocated 261 ms, 132 MB
One product's lines (2,474 rows) 1,106 ms 344 ms 369 ms, 318 MB allocated 231 ms, 130 MB

.NET JSON columns (1.3.0) are read from their stored UTF-8 straight into typed members:

A LINQ report over 200,000 orders (customer and items as JSON columns) Time Allocated
JAZMIN 1.2.0 6.8 s 1,629 MB
JAZMIN 1.3.0 3.1 s 965 MB
System.Text.Json, the same data as one JSON file 4.4 s 828 MB
System.Text.Json, streamed 2.8 s 638 MB

.NET LINQ sub-queries per row (1.3.0) are read once. 5,000 customers and 50,000 orders, with products and payments:

Query 1.2.0 1.3.0 Same code over in-memory lists
Latest 5 orders of each customer, with product and payment 64 s, 9.9 GB allocated 0.43 s, 56 MB 2.1 s
SelectMany per customer + First / FirstOrDefault + GroupBy report 107 s, 16.5 GB 0.44 s, 45 MB 2.1 s

.NET LINQ (1.3.0) reads only the columns a query uses, through GroupBy, SelectMany, Join and anonymous objects: grouping 200,000 orders by region and summing amounts went from 1.1 s and 261 MB to 0.25 s and 57 MB (details).

200,000 transactions (npm run bench:proposals), bytes read and time per query, 1.0.0 against 1.2.0:

Query 1.0.0 1.2.0
Deep page (offset 150,000) 3,563 KB, 59 ms 97 KB, 1.7 ms
Newest 50 rows 4,709 KB, 78 ms 82 KB, 1.5 ms
A page of one account's rows after a time (file sorted by account) 3,785 KB, 109 ms 102 KB, 2.2 ms
One day across all accounts 7,467 KB, 206 ms 4,938 KB, 70 ms
12 monthly counts over a year (file sorted by time) 5,771 KB, 104 ms 1,062 KB, 15 ms
One account's count (sorted by time, account indexed) 4,766 KB, 81 ms 57 KB, 1.2 ms
Fetching one day's 549 rows by position Read Time
In time order, as they came 53,815 KB 673 ms
Sorted by position first 4,895 KB 62 ms
Sorted index over unique values, open + one lookup (Node) Index in one piece (before 1.0) Index in pages
1,000,000 rows 149 ms, 189 MB, 7.6 MB read 8.5 ms, 56 MB, 72 KB read
5,000,000 rows 886 ms, 730 MB, 36 MB read 10.6 ms, 58 MB, 110 KB read
Compact indexes (New in 1.2), 200,000 rows, indexes on an id, a code and a date Off On
File size 4.5 MB 1.8 MB
Lookup time 5–16% less
Column arrays, 200,000 rows of a date and an amount, memory kept Row objects columnArrays()
Node 30.7 MB 3.3 MB
Browser reader 34.8 MB 3.8 MB

columnArrays() over one million rows of two columns: 121–135 ms in Node, 300–305 ms in the browser reader.

Shared files

1.37M lines, 5,000 sections, one client key per section:

JavaScript .NET
Write 2.0 s 1.2 s
Client: open the file + read their section 1.7 ms 1.6 ms
Owner: open the file + read one section 2.1 ms 1.8 ms
Owner: each further section, file open 0.27 ms 0.21 ms
Owner: find one record by id among 250,000 JavaScript .NET
100 partitions 1.7 ms (4.9 ms without the chunk map) 3.1 ms (5.0 ms without)
1,000 partitions 2.0 ms (49 ms without) 2.7 ms (28 ms without)
  • A client's open stays about the same however many keys the file has: 1.4 ms in .NET with 1, 5,000 or 50,000 keys.
  • A one-record append takes about the same with 1,000 partitions as with 100 (JavaScript 16 and 12 ms, .NET 23 and 14 ms).
  • Changing one section of the 1.37M-row file: update() 2.7 s, append() 63 ms.
  • A year of syncs (50 people, 37,500 appends), compacted with regroup: 538 KB and 50 chunks instead of 7,241 KB and 37,500; one person's 3,000 rows read 10 KB in 2 ms instead of 125 KB in 23 ms.
  • Filing a batch of returned records: 20–25 ms, whether 1 record or 50, and with 10,000 or 250,000 records in the file.
  • 1.6.0: a full count of a shared file of 250,000 rows written as 250,000 chunks (every row a different partition) uses 152 MB instead of 234 MB in JavaScript, and is 8% faster; other reads are unchanged (median of 7 rounds against the published 1.5.0).
  • 1.6.0, reader.storage() (is compacting worth it?): 0.1 ms on a 1,000,000-row file, 3 ms after 200 appends, 31 ms in JavaScript and 14 ms in .NET for a shared file of 250,000 rows in 1,000 partitions.
  • Key rotation without decoding rows, 176 MB file: 0.7 s (Node, 138 MB; .NET, 45 MB), against 14–33 s for a rewrite.

LINQ: AsQueryable

.NET 10, 200,000 rows × 300 columns, a type mapping 9 of them; 1.1.0's Query<T> followed by LINQ to Objects, against 1.2's AsQueryable:

Query 1.1.0 AsQueryable
Every row as objects 3.2–3.7 s 0.30 s
Where + Select of two columns 3.6 s 0.35–0.37 s
Count(condition) 3.3–3.9 s, 63 MB 0.25 s, 56 MB
Skip(150_000).Take(10) 2.5–2.7 s, 63 MB 0.05 s, 35 MB
OrderBy(o => o.Id).Take(10) (file sorted by id) 3.5 s, 100 MB 0.05 s, 35 MB
Sum(o => o.Qty) 3.2–3.4 s 0.25 s
First(o => o.Id == 123_456) 0.08–0.09 s 0.07 s

Memory and priority

200,000 rows × 300 columns, medians of 3–5 runs, each in its own process, peak memory of the process:

memory balanced (default) speed
.NET: sum 3 columns 0.76 s, 49 MB 0.27 s, 54 MB 0.19 s, 65 MB
.NET: filter on 2 columns 0.74 s, 49 MB 0.26 s, 54 MB 0.19 s, 65 MB
.NET: read every row 2.33 s, 60 MB 2.29 s, 60 MB 2.10 s, 95 MB
.NET: write 11.2 s, 68 MB 4.3 s, 79 MB 4.5 s, 82 MB
Node: sum 3 columns (columnArrays) 0.66 s, 125 MB 0.66 s, 125 MB 0.24 s, 196 MB
Node: filter on 2 columns 0.70 s, 96 MB 0.70 s, 96 MB 0.23 s, 162 MB
Node: read every row 4.80 s, 226 MB 4.80 s, 226 MB 4.34 s, 256 MB
Node: write 19.6 s, 233 MB 15.0 s, 257 MB as balanced
Node, 1M × 300 columns Default --max-semi-space-size=8
Read every row 15.9 s, 204 MB 16.1 s, 98 MB
Sum 3 columns 3.4 s, 121 MB 3.2 s, 118 MB
Write 87.1 s, 299 MB 86.1 s, 302 MB

Conversion

A 1 GB file CSV (10 million rows) XML (4.6 million rows)
.NET 14 s, 63 MB 11 s, 65 MB
Node, --max-old-space-size=64 --max-semi-space-size=2 44 s, 92 MB 48 s, 86 MB
Node, default flags 32 s, 316 MB 41 s, 250 MB
Export shapes, 1M transactions for 10,000 clients, nested lines Node .NET
File sorted by the grouping column 2.3 s 1.8 s, 66 MB
Not sorted 8.7 s 5.6 s, 153 MB
1.3.0: sorted 1.9 s, 147 MB 1.6–1.8 s, 69 MB
1.3.0: not sorted, priority: 'speed' 4.0 s, 559 MB 5.3 s, 381 MB
Export shapes across tables (1.3.0): 100,000 customers, 1M orders, 2.5M lines (360 MB of JSON) Node .NET
Each linked table sorted by its link 7.2 s, 220 MB 4.5 s, 84 MB
The same, one customer 0.08 s 0.17 s
Orders and lines in time order (batches), by default 27 s, 578 MB 19 s, 238 MB
For comparison: the three tables exported one by one 4.8 s, 200 MB 2.6 s, 70 MB
An in list of 20,000 values against 100,000 rows Node .NET
1.2.0 (each row compared with every value) 24 s 7.3 s
1.3.0 (a hash set) 18 ms 36 ms
Rows as JSON (.NET), 200,000 × 300 columns (1.3 GB of JSON) Time Memory
JazminJsonStream 7.1 s 63 MB
JazminJsonReader (120 million tokens) 4.8 s 62 MB
DeserializeAsyncEnumerable over the stream 13 s 70 MB
Several tables: 20,000 clients, 300,000 transactions (Node) Flat Two tables
File size 3.70 MB 3.39 MB
Write 1.28 s 0.94 s
Read every transaction 176 ms 145 ms

Beside other formats: Parquet, Arrow, SQLite, MessagePack

For reference, the same 200,000 customers in formats built for other jobs:

  • Parquet: columnar analytics files;
  • Arrow IPC: columns as they are in memory;
  • SQLite: an embedded SQL database with B-tree indexes;
  • MessagePack: binary JSON.

Each format's library is used with its defaults, each query opens the file, and every answer is checked. They were measured on 9 October 2026 on a laptop that was about a third busy with other work, so compare across a row. Each cell is the best of 2-3 runs. Some rows were measured again later the same day, after changes of 1.4, each as the best of 2 runs: Node's text search, "Find one row by id" and "Write every row" rows. The .NET tables were measured again, whole, on the quiet machine after those changes. JAZMIN counts without building an object per row, as its guides show: count() in Node, AsQueryable<T>().Count(predicate) in .NET.

.NET 10 (Parquet.Net, Apache.Arrow, Microsoft.Data.Sqlite, MessagePack-CSharp):

Measure JAZMIN Parquet Arrow IPC SQLite MessagePack
File size 2,232 KB 3,659 KB 13,980 KB 15,340 KB 11,872 KB
Write every row 123 ms 98 ms 51 ms 476 ms 34 ms
Read every row (objects) 39 ms 162 ms 323 ms 233 ms 46 ms
Sum one column 26.7 ms 2.8 ms 5.5 ms 14.0 ms 48.7 ms
Find one row by id (open → row) 0.9 ms 95.8 ms 5.3 ms 0.4 ms 45.6 ms
Filter: country = NA, age > 80 6.0 ms 12.4 ms 5.8 ms 12.9 ms 44.3 ms
Text search: name contains 'Ndlovu' 6.8 ms 15.6 ms 9.0 ms 18.7 ms 47.2 ms

Node.js 24 (hyparquet, apache-arrow, node:sqlite, msgpackr):

Measure JAZMIN Parquet Arrow IPC SQLite MessagePack
File size 2,227 KB 2,935 KB 12,027 KB 15,340 KB 21,626 KB
Write every row 326 ms 424 ms 418 ms 405 ms 110 ms
Read every row (objects) 109 ms 158 ms 612 ms 717 ms 255 ms
Sum one column 30.6 ms 32.3 ms 15.4 ms 29.8 ms 265.8 ms
Find one row by id (open → row) 1.9 ms 82.5 ms 7.3 ms 0.7 ms 162.7 ms
Filter: country = NA, age > 80 23.9 ms 77.2 ms 14.6 ms 27.0 ms 243.0 ms
Text search: name contains 'Ndlovu' 16.6 ms 17.3 ms 15.8 ms 18.5 ms 168.1 ms

Memory (peak memory added; each operation in a process of its own, after the same operation on a 1,000-row file so each library's code is loaded, with memory sampled throughout; native memory counts too):

Node.js 24 JAZMIN Parquet Arrow IPC SQLite MessagePack
Write every row 124.1 MB 179.9 MB 230.1 MB 16.8 MB 44.1 MB
Read every row (objects) 75.5 MB 114.1 MB 102.2 MB 282.5 MB 153.8 MB
Sum one column 11.1 MB 35.1 MB 12.9 MB 1.9 MB 154.3 MB
Find one row by id 0.3 MB 87.5 MB 12.9 MB 0.1 MB 151.2 MB
Filter 11.3 MB 48.3 MB 14.0 MB 1.7 MB 150.9 MB
Text search 12.1 MB 19.5 MB 15.4 MB 2.0 MB 151.9 MB
.NET 10 JAZMIN Parquet Arrow IPC SQLite MessagePack
Write every row 72.8 MB 58.0 MB 46.7 MB 18.2 MB 23.8 MB
Read every row (objects) 59.1 MB 99.5 MB 77.9 MB 12.6 MB 55.7 MB
Sum one column 19.3 MB 10.5 MB 14.1 MB 1.8 MB 55.8 MB
Find one row by id 0.7 MB 83.0 MB 13.8 MB 0.1 MB 55.7 MB
Filter 15.9 MB 27.7 MB 15.4 MB 1.8 MB 55.7 MB
Text search 10.7 MB 33.7 MB 23.5 MB 1.8 MB 55.7 MB

SQLite needs almost no memory for a query: it reads its file a page at a time through a small cache.

  • Where JAZMIN leads:
    • the smallest file, with its 3 indexes;
    • the fastest read of every row (in Node, also with the least memory);
    • in .NET, the fastest text search (6.8 ms), and filters level with Arrow;
    • finding one row by id second only to SQLite;
    • the only one with keys, encryption and per-key access.
  • What it can work towards:
    • finding one row as fast as SQLite on a process's first calls. In 1.4:
      • Node and the viewer decode only the row's values: Node 2.7 -> 1.9 ms here, and 1.15 -> 0.70 ms once Node has optimised the code, as fast as SQLite; the viewer 1.8 -> 1.3-1.4 ms with the file open;
      • .NET decodes a lookup's chunk on the query's thread and reads each column only as far as the row: 1.6 -> 0.9 ms here, 6.3 -> 0.7 MB, and a LINQ lookup 0.63 -> 0.49 ms once warm;
    • sums of one column, where Parquet.Net (2.8 ms) and Arrow (5.5 ms) are 5-10 times faster in .NET: JAZMIN compresses each chunk whole, all its columns together, so a sum of one column decompresses all of them. Closing this needs columns compressed apart or a faster codec, a format change for after 1.4;
    • filters in Node, where Arrow is about twice as fast;
    • writing, where MessagePack is 3-5 times faster;
    • memory for writing (in 1.4, Node writes this file with 124 MB, down from 181-185 MB).
  • Text search, in 1.4: a search reads only the parts of the trigram index it needs, and scans when the word is in most rows. Node 35.0 -> 16.6 ms and 34 -> 12 MB; .NET 6.8 ms and 11 MB, with the index read into a reused buffer.
  • Run them yourself:
    • Node: npm install then npm run bench in js/bench/formats;
    • .NET: dotnet run -c Release --project bench/Jazmin.FormatBenchmarks in dotnet/.

Re-run the benchmarks

From a clone of the repository:

Benchmark Node (in js/) .NET (in dotnet/)
Headline (200,000 records vs JSON) npm run bench dotnet run -c Release -f net10.0 --project bench/Jazmin.Benchmarks
Wide tables node bench/wide.js 1000000 300 dotnet run -c Release -f net10.0 --project bench/Jazmin.Benchmarks -- wide 1000000 300
Sections of one large file node bench/sections.js 300 5000 ... -- streaming statements.json 5000
Shared files node bench/access.js ... -- access
Typical statement queries (bytes, rows, time) npm run bench:proposals
Filing returned records npm run bench:filing
reader.storage() (1.6.0) npm run bench:storage
Changing indexes, the sort order and tables; set (1.6.0) npm run bench:layout
Checking links between tables (1.6.0) npm run bench:links

Each measurement runs in its own process and reports time and peak memory. Run .NET benchmarks with -c Release; debug builds are several times slower.

Measure your own data

Timings depend on the machine, so this script prints them rather than checking them. Run it on your own data shape by changing the row generator:

import { performance } from 'node:perf_hooks';
import { open, write } from '@smithsoft-studios/jazmin';

const rows = Array.from({ length: 200_000 }, (_, i) => ({
  id: i + 1, name: `Customer ${i + 1}`, country: ['ZA', 'NA', 'BW', 'KE'][i % 4], age: 18 + (i % 70), balance: (i * 7919) % 100000 / 100,
}));
// Like the published benchmarks: a warm-up run, then the best of 5
const time = (label, fn) => {
  let result = fn();
  let best = Infinity;
  for (let run = 0; run < 5; run++) {
    const start = performance.now();
    result = fn();
    best = Math.min(best, performance.now() - start);
  }
  console.log(label.padEnd(28), `${best.toFixed(1)} ms`);
  return result;
};

const json = time('JSON.stringify', () => JSON.stringify(rows));
const jzm = time('JAZMIN write', () => write(null, rows, { sortedBy: ['id'], indexes: { country: 'sorted' } }));
console.log('sizes'.padEnd(28), `${Math.round(json.length / 1024)} KB JSON, ${Math.round(jzm.length / 1024)} KB JAZMIN`);
time('JSON: find one by id', () => JSON.parse(json).find((r) => r.id === 123_456));
time('JAZMIN: find one by id', () => [...open(jzm).find({ id: 123_456 })]);
time('JSON: filter', () => JSON.parse(json).filter((r) => r.country === 'NA' && r.age > 80).length);
time('JAZMIN: filter', () => open(jzm).count({ country: 'NA', age: { gt: 80 } }));

One run on the benchmark laptop (7 October 2026, Node 24):

JSON.stringify               43.7 ms
JAZMIN write                 163.4 ms
sizes                        15363 KB JSON, 1115 KB JAZMIN
JSON: find one by id         48.4 ms
JAZMIN: find one by id       1.0 ms
JSON: filter                 48.7 ms
JAZMIN: filter               12.7 ms

Writing costs more than JSON.stringify here because JAZMIN also builds an index and compresses; the JSON would still need gzip and encryption to compare like with like. Every read is faster.

For a measured view of what a query reads (bytes, chunks, columns) rather than its time, use explain(filter, { analyze: true }): see Reading and Querying.

Clone this wiki locally