Benchmark and comparison: QuestDB vs. ClickHouse
Benchmarks and an overview of ClickHouse versus QuestDB to compare features, functionality, performance, and ease of use.
ClickHouse is one of the most widely used analytical databases. It started at Yandex for web analytics and now runs observability, product analytics and general OLAP at very large scale, with a broad ecosystem behind it. QuestDB is the leading time-series database for streaming ingestion and low-latency queries, used for mission-critical workloads where throughput is high and data cannot be lost, capital markets first among them. Teams looking for a fast analytical engine often end up evaluating both.
ClickHouse is the broader analytical database. QuestDB is the database for the live, authoritative event record: rows arrive continuously, none of them can be lost, and the same SQL engine serves the newest state in milliseconds and years of history. Choose ClickHouse for log monitoring and observability, product analytics, semi-structured data and general OLAP. Choose QuestDB for the data a business runs on: market data and trades, telemetry from an energy plant, a rocket or a fleet of robots, where a single lost record has consequences.
This article runs the two through the same TSBS benchmark on the same machine, ClickHouse 26.7.5.10 against QuestDB 9.3.3, publishes every number, and then looks at where they differ in architecture and focus.
These benchmarks predate QWP, the binary protocol we shipped in QuestDB 10.0 in August 2026. Ingestion here runs over InfluxDB Line Protocol, a text format the server parses row by row. QWP sends binary columns instead, up to 3.6x faster over a network, and it streams reads back as Arrow. Read the QuestDB figures below as a floor.
Two databases, different strengths
QuestDB is built for streaming ingestion and low-latency queries, at any batch size from a single row up. It came out of trading floors, eFX desks at tier-1 banks, and today sits on the critical path in trading, defense, energy and robotics systems. It is lightweight and runs out of the box. Zero-GC Java and C++, Apache 2.0; the Enterprise edition adds a Rust layer for high availability and scaling.
ClickHouse is an OLAP engine with a much wider range. It started at Yandex for web and ad analytics and now runs log monitoring, observability and product analytics, positioned against tools like Datadog and as the destination for data coming out of Postgres. Batch loading is its strength, streaming is not, and it is moving toward the warehouse model. C++, Apache 2.0.
| Aspect | QuestDB | ClickHouse |
|---|---|---|
| License | Apache 2.0 | Apache 2.0 |
| Implementation | Zero-GC Java, C++, Rust | C++ |
| Query language | SQL with time-series extensions | SQL (ClickHouse dialect) |
| Data model | Relational (tables + rows) | Relational (tables + rows) |
| Ingestion protocols | QWP (binary), ILP, PostgreSQL wire, HTTP | HTTP, Native protocol, Kafka, OpenTelemetry |
| Primary use case | Time series, capital markets, streaming | OLAP, observability (logs/traces/metrics) |
Performance benchmarks
Summary: QuestDB ingests between 7.1 and 8.6 million rows per second where ClickHouse holds about 1.7 million. On queries the two trade wins. QuestDB is faster on the short single-host reads, on cpu-max-all and on lastpoint; ClickHouse is faster on double-groupby-1 and double-groupby-5, on the full-table scan and on top-N.
We use the open-source Time Series Benchmark Suite (TSBS) for all benchmarks, which supports both ClickHouse and QuestDB out of the box. TSBS was created by Timescale and has seen no real development upstream since 2023, so we maintain our own fork.
Hardware: AWS EC2 r8a.8xlarge (32 vCPU, 256 GB RAM, AMD EPYC), GP3 EBS storage (20,000 IOPS, 1 GB/s throughput)
Software: Ubuntu 22.04, ClickHouse 26.7.5.10, QuestDB 9.3.3, all with default configurations.
Measured: ClickHouse 26.7.5.10, benchmarked August 2026. QuestDB 9.3.3, on the same r8a.8xlarge instance. New benchmarks on QuestDB 10.0 are coming.
Core allocation: the TSBS client and the database run on the same instance with no CPU pinning.
Query statistic: each figure is the median latency within a run, averaged over 3 runs, with a warm query cache.
Ingestion benchmark
We test a cpu-only scenario with two days of CPU data for various numbers of simulated hosts (1K, 4K, 100K, and 1M). This tests how each database handles increasing data volumes and cardinality.
We start at 1,000 hosts. Smaller datasets finish in under a second on the faster engines, where setup cost dominates and the result stops measuring throughput.
In time-series databases, high cardinality means having many unique values in indexed columns (millions of unique symbols, account IDs, or trading venues). More hosts in this benchmark means higher cardinality.
Commands to generate and ingest data for 4K hosts at 10s intervals:
$ ./tsbs_generate_data --use-case="cpu-only" --seed=123 --scale=4000 \--timestamp-start="2016-01-01T00:00:00Z" \--timestamp-end="2016-01-03T00:00:00Z" \--log-interval="10s" --format="clickhouse" > /tmp/clickhouse_data$ ./tsbs_load_clickhouse --db-name=benchmark --file=/tmp/clickhouse_data \--host=localhost --workers=32
The same data, seed and worker count against QuestDB:
$ ./tsbs_generate_data --use-case="cpu-only" --seed=123 --scale=4000 \--timestamp-start="2016-01-01T00:00:00Z" \--timestamp-end="2016-01-03T00:00:00Z" \--log-interval="10s" --format="questdb" > /tmp/questdb_data$ ./tsbs_load_questdb --file=/tmp/questdb_data --workers=32
The results for ingestion with 32 workers:
↑ Higher is better
QuestDB peak rows/sec, at 100K hosts
ClickHouse peak rows/sec, at 4K hosts
| Scale | ClickHouse | QuestDB | QuestDB vs ClickHouse |
|---|---|---|---|
| 1,000 hosts | 1.74M rows/sec | 7.77M rows/sec | QuestDB 4.5x faster |
| 4,000 hosts | 1.76M rows/sec | 7.96M rows/sec | QuestDB 4.5x faster |
| 100,000 hosts | 1.75M rows/sec | 8.59M rows/sec | QuestDB 4.9x faster |
| 1,000,000 hosts | 1.67M rows/sec | 7.14M rows/sec | QuestDB 4.3x faster |
Both lines are flat. ClickHouse holds about 1.7M rows/sec from 1,000 hosts to 1M and QuestDB stays between 7.1M and 8.6M, so cardinality is not what separates them here. The gap is the cost of the write path per row.
QuestDB's write path
Close to a sequential append. Rows arrive with a designated timestamp, go into the WAL, and are committed to per-column files inside the partition their timestamp belongs to.
Throughput peaks when data arrives in time order and falls off as more of it arrives out of order: out-of-order rows are sorted in memory within a lag window, and anything beyond it is merged into the partition. This benchmark is nearly all in order, so nearly every commit is an append.
Batch size matters little. A single row and a batch of ten thousand take the same path, which is what a columnar model built around time gives you.
The hot tier is uncompressed, symbol columns such as the host name are dictionary-encoded, and there are no indexes to maintain on write. The CPU spent per row is small.
ClickHouse's write path
More work per insert, by design. A MergeTree behaves like a B+tree with batched updates: each insert becomes an immutable part, sorted by the table's ORDER BY key, compressed with LZ4, written with its sparse primary index, and background merges fold the parts together, rewriting the same rows several times as small parts combine into larger ones.
That gives it a lower ceiling and makes it sensitive to batch size. Small batches mean many parts and more merging competing with ingestion for CPU, which is why ClickHouse's own guidance is to insert in large blocks.
Async inserts do not change that, they move the batching to the server. Incoming rows are held in an in-memory buffer and flushed into a part when the buffer reaches 100 MiB, 200 ms have passed (1 s on ClickHouse Cloud) or 450 insert queries have accumulated, whichever comes first. Many clients can share one buffer, which is the point, but the write path is still batch, part, merge.
It can be tuned past the figure here, with larger blocks, async inserts and more merge threads, at the cost of configuration work. The QuestDB figures are its defaults.
The short version: QuestDB's write path is optimized for time series, ClickHouse's for general analytics workloads.
Getting data out is benchmarked separately. Streaming 500 million rows into Apache Arrow measures QuestDB 10.0's QWP, which returns query results as Arrow, against ClickHouse's native and Arrow paths on the same machine.
Query performance
Queries next, from the standard TSBS suite:
- single-groupby: Aggregate CPU metrics for random hosts over specified time ranges
- double-groupby: Aggregate across ALL hosts, grouped by host and time intervals
- cpu-max-all: Maximum of all ten CPU metrics across 1, 8 or 32 hosts
- high-cpu: Finding hosts with CPU utilization above threshold (single host and all hosts)
- lastpoint: Retrieving the most recent data point per host
- groupby-orderby-limit: Top-N aggregation with ordering
All queries target two days of 4000 emulated host data.
To run the benchmark:
$ ./tsbs_generate_queries --use-case="devops" --seed=123 --scale=4000 \--timestamp-start="2016-01-01T00:00:00Z" \--timestamp-end="2016-01-03T00:00:00Z" \--queries=1000 --query-type="single-groupby-1-1-1" \--format="clickhouse" > /tmp/clickhouse_query$ ./tsbs_run_queries_clickhouse --file=/tmp/clickhouse_query \--db-name=benchmark --workers=1
And against QuestDB:
$ ./tsbs_generate_queries --use-case="devops" --seed=123 --scale=4000 \--timestamp-start="2016-01-01T00:00:00Z" \--timestamp-end="2016-01-03T00:00:00Z" \--queries=1000 --query-type="single-groupby-1-1-1" \--format="questdb" > /tmp/questdb_query$ ./tsbs_run_queries_questdb --file=/tmp/questdb_query --workers=1
Single-groupby queries
Query names encode metrics, hosts and hours: 5-8-1 is five metrics, eight hosts, one hour. Every query on this page is described in the appendix.
↓ Lower is better
Query format: metrics-hosts-hours · median of 3 runs, warm cache
QuestDB, single-groupby-5-1-12
ClickHouse, single-groupby-5-1-12
| Query | ClickHouse | QuestDB | Best |
|---|---|---|---|
| single-groupby-1-1-1 | 3.26 ms | 0.41 ms | QuestDB 8.0x faster |
| single-groupby-1-1-12 | 3.67 ms | 1.43 ms | QuestDB 2.6x faster |
| single-groupby-1-8-1 | 3.76 ms | 0.91 ms | QuestDB 4.1x faster |
| single-groupby-5-1-1 | 3.85 ms | 0.60 ms | QuestDB 6.4x faster |
| single-groupby-5-1-12 | 4.48 ms | 1.94 ms | QuestDB 2.3x faster |
| single-groupby-5-8-1 | 5.60 ms | 1.17 ms | QuestDB 4.8x faster |
Double-groupby queries
These queries aggregate across ALL hosts, grouped by host and 1-hour intervals.
↓ Lower is better
Aggregates across ALL hosts, grouped by host and 1-hour intervals · median of 3 runs, warm cache
QuestDB, double-groupby-all
ClickHouse, double-groupby-all
ClickHouse, double-groupby-1
| Query | ClickHouse | QuestDB | Best |
|---|---|---|---|
| double-groupby-1 | 18.90 ms | 32.20 ms | ClickHouse 1.7x faster |
| double-groupby-5 | 33.50 ms | 41.20 ms | ClickHouse 1.2x faster |
| double-groupby-all | 56.70 ms | 56.10 ms | ~Tied |
CPU-max-all queries
Maximum of all ten CPU metrics, across 1, 8 and 32 hosts.
↓ Lower is better
Maximum of all 10 CPU metrics, across 1, 8 and 32 hosts
QuestDB, cpu-max-all-32-24
ClickHouse, cpu-max-all-32-24
| Query | ClickHouse | QuestDB | Best |
|---|---|---|---|
| cpu-max-all-1 | 5.30 ms | 1.48 ms | QuestDB 3.6x faster |
| cpu-max-all-8 | 8.70 ms | 5.40 ms | QuestDB 1.6x faster |
| cpu-max-all-32-24 | 17.30 ms | 5.37 ms | QuestDB 3.2x faster |
Heavy queries
Finding hosts with CPU utilization above threshold.
↓ Lower is better
Finding hosts with CPU utilization above threshold · median of 3 runs, warm cache
QuestDB, high-cpu-all
ClickHouse, high-cpu-all
| Query | ClickHouse | QuestDB | Best |
|---|---|---|---|
| high-cpu-1 | 3.89 ms | 3.31 ms | QuestDB 1.2x faster |
| high-cpu-all | 966.70 ms | 1,027.50 ms | ClickHouse 1.1x faster |
NB: QuestDB 10.0 was measured in the same run. Over QWP, its new binary protocol, it answers high-cpu-all in 672.60 ms, which would put QuestDB ahead of ClickHouse here. The tables on this page stay on 9.3.3, the version the whole suite was run against; full 10.0 results will come with our next benchmark suite rather than a TSBS re-run.
Lastpoint and additional queries
↓ Lower is better
QuestDB, lastpoint
ClickHouse, lastpoint
ClickHouse, groupby-orderby-limit
| Query | ClickHouse | QuestDB | Best |
|---|---|---|---|
| lastpoint | 25.60 ms | 1.70 ms | QuestDB 15x faster |
| groupby-orderby-limit | 11.97 ms | 16.76 ms | ClickHouse 1.4x faster |
The lastpoint query retrieves the most recent data point per host, one of the most common time-series operations. QuestDB's LATEST ON syntax is purpose-built for this pattern and completes in 1.70ms vs ClickHouse's 25.60ms. The groupby-orderby-limit query tests top-N aggregation, where ClickHouse is ahead.
NB: QuestDB 10.0 was measured in the same run. Over QWP, its new binary protocol, it answers groupby-orderby-limit in 11.75 ms. The tables on this page stay on 9.3.3, the version the whole suite was run against; full 10.0 results will come with our next benchmark suite rather than a TSBS re-run.
Why these differences?
Both are columnar databases, but they make different architectural trade-offs that explain these results:
Why QuestDB wins on ingestion and point queries:
- Designated timestamp lets QuestDB prune time ranges without scanning
- JIT compilation compiles query filters to machine code at runtime
- Time-partitioned storage organizes data physically by time, which matches how time-series queries actually access data
Where ClickHouse architecture pays off:
- Flexible sorting via MergeTree's ORDER BY can optimize for non-time-based access patterns
- Distributed queries with native sharding for horizontal scalability
- Observability stack with native JSON type, inverted indices for text search, and OpenTelemetry integration
Architecture
How data is stored
ClickHouse uses the MergeTree engine. Data is written to immutable parts (one compressed file per column + sparse index), which are continuously merged in the background. You define the sort order via ORDER BY, and rows within each part are physically sorted by that key.
CREATE TABLE trades (timestamp DateTime64(6),symbol String,exchange String,side String,price Float64,quantity Float64,trade_id UInt64) ENGINE = MergeTree()ORDER BY (symbol, timestamp);
QuestDB uses a three-tier architecture: a write-ahead log (WAL) for durability and out-of-order handling, time-partitioned columnar files for query performance, and optional Parquet cold storage on S3/Azure/GCS.
CREATE TABLE trades (timestamp TIMESTAMP,symbol SYMBOL,exchange SYMBOL,side SYMBOL,price DOUBLE,quantity DOUBLE,trade_id LONG) TIMESTAMP(timestamp) PARTITION BY DAY;
Tier One: Hot ingest (WAL), durable by default
Incoming data is appended to the write-ahead log (WAL) with ultra-low latency. Writes are made durable before any processing, preserving order and surviving failures without data loss. The WAL is asynchronously shipped to object storage, so new replicas can bootstrap quickly and read the same history.
Tier Two: Real-time SQL on live data
Data is time-ordered and de-duplicated into QuestDB's native, time-partitioned columnar format and becomes immediately queryable. Power real-time analysis with vectorized, multi-core execution, streaming materialized views, and time-series SQL (e.g., ASOF JOIN, SAMPLE BY). The query planner spans tiers seamlessly.
Tier Three: Cold storage, open and queryable
Older data is automatically tiered to object storage in Apache Parquet. Query it in-place through QuestDB or use any tool that reads Parquet. This delivers predictable costs, interoperability with AI/ML tooling, and zero lock-in.
The difference that matters: ClickHouse's MergeTree needs background merges to consolidate parts, and those merges compete with queries for resources. QuestDB's WAL commits go straight into the time-partitioned columnar files.
SQL and ecosystem
Both use SQL. ClickHouse has its own dialect; QuestDB extends standard SQL with time-series primitives. Here are two examples of QuestDB queries:
-- Hourly OHLCV bars for yesterdaySELECT timestamp, symbol, sum(quantity) AS volume, avg(price) AS avg_priceFROM tradesWHERE timestamp IN '$yesterday'SAMPLE BY 1h;-- Markout analysis: mid-price evolution-- at 1s, 5s, 30s, 1m, 5m after each tradeSELECT t.symbol, h.offset, avg(q.mid - t.price) AS markoutFROM trades tHORIZON JOIN quotes q ON (t.symbol = q.symbol)LIST (1s, 5s, 30s, 1m, 5m) AS hWHERE t.timestamp IN '$today#XNYS'GROUP BY t.symbol, h.offset;
| QuestDB Extension | Purpose | Example |
|---|---|---|
SAMPLE BY | Time-based aggregation | SELECT avg(price) FROM trades SAMPLE BY 1h |
LATEST ON | Last value per group | SELECT * FROM trades LATEST ON timestamp PARTITION BY symbol |
ASOF JOIN | Time-aligned joins | Join trades with quotes at nearest timestamps |
WINDOW JOIN | Rolling aggregation over time windows | Compute rolling stats from a related table within a time range around each row |
HORIZON JOIN | Multi-offset markout analysis | Measure price evolution at multiple time offsets after each trade in a single pass |
TICK syntax | Declarative timestamp filtering | WHERE ts IN '2025-01-[01..31]#XNYS;6h30m' |
Both databases have broad ecosystem support (Grafana, Kafka, all major client libraries, dbt). QuestDB speaks the PostgreSQL wire protocol, so any PG client library works out of the box. ClickHouse has a more mature ecosystem with a wider variety of table engines and community tooling.
Two directions: real time versus batch
QuestDB and ClickHouse differ less in raw engine speed than in what each is built around. QuestDB is built around the moment a row arrives. ClickHouse is built around the batch that was loaded.
Streaming versus batch
ClickHouse loads batches and queries them afterwards. Each insert becomes an immutable part, background merges consolidate the parts, and small frequent writes mean more merging competing with queries for CPU. Clients poll for new rows; nothing pushes them. Deduplicating on ingest is expensive on MergeTree; QuestDB does it in the WAL at commit.
QuestDB starts from the other end. Rows are queryable as soon as the WAL commits, and live views, in beta since 10.0, keep results such as a rolling VWAP current as each row arrives. Pub/sub is next on the roadmap, so a client subscribes to a live view and receives rows instead of polling. The directions diverge from here: ClickHouse is becoming a warehouse alongside Postgres, with Postgres replication built in; QuestDB is heading to the streaming end, live views, push delivery, Arrow out over QWP.
Time-series SQL
ClickHouse's SQL is a general analytics dialect. QuestDB's adds operators for the questions time-series users actually ask: SAMPLE BY, LATEST ON, ASOF JOIN, WINDOW JOIN, HORIZON JOIN and TICK syntax. Some have ClickHouse workarounds (GROUP BY toStartOfInterval(...), subqueries, self-joins); multi-offset markout analysis with HORIZON JOIN does not.
Where a workaround exists, the cost shows in the tables above: LATEST ON answers lastpoint in 1.7 ms where ClickHouse needs 25.6 ms.
Open formats
QuestDB runs two storage tiers under one SQL engine. Active partitions live in QuestDB's own native columnar format, uncompressed and memory-mapped, which is where the ingestion and query figures on this page come from. Older partitions are converted to Apache Parquet: on a schedule you set through a storage policy in QuestDB Enterprise, an hour or a day after they stop receiving writes, or with an ALTER TABLE in QuestDB open source. Every query runs across both tiers as one table.
Since 10.0 those Parquet files follow the conventions the Iceberg ecosystem expects, so DuckDB, Spark, Trino or pandas read them directly and they register into Iceberg or DuckLake without a copy. QuestDB Enterprise can also move them to S3, Azure Blob or GCS while keeping them queryable. Query results come back as Apache Arrow over QWP.
ClickHouse reads and writes Parquet and Arrow as import and export formats, through table functions such as s3() and file(), but its own storage is MergeTree, including what it tiers to object storage. Getting that data out in an open format means running a ClickHouse instance to convert it.
Operating each
ClickHouse gives you levers and expects you to use them: part counts (300 per partition by default before inserts start failing), merge scheduling, and the ORDER BY key, which is hard to change once the table exists. ClickHouse Cloud takes most of that on; self-hosted, it is yours. QuestDB has fewer knobs, and every number on this page is its default configuration.
Where each fits
ClickHouse covers more ground: product analytics, log search, ad-hoc BI over semi-structured data, and it has a larger ecosystem behind it, which shows in integrations and tooling. QuestDB is narrower by design: time series, from market data to any machine data, aerospace and robotics included, anywhere rows arrive fast and the questions are about time. That is where the streaming path and the time-series SQL are the point.
Is QuestDB a ClickHouse replacement?
Not across ClickHouse's full range. ClickHouse is excellent for observability, product analytics, semi-structured data, ad-hoc BI and warehouse-style analytics, and when broad analytical coverage is the priority it is usually the better choice, with a managed cloud service and a large ecosystem behind it.
Choose QuestDB where speed is mission critical and data cannot afford to be lost. Those workloads, in capital markets, aerospace, energy and robotics, are high-speed time series, and QuestDB's architecture is built for exactly that: high-rate ordered and out-of-order writes, the latest state available immediately, time-series joins and rolling calculations, and one SQL surface from live data to history on object storage. Data goes out as Parquet and Arrow, so nothing is locked in.
The benchmark shows the split. QuestDB ingests between 7.1 and 8.6 million rows per second where ClickHouse holds about 1.7 million, and it is faster on the short reads and the latest-point query; ClickHouse wins the broad grouped scans, the full-table scan and top-N. The difference underneath is architectural. ClickHouse is built for batch: data is loaded in large blocks, each becomes an immutable part, and background merges tidy up afterwards, which is why it likes big inserts and struggles with a constant stream of small ones. QuestDB is built for streaming: rows are written as they arrive, in order or out of order, become queryable as soon as they commit, and the latest state is available in milliseconds, with hot and cold data under one engine.
Ready to try QuestDB? Get started with the quickstart guide or join our Slack community to ask questions.
For a single-workload comparison showing how QuestDB's dedicated SQL operators affect performance vs. standard SQL rewrites on ClickHouse, see How we made WINDOW JOIN parallel and vectorized. This is not a standardized benchmark like TSBS.
Appendix: what each TSBS query does
Query names encode their parameters: single-groupby-5-8-1 is five metrics, eight hosts, one hour.
| Query | What it measures |
|---|---|
| single-groupby-M-H-T | Maximum of M CPU metrics for H random hosts, in 5-minute buckets over T hours. 1-1-1 is one metric, one host, one hour; the -12 variants cover twelve hours |
| double-groupby-M | Average of M CPU metrics for every host, grouped by host and by hour, over 24 hours. all is all ten metrics |
| cpu-max-all-H | Maximum of all ten CPU metrics for H random hosts, per hour. -1 and -8 cover one hour; -32-24 covers 32 hosts over 24 hours |
| high-cpu-1 | Every reading where one metric is above a threshold, for one random host |
| high-cpu-all | The same threshold scan across every host, which is a full-table scan |
| lastpoint | The most recent reading for every host |
| groupby-orderby-limit | The last five per-minute aggregates before a random point in time, ordered by time, a top-N query |
Definitions follow the TSBS README. Every query is generated with seed 123 against the 4,000-host dataset, so each engine answers the same random hosts and time ranges.