mirexa.
Mirexa · single-node benchmark

Market data benchmark.

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.

Overall

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).

Writing candles

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.

Mirexaothers│ range of three runs

Ingest throughput

rows per second, higher is better

How long each batch waited for its ack

every 10,000-candle request from all three runs · log scale · tick = median, thin tick = p99

Reading candles

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.

One day of one stream

1,440 rows · median ms

Latest 100 bars

newest first · median ms

A whole stream

200,000 rows · median ms

Hourly OHLC over 30 days

server-side rollup · median ms

One-day query, distribution

log scale · tick = median, thin tick = p99

Latest 100, distribution

log scale

One-day queries from 8 clients at once

queries per second over 10 s, higher is better

8-client query latency, distribution

every query of the 10 s phase, all three runs · log scale

Candle footprint

Table size

MiB for 10M candles, each system's own accounting, WAL excluded

Peak memory

MiB of heap and shared memory, not page cache

What each one does

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.

MirexaQuestDBClickHouseTimescaleDBInfluxDB 1InfluxDB 2InfluxDB 3 CoreTimeBase CE
Query languagebinary range, latest-N and multi-range requestsSQLSQLSQL (Postgres)InfluxQLFlux, InfluxQLSQL, InfluxQLQQL (SQL-like) and a cursor API
Server-side rollupsnoSAMPLE BYGROUP BYtime_bucketGROUP BY timeaggregateWindowdate_binQQL OVER TIME
Acked write is fsynced, as shippedyesno (commit mode nosync)noyesyes (WAL fsync delay 0)yeswaits for the 1 s WAL flush to the object store; fsync on the file store not verifiedthe loader streams with no per-batch acknowledgement; fsync not verified
Market closed vs data lostrecorded per hole at write time; a closure cannot be filledabsent rows; FILL invents values at query timeabsent rows; WITH FILL at query timeabsent rows; time_bucket_gapfill at query timeabsent rows; fill()absent rows; fill()absent rows; date_bin_gapfillabsent rows
Restating a stored bara patch chunk over the flushed chunk; each later correction folds in the earlier onesDEDUP UPSERT KEYS on the tableReplacingMergeTree, or ALTER … UPDATEUPDATEa write to the same series and time replaces itsame as 1same as 1loader write modes; the default, REWRITE, replaces the time range written
Push new rows to a clientSUBSCRIBE: history, then livenonological replication, not a client feedserver-side write forwarding onlynonolive cursors: history, then live
Replication in the free editionprimary and copies, automatic failover (off by default)EnterpriseReplicatedMergeTree with KeeperPostgres streaming replicationEnterprisenoEnterprisenot verified
Licencethis repositoryApache 2.0Apache 2.0Apache 2.0 core; some features under the Timescale LicenseMITMITMIT or Apache 2.0Apache 2.0

Every number

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.

How it was run

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.

  • Candles. 50 symbols × 200,000 one-minute bars (≈139 days each), ten million rows. Prices are a random walk in cents with realistic volumes; timestamps end at the current minute.
  • Trades. 50 symbols × 200,000 trades, ten million rows, with irregular arrivals averaging 50 ms apart (≈2.8 hours each) and nanosecond timestamps, strictly increasing per symbol.
  • Writes. 8 concurrent writers, 10,000 records per request, each symbol sent in time order. Throughput is rows divided by wall time until the last request was acknowledged.
  • Visibility. After the last acknowledgement the harness counts rows until all ten million are readable.
  • Single-symbol reads. Candles: 300 one-day ranges, 300 latest-100, 50 hourly rollups over 30 days, 10 whole streams. Trades: 300 one-minute ranges, 30 one-hour ranges, 300 latest-1,000, 50 one-minute OHLC rollups over an hour, 5 whole symbols. Then 8 clients issuing short ranges for 10 s. InfluxDB 3 ran 50 latest-N queries instead of 300, because each takes over a second.
  • Many symbols at once. A replay of 10 random symbols over one day (trades: 5 symbols, 10 minutes), a replay of every symbol over one day (trades: one minute), each merged into a single time-ordered sequence, and a snapshot of every symbol's newest record, now and as of a random past moment. Idioms: SQL 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.
  • Correctness. Every query's row count is checked against the generated data; replays also for time order, snapshots per symbol. Query bounds on trades fall on whole microseconds, because QuestDB and TimescaleDB store microseconds and a bound inside one makes the count ambiguous. Tick rounds run before that was fixed are re-run, or marked where not.
  • Footprint. Table size is each system's own accounting, 60 s after the reads. Memory is the peak of heap plus shared memory sampled every 250 ms; mapped files and page cache are excluded, which flatters systems that read through mmap.
SystemAs shippedDurable runRelaxed runClient protocol
Mirexadurablewal.sync_policy: alwayssync_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)
QuestDBrelaxedQDB_CAIRO_COMMIT_MODE=syncdefaultILP over HTTP; SQL over HTTP, JSON
ClickHouserelaxedfsync_after_insert=1, fsync_part_directory=1defaultHTTP, TSV both ways
TimescaleDBdurabledefaultsynchronous_commit=offPostgres wire (pgx), COPY in
InfluxDB 1durabledefaultwal-fsync-delay=100msline protocol; InfluxQL, JSON
InfluxDB 2durabledefaultstorage-wal-fsync-delay=100msline protocol; InfluxQL via v1 compatibility, JSON
InfluxDB 3 Coredurabledefaultno_sync=true per writeline protocol; SQL, JSON lines
TimeBase CEnot verified–stock server; 8 loaders, APPEND, each feeding its symbols in time order, flushed only at the enddxtick, official Java client; QQL for rollups
Read these numbers with
  • TimeBase is not durable-for-durable. Its loader streams with no per-batch acknowledgement, and whether its server fsyncs is not verified here, so it ran once, in the mode it is used in. Flushing after every 10,000 records, the closest thing to a per-request acknowledgement, cut its ingest to 72K–132K rows/s; it was the round trip, not the server, that was busy.
  • TimeBase returned wrong answers. Loaded by concurrent loaders, it sometimes handed a symbol's bars back out of position, though every bar was stored (see Wrong answers). Its read timings include those runs.
  • A shared machine. Other work kept running on the efficiency cores; the fence keeps it off the benchmark's cores but not out of the shared cache, memory bandwidth or disk. The range across three runs is shown on every throughput bar; read differences inside it as ties.
  • No tuning. TimescaleDB ran without compression, ClickHouse without partitioning, QuestDB and InfluxDB 3 at default WAL settings, TimeBase at its stock server settings. Each could be made faster or smaller by someone who knows it well; so could Mirexa.
  • Different wire formats. Mirexa, TimescaleDB and TimeBase answer in binary; the others in JSON or TSV over HTTP, and decoding is included in the latency. That is how a client would see them, and it favours the binary protocols on large results.
  • The workload favours per-symbol storage. Most queries ask about one symbol. The multi-symbol section is there to show the other side.
  • One laptop, one disk, single node. Mirexa's clustering is not part of this test.

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/.