Time Series Database Solutions for IoT
As sensor networks expand and devices generate high-frequency telemetry, choosing the right time series database shapes cost, reliability, and developer velocity.

If you have spent any time wiring up sensors, gateways, or edge devices, you have likely felt the pain of an unscalable logging strategy. A handful of Raspberry Pi units can produce enough readings to stall a naive PostgreSQL setup, and an HVAC system running thousands of points per second will expose any bottleneck that hides under modest load. The promise of IoT is continuous visibility into the physical world, but that promise depends on the database you put between your devices and your dashboards. Getting it wrong means either runaway infrastructure bills, query latencies that ruin user experience, or a data model that turns routine maintenance into a forensic exercise.
This article takes a practical look at time series database solutions for IoT from the perspective of someone who has shipped production systems collecting vibration data from industrial equipment, managed home-grown weather stations, and pulled metrics from fleets of smart meters. It focuses on real patterns that matter to developers: ingestion throughput, data retention and tiering, query performance, schema choices, and operations under limited resources. We will compare popular options, walk through representative code examples, and talk about tradeoffs you do not always see in benchmarks. By the end, you should know which solutions fit which IoT contexts and how to structure a project so your data pipeline scales with your device fleet.
Where time series databases fit in modern IoT stacks
IoT workloads are unusual in their volume and shape. Devices often send small, frequent bursts of measurements over unreliable networks. Applications range from near real‑time dashboards to long‑term analytics for forecasting and maintenance. A time series database is not just a place to store metrics; it is a system designed to compress and index time‑ordered data so queries over time windows remain fast as datasets grow to terabytes.
In production, teams typically combine a time series database with a message broker for buffering and a stream processor for aggregation or alerting. MQTT brokers like Mosquitto or EMQX ingest device data, then a lightweight consumer writes points to the database. For the database layer, developers favor systems that offer efficient ingestion, automatic downsampling, and long‑term retention policies. Popular choices include InfluxDB, TimescaleDB, Prometheus, and more recently, M3DB and VictoriaMetrics. The landscape also includes cloud options like AWS Timestream and Google Cloud BigQuery for large‑scale analytics, but many IoT teams prefer on‑prem or hybrid setups for latency and data sovereignty.
Compared to general purpose databases, time series databases optimize for append‑only writes and time‑bounded reads. They support tag indexing for high cardinality metadata, compress repeated values, and provide continuous queries to precompute rollups. For IoT, that means you can store raw sensor readings at high frequency, keep them for days or weeks, and automatically aggregate to hourly or daily buckets for long‑term retention without custom ETL jobs. The challenge is choosing a system that balances write performance with query flexibility while fitting operational constraints like memory, CPU, and storage budgets on edge gateways or small cloud instances.
Core concepts and practical examples
Choosing a data model that survives scale
Most time series databases for IoT use a metric‑plus‑tags model. Measurements are numeric values, tags are indexed metadata such as device ID, location, or sensor type. This model excels at filtering and grouping by tags across time ranges. The pitfall is cardinality. Too many unique tag combinations can explode index sizes and degrade performance. In practice, I keep tags small and predictable: device ID, site, sensor type, and unit. High cardinality fields like “error code” or raw event IDs often belong in a separate log store rather than the primary time series index.
# Example of writing sensor data to InfluxDB using the v2 client
# Real-world pattern: batch points to reduce request overhead and smooth network usage
import time
from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS
url = "http://localhost:8086"
token = "my-token"
org = "my-org"
bucket = "iot_raw"
client = InfluxDBClient(url=url, token=token, org=org)
write_api = client.write_api(write_options=SYNCHRONOUS)
def build_point(sensor_id, site, value, field_name="vibration"):
return Point("vibration") \
.tag("sensor_id", sensor_id) \
.tag("site", site) \
.field(field_name, float(value)) \
.time(time.time_ns())
# Batch a list of measurements
def write_batch(measurements):
points = [build_point(m["sensor_id"], m["site"], m["value"]) for m in measurements]
write_api.write(bucket=bucket, org=org, record=points)
# Simulate a burst of readings from multiple sensors
batch = [
{"sensor_id": "A1", "site": "plant_alpha", "value": 2.34},
{"sensor_id": "A2", "site": "plant_alpha", "value": 1.98},
{"sensor_id": "B1", "site": "plant_beta", "value": 3.05},
]
write_batch(batch)
The tag set in the example is deliberately narrow. In a system with tens of thousands of sensors, I have seen tag explosion when teams added tags for dynamic metadata like “firmware_version” and “last_error_message.” The lesson is to keep the tag space static and use fields for anything that changes frequently. If you need to query by a dynamic attribute, join with a device metadata store rather than indexing it directly in the time series schema.
Ingestion, buffering, and resilience
Devices are often offline or rate‑limited, so ingestion must be resilient. A common pattern is to write to a local buffer on the edge gateway, then forward to the database in batches. SQLite is a surprisingly effective local buffer for small gateways; it tolerates reboots and lets you backfill data after outages.
# Edge gateway buffering using SQLite
# This keeps a small, durable queue of measurements before sending to the cloud
import sqlite3
import json
DB_PATH = "/var/lib/iot/measurements.db"
def init_db():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS measurements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
sensor_id TEXT NOT NULL,
site TEXT NOT NULL,
value REAL NOT NULL,
sent INTEGER NOT NULL DEFAULT 0
)
""")
conn.commit()
conn.close()
def add_measurement(ts, sensor_id, site, value):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("INSERT INTO measurements (ts, sensor_id, site, value) VALUES (?, ?, ?, ?)",
(ts, sensor_id, site, value))
conn.commit()
conn.close()
def get_unsent(limit=100):
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("""
SELECT id, ts, sensor_id, site, value
FROM measurements
WHERE sent = 0
ORDER BY ts
LIMIT ?
""", (limit,))
rows = c.fetchall()
conn.close()
return rows
def mark_sent(ids):
if not ids:
return
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
placeholders = ",".join("?" for _ in ids)
c.execute(f"UPDATE measurements SET sent = 1 WHERE id IN ({placeholders})", ids)
conn.commit()
conn.close()
# Usage
init_db()
add_measurement(ts=1710350000, sensor_id="A1", site="plant_alpha", value=2.34)
def send_to_cloud():
rows = get_unsent(limit=100)
if not rows:
return
batch = []
ids = []
for row in rows:
_, ts, sensor_id, site, value = row
batch.append({"ts": ts, "sensor_id": sensor_id, "site": site, "value": value})
ids.append(row[0])
# Replace with actual cloud send call; here we simulate success
try:
# write_batch(batch) # e.g., call the InfluxDB writer
mark_sent(ids)
except Exception:
# On failure, we leave sent=0 so the next run retries
pass
send_to_cloud()
This SQLite buffer pattern is small but powerful. It decouples network issues from ingestion and lets you implement backpressure strategies. When the network returns, you flush the backlog. On larger gateways or in cloud microservices, you might replace SQLite with NATS or Kafka. The core idea is the same: never lose data due to transient failures, and never overload the database with unbounded bursts.
Querying and downsampling for dashboards
Dashboards usually need aggregated views: averages over five minutes, maxima over an hour, or rolling windows for anomaly detection. Most time series databases provide continuous queries or materialized views to precompute rollups. In TimescaleDB, you can create hypertables and continuous aggregates. In InfluxDB, you can define tasks with Flux.
-- TimescaleDB example: continuous aggregate for 5-minute averages
-- Assumes a hypertable named sensor_readings with time column 'ts' and numeric column 'value'
CREATE MATERIALIZED VIEW vibration_5min
WITH (timescaledb.continuous) AS
SELECT
sensor_id,
time_bucket('5 minutes', ts) AS bucket,
avg(value) AS avg_value,
max(value) AS max_value,
min(value) AS min_value
FROM sensor_readings
GROUP BY sensor_id, bucket;
-- Add a refresh policy to keep the aggregate current
SELECT add_continuous_aggregate_policy('vibration_5min',
start_offset => INTERVAL '1 day',
end_offset => INTERVAL '1 minute',
schedule_interval => INTERVAL '5 minutes');
Continuous aggregates in TimescaleDB help keep dashboard queries fast by limiting how much raw data is scanned. They also simplify retention because you can drop raw data older than your retention window while keeping aggregated views for long‑term trends. For IoT, this is crucial: you might keep raw vibration data for 7 days for detailed fault analysis, but keep hourly aggregates for a year to monitor overall equipment health.
Basic anomaly detection at query time
Sometimes you want lightweight anomaly detection without a full ML pipeline. A common approach is to compute a rolling mean and standard deviation and flag points that deviate beyond N sigma. In many time series databases, you can write user‑defined functions or run ad‑hoc queries. Here is a simple example using SQL window functions in TimescaleDB to compute deviations and flag anomalies.
-- Compute a rolling 10-point mean and stddev; flag outliers
WITH stats AS (
SELECT
ts,
sensor_id,
value,
AVG(value) OVER (PARTITION BY sensor_id ORDER BY ts ROWS BETWEEN 9 PRECEDING AND CURRENT ROW) AS mean10,
STDDEV(value) OVER (PARTITION BY sensor_id ORDER BY ts ROWS BETWEEN 9 PRECEDING AND CURRENT ROW) AS stddev10
FROM sensor_readings
WHERE sensor_id = 'A1'
ORDER BY ts
)
SELECT
ts,
value,
mean10,
stddev10,
CASE
WHEN stddev10 IS NOT NULL AND ABS(value - mean10) > 3 * stddev10 THEN 1
ELSE 0
END AS is_anomaly
FROM stats
WHERE ABS(value - mean10) > 3 * stddev10
ORDER BY ts DESC
LIMIT 50;
This query is intentionally simple. It works well for signals with relatively stable baselines and short windows. In production, you might move to streaming anomaly detection with a proper model, but the SQL approach is often enough to reduce alert fatigue during early stages of a project.
Honest evaluation of popular choices
InfluxDB
InfluxDB (especially the OSS 2.x line) is a go‑to for many IoT teams because of its straightforward metric-plus-tags model and excellent write performance. Flux is powerful for ETL and continuous queries, though it has a learning curve. If you are mostly ingesting device telemetry and building dashboards, InfluxDB provides a balanced developer experience with reasonable operational overhead.
Strengths:
- Efficient ingestion for high frequency metrics.
- Native support for downsampling and retention policies.
- Good client libraries for Python and Go.
Weaknesses:
- Cardinality can bite if tags are not curated.
- Flux can feel heavy compared to SQL for simple queries.
- Clustering features are more polished in the enterprise version.
When to choose:
- You need a dedicated time series store optimized for device metrics.
- You prefer a simple tag indexing model and plan to manage tag cardinality carefully.
TimescaleDB
TimescaleDB is an extension on top of PostgreSQL, which makes it attractive if you already live in the Postgres ecosystem. It supports SQL, continuous aggregates, and native compression. The tradeoff is operational complexity: you manage a Postgres cluster and tune hypertables. The benefit is query flexibility and the ability to join time series with relational metadata.
Strengths:
- Full SQL power and Postgres ecosystem.
- Continuous aggregates and compression for cost control.
- Familiar tooling for developers and analysts.
Weaknesses:
- Requires careful hypertable and chunk sizing for performance.
- Higher baseline memory and disk usage than leaner TSDBs.
When to choose:
- You need complex queries or frequent joins with device metadata.
- You want to keep stack complexity low by reusing Postgres expertise.
Prometheus and VictoriaMetrics
Prometheus is the de facto standard for metrics in cloud‑native environments. It pulls metrics from HTTP endpoints and is excellent for infrastructure and edge services. VictoriaMetrics is a Prometheus‑compatible system focused on high ingestion rates and lower resource usage. For IoT, Prometheus works best for device‑side metrics exposed by services or gateways, while VictoriaMetrics can serve as the long‑term store.
Strengths:
- Strong community and ecosystem for alerting and visualization.
- Simple pull model for services; efficient remote write for edge.
- VictoriaMetrics offers great compression and query speed.
Weaknesses:
- Not ideal for high cardinality device tags.
- Pull model can be awkward for devices behind NAT or intermittent connectivity.
When to choose:
- You are measuring services and gateways rather than raw sensor streams.
- You need battle‑tested alerting and short‑term retention.
M3DB, AWS Timestream, and BigQuery
M3DB is a distributed time series database designed for very high ingestion rates and long retention. It is a good fit for large fleets where you need horizontal scalability. Cloud options like AWS Timestream and Google BigQuery excel at large‑scale analytics and serverless operation, but they can be costlier for high‑frequency raw data and less suitable for low‑latency dashboards.
Strengths:
- M3DB: horizontal scale, multi‑tenant isolation.
- Timestream: serverless, integrations with AWS ecosystem.
- BigQuery: powerful analytics, long retention.
Weaknesses:
- Operational complexity for M3DB; higher latency for Timestream and BigQuery in interactive dashboards.
When to choose:
- M3DB: large fleets, multi‑tenant systems, strict retention needs.
- Timestream: AWS‑centric stacks needing serverless ingestion.
- BigQuery: analytics‑first workloads and heavy aggregations.
Personal experience and common pitfalls
Over the years, I have learned that data modeling is more important than the specific database choice. Early on, I added tags for dynamic attributes like “software_version” and “environment_stage,” thinking it would make queries easier. Within weeks, we hit performance issues caused by cardinality spikes. Moving those attributes to a device metadata table and joining at query time solved the problem without losing insight.
Another hard‑won lesson is the value of local buffering on edge gateways. In one project, a factory network went down for hours during maintenance. Without a buffer, we lost a day’s worth of vibration data. With SQLite on the gateway, we re‑buffered and re‑played the data once connectivity returned. That pattern also helped during firmware updates, where devices rebooted unpredictably.
Finally, retention policies are not “set and forget.” I have seen teams keep raw data for months because “we might need it,” only to run into storage costs that force a panic migration. A staged approach with continuous aggregates and tiered storage is safer: keep raw data for a short window, aggregated data for a longer window, and cold storage for archival. When you actually need detailed raw data for an incident, you can dig into the short‑term store or a targeted archive.
Getting started: workflow, structure, and tooling
A healthy IoT data pipeline separates concerns: ingestion, buffering, processing, storage, and visualization. The mental model is a pipeline with bounded stages and explicit backpressure. You do not need a heavy framework; you need clear boundaries and a small set of tools you can operate.
Suggested folder structure for a small project:
iot-pipeline/
├── README.md
├── docker-compose.yml
├── config/
│ ├── influxdb.conf
│ └── telegraf.conf
├── edge/
│ ├── buffer.py
│ ├── sensor_reader.py
│ └── requirements.txt
├── cloud/
│ ├── writer.py
│ ├── tasks/
│ │ └── downsampling.flux
│ └── dashboards/
│ └── overview.json
└── docs/
├── data-model.md
└── runbook.md
A minimal docker-compose for local development:
version: "3.8"
services:
influxdb:
image: influxdb:2.7
ports:
- "8086:8086"
environment:
- DOCKER_INFLUXDB_INIT_MODE=setup
- DOCKER_INFLUXDB_INIT_USERNAME=admin
- DOCKER_INFLUXDB_INIT_PASSWORD=supersecret
- DOCKER_INFLUXDB_INIT_ORG=my-org
- DOCKER_INFLUXDB_INIT_BUCKET=iot_raw
- DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-token
volumes:
- influx_data:/var/lib/influxdb2
telegraf:
image: telegraf:1.29
volumes:
- ./config/telegraf.conf:/etc/telegraf/telegraf.conf:ro
depends_on:
- influxdb
volumes:
influx_data:
Telegraf configuration for MQTT ingestion:
# config/telegraf.conf
[[inputs.mqtt_consumer]]
servers = ["tcp://localhost:1883"]
topics = ["iot/+/sensor/vibration"]
username = ""
password = ""
data_format = "influx"
[[outputs.influxdb_v2]]
urls = ["http://influxdb:8086"]
token = "my-token"
organization = "my-org"
bucket = "iot_raw"
With this setup, devices publish to MQTT, Telegraf pulls and writes to InfluxDB, and you can build dashboards in Grafana. For production, you would replace local MQTT with a broker like EMQX, add authentication, and deploy Telegraf close to the devices. On gateways, you can run a lightweight version of Telegraf or your own buffering script. The key is to keep the pipeline simple at first and expand only when you hit concrete constraints.
What stands out about time series databases for IoT
A few features make time series databases particularly valuable in IoT contexts:
- Compression: Many systems use Gorilla or similar compression for repeated values and deltas. This reduces storage costs dramatically for high‑frequency sensor data.
- Continuous queries: Precomputing aggregates keeps dashboards responsive and limits scan sizes for long retention.
- Tag indexing: With careful schema design, you can filter and group by device, site, and sensor type efficiently.
- Retention and tiering: Policies that move data to cheaper storage or drop raw data after a window align with real business needs.
Developer experience matters as well. SQL‑based systems like TimescaleDB make it easier for analysts to participate, while InfluxDB and Prometheus have ecosystems that fit naturally into observability stacks. For edge devices, lean ingestion agents and resilient buffers are more important than the database’s query language. For cloud analytics, scale and cost control dominate.
Free learning resources
- InfluxDB documentation: https://docs.influxdata.com/influxdb Useful for understanding the data model, Flux queries, and retention policies.
- TimescaleDB docs: https://docs.timescale.com Excellent for learning hypertables, continuous aggregates, and compression.
- Prometheus docs: https://prometheus.io/docs/ Best for understanding pull‑based metrics, exporters, and alerting.
- VictoriaMetrics documentation: https://docs.victoriametrics.com/ Great for high‑ingestion scenarios and Prometheus compatibility.
- MQTT Essentials by HiveMQ: https://www.hivemq.com/mqtt-essentials/ A practical primer on MQTT, which sits at the heart of many IoT ingestions.
Summary: who should use what, and when to skip
If you are building a device telemetry pipeline with high‑frequency metrics and need a dedicated time series store, start with InfluxDB. Its tag model and retention features fit IoT well, provided you manage cardinality. If you live in the Postgres ecosystem and need flexible queries with joins, choose TimescaleDB. If you are measuring services and gateways more than raw sensor streams, Prometheus and VictoriaMetrics are strong choices, with VictoriaMetrics offering better long‑term storage efficiency.
Skip heavy cloud analytics like Timestream or BigQuery for raw high‑frequency data unless you need serverless scale and have budget for it. Avoid over‑indexing dynamic attributes as tags, and resist the urge to keep raw data forever without downsampling. If you operate at the edge, invest early in local buffering and a clear backpressure strategy.
For most IoT projects, the winning combination is simple: a reliable ingestion layer, a resilient buffer, a time series database that compresses well and supports continuous aggregates, and a dashboard tool for visibility. The database is not the whole system, but it sets the tone for cost, performance, and maintenance. Choose based on your data model, query patterns, and operational appetite, and be ready to evolve the schema as your fleet grows.




