Benchmarks
The xqdb repository includes two benchmark harnesses: a Node.js suite and a Python suite. Each compares xqdb with every other q IPC client that can be measured legally and technically, all against one fixed q server. Every number on this page is a client-side measurement. None of it is a claim about kdb+ or KDB-X performance.
These numbers come from the v0.1.3 codec
The reports in benchmarks/results/ at the v0.1.8 tag were measured on the v0.1.3 codec. The Node.js suite recorded @xbbg/xqdb 0.1.3, and the Python suite recorded a build of the v0.1.3 commit. Release 0.1.4 changed nothing the suite measures. Releases 0.1.5 to 0.1.8 did change the codec: fallible allocations, 40-bit message framing, lossy text decoding, symbol lists validated in one pass instead of two, and empty dictionaries. The checked-in reports have not been updated for those releases.
The release notes for 0.1.5 include a separate regression check against kola in Python, with 100,000 rows and 50 iterations, compared within each run. It measured xqdb against kola at 43.3 ms vs 96.3 ms for read wide, 12.4 ms vs 15.7 ms for read trade, 14.3 ms vs 17.1 ms for read depth, and 17.9 ms vs 22.4 ms for send trade.
Results at a glance
These results compare the fastest median for each operation, exactly as measured:
- Node.js. xqdb had the fastest median on all six table operations. The next fastest client took between 1.31× (
send wide) and 33.8× (read wide) as long. jkdb had the fastest scalar round trip, 0.346 ms against xqdb's 0.351 ms, a difference of 5 µs. - Python. xqdb with the PyArrow backend had the fastest median on all seven operations. In the scalar round trip, which measures the latency floor of a single request, the xqdb backends and kola finished within 0.02 ms of each other.
In each chart below, the filled bar is the median and the outlined bar is the 90th percentile (p90), both in milliseconds. Lower is better. The highlighted row has the fastest median for that operation.
Node.js
Reads
Query a 100,000-row table and decode it into the client's own representation.
Trade table (14 columns)
- xqdb0.1.3(fastest median)median: 24.8 ms p90: 26.9 ms
- jkdb1.4.0median: 186.9 ms p90: 203.9 ms
- node-q2.7.0median: 198.1 ms p90: 215.5 ms
Wide table (64 columns)
- xqdb0.1.3(fastest median)median: 91.0 ms p90: 102.9 ms
- jkdb1.4.0median: 3,073 ms p90: 3,298 ms
- node-q2.7.0median: 3,198 ms p90: 3,368 ms
Depth table (nested lists)
- xqdb0.1.3(fastest median)median: 26.3 ms p90: 28.7 ms
- jkdb1.4.0median: 203.0 ms p90: 211.8 ms
- node-q2.7.0median: 201.6 ms p90: 216.0 ms
Sends
Send the client's own decoded table back to q. node-q is excluded because its decoded tables do not re-encode to a q-identical value.
Trade table (14 columns)
- xqdb0.1.3(fastest median)median: 26.7 ms p90: 70.1 ms
- jkdb1.4.0median: 46.4 ms p90: 88.1 ms
- node-q2.7.0median: not comparable
Wide table (64 columns)
- xqdb0.1.3(fastest median)median: 137.1 ms p90: 144.2 ms
- jkdb1.4.0median: 179.4 ms p90: 194.6 ms
- node-q2.7.0median: not comparable
Depth table (nested lists)
- xqdb0.1.3(fastest median)median: 28.8 ms p90: 71.8 ms
- jkdb1.4.0median: 54.9 ms p90: 95.1 ms
- node-q2.7.0median: not comparable
Scalar round trip
Scalar round trip
- xqdb0.1.3median: 0.351 ms p90: 0.377 ms
- jkdb1.4.0(fastest median)median: 0.346 ms p90: 0.380 ms
- node-q2.7.0median: 0.361 ms p90: 0.404 ms
| Operation | Payload | xqdb 0.1.3 | jkdb 1.4.0 | node-q 2.7.0 |
|---|---|---|---|---|
read trade | 10.5 MiB | ★ 24.8 ms · 424 MiB/s | 186.9 ms (7.55×) | 198.1 ms (8.00×) |
read wide | 48.6 MiB | ★ 91.0 ms · 535 MiB/s | 3,073 ms (33.8×) | 3,198 ms (35.2×) |
read depth | 10.8 MiB | ★ 26.3 ms · 409 MiB/s | 203.0 ms (7.71×) | 201.6 ms (7.66×) |
send trade | 10.5 MiB | ★ 26.7 ms · 393 MiB/s | 46.4 ms (1.74×) | not comparable |
send wide | 48.6 MiB | ★ 137.1 ms · 355 MiB/s | 179.4 ms (1.31×) | not comparable |
send depth | 10.8 MiB | ★ 28.8 ms · 374 MiB/s | 54.9 ms (1.90×) | not comparable |
scalar round trip | none | 0.351 ms | ★ 0.346 ms (0.99×) | 0.361 ms (1.03×) |
Each cell gives the median. ★ marks the fastest median. Ratios are each client's median divided by xqdb's. Throughput is the payload size divided by xqdb's median.
Python
Reads
Each client materializes its own frame type: kola returns Polars and qconnect returns pandas. xqdb is shown with all three backends.
Trade table (14 columns)
- xqdbpyarrow(fastest median)median: 14.6 ms p90: 15.7 ms
- xqdbpolarsmedian: 15.3 ms p90: 16.6 ms
- xqdbpandasmedian: 17.6 ms p90: 20.0 ms
- kola2.5.1median: 17.7 ms p90: 19.6 ms
- qconnect0.1.6median: 97.7 ms p90: 104.1 ms
Wide table (64 columns)
- xqdbpyarrow(fastest median)median: 50.3 ms p90: 55.1 ms
- xqdbpolarsmedian: 51.2 ms p90: 56.3 ms
- xqdbpandasmedian: 54.7 ms p90: 70.6 ms
- kola2.5.1median: 105.8 ms p90: 139.5 ms
- qconnect0.1.6median: 191.2 ms p90: 239.3 ms
Depth table (nested lists)
- xqdbpyarrow(fastest median)median: 18.3 ms p90: 22.8 ms
- xqdbpolarsmedian: 19.1 ms p90: 22.4 ms
- xqdbpandasmedian: 35.7 ms p90: 43.7 ms
- kola2.5.1median: 20.9 ms p90: 25.3 ms
- qconnect0.1.6median: 11,711 ms (beyond the chart scale) p90: 14,311 ms
Sends
kola is excluded from send depth because it aborts the interpreter when it sends a frame with list columns.
Trade table (14 columns)
- xqdbpyarrow(fastest median)median: 20.3 ms p90: 60.8 ms
- xqdbpolarsmedian: 20.6 ms p90: 62.9 ms
- xqdbpandasmedian: 23.3 ms p90: 64.1 ms
- kola2.5.1median: 24.5 ms p90: 65.8 ms
- qconnect0.1.6median: 64.0 ms p90: 104.3 ms
Wide table (64 columns)
- xqdbpyarrow(fastest median)median: 111.8 ms p90: 133.8 ms
- xqdbpolarsmedian: 115.7 ms p90: 147.4 ms
- xqdbpandasmedian: 131.5 ms p90: 183.3 ms
- kola2.5.1median: 124.0 ms p90: 162.1 ms
- qconnect0.1.6median: 262.9 ms p90: 338.6 ms
Depth table (nested lists)
- xqdbpyarrow(fastest median)median: 23.6 ms p90: 66.2 ms
- xqdbpolarsmedian: 23.8 ms p90: 66.6 ms
- xqdbpandasmedian: 54.1 ms p90: 95.5 ms
- kola2.5.1median: not comparable
- qconnect0.1.6median: 2,186 ms (beyond the chart scale) p90: 2,568 ms
Scalar round trip
Scalar round trip
- xqdbpyarrow(fastest median)median: 0.347 ms p90: 0.417 ms
- xqdbpolarsmedian: 0.366 ms p90: 0.395 ms
- xqdbpandasmedian: 0.352 ms p90: 0.401 ms
- kola2.5.1median: 0.350 ms p90: 0.419 ms
- qconnect0.1.6median: 0.400 ms p90: 0.467 ms
| Operation | xqdb pyarrow | xqdb polars | xqdb pandas | kola 2.5.1 | qconnect 0.1.6 |
|---|---|---|---|---|---|
read trade | ★ 14.6 ms · 717 MiB/s | 15.3 ms (1.04×) | 17.6 ms (1.20×) | 17.7 ms (1.21×) | 97.7 ms (6.68×) |
read wide | ★ 50.3 ms · 967 MiB/s | 51.2 ms (1.02×) | 54.7 ms (1.09×) | 105.8 ms (2.10×) | 191.2 ms (3.80×) |
read depth | ★ 18.3 ms · 590 MiB/s | 19.1 ms (1.05×) | 35.7 ms (1.95×) | 20.9 ms (1.14×) | 11,711 ms (642×) |
send trade | ★ 20.3 ms · 517 MiB/s | 20.6 ms (1.01×) | 23.3 ms (1.15×) | 24.5 ms (1.21×) | 64.0 ms (3.15×) |
send wide | ★ 111.8 ms · 435 MiB/s | 115.7 ms (1.04×) | 131.5 ms (1.18×) | 124.0 ms (1.11×) | 262.9 ms (2.35×) |
send depth | ★ 23.6 ms · 457 MiB/s | 23.8 ms (1.01×) | 54.1 ms (2.30×) | not comparable | 2,186 ms (92.7×) |
scalar round trip | ★ 0.347 ms | 0.366 ms (1.05×) | 0.352 ms (1.01×) | 0.350 ms (1.01×) | 0.400 ms (1.15×) |
Ratios are each subject's median divided by the median of xqdb with PyArrow. Because kola and qconnect produce different frame types, the report also compares each of them with the xqdb backend that builds the same frame type:
| Operation | kola vs xqdb polars | qconnect vs xqdb pandas |
|---|---|---|
read trade | 1.16× | 5.55× |
read wide | 2.06× | 3.49× |
read depth | 1.10× | 328× |
send trade | 1.19× | 2.75× |
send wide | 1.07× | 2.00× |
send depth | not comparable | 40.4× |
scalar round trip | 0.96× | 1.14× |
A ratio below 1 means the other client was faster. kola's scalar round trip median was 4% lower than that of xqdb with Polars.
Fixture
The server is KDB-X 5.0 (build 2026.07.23) running in a container on the same machine as the client, reached over loopback TCP. Its init.q sets the random seed with \S 42 and publishes three tables of 100,000 rows each:
| Table | Columns | What it stresses | Payload (count -8!t) |
|---|---|---|---|
trade | 14 | Narrow table: a symbol, a timestamp, a long, a char vector, and 10 floats | 11,000,178 bytes (10.5 MiB) |
wide | 64 | Column count | 51,000,768 bytes (48.6 MiB) |
depth | 5 | Nested lists: ask and bid hold a list of 5 floats per row | 11,300,077 bytes (10.8 MiB) |
Before any timing, every client is asked for the row count, the q version, and the seed, and the run stops unless all clients agree.
| Suite | Runtime | Clients |
|---|---|---|
| Node.js | Node.js 26.3.0 and apache-arrow 21.2.0 on Windows x64 | @xbbg/xqdb 0.1.3; jkdb 1.4.0 with useBigInt: true; node-q 2.7.0 with flipTables: false |
| Python | CPython 3.12.13 on Windows x64, with narwhals 2.25.0, pyarrow 25.0.1, pandas 3.0.5, polars 1.44.1, and numpy 2.5.2 | xqdb (v0.1.3 codec) with its PyArrow, Polars, and pandas backends; kola 2.5.1; qconnect 0.1.6 with TLS disabled for parity |
Method
| Operation | Work timed |
|---|---|
scalar | A 6f*7f round trip, the latency floor of a single request |
read <table> | Query the table and build the client's own frame type |
send <table> | Send the client's own decoded frame to {[x]count x} |
- Rounds. 3 warm-up runs, then 50 measured rounds per client per operation. Each round runs every client once, in an order reshuffled from seed 42. A simple rotation is not enough, because it keeps each client behind the same predecessor every round.
- Timing.
process.hrtime.bigint()ortime.perf_counter_ns()around a single request, with one request in flight per client. Percentiles use the nearest rank of the raw durations. - Validation. Every measured result is checked after its timed interval.
- Throughput. The payload size, the server's
count -8!tfor the table, divided by the median duration. It is one logical size shared by every client, not the bytes observed on the wire.
Comparability
The suite ranks speed only when clients do the same work:
scalarmust return the exact value.readis ranked for every client that can decode the table, because the server sends the same bytes to all of them.sendis ranked only for clients whose decoded frame re-encodes to a value that q reports as identical (~) and that has the samecount -8!size. Otherwise the client would be timed encoding a different value.- Exactness of int64 values and nanosecond timestamps is checked before timing and is never timed itself.
Fidelity findings
- xqdb. Round-trips all three tables to q-identical values. Python raises
ValueErrorrather than silently truncating a sub-microsecond timestamp atom into adatetime. - jkdb 1.4.0. Round-trips all three tables to q-identical values. Sub-millisecond timestamps need
includeNanosecond: true, which turns temporal values into text that its own encoder then rejects, so that check runs on a second connection. - node-q 2.7.0. Decodes int64 values to doubles (
9007199254740993reads back as9007199254740992) and timestamps to millisecondDatevalues. None of its decoded tables re-encodes to a q-identical value, so it is excluded from everysend. Its other documented modes restore exactness but roughly double decode time. - kola 2.5.1. Rounds nanosecond timestamps to microseconds without warning. When sending a frame with list columns at 100,000 rows, its serializer panics and aborts the process, so it is excluded from
send depth. Itsdepthread is unaffected, but the read's round trip could not be verified. - qconnect 0.1.6. Decodes int64 values and nanosecond timestamps exactly.
Not measured
| Candidate | Reason |
|---|---|
| PyKX | The license for its bundled q runtime forbids publishing performance comparisons. |
| qpython, qpython3 | They need numpy below 1.20 and Python 3.9 or older. qconnect is the maintained fork and is measured instead. |
| PyQ | It embeds Python inside q rather than acting as a client. |
npm kx, qnode | Unrelated packages that happen to use those names. KX publishes no Node.js q client on npm. |
Reproduce
The harnesses, the fixture, and the raw JSON reports are in benchmarks/ in the repository. Building the fixture container needs your own KX download token and KDB-X license. Each run prints a summary and writes a JSON report. The reports also record a memory diagnostic, which is not shown here because it measures the whole process and is too noisy to compare libraries.
