QuestDB 10.0: QWP, one binary streaming protocol for writes and Arrow reads | QuestDB<br>New Release: QuestDB 10.0<br>New Release: QuestDB 10.0<br>Learn more
QuestDB is the open-source time-series database for demanding workloads—from trading floors to mission control.<br>It delivers ultra-low latency, high ingestion throughput, and a multi-tier storage engine.<br>Native support for Parquet and SQL keeps your data portable, AI-ready—no vendor lock-in.
Fast ingestion is what QuestDB is known for. Getting the data back out at the<br>same speed was the harder problem, and for years the answer was the PostgreSQL<br>wire protocol, which was never designed for it.
10.0 closes that gap, and speeds up the write side on the way past. QWP, the<br>QuestDB Wire Protocol, is a binary columnar protocol over WebSocket that handles<br>both writes and reads from a single client:<br>around 3.6x faster than the<br>InfluxDB Line Protocol ("ILP") for ingestion over a network, and query results<br>streaming into Apache Arrow at 220 million rows a second.
There is a good deal more in the release. Live views arrive in beta, the Web<br>Console gains notebooks that a coding agent can drive, ALTER COLUMN TYPE works<br>on Parquet partitions, and the storage engine gains the foundations that QuestDB<br>Enterprise 4.0 builds cold storage on.
It is a long post, so here is what is in it:
One protocol, both directions: QWP on the<br>write side and the read side, the benchmarks, and what ships in which client
Live views, in beta: window functions maintained<br>incrementally in memory as rows arrive
Parquet as a first-class citizen:<br>Parquet tables, schema evolution on them, and files the rest of the ecosystem<br>can read
Cold storage, and the lakehouse it opens up:<br>partitions tiered to object storage in QuestDB Enterprise 4.0, still queryable,<br>and registrable in Iceberg or DuckLake without a copy
Notebooks, and coding agents that can drive them:<br>Web Console 2.0 and the QuestDB MCP
The covering index gets its parallel decode:<br>the 9.4.0 caveat, closed
New SQL features: SHOW CREATE DATABASE, scalar<br>sub-queries in comparisons, and a few new functions
Operations: per-query memory limits, cancellation that works,<br>REBASE WAL
Performance and breaking changes
Bug fixes, and the fuzzer behind them
Getting 10.0
One protocol, both directions
Getting data in meant ILP, and getting it back out meant the PostgreSQL wire<br>protocol: two libraries, two wire formats, and one of them<br>serialising every row to text on the server for the client to parse back into<br>typed values. Fine for a dashboard query returning forty rows, a bottleneck when<br>a quant pulls a year of ticks into pandas.
QWP is a better alternative to both halves. It is binary and columnar, sending<br>whole column blocks in close to the shape they live on disk, with symbols sent<br>once and referenced by an id and timestamps delta encoded. Query results can come<br>back as Apache Arrow record batches, which makes<br>handing them to polars or pandas zero copy, with no row-by-row deserialising in between.
There is more to it than speed. The QWP clients also handle buffering and node<br>failover themselves, with no queue or proxy in front of QuestDB, which is<br>further down.
That does not make the older paths go away. ILP and the PostgreSQL wire<br>protocol are both still here, still supported, and nothing you run today stops<br>working when you upgrade. ILP in particular is simple, well optimised, and<br>spoken by a wide range of tools and older QuestDB versions, and we have no plans<br>to retire it. If you have a Telegraf agent or a Grafana datasource pointed at<br>QuestDB, it keeps working exactly as before.
Switching is a connect-string change rather than a rewrite, because it is the<br>same client library either way. Sender.fromConfig() takes http:: or tcp::<br>for ILP and ws:: or wss:: for QWP, and the builder API around it is<br>unchanged:
The same client, the same call, a different scheme<br>// ILP over HTTP, as before<br>Sender sender = Sender.fromConfig("http::addr=localhost:9000;");
// QWP over WebSocket<br>Sender sender = Sender.fromConfig("ws::addr=localhost:9000;");
Which means you can move one service at a time, and move it back if you do not<br>like what you see.
Ingestion
We benchmarked QWP against ILP with TSBS, the<br>standard Time Series Benchmark Suite, feeding both protocols wire data prepared<br>ahead of time so neither gets a serialisation head start.
↑ Higher is better<br>TSBS cpu-only at 1,000,000 hosts, wire data prepared ahead of time for both protocols
+3.6xover the network
+3.1xon a single machine
1Mdistinct series
The gain is mostly the format. A TSBS row is around 347 bytes as line protocol<br>text and around 97 bytes as QWP. Both protocols fill the same 14.7 Gbit/s link,<br>so the one with the smaller rows gets about 3.6x more of them through. The<br>full ingestion benchmark has the<br>cardinality sweep, the methodology, and why running the loader on the same box<br>as the server makes the two protocols look level.
Egress
On the read side we measured how fast a...