← All QuantalCount performance reports

Measured on our own hardware

How long does an export take?

We exported four standard stocktake datasets at 1,000 and 10,000 input rows, in CSV, TXT and XLSX, through the real report queue — 96 exports in all — and checked every downloaded file line by line. Here is what actually happened.

The short answer: at 1,000 input rows nothing took longer than 131 milliseconds. At 10,000 it depends on the dataset — raw records as CSV took 224 milliseconds, variance summary as XLSX took 1.20 seconds. Almost all of that wait is the server building the file. Across all 96 exports the longest anyone waited in the queue was 11 milliseconds, and the gap between "requested" and "downloadable" was only 21–124 milliseconds — most of which is the 100-millisecond poll that notices the job finished.

Slowest group measured

1.20 s

10,000 rows · Variance summary · XLSX

Fastest group measured

123 ms

1,000 rows · Variance summary · XLSX

Longest queue wait

11 ms

the worst case across all 96 exports

Files reconciled

72 of 72

every downloaded file was checked line by line

Every wait below is a median; maxima are shown alongside it where they matter. Each group has only 3 samples, which is far too few to publish a trustworthy p95 or p99, so none is claimed, and nothing here is a concurrency capacity.

1. How long from clicking export to being able to download

Changing format inside one dataset moves the wait far less than changing dataset does.

1,000 input rows: request to first observed downloadable, median. Bars are scaled against the slowest group here, which is highlighted.

Location and product summary · XLSX
131 milliseconds
Location and product summary · CSV
130 milliseconds
Raw records · CSV
129 milliseconds
Location and product summary · TXT (fixed width)
128 milliseconds
Raw records · TXT (fixed width)
128 milliseconds
Raw records · XLSX
128 milliseconds
Inventory summary · XLSX
127 milliseconds
Inventory summary · CSV
126 milliseconds
Variance summary · CSV
125 milliseconds
Variance summary · TXT (fixed width)
124 milliseconds
Inventory summary · TXT (fixed width)
123 milliseconds
Variance summary · XLSX
123 milliseconds

10,000 input rows: request to first observed downloadable, median. Bars are scaled against the slowest group here, which is highlighted.

Variance summary · XLSX
1.20 seconds
Inventory summary · XLSX
1.20 seconds
Variance summary · TXT (fixed width)
1.20 seconds
Inventory summary · CSV
1.09 seconds
Inventory summary · TXT (fixed width)
1.09 seconds
Variance summary · CSV
1.09 seconds
Location and product summary · CSV
666 milliseconds
Location and product summary · XLSX
665 milliseconds
Location and product summary · TXT (fixed width)
660 milliseconds
Raw records · XLSX
445 milliseconds
Raw records · TXT (fixed width)
333 milliseconds
Raw records · CSV
224 milliseconds

The measurement is split into five separate clocks, and the tables below break them out:

These numbers come from a Release build of the API running natively on macOS, talking to its own PostgreSQL and Azurite, going through the real JobReport and shared reports queue rather than calling the renderer directly.

2. Choosing the dataset and the format

At 10,000 rows, picking the wrong dataset costs about 4.9× — picking the wrong format costs far less.

Dataset (10,000 rows)CSVTXT (fixed width)XLSXLargest / CSV
Raw records inventory-raw-data 224 milliseconds
1.06 MiB
333 milliseconds
6.73 MiB
445 milliseconds
328.4 KiB
6.4×
Inventory summary inventory-summary 1.09 seconds
556.7 KiB
1.09 seconds
5.78 MiB
1.20 seconds
312.1 KiB
11×
Location and product summary location-product-summary 666 milliseconds
507.9 KiB
660 milliseconds
4.83 MiB
665 milliseconds
253.4 KiB
9.7×
Variance summary variance-summary 1.09 seconds
761.8 KiB
1.20 seconds
7.69 MiB
1.20 seconds
377.9 KiB
10×

The dataset matters, the format mostly does not. Taking the fastest format available for each dataset, the wait at 10,000 rows runs from 224 milliseconds to 1.09 seconds — a factor of 4.9×. Inside a single dataset the three formats differ by at most 2.0× (Raw records: 224 milliseconds at best against 445 milliseconds at worst), and for Location and product summary the choice of format made no practical difference at all.

What the format really changes is the size of the file and how long it takes to download, not how long the server takes to build it. TXT pads every column out to a fixed width, so at 10,000 rows the four datasets together come to 25.02 MiB against 2.84 MiB for the same content in CSV — a factor of 8.8×. XLSX compresses and is smallest at 1.24 MiB. Bigger files take longer to fetch: at 10,000 rows the median download was 8.4–13.4 milliseconds for CSV, 22.3–29.3 for TXT and 7.8–10.9 for XLSX.

Worth knowing: if the file is going to a person rather than back into a system, XLSX was both the smallest and the fastest to download here. CSV is the more practical choice for scripts and re-imports. TXT's fixed-width layout exists to line columns up for printing, not to move data efficiently — the same 10,000 rows are 6.4–10.6 times larger than CSV, so it is worth confirming someone actually needs aligned columns before choosing it.

3. What happens when the input grows ten times

Two different kinds of dataset, two different kinds of growth.

Execution time when the input grows from 1,000 rows to 10,000, a factor of ten. Bars are scaled against the worst dataset.

Raw records
5.8× to 7.7×
Inventory summary
10× to 16×
Location and product summary
12× to 13×
Variance summary
12× to 14×

Raw records grow more slowly than the input; the other three grow with it, or a little worse. Inventory summary was the most expensive, taking 10× to 16× longer from 1,000 rows to 10,000. Raw records only took 5.8× to 7.7×, because at 1,000 rows its fixed costs — creating the job, reading the catalogue snapshot, finishing the file — are a large share of a small job, and 10,000 rows amortise them.

Dataset (10,000 rows)SQL calls, medianShare of execution spent in SQLInput rows per secondPer-row cost, 1,000 vs 10,000
Raw records 348–382 47–55% 27,733–51,146 0.6×
Inventory summary 723–747 37–42% 8,840–9,460 1.0×
Location and product summary 416–418 29–34% 16,484–18,450 1.2×
Variance summary 723–747 35–41% 8,732–9,491 1.4×

Per-row cost is milliseconds of execution per input row, comparing the CSV export at 1,000 rows with the same at 10,000 rows. Above 1× means each row got more expensive at ten times the size; below it means the fixed costs were spread more thinly. Raw records is the interesting case: its rows actually got cheaper, because at 1,000 rows most of the work was not per-row work at all. The three summary reports have to materialise every row and then match and roll it up in memory, which is why each of their rows costs several times more than a row of the raw export.

4. What the database and the app did while it worked

The cost of producing these files, as measured rather than estimated.

MeasureMeasuredWhat it covers
API sampled RSS peak1363.56 MiBthe peak was reached during an export itself, not only between them; sampling spans warmup, HTTP polling and file verification, so this is neither managed live memory nor container memory
Driver RSS peak425.88 MiBthe Node driver on the same host
Allocated during measurement11.79 GiBSystem.Runtime counters cover the whole measurement window and cannot be attributed to one export
GC pause, total0.16 secondsacross all 96 exports; sampled GC committed peak 1105.17 MiB
API process CPU40.13 secondssame sampling window
Resource samples112one every 500 milliseconds, 0 of them failed; 776 System.Runtime samples
PostgreSQL temporary files0 bytesno query at 10,000 rows or below spilled to a temporary file. The application's own external-sort disk peak was not collected
Dataset and format (10,000 rows)SQL calls, medianSQL execution, median msPostgreSQL temp bytes, max
Raw records · CSV 348 107.47 0
Raw records · TXT (fixed width) 372 146.10 0
Raw records · XLSX 382 180.34 0
Inventory summary · CSV 723 442.55 0
Inventory summary · TXT (fixed width) 739 389.03 0
Inventory summary · XLSX 747 461.21 0
Location and product summary · CSV 418 184.67 0
Location and product summary · TXT (fixed width) 416 176.96 0
Location and product summary · XLSX 416 204.73 0
Variance summary · CSV 723 431.34 0
Variance summary · TXT (fixed width) 739 395.71 0
Variance summary · XLSX 747 464.51 0

The database figures include the HTTP admission, the status polling, the download and the verification queries. They are not the report builder's SQL time on its own, and at 10,000 rows SQL accounts for 29–55% of execution time depending on the dataset.

Do not read the memory figures as a pass mark. Later samples share one process, its retained heap and a warm database cache, so the RSS peak includes memory left behind by earlier exports and is not a managed live size. This run makes no claim about a memory target being met, and none of these numbers is evidence of a leak. Answering "how much memory does one export need" needs a separate run that forces a collection between samples.

5. Every group, one row each

All 24 measured groups behind everything above. Nothing is sampled out.

Input rowsDatasetFormatAdmitted msQueued msExecuted ms, median / maxRequest to downloadable ms, median / maxDownloaded msOutput KiBInput rows per second
1,000 Raw records inventory-raw-data CSV 15.79 10.55 33.91 / 39.73 128.93 / 134.15 12.54 107.39 KiB 29,485
1,000 Raw records inventory-raw-data TXT (fixed width) 14.65 9.20 44.38 / 46.94 127.95 / 129.05 12.96 689.45 KiB 22,534
1,000 Raw records inventory-raw-data XLSX 13.61 8.78 46.89 / 60.50 127.80 / 129.32 9.89 35.15 KiB 21,327
1,000 Inventory summary inventory-summary CSV 13.97 8.71 104.74 / 117.58 125.69 / 237.23 6.78 55.72 KiB 9,547
1,000 Inventory summary inventory-summary TXT (fixed width) 12.82 8.25 81.06 / 103.56 123.22 / 124.99 7.59 591.80 KiB 12,336
1,000 Inventory summary inventory-summary XLSX 13.45 8.05 70.61 / 109.63 126.56 / 244.68 8.46 33.30 KiB 14,162
1,000 Location and product summary location-product-summary CSV 14.21 9.46 44.60 / 46.02 130.23 / 130.58 11.89 50.83 KiB 22,421
1,000 Location and product summary location-product-summary TXT (fixed width) 14.41 8.96 46.41 / 49.45 128.31 / 128.63 12.58 494.14 KiB 21,546
1,000 Location and product summary location-product-summary XLSX 15.36 9.04 50.03 / 51.59 131.20 / 131.56 12.08 27.40 KiB 19,988
1,000 Variance summary variance-summary CSV 13.03 7.99 77.84 / 81.47 124.94 / 126.86 7.85 76.25 KiB 12,847
1,000 Variance summary variance-summary TXT (fixed width) 12.33 7.76 92.72 / 96.05 124.04 / 124.46 8.78 787.11 KiB 10,785
1,000 Variance summary variance-summary XLSX 12.98 8.20 85.16 / 91.70 122.97 / 123.20 6.40 39.85 KiB 11,743
10,000 Raw records inventory-raw-data CSV 9.75 5.81 195.52 / 197.12 223.54 / 227.62 8.96 1085.03 KiB 51,146
10,000 Raw records inventory-raw-data TXT (fixed width) 10.14 5.83 308.61 / 340.86 332.87 / 440.43 24.66 6894.53 KiB 32,404
10,000 Raw records inventory-raw-data XLSX 13.84 8.74 360.58 / 383.68 445.23 / 446.75 7.84 328.42 KiB 27,733
10,000 Inventory summary inventory-summary CSV 13.36 8.44 1057.09 / 1066.86 1089.95 / 1091.40 8.43 556.70 KiB 9,460
10,000 Inventory summary inventory-summary TXT (fixed width) 11.12 6.72 1061.72 / 1136.96 1089.69 / 1201.39 22.26 5917.97 KiB 9,419
10,000 Inventory summary inventory-summary XLSX 15.12 9.83 1131.21 / 1133.12 1199.37 / 1203.67 10.93 312.08 KiB 8,840
10,000 Location and product summary location-product-summary CSV 14.35 8.65 542.01 / 563.87 666.21 / 666.41 13.24 507.87 KiB 18,450
10,000 Location and product summary location-product-summary TXT (fixed width) 10.06 6.14 601.90 / 619.39 660.01 / 666.92 23.29 4941.41 KiB 16,614
10,000 Location and product summary location-product-summary XLSX 14.65 9.51 606.63 / 626.34 664.99 / 668.52 8.90 253.43 KiB 16,484
10,000 Variance summary variance-summary CSV 13.09 7.30 1053.62 / 1067.48 1087.13 / 1089.83 13.43 761.79 KiB 9,491
10,000 Variance summary variance-summary TXT (fixed width) 10.25 5.94 1119.48 / 1150.16 1197.97 / 1203.94 29.35 7871.09 KiB 8,933
10,000 Variance summary variance-summary XLSX 14.24 9.16 1145.26 / 1154.89 1203.71 / 1205.39 9.55 377.90 KiB 8,732

Each group is 3 measured samples; the 1 warmup is excluded. "Request to downloadable" includes the 100-millisecond polling interval, and "downloaded" is timed separately afterwards. Every output file reconciled against the source data.

6. What this did not test

Being straight about the edges of this test, because they decide what we should not promise yet.

7. How to trust this

Run def26ed0 · version 20261011-exports · code under test a21df821 (with uncommitted changes to the experiment scripts) · measured 2026-10-11 · 1.69 minutes end to end, including build, setup and cleanup · fixed catalogue of 10,000 products, 100% matching. Every figure on this page was generated by tests/performance/20261011-exports/page.mjs from the measured result files in tests/performance/20261011-exports/reports; none is typed in by hand.

24 groups, 72 measurements, all reconciled 3 samples per group: medians and maxima only 100% matching, fixed 10,000-product catalogue One job at a time, not a throughput number