Back to Blog
IoT Engineering

Time-Series Databases for IoT: InfluxDB vs TimescaleDB vs AWS Timestream

The wrong time-series database can cost you 10x in cloud bills or leave your team fighting a query language nobody knows. This comparison gives IoT engineers a practical framework to choose between InfluxDB, TimescaleDB, and AWS Timestream.

March 4, 2024
13 min read
InfluxDBTimescaleDBAWS TimestreamTime-Series

Time-Series Databases for IoT: InfluxDB vs TimescaleDB vs AWS Timestream

Your IoT sensors generate timestamps and values — a perfect fit for a time-series database. But InfluxDB, TimescaleDB, and AWS Timestream make radically different trade-offs in query language, cost model, and operational complexity. Picking wrong costs real money and team frustration.

What Makes a Time-Series Database Different

Relational databases store data in row order. Time-series databases organize data by time and measurement, enabling:

  • Compressed columnar storage: sensor readings are repetitive — temperature rarely jumps 100 degrees. Column compression achieves 10–50x over row storage.
  • Automatic downsampling: keep raw data for 7 days, hourly averages for 90 days, daily averages forever — without manual scripts.
  • Time-bucketing queries: "average temperature per 15-minute bucket over the last 24 hours" is a first-class operation.
  • Retention policies: automatic data expiry without application-level cleanup jobs.
  • InfluxDB (v2/v3)

    InfluxDB is the most widely deployed dedicated time-series database in IoT. Its data model uses measurements, tags (indexed metadata), and fields (time-series values).

    Write API:

    // Node.js: write to InfluxDB v2
    const { InfluxDB, Point } = require('@influxdata/influxdb-client')

    const client = new InfluxDB({ url: process.env.INFLUXDB_URL, token: process.env.INFLUXDB_TOKEN, }) const writeApi = client.getWriteApi('my-org', 'sensors', 'ms') writeApi.useDefaultTags({ gatewayId: 'gw-001' })

    function writeSensorData(deviceId, temperature, humidity, battery) { const point = new Point('environment') .tag('deviceId', deviceId) .tag('location', 'building-a') .floatField('temperature', temperature) .floatField('humidity', humidity) .intField('battery', battery) writeApi.writePoint(point) }

    // Flush every 1 second or 5000 points, whichever comes first setInterval(() => writeApi.flush(), 1000)

    Flux query (InfluxDB v2):

    // 15-minute average temperature for the last 24 hours
    from(bucket: "sensors")
      |> range(start: -24h)
      |> filter(fn: (r) => r._measurement == "environment" and r._field == "temperature")
      |> filter(fn: (r) => r.deviceId == "sensor-42")
      |> aggregateWindow(every: 15m, fn: mean, createEmpty: false)
      |> yield(name: "mean")
    

    Strengths:

  • Excellent write throughput (500k+ points/sec on adequate hardware)
  • Flux is powerful for complex transformations
  • Native Grafana datasource with one-click setup
  • Continuous queries / Tasks for automatic downsampling
  • Weaknesses:

  • Flux has a steep learning curve — it's a functional language, not SQL
  • InfluxDB v3 is a significant breaking change from v2
  • Self-hosted operational burden (disk management, compaction)
  • Best for: Teams comfortable with a dedicated TSDB tool, needing high write throughput and complex time-series math.

    TimescaleDB

    TimescaleDB is PostgreSQL with time-series superpowers. If your team already knows SQL and PostgreSQL, the learning curve is near-zero.

    -- Create a hypertable (auto-partitioned by time)
    CREATE TABLE sensor_readings (
      time        TIMESTAMPTZ NOT NULL,
      device_id   TEXT        NOT NULL,
      temperature DOUBLE PRECISION,
      humidity    DOUBLE PRECISION,
      battery     INTEGER
    );
    SELECT create_hypertable('sensor_readings', 'time', chunk_time_interval => INTERVAL '1 day');

    -- Add compression (50–70% size reduction) ALTER TABLE sensor_readings SET ( timescaledb.compress, timescaledb.compress_segmentby = 'device_id' ); SELECT add_compression_policy('sensor_readings', INTERVAL '7 days');

    -- Continuous aggregate: hourly averages CREATE MATERIALIZED VIEW hourly_averages WITH (timescaledb.continuous) AS SELECT time_bucket('1 hour', time) AS bucket, device_id, AVG(temperature) AS avg_temp, AVG(humidity) AS avg_humidity FROM sensor_readings GROUP BY bucket, device_id WITH NO DATA;

    SELECT add_continuous_aggregate_policy('hourly_averages', start_offset => INTERVAL '3 hours', end_offset => INTERVAL '1 hour', schedule_interval => INTERVAL '1 hour');

    Query with familiar SQL:

    -- 15-minute averages for the last 24 hours, with gap filling
    SELECT
      time_bucket_gapfill('15 minutes', time) AS bucket,
      device_id,
      interpolate(AVG(temperature)) AS temperature
    FROM sensor_readings
    WHERE device_id = 'sensor-42'
      AND time > NOW() - INTERVAL '24 hours'
    GROUP BY bucket, device_id
    ORDER BY bucket;
    

    Strengths:

  • Full SQL — no new language to learn
  • JOIN with your existing relational data (devices table, users table)
  • All PostgreSQL extensions work (PostGIS for geospatial, pgcrypto, etc.)
  • Mature ecosystem: pgAdmin, existing ORM support
  • Weaknesses:

  • Lower raw write throughput than InfluxDB for pure time-series workloads
  • Requires PostgreSQL ops knowledge at scale
  • Best for: Teams already running PostgreSQL, or systems where sensor data joins relational data frequently.

    AWS Timestream

    Timestream is a fully managed serverless TSDB. No servers, no disk management, automatic scaling.

    // Node.js: write to AWS Timestream
    const { TimestreamWriteClient, WriteRecordsCommand } = require('@aws-sdk/client-timestream-write')

    const client = new TimestreamWriteClient({ region: 'us-east-1' })

    async function writeToTimestream(deviceId, temperature, humidity) { const now = BigInt(Date.now()).toString()

    await client.send(new WriteRecordsCommand({ DatabaseName: 'IoTDatabase', TableName: 'SensorReadings', CommonAttributes: { Dimensions: [{ Name: 'deviceId', Value: deviceId }], Time: now, TimeUnit: 'MILLISECONDS', }, Records: [ { MeasureName: 'temperature', MeasureValue: String(temperature), MeasureValueType: 'DOUBLE' }, { MeasureName: 'humidity', MeasureValue: String(humidity), MeasureValueType: 'DOUBLE' }, ], })) }

    Query (SQL-compatible):

    -- 15-minute averages, Timestream syntax
    SELECT
      bin(time, 15m) AS bucket,
      deviceId,
      AVG(measure_value::double) AS avg_temperature
    FROM "IoTDatabase"."SensorReadings"
    WHERE measure_name = 'temperature'
      AND deviceId = 'sensor-42'
      AND time BETWEEN ago(24h) AND now()
    GROUP BY bin(time, 15m), deviceId
    ORDER BY bucket
    

    Strengths:

  • Zero operational overhead — truly serverless
  • Deep AWS integration (IoT Core Rules, Lambda, Grafana via plugin)
  • Automatic tiering: hot memory store (recent data) + cheap S3-backed magnetic store (historical)
  • Weaknesses:

  • Cost model is complex and can surprise at scale ($0.50/GB ingested + $0.036/GB stored in memory + query scan costs)
  • Less flexible for complex analytics than InfluxDB Flux
  • Grafana plugin is less mature than native InfluxDB support
  • Best for: AWS-native deployments, teams that want zero ops burden, and systems with manageable data volumes.

    Head-to-Head Comparison

    | Dimension | InfluxDB | TimescaleDB | Timestream | |-----------|----------|-------------|------------| | Write throughput | Excellent | Good | Good | | Query language | Flux (custom) | SQL | SQL-like | | Ops overhead | Medium | Medium | Zero | | Grafana support | Native | Via PostgreSQL | Plugin | | Cost at 1M readings/day | ~$50/mo (cloud) | ~$30/mo (EC2) | ~$80/mo | | Data joins (relational) | Poor | Excellent | Poor | | AWS-native | No | No | Yes |

    Retention and Downsampling

    All three support automatic retention, but the configuration differs significantly.

    For Timestream, set it in the table configuration: memory store retention (hours), magnetic store retention (days). For InfluxDB, define retention policies in the bucket settings. For TimescaleDB, use add_retention_policy('sensor_readings', INTERVAL '90 days').

    The sweet spot for most IoT deployments: raw data for 30 days, 1-hour aggregates for 1 year, 1-day aggregates forever.

    For the full picture of how these databases fit into your data pipeline, see [IoT Data Pipeline: Sensor to Dashboard](/blog/iot-data-pipeline-sensor-to-dashboard).

    For real-time alerting on top of this data, see [Real-Time IoT Alerting](/blog/realtime-iot-alerting-threshold-anomaly).

    Need help with IoT data storage architecture? [Contact Code Caracal](/contact) — we've shipped these systems for clients across 15+ countries.

    ⚡
    Written by CodeCaracal Engineering

    We write from production experience — every technique in our articles has been deployed to real clients. No academic theory.

    Let's Build Together

    Got an IoT challenge?
    We've shipped it.

    Whether you need a fleet to track, a factory to monitor, or a farm to automate — our team has done it before and we'd love to build it with you. Typical response time: under 24 hours.

    No upfront commitment99.9% uptime SLANDA on requestFixed-price options