Time Series Database for IoT: QuestDB vs InfluxDB vs TDengine 2026
Time Series Database for IoT: QuestDB vs InfluxDB vs TDengine
In the IoT landscape of 2026, your sensors spit out tens of thousands of data points per second — temperature, humidity, vibration frequency, GPS coordinates... Traditional MySQL? Write bottleneck crashes it in 5 minutes. MongoDB? Time-series aggregation queries are so slow you question reality.
Choose the wrong time series database, and you'll face query timeouts, storage explosions, or even total data pipeline collapse. QuestDB, InfluxDB, and TDengine each have their strengths — but which one is the optimal choice for your IoT scenario? This article walks through 5 core production patterns to help you decide definitively.
Core Concepts at a Glance
| Concept | Description | Typical Scenario |
|---|---|---|
| Time Series | Chronologically ordered data point sequence | Sensor collection, monitoring metrics |
| Tag | Dimension/index field of data | Device ID, region, type |
| Field | Measured value of data | Temperature value, voltage value |
| Downsampling | Aggregating high-frequency data to low-frequency | Second-level → minute-level → hour-level |
| Retention Policy | Automatic data expiration and cleanup | Hot data 7 days, cold data 1 year |
| Super Table | TDengine-specific, template for multi-device tables | 1000 devices sharing one template |
5 Major Pain Points of IoT Time Series Data
- Write throughput bottleneck: Million-level concurrent device writes, traditional databases can't handle the IOPS
- Inefficient query aggregation: Time window aggregation and downsampling queries have poor performance, P99 latency spikes
- Storage cost out of control: Large and continuously growing time series data, difficult hot/cold data separation
- Complex multi-device management: Thousands of devices each with tags, schema management chaos
- Fragmented ecosystem integration: High integration cost with Grafana, Kafka, Telegraf and other toolchains
Pattern 1: IoT Time Series Data Characteristics and Modeling
IoT time series data has distinct characteristics: high-cardinality tags, continuous append-only writes, strong time locality, read-heavy and write-heavy. Understanding these characteristics is the prerequisite for selection.
# Python: Simulate IoT time series data generation
# Environment: Python 3.12+ / No extra dependencies
import time
import json
import random
from datetime import datetime, timezone
def generate_iot_sensor_data(device_count: int = 100, interval_ms: int = 1000) -> dict:
"""Generate IoT sensor time series data point
Args:
device_count: Number of devices
interval_ms: Collection interval (milliseconds)
Returns:
Single time series data point
"""
device_id = f"sensor-{random.randint(1, device_count):04d}"
region = random.choice(["us-east", "us-west", "eu-central", "ap-southeast"])
sensor_type = random.choice(["temperature", "humidity", "vibration", "pressure"])
base_values = {
"temperature": 25.0,
"humidity": 60.0,
"vibration": 0.5,
"pressure": 101.3,
}
value = round(base_values[sensor_type] + random.gauss(0, 2), 3)
return {
"timestamp": int(time.time() * 1_000_000), # Microsecond precision
"tags": {
"device_id": device_id,
"region": region,
"sensor_type": sensor_type,
"factory": f"plant-{region.split('-')[0]}",
},
"fields": {
"value": value,
"quality": random.choice(["good", "good", "good", "warning", "error"]),
"battery": round(random.uniform(10, 100), 1),
},
}
def simulate_iot_stream(duration_seconds: int = 10, devices: int = 100) -> list[dict]:
"""Simulate IoT data stream
Args:
duration_seconds: Simulation duration (seconds)
devices: Number of devices
Returns:
List of time series data points
"""
data_points = []
start = time.time()
while time.time() - start < duration_seconds:
point = generate_iot_sensor_data(device_count=devices)
data_points.append(point)
time.sleep(0.01) # Simulate 100Hz collection
print(f"Generated {len(data_points)} data points, "
f"duration {duration_seconds}s, "
f"devices {devices}")
return data_points
if __name__ == "__main__":
points = simulate_iot_stream(duration_seconds=5, devices=50)
print(json.dumps(points[0], indent=2))
Pattern 2: QuestDB SQL Time Series Queries
QuestDB is known for its "SQL-first" approach, with time series extensions to standard SQL that make queries extremely intuitive. Zero-dependency installation and high-performance writes are its killer features.
-- QuestDB: IoT time series SQL query patterns
-- Environment: QuestDB 8.x / ILP (InfluxDB Line Protocol) writes
-- 1. Create table
CREATE TABLE IF NOT EXISTS sensor_data (
timestamp TIMESTAMP,
device_id SYMBOL,
region SYMBOL,
sensor_type SYMBOL,
factory SYMBOL,
value DOUBLE,
quality SYMBOL,
battery DOUBLE
) TIMESTAMP(timestamp) PARTITION BY DAY WAL;
-- 2. Time window aggregation - 5-minute average temperature
SELECT
timestamp,
device_id,
avg(value) AS avg_temp,
min(value) AS min_temp,
max(value) AS max_temp,
count() AS sample_count
FROM sensor_data
WHERE sensor_type = 'temperature'
AND timestamp >= dateadd('h', -1, now())
SAMPLE BY 5m ALIGN TO CALENDAR;
-- 3. Latest value query - LATEST ON syntax
SELECT * FROM sensor_data LATEST ON timestamp PARTITION BY device_id;
-- 4. Downsampling + multi-dimension grouping
SELECT
timestamp,
region,
sensor_type,
avg(value) AS avg_value,
stddev(value) AS std_value,
count() AS data_points
FROM sensor_data
WHERE timestamp >= dateadd('d', -7, now())
SAMPLE BY 1h ALIGN TO CALENDAR
GROUP BY region, sensor_type;
-- 5. Anomaly detection - 3 consecutive points exceeding threshold
WITH spike_detected AS (
SELECT
timestamp,
device_id,
value,
case when value > 35 OR value < 15 THEN 1 ELSE 0 END AS is_spike
FROM sensor_data
WHERE sensor_type = 'temperature'
)
SELECT
timestamp,
device_id,
value,
sum(is_spike) OVER (
ORDER BY timestamp
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS consecutive_spikes
FROM spike_detected
WHERE consecutive_spikes >= 3;
# Python: QuestDB ILP write client
# Environment: Python 3.12+ / pip install questdb
from questdb.ingress import IngressError, Sender, Buffer, Protocol
def write_iot_data_questdb(host: str = "localhost", port: int = 9009):
"""Write IoT data to QuestDB via ILP protocol"""
conf = f"http::addr={host}:{port};"
try:
with Sender.from_conf(conf) as sender:
for i in range(1000):
device_id = f"sensor-{i % 50:04d}"
region = ["us-east", "us-west", "eu-central"][i % 3]
sender.row(
"sensor_data",
symbols={
"device_id": device_id,
"region": region,
"sensor_type": "temperature",
"factory": f"plant-{region.split('-')[0]}",
},
columns={
"value": 25.0 + (i % 10) * 0.5,
"battery": 80.0 + (i % 20),
},
at=Sender.current_timestamp_with_nanos(),
)
sender.flush()
print(f"Successfully wrote 1000 data points to QuestDB")
except IngressError as e:
print(f"Write failed: {e}")
if __name__ == "__main__":
write_iot_data_questdb()
Pattern 3: InfluxDB Flux Queries
InfluxDB 3.x has moved to SQL queries, but Flux is still widely used in the 2.x ecosystem. Mastering Flux is essential for maintaining existing systems.
# Python: InfluxDB 2.x Flux query patterns
# Environment: Python 3.12+ / pip install influxdb-client[async]
from influxdb_client import InfluxDBClient, Point, WriteOptions
from influxdb_client.client.write_api import SYNCHRONOUS
import datetime
INFLUX_URL = "http://localhost:8086"
INFLUX_TOKEN = "your-api-token"
INFLUX_ORG = "iot-org"
INFLUX_BUCKET = "sensor-bucket"
def write_iot_data_influxdb():
"""Write IoT data to InfluxDB 2.x"""
client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
write_api = client.write_api(write_options=SYNCHRONOUS)
points = []
for i in range(500):
point = (
Point("sensor_data")
.tag("device_id", f"sensor-{i % 50:04d}")
.tag("region", ["us-east", "us-west", "eu-central"][i % 3])
.tag("sensor_type", "temperature")
.field("value", 25.0 + (i % 10) * 0.5)
.field("battery", 80.0 + (i % 20))
.time(datetime.datetime.utcnow(), write_precision="ms")
)
points.append(point)
write_api.write(bucket=INFLUX_BUCKET, org=INFLUX_ORG, record=points)
print(f"Successfully wrote {len(points)} data points to InfluxDB")
client.close()
FLUX_QUERIES = """
// 1. Time window aggregation - 5-minute average temperature
from(bucket: "sensor-bucket")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "sensor_data")
|> filter(fn: (r) => r.sensor_type == "temperature")
|> filter(fn: (r) => r._field == "value")
|> aggregateWindow(every: 5m, fn: mean, createEmpty: false)
|> yield(name: "avg_temp_5m")
// 2. Multi-dimension grouping statistics
from(bucket: "sensor-bucket")
|> range(start: -7d)
|> filter(fn: (r) => r._measurement == "sensor_data")
|> filter(fn: (r) => r._field == "value")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> group(columns: ["region", "sensor_type"])
|> yield(name: "hourly_by_region_type")
// 3. Anomaly detection - consecutive threshold exceedances
from(bucket: "sensor-bucket")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "sensor_data")
|> filter(fn: (r) => r._field == "value")
|> map(fn: (r) => ({r with is_spike: if r._value > 35.0 or r._value < 15.0 then 1 else 0}))
|> window(every: 5m)
|> sum(column: "is_spike")
|> filter(fn: (r) => r.is_spike >= 3)
|> yield(name: "spike_alert")
// 4. Downsampling write to continuous query
from(bucket: "sensor-bucket")
|> range(start: -30d)
|> filter(fn: (r) => r._measurement == "sensor_data")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> to(bucket: "sensor-downsampled", org: "iot-org")
"""
if __name__ == "__main__":
write_iot_data_influxdb()
print("\nFlux query examples:")
print(FLUX_QUERIES)
Pattern 4: TDengine Super Tables
TDengine's super table is a killer feature for IoT scenarios: one super table template manages thousands of devices, sub-tables automatically inherit tags, and query performance is extremely high.
-- TDengine 3.x: IoT super table patterns
-- Environment: TDengine 3.3+ / taos CLI
-- 1. Create super table (template)
CREATE STABLE IF NOT EXISTS sensor_data (
ts TIMESTAMP,
value FLOAT,
battery FLOAT,
quality NCHAR(10)
) TAGS (
device_id NCHAR(20),
region NCHAR(20),
sensor_type NCHAR(20),
factory NCHAR(20)
);
-- 2. Auto-create sub-table and write
INSERT INTO d_sensor_0001 USING sensor_data TAGS ('sensor-0001', 'us-east', 'temperature', 'plant-us')
VALUES (NOW + 0a, 25.3, 85.2, 'good');
INSERT INTO d_sensor_0002 USING sensor_data TAGS ('sensor-0002', 'us-west', 'humidity', 'plant-us')
VALUES (NOW + 0a, 62.1, 91.5, 'good');
-- 3. Batch write to multiple devices
INSERT INTO
d_sensor_0001 VALUES (NOW + 1a, 25.5, 85.0, 'good') (NOW + 2a, 25.7, 84.8, 'good')
d_sensor_0002 VALUES (NOW + 1a, 62.3, 91.2, 'good') (NOW + 2a, 62.0, 91.0, 'warning');
-- 4. Super table aggregation query - by region
SELECT
region,
sensor_type,
AVG(value) AS avg_value,
STDDEV(value) AS std_value,
COUNT(*) AS data_points
FROM sensor_data
WHERE ts >= NOW - 1h
INTERVAL(5m)
GROUP BY region, sensor_type;
-- 5. Latest value query - LAST_ROW
SELECT
device_id,
LAST_ROW(value) AS latest_value,
LAST_ROW(ts) AS last_update
FROM sensor_data
GROUP BY device_id;
-- 6. Downsampling + window function
SELECT
_wstart AS window_start,
_wend AS window_end,
device_id,
AVG(value) AS avg_value,
MIN(value) AS min_value,
MAX(value) AS max_value
FROM sensor_data
WHERE ts >= NOW - 7d AND sensor_type = 'temperature'
INTERVAL(1h) SLIDING(30m)
GROUP BY device_id;
# Python: TDengine write client
# Environment: Python 3.12+ / pip install taos-ws-py
import taosws
def write_iot_data_tdengine(host: str = "localhost", port: int = 6041):
"""Write IoT data to TDengine 3.x"""
conn = taosws.connect(f"ws://{host}:{port}/rest/sql", user="root", password="taosdata")
conn.execute("CREATE DATABASE IF NOT EXISTS iot_db KEEP 3650")
conn.execute("USE iot_db")
conn.execute("""
CREATE STABLE IF NOT EXISTS sensor_data (
ts TIMESTAMP,
value FLOAT,
battery FLOAT,
quality NCHAR(10)
) TAGS (
device_id NCHAR(20),
region NCHAR(20),
sensor_type NCHAR(20),
factory NCHAR(20)
)
""")
for i in range(100):
device_id = f"sensor-{i:04d}"
region = ["us-east", "us-west", "eu-central"][i % 3]
table_name = f"d_sensor_{i:04d}"
sql = f"""
INSERT INTO {table_name} USING sensor_data
TAGS ('{device_id}', '{region}', 'temperature', 'plant-{region.split('-')[0]}')
VALUES (NOW + {i}a, {25.0 + i * 0.1}, {80.0 + i * 0.2}, 'good')
"""
conn.execute(sql)
print("Successfully wrote 100 data points to TDengine")
result = conn.execute("SELECT COUNT(*) FROM sensor_data")
for row in result:
print(f"Total data count: {row[0]}")
conn.close()
if __name__ == "__main__":
write_iot_data_tdengine()
Pattern 5: Production-Grade Selection Comparison
# Python: Time series database selection evaluation framework
# Environment: Python 3.12+ / No extra dependencies
from dataclasses import dataclass
@dataclass
class TSDBEvaluation:
"""Time series database selection evaluation model"""
name: str
write_throughput_per_sec: int
query_latency_p99_ms: float
compression_ratio: float
sql_compatibility: int
ecosystem_maturity: int
cluster_scalability: int
learning_curve: int
license_type: str
best_for: str
caution: str
def evaluate_tsdb_choices() -> list[TSDBEvaluation]:
"""Generate evaluation results for three major TSDBs"""
return [
TSDBEvaluation(
name="QuestDB",
write_throughput_per_sec=1_500_000,
query_latency_p99_ms=15.0,
compression_ratio=8.5,
sql_compatibility=9,
ecosystem_maturity=6,
cluster_scalability=5,
learning_curve=9,
license_type="Apache 2.0",
best_for="SQL teams, high-frequency writes, financial/IoT real-time analytics",
caution="Cluster edition requires commercial license, fewer ecosystem plugins",
),
TSDBEvaluation(
name="InfluxDB",
write_throughput_per_sec=500_000,
query_latency_p99_ms=45.0,
compression_ratio=5.2,
sql_compatibility=5,
ecosystem_maturity=9,
cluster_scalability=7,
learning_curve=6,
license_type="MIT / Commercial",
best_for="DevOps monitoring, Telegraf ecosystem, small-to-medium IoT",
caution="Steep Flux learning curve, high 3.x migration cost, paid cluster edition",
),
TSDBEvaluation(
name="TDengine",
write_throughput_per_sec=2_000_000,
query_latency_p99_ms=8.0,
compression_ratio=12.0,
sql_compatibility=7,
ecosystem_maturity=7,
cluster_scalability=8,
learning_curve=7,
license_type="AGPL-3.0 / Commercial",
best_for="Ultra-large-scale IoT, connected vehicles, industrial internet",
caution="AGPL license restrictions, community edition feature limits, SQL dialect differences",
),
]
def print_comparison_table(evaluations: list[TSDBEvaluation]):
"""Print selection comparison table"""
print(f"{'Metric':<30} {'QuestDB':<20} {'InfluxDB':<20} {'TDengine':<20}")
print("-" * 90)
fields = [
("Write throughput (pts/sec)", "write_throughput_per_sec", True),
("Query P99 latency (ms)", "query_latency_p99_ms", False),
("Compression ratio", "compression_ratio", True),
("SQL compatibility (1-10)", "sql_compatibility", True),
("Ecosystem maturity (1-10)", "ecosystem_maturity", True),
("Cluster scalability (1-10)", "cluster_scalability", True),
("Learning curve (1-10)", "learning_curve", True),
("License", "license_type", None),
]
for label, attr, _ in fields:
values = [str(getattr(e, attr)) for e in evaluations]
print(f"{label:<30} {values[0]:<20} {values[1]:<20} {values[2]:<20}")
print("\nBest for:")
for e in evaluations:
print(f" {e.name}: {e.best_for}")
print("\nCaution:")
for e in evaluations:
print(f" {e.name}: {e.caution}")
if __name__ == "__main__":
evaluations = evaluate_tsdb_choices()
print_comparison_table(evaluations)
Pitfall Guide: 5 Common Traps
Trap 1: High-Cardinality Tags Causing OOM
❌ Wrong: Using device_id as an InfluxDB Tag
✅ Right: Put high-cardinality fields in Field, low-cardinality dimensions in Tag
InfluxDB Tags create inverted indexes — 100K device_ids = 100K Series, memory explodes instantly. QuestDB's SYMBOL type and TDengine's super tables solve this natively.
Trap 2: Ignoring Time Zones Causing Aggregation Misalignment
-- ❌ Wrong: No timezone specified, cross-timezone aggregation misaligned
SAMPLE BY 1h
-- ✅ Right: QuestDB timezone alignment
SAMPLE BY 1h ALIGN TO CALENDAR WITH TIME ZONE 'America/New_York'
Trap 3: No Retention Policy Leading to Disk Full
-- InfluxDB: Must set RP
CREATE RETENTION POLICY "7days" ON "sensor-bucket" DURATION 7d REPLICATION 1 DEFAULT
-- TDengine: Specify KEEP when creating database
CREATE DATABASE iot_db KEEP 3650
Trap 4: Improper Batch Write Size
# ❌ Wrong: Writing one by one, massive network overhead
for point in data_points:
write_api.write(bucket=BUCKET, record=point)
# ✅ Right: Batch write, recommended 5000-10000 points per batch
BATCH_SIZE = 5000
for i in range(0, len(data_points), BATCH_SIZE):
batch = data_points[i:i + BATCH_SIZE]
write_api.write(bucket=BUCKET, record=batch)
Trap 5: Confusing TDengine Super Tables with Sub-Tables
-- ❌ Wrong: INSERT directly into super table (no sub-table)
INSERT INTO sensor_data VALUES (NOW, 25.0, 80.0, 'good');
-- ✅ Right: Write through sub-table, auto-inherits super table tags
INSERT INTO d_sensor_0001 USING sensor_data TAGS (...)
VALUES (NOW, 25.0, 80.0, 'good');
Error Troubleshooting Table
| Error Message | Database | Cause | Solution |
|---|---|---|---|
too many series |
InfluxDB | High-cardinality Tag exceeds Series limit | Move high-cardinality fields to Field, or upgrade to InfluxDB 3.x |
memory limit exceeded |
QuestDB | Query result set too large | Add LIMIT, use SAMPLE BY for downsampling |
Invalid column type |
QuestDB | SYMBOL column with oversized string | Keep SYMBOL length < 64 bytes, or use STRING |
table does not exist |
TDengine | Database not selected with USE | Execute USE iot_db first |
out of memory |
TDengine | Single query memory limit exceeded | Adjust queryMemory parameter, narrow time range |
connection refused :9009 |
QuestDB | ILP port not enabled | Configure tcp.enabled=true |
authorization failed |
InfluxDB | Insufficient Token permissions | Check Token's read/write permissions |
database not found |
InfluxDB | Bucket doesn't exist | Create Bucket first or check spelling |
invalid timestamp |
QuestDB | Timestamp precision mismatch | ILP defaults to nanoseconds, verify timestamp unit |
duplicate table name |
TDengine | Sub-table name conflict | Ensure sub-table names are globally unique |
Advanced Optimization: 5 Production-Grade Tips
Tip 1: Hot/Cold Data Tiered Storage
-- QuestDB: Partition + O3 engine hot/cold separation
ALTER TABLE sensor_data ALTER PARTITION LIST
SET ATTRIBUTE 'cold' WHERE timestamp < dateadd('d', -30, now());
-- TDengine: Multi-level storage
ALTER DATABASE iot_db KEEP 30,365,3650;
-- 30 days hot / 365 days warm / 3650 days cold
Tip 2: Pre-Aggregated Materialized Views
-- QuestDB: Materialized view auto-downsampling
CREATE MATERIALIZED VIEW sensor_hourly AS (
SELECT
timestamp,
device_id,
avg(value) AS avg_value,
min(value) AS min_value,
max(value) AS max_value,
count() AS sample_count
FROM sensor_data
SAMPLE BY 1h ALIGN TO CALENDAR
) PARTITION BY DAY;
Tip 3: Write Batching with Backpressure Control
# Python: QuestDB writer with backpressure control
# Environment: Python 3.12+ / pip install questdb
import time
import threading
from queue import Queue, Full
from questdb.ingress import Sender, IngressError
class BatchingWriter:
"""Batch writer with backpressure control"""
def __init__(self, host: str = "localhost", port: int = 9009,
batch_size: int = 5000, max_queue: int = 100_000):
self.batch_size = batch_size
self.queue = Queue(maxsize=max_queue)
self.sender = Sender.from_conf(f"http::addr={host}:{port};")
self._running = True
self._worker = threading.Thread(target=self._flush_loop, daemon=True)
self._worker.start()
def write(self, table: str, symbols: dict, columns: dict):
"""Non-blocking write, drops data when queue is full"""
try:
self.queue.put_nowait((table, symbols, columns))
except Full:
print("Warning: Write queue full, dropping data point")
def _flush_loop(self):
"""Background batch flush"""
batch = []
while self._running or not self.queue.empty():
try:
item = self.queue.get(timeout=1.0)
batch.append(item)
if len(batch) >= self.batch_size:
self._flush(batch)
batch = []
except Exception:
if batch:
self._flush(batch)
batch = []
def _flush(self, batch: list):
"""Execute batch write"""
try:
for table, symbols, columns in batch:
self.sender.row(table, symbols=symbols, columns=columns,
at=Sender.current_timestamp_with_nanos())
self.sender.flush()
print(f"Flushed {len(batch)} data points")
except IngressError as e:
print(f"Write failed: {e}")
def close(self):
self._running = False
self._worker.join(timeout=10)
self.sender.close()
Tip 4: Grafana Visualization Integration
# docker-compose.yml: QuestDB + Grafana integrated deployment
# Environment: Docker Compose v2.30+
version: "3.8"
services:
questdb:
image: questdb/questdb:8.2
ports:
- "9000:9000" # Web Console
- "9009:9009" # ILP
- "9003:9003" # Min health server
volumes:
- questdb_data:/root/.questdb
environment:
QDB_MALLOC_SIZE: 4G
grafana:
image: grafana/grafana:11.4
ports:
- "3000:3000"
environment:
GF_INSTALL_PLUGINS: grafana-postgresql-datasource
volumes:
- grafana_data:/var/lib/grafana
depends_on:
- questdb
volumes:
questdb_data:
grafana_data:
Tip 5: Multi-Active Write and High Availability
# TDengine 3.x cluster deployment
# Environment: TDengine 3.3+ / 3-node cluster
cluster: 1
numOfMnodes: 3
mnodeEqualVnodeNum: 4
statusInterval: 1
maxTablesPerVnode: 1000
minRowsPerBlock: 100
maxRowsPerBlock: 4096
Three TSDBs Comparison Analysis
| Dimension | QuestDB | InfluxDB | TDengine |
|---|---|---|---|
| Query Language | SQL (with time series extensions) | Flux / SQL (3.x) | SQL dialect |
| Write Protocol | ILP / REST / PostgreSQL wire | ILP / REST | REST / WebSocket / JNI |
| Single Node Write | 1.5M pts/sec | 500K pts/sec | 2M pts/sec |
| Compression Ratio | 8-10x | 4-6x | 10-15x |
| Cluster Solution | Enterprise edition | Open source cluster (3.x) | Open source cluster |
| Grafana Integration | ✅ PostgreSQL data source | ✅ Native plugin | ✅ Native plugin |
| Kafka Integration | ✅ Community | ✅ Native | ✅ Native |
| Learning Cost | Low (SQL out of the box) | Medium-High (unique Flux syntax) | Medium (super table concept) |
| Scale Fit | Small-Medium → Large | Small-Medium | Medium-Large → Ultra-large |
Conclusion
There's no silver bullet for IoT time series database selection — it depends on your scenario fit:
- QuestDB: First choice for SQL teams, high-frequency writes + real-time analytics, lowest learning cost, but cluster edition requires commercial license
- InfluxDB: Most mature DevOps monitoring ecosystem, Telegraf+Grafana all-in-one, but steep Flux learning curve and high 3.x migration cost
- TDengine: Ultra-large-scale IoT killer, super tables + compression ratio dominance, but watch out for AGPL license
Selection decision tree: SQL team → QuestDB / DevOps ecosystem → InfluxDB / Ultra-large IoT + localization → TDengine.
Recommended Online Tools
- /en/json/format - JSON formatter for time series API responses
- /en/dev/curl-to-code - cURL to code generator for TSDB API calls
- /en/encode/hash - Hash calculator for device ID deduplication
- /en/text/diff - Text diff for SQL query version comparison
Try these browser-local tools — no sign-up required →