Skip to content

IoT Data Storage with Time-Series Databases — InfluxDB & TimescaleDB Guide

DodaTech Updated 2026-06-24 7 min read

In this tutorial, you'll learn about IoT Data Storage with Time. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Time-series databases (TSDBs) are purpose-built storage engines optimized for ingesting, querying, and analyzing timestamped data points from IoT sensors, industrial equipment, and monitoring systems at massive scale.

Why Time-Series Databases Matter

A single IoT deployment with 1,000 sensors reporting every 10 seconds generates 8.6 million data points per day. Storing this in a relational database like PostgreSQL with standard indexing leads to slow queries and bloat. Time-series databases use columnar storage, automatic Partitioning by time, and specialized compression to achieve 10-100x better performance and 90% storage reduction. InfluxDB and TimescaleDB are the two most popular open-source TSDBs. TimescaleDB is built as a PostgreSQL extension, so it supports full SQL. InfluxDB uses a custom query language (Flux) and is optimized for high write throughput. DodaZIP's asset tracking platform uses TimescaleDB to store location and environmental data from thousands of shipping sensors.

Plain-Language Explanation

Think of a standard database like a filing cabinet. Each drawer holds a folder (table), each folder holds documents (rows). If you need to find sensor readings from "last Tuesday," the cabinet must open every folder and check every document.

A time-series database is like a diary organized by date. Each day has its own section. To find "last Tuesday," you flip directly to that week and read the entries. The diary automatically compresses older entries into summaries, keeping only the details you need. This makes it fast and storage-efficient for time-centric data.

graph TD
    Sensors[Sensors
1,000 devices] -->|MQTT / HTTP| Ingest[Data Ingest
Telegraf / Custom API] Ingest --> TSDB[Time-Series Database] TSDB -->|Write Path| Mem[In-Memory Buffer] Mem -->|Every 10 min| WAL[Write-Ahead Log] WAL -->|Flush| Shard1[Shard: 2026-06-24] WAL -->|Flush| Shard2[Shard: 2026-06-25] Shard1 -->|Downsample after 7 days| Rollup[Rollup: 1-hour avg] TSDB -->|Read Path| Query[Query Service] Query --> Dashboard[Dashboard
Grafana] Query --> Alert[Alert System] Query --> ML[ML Model Training] style TSDB fill:#e67e22,color:#fff style Shard1 fill:#3498db,color:#fff style Shard2 fill:#3498db,color:#fff

InfluxDB Schema Design

InfluxDB stores data in measurements (like tables), with tags (indexed metadata) and fields (actual values):

-- Create a measurement for temperature sensors
-- Tags: device_id, location (indexed)
-- Fields: temperature, humidity (values)

Python client for writing data:

from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS
import random, time, json

INFLUX_URL = "http://localhost:8086"
INFLUX_TOKEN = "your-token"
INFLUX_ORG = "dodatech"
INFLUX_BUCKET = "iot_sensors"

client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
write_api = client.write_api(write_type=SYNCHRONOUS)

for i in range(10):
    point = (
        Point("environment")
        .tag("device_id", f"sensor-{i:03d}")
        .tag("location", random.choice(["warehouse", "office", "outdoor"]))
        .field("temperature", round(random.uniform(15.0, 35.0), 1))
        .field("humidity", round(random.uniform(30.0, 80.0), 1))
        .field("battery", round(random.uniform(2.5, 3.7), 2))
    )
    write_api.write(bucket=INFLUX_BUCKET, record=point)
    time.sleep(0.1)

print("10 data points written to InfluxDB")

# Query recent data
query_api = client.query_api()
result = query_api.query(
    f'from(bucket:"{INFLUX_BUCKET}")
     |> range(start: -1h)
     |> filter(fn: (r) => r._measurement == "environment")
     |> filter(fn: (r) => r._field == "temperature")'
)
for table in result:
    for record in table.records:
        print(f"{record.values.get('device_id')}: {record.get_value()}°C at {record.get_time()}")

Expected output:

10 data points written to InfluxDB
sensor-001: 24.3°C at 2026-06-24T10:00:00Z
sensor-002: 29.8°C at 2026-06-24T10:00:01Z
sensor-003: 18.2°C at 2026-06-24T10:00:02Z

TimescaleDB Setup

TimescaleDB is a PostgreSQL extension. Install and create a hypertable:

-- Install extension
CREATE EXTENSION IF NOT EXISTS timescaledb;

-- Create sensor table
CREATE TABLE sensor_data (
    time        TIMESTAMPTZ NOT NULL,
    device_id   TEXT NOT NULL,
    location    TEXT NOT NULL,
    temperature DOUBLE PRECISION,
    humidity    DOUBLE PRECISION,
    battery     DOUBLE PRECISION
);

-- Convert to hypertable partitioned by time
SELECT create_hypertable('sensor_data', 'time', chunk_time_interval => INTERVAL '1 day');

Python client for TimescaleDB:

import psycopg2
import random, time
from datetime import datetime, timezone

conn = psycopg2.connect(
    host="localhost",
    port=5432,
    dbname="iot",
    user="dodatech",
    password="your-password"
)
cur = conn.cursor()

# Insert batch of sensor readings
readings = []
for i in range(100):
    readings.append((
        datetime.now(timezone.utc),
        f"sensor-{i:03d}",
        random.choice(["warehouse", "office", "outdoor"]),
        round(random.uniform(15.0, 35.0), 1),
        round(random.uniform(30.0, 80.0), 1),
        round(random.uniform(2.5, 3.7), 2)
    ))

cur.executemany(
    """INSERT INTO sensor_data (time, device_id, location, temperature, humidity, battery)
       VALUES (%s, %s, %s, %s, %s, %s)""",
    readings
)
conn.commit()

# Query hourly average temperature by location
cur.execute("""
    SELECT
        location,
        time_bucket('1 hour', time) AS hour,
        AVG(temperature) AS avg_temp,
        COUNT(*) AS readings
    FROM sensor_data
    WHERE time > NOW() - INTERVAL '1 day'
    GROUP BY location, hour
    ORDER BY hour, location
""")
for row in cur.fetchall():
    print(f"{row[0]} | {row[1]} | Avg: {row[2]:.1f}°C | Count: {row[3]}")

cur.close()
conn.close()

Expected output:

warehouse | 2026-06-24 10:00:00+00 | Avg: 24.5°C | Count: 33
office    | 2026-06-24 10:00:00+00 | Avg: 22.1°C | Count: 34
outdoor   | 2026-06-24 10:00:00+00 | Avg: 28.3°C | Count: 33

Downsampling and Retention

Older data at full resolution is rarely needed. Downsample after a retention period:

-- Create a downsampled table
CREATE TABLE sensor_data_hourly (
    time        TIMESTAMPTZ NOT NULL,
    device_id   TEXT NOT NULL,
    location    TEXT NOT NULL,
    avg_temp    DOUBLE PRECISION,
    min_temp    DOUBLE PRECISION,
    max_temp    DOUBLE PRECISION,
    reading_count INTEGER
);

SELECT create_hypertable('sensor_data_hourly', 'time');

-- Periodic aggregation job
INSERT INTO sensor_data_hourly
SELECT
    time_bucket('1 hour', time) AS time,
    device_id,
    location,
    AVG(temperature) AS avg_temp,
    MIN(temperature) AS min_temp,
    MAX(temperature) AS max_temp,
    COUNT(*) AS reading_count
FROM sensor_data
WHERE time < NOW() - INTERVAL '30 days'
GROUP BY time_bucket('1 hour', time), device_id, location;

-- Drop raw data older than 90 days
DELETE FROM sensor_data WHERE time < NOW() - INTERVAL '90 days';

Grafana Dashboard Integration

Both InfluxDB and TimescaleDB have native Grafana data source plugins. A typical dashboard panel query for TimescaleDB:

SELECT
    time_bucket('5 minutes', time) AS time,
    AVG(temperature) AS avg_temp,
    MAX(temperature) AS max_temp,
    MIN(temperature) AS min_temp
FROM sensor_data
WHERE $__timeFilter(time)
GROUP BY time_bucket('5 minutes', time)
ORDER BY time

Common Mistakes

  1. No tag cardinality management: Tags in InfluxDB are indexed. Using a unique value per data point (e.g., timestamp as a tag) causes high cardinality, crashing the indexing engine. Keep tag values below 10 million unique combinations.

  2. Wrong chunk interval in TimescaleDB: A chunk interval too small creates too many chunks. Too large makes queries slow. Start with 1 day for high-write workloads, 7 days for moderate.

  3. No retention policy: Raw data grows without bound, consuming disk and degrading query performance. Always set a retention policy or automated downsampling job.

  4. Storing metadata as fields: Fields in InfluxDB are not indexed. Store queryable metadata (device ID, location) as tags, and numerical values as fields.

  5. Not using batch writes: Writing one point at a time creates massive HTTP overhead. Batch 100-10,000 points per write for optimal throughput.

Practice Questions

  1. What makes time-series databases different from relational databases? TSDBs use time-based Partitioning, columnar storage, specialized compression, and built-in downsampling. They optimize for append-heavy write patterns and time-range queries.

  2. What is a hypertable in TimescaleDB? A hypertable is a PostgreSQL table automatically partitioned by time into chunks. Queries are automatically optimized to scan only relevant chunks based on the time filter.

  3. Why is tag cardinality important in InfluxDB? Each unique tag combination creates an index entry. High cardinality (millions of unique tag sets) exhausts memory and crashes the TSI (Time Series Index).

  4. What is downsampling and why is it needed? Downsampling aggregates raw data into lower-resolution summaries (e.g., hourly averages). It reduces storage for older data that doesn't need second-level precision.

  5. What is the recommended batch size for InfluxDB writes? 1,000 to 10,000 points per write request balances latency and throughput. Larger batches improve write performance up to the HTTP request size limit.

Mini Project

Build a mock IoT data generator and query tool:

import random, json, sqlite3
from datetime import datetime, timedelta

class IoTTimeSeries:
    def __init__(self, db_path: str = ":memory:"):
        self.conn = sqlite3.connect(db_path)
        self.cur = self.conn.cursor()
        self.cur.execute("""
            CREATE TABLE IF NOT EXISTS sensor_data (
                timestamp TEXT,
                device_id TEXT,
                location TEXT,
                temperature REAL,
                humidity REAL
            )
        """)
        self.cur.execute("CREATE INDEX IF NOT EXISTS idx_time ON sensor_data(timestamp)")

    def generate_data(self, num_devices: int = 5, hours: int = 24):
        start_time = datetime.utcnow() - timedelta(hours=hours)
        for hour_offset in range(hours):
            for device in range(num_devices):
                ts = start_time + timedelta(hours=hour_offset)
                self.cur.execute(
                    """INSERT INTO sensor_data VALUES (?, ?, ?, ?, ?)""",
                    (
                        ts.isoformat(),
                        f"sensor-{device:03d}",
                        random.choice(["warehouse", "office", "outdoor"]),
                        round(random.uniform(15.0, 35.0), 1),
                        round(random.uniform(30.0, 80.0), 1)
                    )
                )
        self.conn.commit()
        print(f"Generated {num_devices * hours} data points")

    def query_hourly_average(self, device_id: str) -> list:
        self.cur.execute("""
            SELECT SUBSTR(timestamp, 0, 14) || ':00:00' AS hour,
                   AVG(temperature) AS avg_temp,
                   AVG(humidity) AS avg_hum
            FROM sensor_data
            WHERE device_id = ?
            GROUP BY hour
            ORDER BY hour
        """, (device_id,))
        return self.cur.fetchall()

ts = IoTTimeSeries()
ts.generate_data(num_devices=3, hours=48)
results = ts.query_hourly_average("sensor-000")
for row in results[:5]:
    print(f"{row[0]} | Temp: {row[1]:.1f}°C | Hum: {row[2]:.1f}%")

Expected output:

Generated 144 data points
2026-06-22 10:00:00 | Temp: 24.5°C | Hum: 55.2%
2026-06-22 11:00:00 | Temp: 25.1°C | Hum: 53.8%
2026-06-22 12:00:00 | Temp: 26.3°C | Hum: 51.4%

Cross-References

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro