
TDengine is the time-series database we meet most often in industrial IoT, energy storage and electric-vehicle projects. It is well documented, it publishes its own benchmarks, and its design (one table per device) is a deliberate bet on how device data gets queried. If you are evaluating it, you deserve a comparison that runs the same data and the same queries through both engines and shows every result.
That is what this post is. I run Basekick Labs, which builds Arc, so read it with that in mind; it is also why everything below is reproducible from a public harness, with every raw result committed next to it.
The short version
On the TSBS IoT workload at TDengine's 10,000-tag and 100,000-tag price points:
- Storage: Arc stored the same data 23% to 25% smaller as shipped, and 40% to 47% smaller with one setting (
ingest.sort_keys), in open Parquet files that other engines can read. - Ingest: with durable acknowledgement (a write is answered only once it is on disk) and sort keys, Arc loaded 155.6M rows in 24.4 s against TDengine's 26.1 s, and the smaller dataset 2.5x faster.
- Aggregations over history: at 100,000 tags Arc was faster on all four, 1.6x to 6.5x as shipped and 3.3x to 6.3x with sort keys, and faster on the 10-minute window query too.
- Latest value per device: TDengine was faster on the three latest-value lookups and on average load per model. Its one-table-per-device design is built for exactly that. Sort keys cut its lead on the lookups from 3.5x–16.3x to 1.8x–6.9x.
- Pricing: TDengine's All-in-One edition is priced per tag; Arc Enterprise is priced per core and never counts tags. Both have free AGPL-3.0 editions, and the benchmark compared those.
What was tested
| Setting | Arc | TDengine |
|---|---|---|
| Version | main at 0080aa31, pre-release code planned to ship as 27.01.1 | TSDB-OSS 3.4.3.3, released 2026-10-18 |
| Install | built from source, native | official macOS arm64 package, native |
| Configuration | the shipped arc.toml with the WAL turned on (fdatasync, which Arc performs as F_FULLFSYNC on macOS) and telemetry off; tested as shipped and with ingest.sort_keys | database parameters from TDengine's own TSBS comparison scripts |
Arc ran pre-release 27.01.1 code because this benchmark drove five improvements that are now merged (more on that below); sort keys themselves are already in released Arc.
The workload is the TSBS IoT use case: trucks that report readings (position, speed, fuel consumption) and diagnostics (load, fuel state, status) every 10 seconds, 24 hours of data, the same seeded dataset in both engines. TSBS deliberately includes gaps and out-of-order points.
The scales come from TDengine's pricing. TDengine defines a tag as "a unique time-series signal, typically Asset/Device + Measurement Name", and a TSBS truck carries 10 of them. So 1,000 trucks is 10,000 tags (15.6M rows) and 10,000 trucks is 100,000 tags (155.6M rows): the two price points I quote later.
Both engines ran natively on one Apple M3 Max (14 cores, 36 GB), one at a time with the other stopped, three or more runs each. A laptop is not a server, so read the absolute numbers as relative ones.
Making it a fair fight
Most of the work in a benchmark like this is making sure both engines answer the same question. These are the decisions that mattered:
- TDengine's own settings. TDengine maintains a fork of TSBS with a comparison script. I used its IoT parameters verbatim:
vgroups 12,buffer 256,pages 4096,stt_trigger 1,wal_level 2with a 3-second fsync period, 12 workers, batches of 10,000 rows.cachemodelstayed at its default,none. - Identical data. Arc and TDengine each need their own TSBS loader, so each uses a different fork. Their IoT generators are identical apart from Go module paths, so the same seed produces the same rows. I verified the stored row counts match exactly.
- No random null tags. Stock TSBS nulls one tag on about 1% of points. Arc stores tags per row; TDengine stores them once per device table, so it cannot represent a tag that is missing on some of a truck's points. Left in, the engines answered different questions. I disabled it identically in both forks.
- UTC on both sides. TDengine's daily windows follow the server timezone. Before I set it to UTC, its "daily activity" query split one UTC day into two local days.
- The same queries. TDengine's fork generates its own SQL for each query type. A small tool decodes those generated queries and translates each one into standard SQL for Arc with the same parameters, so both engines run the same 20 instances of each type. For
stationary-trucksI matched TDengine's definition (the first aligned 10-minute window per truck) rather than stock TSBS. - Checked row for row. All 180 query instances at each scale returned the same number of rows on both engines, and spot checks matched to the last decimal.
- TDengine's best client. I timed both engines from the same HTTP client, and also ran TDengine's native TSBS query runner. Each TDengine figure below is the faster of the two, except daily activity, where the native runner bucketed days in the local time zone, so only the REST figure is used.
- Arc's loader, fixed rather than tuned. Our own TSBS loader for Arc needed fixes for the IoT workload; I made them and set its payloads to 10,000 rows to match TDengine's batch size.
Ingest
| Scale | Arc, sort keys | Arc, as shipped | TDengine | Arc, sort keys, queryable after |
|---|---|---|---|---|
| 10,000 tags (15.6M rows) | 2.77 s (2.73 to 2.79) | 2.88 s (2.75 to 2.92) | 6.92 s (6.37 to 6.99) | 4.91 s (4.88 to 5.89) |
| 100,000 tags (155.6M rows) | 24.4 s (24.3 to 24.9) | 25.9 s (25.8 to 26.0) | 26.1 s (25.5 to 26.2) | 27.5 s (27.1 to 27.6) |
Medians, with the range across runs in parentheses, every Arc write acknowledged durably. That is the like-for-like comparison, and Arc's guarantee is the stricter one. With wal.ack_mode = "durable", Arc answers a write only after a sync covering it has completed. TDengine's tested configuration answers before its WAL sync, which runs every 3 seconds, and at its default WAL level it does not sync at all. Arc was 2.5x faster at 10,000 tags and 7% faster at 100,000, even though macOS makes every durable Arc sync a full drive cache flush (F_FULLFSYNC).
Arc's queued mode, the default, answers once a write is queued for the WAL writer. It was the fastest option on the small load (1.5 s) and variable on the large one (21.8 to 45.6 s as shipped), so it is not the comparison here. The WAL itself is off in Arc's shipped config and was on for every run here.
What the benchmark changed in Arc
Profiling and comparing the first runs turned up five improvements, all merged for 27.01.1:
- Every write was copied three times into fresh buffers before reaching the WAL. Building each entry in one allocation cut Arc's CPU time on this load by 19% (#1407).
- A new acknowledgement mode,
wal.ack_mode = "durable", answers a write only after it is synced to disk. At 100,000 tags it was also the steadier mode and used about half the CPU of queued mode, because waiting for the disk keeps the queue bounded (#1408). - Sorting a flush by sort keys used a comparison sort that checked each column's type on every comparison. It is now one linear pass per key, 5x faster on 5M rows; durable ingest with sort keys went from 36.6 s to 24.4 s (#1411).
- The daily compaction sorted a whole day by the sort keys, so every row group spanned the day and a 10-minute query read them all. It now orders by hour first: the stationary-trucks query went from 36.5 ms to 13.3 ms, and the files got smaller (#1409).
- A sort key the data had no column for left files unsorted at ingest and failed compaction; both now use the keys that exist (#1410).
The WAL documentation states each mode's guarantee, and the data model guide covers sort keys.
Storage
| Scale | Arc, sort keys | Arc, as shipped | TDengine | Arc vs TDengine |
|---|---|---|---|---|
| 10,000 tags | 126 MB | 162 MB | 209 MB | 40% smaller (23% as shipped) |
| 100,000 tags | 1.27 GB | 1.78 GB | 2.38 GB | 47% smaller (25% as shipped) |
Arc was measured after a compaction cycle, which Arc's open-source edition runs automatically every hour and day. TDengine's open-source edition cannot compact: COMPACT DATABASE fails with "Operation not supported in community edition". So TDengine was measured after FLUSH DATABASE, counting only its data files. Neither figure includes the WAL: at 100,000 tags Arc's was 0.1 GB and TDengine's 9.6 GB, which it retains for 3,600 seconds by default.
The bigger difference is what the bytes are. Arc's files are standard Parquet, partitioned by database, measurement and hour, on local disk, S3 or Azure. Spark, Trino or any Parquet reader can query them without Arc, and on the local backend Arc can register them as Apache Iceberg tables. TDengine keeps data in its own format; its open-source taosdump can back it up to Parquet, and continuous export through taosX is an Enterprise feature.
Queries
Warm median latency in milliseconds at 100,000 tags, 20 instances of each query type. "Faster" compares Arc with sort keys against TDengine, with the as-shipped result added where it differs.
| Query | Arc, sort keys | Arc, as shipped | TDengine | Faster |
|---|---|---|---|---|
| Aggregations over history | ||||
| Average vs projected fuel consumption per fleet | 87.2 | 83.3 | 544.7 | Arc 6.3x |
| Daily truck activity per fleet and model | 195.7 | 306.4 | 819.6 | Arc 4.2x |
| Trucks with long daily sessions (24 h) | 63.5 | 159.8 | 259.6 | Arc 4.1x |
| Trucks with long driving sessions (4 h) | 19.5 | 35.2 | 64.5 | Arc 3.3x |
| Latest value per truck | ||||
| Last location per truck | 45.4 | 120.7 | 13.9 | TDengine 3.3x |
| Trucks with high load (latest reading) | 75.4 | 176.5 | 10.9 | TDengine 6.9x |
| Trucks with low fuel (latest reading) | 27.1 | 53.4 | 15.2 | TDengine 1.8x |
| Per-truck aggregates | ||||
| Average load per truck model | 147.4 | 185.2 | 36.8 | TDengine 4.0x |
| Stationary trucks (first aligned 10-minute window) | 13.3 | 13.9 | 19.8 | Arc 1.5x |
The split follows the designs. TDengine keeps a table per device and stores per-block sums, minimums and maximums, so "the latest reading of every truck" or "each truck's average load" is a cheap lookup. Arc stores each measurement as columns across the whole fleet, so aggregating hours of fuel consumption or activity is a scan it is built for, and that advantage grows with scale.
One setting: sort keys
As shipped, Arc sorts each measurement by time, so every Parquet row group holds every truck, and a question about one truck still reads all of them. Arc lets you choose the sort order per measurement:
[ingest]
sort_keys = ["readings:fleet,name", "diagnostics:fleet,name"]Arc appends time automatically, so this sorts by fleet, then truck, then time: in effect the layout TDengine gets from one table per device, inside open Parquet files. Compaction keeps the order hour by hour. On this workload it made the data another 29% smaller at 100,000 tags, cut the three latest-value lookups by about half or more, pushed Arc's lead on daily activity and long daily sessions from 2.7x and 1.6x to 4.2x and 4.1x, and cost nothing at ingest: 24.4 s against 25.9 s without it. Sort keys are in released Arc today; see the data model guide.
At 10,000 tags
Every query finished in under 40 ms on both engines, so the differences are a few milliseconds:
| Query | Arc, sort keys | Arc, as shipped | TDengine |
|---|---|---|---|
| Average vs projected fuel consumption per fleet | 11.4 | 10.9 | 37.0 |
| Daily truck activity per fleet and model | 25.6 | 28.9 | 31.1 |
| Trucks with long daily sessions (24 h) | 14.7 | 21.6 | 20.3 |
| Trucks with long driving sessions (4 h) | 7.9 | 9.1 | 7.4 |
| Last location per truck | 15.6 | 18.9 | 7.0 |
| Trucks with high load (latest reading) | 15.9 | 23.5 | 4.5 |
| Trucks with low fuel (latest reading) | 8.4 | 9.8 | 5.3 |
| Average load per truck model | 19.9 | 21.3 | 3.1 |
| Stationary trucks (first aligned 10-minute window) | 5.3 | 5.5 | 4.5 |
Arc's lead on aggregations over history grows with the fleet: from 1.2x to 4.2x on daily activity, and from 1.4x to 4.1x on long daily sessions, between 10,000 and 100,000 tags.
For last-loc, Arc's SQL uses the latest timestamp per truck joined back to its row, which was 2.2x faster on Arc than DISTINCT ON with identical results. Both forms are standard SQL; TDengine uses its native last_row.
What is open source in each
Both engines have free AGPL-3.0 editions, but they draw the line between open source and Enterprise in different places.
| Capability | Arc | TDengine |
|---|---|---|
| Storage on S3-compatible object storage | Open source | Enterprise (older data migrated to object storage) |
| Compaction | Open source, automatic | Enterprise |
| Configurable sort order | Open source (ingest.sort_keys) | Fixed: one table per device |
| Apache Iceberg tables | Open source (26.09.1+, local storage backend) | Not documented |
| Edge-to-cloud sync | Open source: manual sync and signed air-gap bundles (26.09.1+); scheduled sync planned for 27.01.1 with a license | Enterprise (taosX) |
| MQTT ingestion | Open source, native subscriber | Enterprise (taosX) |
| OPC UA ingestion | Via Telegraf | Enterprise (taosX) |
| Clustering and high availability | Enterprise (Tier II and up) | Open source, three-replica Raft |
| Hot/cold tiering | Enterprise (Tier II and up) | Enterprise |
| SQL | PostgreSQL-compatible | Based on standard SQL with time-series extensions; OVER() window functions since 3.4.2.0 |
Pricing
List prices per year, self-hosted, as published on 2026-10-28.
TDengine sells All-in-One (its database plus the IDMP data management platform) per tag: $15,500 at 10,000 tags, $46,000 at 100,000 and $110,000 at 500,000. High availability adds 30%, and Standard Support is included with annual plans. Up to 5,000 tags is free with registration renewed each year, but direct third-party queries to the bundled database require a separate TSDB-Enterprise license. The database alone, TSDB-Enterprise, is priced per core: $5,000 for 2 cores up to $80,000 for 32.
Arc Enterprise is priced per core and machine and never counts tags, series or rows: $5,000 for up to 8 cores on one machine, $15,000 for up to 32 cores on up to 5 machines (this tier adds clustering and tiered storage), $40,000 for up to 128 cores.
Per tag, the bill grows with every signal you instrument. Per core, it grows with the hardware the workload actually needs.
A pattern from TDengine's own customers
TDengine's case study with CATL, the battery manufacturer, describes the architecture plainly: "The production system retains data from the last 30 days to meet real-time and near-term query needs, while historical data older than 30 days is stored exclusively in the UAT system to support long-term analysis and historical queries."
That is a reasonable design, and a common one: a hot window in one system, history in another. It is also the problem Arc was built around. Arc's files already live on object storage as Parquet in the open-source edition, so years of history can stay in one bucket that Arc, Spark or Trino query in place, with tiering in Enterprise for keeping recent data on fast disk.
When TDengine fits
- Your dominant query is the current state of each device: dashboards of the latest reading per asset, alarms on the last value.
- You need high availability without a commercial license: TDengine's open-source edition clusters.
- You want industrial connectors (OPC UA, OPC DA, PI, MQTT) managed inside the database and are buying TDengine Enterprise anyway.
When Arc fits
- You keep years of telemetry and query across it: aggregations, trends, activity per model or site. Arc was up to 6.5x faster on those at 100,000 tags as shipped, up to 6.3x with sort keys, and the gap grew with scale.
- You want the data in open files you own, readable by other engines, on object storage, without an Enterprise license.
- Your tag count will grow and you want pricing that does not.
- You need every acknowledged write on disk without giving up ingest speed: with durable acknowledgement and sort keys, Arc loaded 155.6M rows faster than TDengine.
Run it yourself
Everything is public:
- The harness, configs and every raw result, including
results/final.jsonwith the medians used here - The Arc TSBS loader and the patch applied to TDengine's TSBS fork
- The side-by-side comparison page
If you run it on a Linux server, or tune TDengine further, I would like to see the numbers.