Mirexa, QuestDB, ClickHouse, TimescaleDB, TimeBase and InfluxDB 1, 2 and 3, run on one laptop with the same 10 million candles, the same 10 million trades, the same batch sizes and the same queries.
Each metric's three-run mean as a ratio to the best system, combined by geometric mean: 1.00 would be best at everything. Rollups are left out because Mirexa does not do them yet (planned: Phase 8.1 materialization).
Ten million 1-minute candles, 8 writers, 10,000 per request, every acknowledged write fsynced (each system's durable setting). TimeBase is the exception, marked relaxed and drawn pale: its loader streams with no per-batch acknowledgement, and whether it fsyncs is not verified. Bars are the mean of three runs; the whisker is the slowest to fastest run.
Single-client latency, measured in the client including decoding the response. Bars are the mean of the three runs' medians; histograms pool every query of all three runs.
From each project's documentation, not measured here, except where a cell says so. Where a row matters to candle data, Mirexa's answer is checked against its own tests.
| Mirexa | QuestDB | ClickHouse | TimescaleDB | InfluxDB 1 | InfluxDB 2 | InfluxDB 3 Core | TimeBase CE | |
|---|---|---|---|---|---|---|---|---|
| Query language | binary range, latest-N and multi-range requests | SQL | SQL | SQL (Postgres) | InfluxQL | Flux, InfluxQL | SQL, InfluxQL | QQL (SQL-like) and a cursor API |
| Server-side rollups | no | SAMPLE BY | GROUP BY | time_bucket | GROUP BY time | aggregateWindow | date_bin | QQL OVER TIME |
| Acked write is fsynced, as shipped | yes | no (commit mode nosync) | no | yes | yes (WAL fsync delay 0) | yes | waits for the 1 s WAL flush to the object store; fsync on the file store not verified | the loader streams with no per-batch acknowledgement; fsync not verified |
| Market closed vs data lost | recorded per hole at write time; a closure cannot be filled | absent rows; FILL invents values at query time | absent rows; WITH FILL at query time | absent rows; time_bucket_gapfill at query time | absent rows; fill() | absent rows; fill() | absent rows; date_bin_gapfill | absent rows |
| Restating a stored bar | a patch chunk over the flushed chunk; each later correction folds in the earlier ones | DEDUP UPSERT KEYS on the table | ReplacingMergeTree, or ALTER … UPDATE | UPDATE | a write to the same series and time replaces it | same as 1 | same as 1 | loader write modes; the default, REWRITE, replaces the time range written |
| Push new rows to a client | SUBSCRIBE: history, then live | no | no | logical replication, not a client feed | server-side write forwarding only | no | no | live cursors: history, then live |
| Replication in the free edition | primary and copies, automatic failover (off by default) | Enterprise | ReplicatedMergeTree with Keeper | Postgres streaming replication | Enterprise | no | Enterprise | not verified |
| Licence | this repository | Apache 2.0 | Apache 2.0 | Apache 2.0 core; some features under the Timescale License | MIT | MIT | MIT or Apache 2.0 | Apache 2.0 |
Each cell is the mean of three runs; hover for every run. Latencies are the mean of the runs' medians, with the pooled p99 after the slash.
Isolation. Every chart comes from three rounds, run on 2026-10-17, each round running every system once, so slow drift in the machine falls on all of them alike. The system under test was pinned to CPUs 0–5 and the load generator to 6–7; every other process on the machine, and every other container, was fenced onto the four efficiency cores, 8–11 (bench/compare/fence.sh, CPU affinity, no root). Not isolated: root-owned processes, the shared L3 cache, memory bandwidth and the disk. From the multi-symbol rounds on, each run also waited for a quiet machine before starting (1-minute load under 9, disk pressure under 5%). Five tick runs that started while another session's test suite was loading the disk were discarded and run again. The laptop was on AC power throughout.
Each system ran alone on the host network, from an empty data directory, at its stock configuration except for the one durability switch below. The load generator is one Go program (bench/compare) with an adapter per system; TimeBase, which has no Go client, ran the same phases and queries through its Java client (bench/compare/tbbench), fed from a dump of the same data. Every system received byte-identical records in the same order.
symbol IN (…) ORDER BY ts; QuestDB LATEST ON … PARTITION BY symbol; ClickHouse max(ts), argMax(…) per symbol; TimescaleDB and InfluxDB 3 DISTINCT ON (symbol); InfluxDB 1 and 2 InfluxQL without GROUP BY (it merges series by time) and last() per symbol; TimeBase one cursor over all the symbols (reversed for the snapshot); Mirexa one multi-query request, merged into time order by the client inside the timing. 30, 10, 50 and 50 queries; 10 of each for InfluxDB 3.| System | As shipped | Durable run | Relaxed run | Client protocol |
|---|---|---|---|---|
| Mirexa | durable | wal.sync_policy: always | sync_policy: interval (1 s) | its own binary protocol, Go SDK; candles at its default settings, trades at whichever of its default and tuned retry settings ingested faster (the Trades section says which) |
| QuestDB | relaxed | QDB_CAIRO_COMMIT_MODE=sync | default | ILP over HTTP; SQL over HTTP, JSON |
| ClickHouse | relaxed | fsync_after_insert=1, fsync_part_directory=1 | default | HTTP, TSV both ways |
| TimescaleDB | durable | default | synchronous_commit=off | Postgres wire (pgx), COPY in |
| InfluxDB 1 | durable | default | wal-fsync-delay=100ms | line protocol; InfluxQL, JSON |
| InfluxDB 2 | durable | default | storage-wal-fsync-delay=100ms | line protocol; InfluxQL via v1 compatibility, JSON |
| InfluxDB 3 Core | durable | default | no_sync=true per write | line protocol; SQL, JSON lines |
| TimeBase CE | not verified | – | stock server; 8 loaders, APPEND, each feeding its symbols in time order, flushed only at the end | dxtick, official Java client; QQL for rollups |
Reproduce with bench/compare/run.sh <system> <durable|relaxed> (ARGS="-workload ticks" for trades, ARGS="-only-multi" for the multi-symbol queries) and bench/compare/report.py. Raw results are in bench/compare/results-v2/, results-ticks/, results-multi-candles/ and results-multi-ticks/.