InfluxDB: The Time-Series Database for IoT and Monitoring

Riassunto

InfluxDB is built for sensors, metrics and time series. This guide covers the data model, the classic tag vs field mistake, batched writes from Python, Flux and SQL queries, retention and downsampling, choosing between InfluxDB 2.x and 3, and three real use cases from a university data engineering course.

Alessandro Fiori Avatar

PhD · Politecnico di Torino

TL;DR. Sensors, system metrics, vital signs, prices: when every data point carries a timestamp and thousands arrive per minute, a relational database starts to struggle. InfluxDB is built for that workload. This guide covers the data model (bucket, measurement, tag, field), the most common schema mistake, batched writes from Python, querying with Flux and SQL, retention and downsampling, and which version to pick in 2026 — with three real use cases from a university data engineering course.

A group of vineyards installs sensors across their plots: air and soil temperature, soil moisture, rainfall, solar radiation, wind speed. Each sensor reports every 15 minutes. With 8 plots and 6 metrics that’s barely anything — around 4,600 rows a day. Then the group grows to 60 farms, sensors move to one reading per minute, and someone asks to compare this season with the last five. Suddenly you’re ingesting hundreds of thousands of rows a day and running queries across years of history.

That’s when whoever put everything into one PostgreSQL table discovers that queries slow down, indexes grow faster than the data, and deleting old rows locks the table. It isn’t PostgreSQL’s fault — it’s an access pattern it wasn’t designed for. Earlier in our Digital Stack 2026 series we saw how Kafka moves these streams and how Grafana visualizes them. Today we look at the layer in between: where the data actually lives.

What a time series is and why it deserves its own database

A time series is a sequence of measurements ordered by time: a weather station’s temperature every 5 minutes, a patient’s heart rate every 30 seconds, a server’s CPU every 10 seconds. The data itself is simple. What makes it special is how it’s written and read.

The time-series access pattern

  • Continuous, append-only writes. Data always arrives “now”; past readings are almost never updated.
  • Range reads. Nobody asks for “row id 48,213,977”. People ask for “hourly average PM10 downtown over the last week”.
  • Aggregations everywhere. Means, maxima, percentiles, moving averages over time windows.
  • Data value decays. Yesterday’s per-second reading is gold; three years from now it only matters as an hourly or daily average.

Why row-oriented databases degrade

A classic relational database stores data row by row and maintains B-tree indexes to find it. At hundreds of thousands of inserts per day, three things happen: indexes on (sensor_id, timestamp) grow and become expensive to maintain; aggregations read entire rows when they need two columns; and purging old data with DELETE causes fragmentation and vacuum overhead. My course deck sums it up in one line of its “wrong choice vs right choice” table: time-series writes at 1M/s → row-oriented DB (wrong) → TimescaleDB or InfluxDB (right).

To be fair: TimescaleDB, a PostgreSQL extension, is an excellent choice if your team already lives in SQL and you need to JOIN time series with relational tables. InfluxDB makes sense when time series are the core of the system and you want an ecosystem built around them: a collection agent (Telegraf), automatic retention, native Grafana integration.

The data model: bucket, measurement, tag and field

Before writing any code you need four concepts. Getting them wrong is the number-one cause of performance problems.

InfluxDB data model: bucket, measurement, indexed tags, numeric fields and timestamp in a line protocol row
Anatomy of an InfluxDB point: tags describe who measured, fields hold what was measured
  • Bucket (called a database in InfluxDB 3): the container, with its own retention policy. E.g. agro.
  • Measurement: roughly a table. E.g. sensors.
  • Tags: indexed key-value pairs describing the source. E.g. farm=north_ridge, plot=P03, sensor_type=soil.
  • Fields: the measured values, not indexed. E.g. soil_temp=8.4, soil_moisture=31.2.

Every point also has a timestamp. In line protocol, the text format InfluxDB ingests, a point looks like this:

sensors,farm=north_ridge,plot=P03,sensor_type=soil soil_temp=8.4,soil_moisture=31.2 1753084800000000000

Tag or field? The classic mistake

The rule is simple: if you filter or group by it, it’s a tag; if you compute on it, it’s a field. Every year I see student teams store everything as fields, including station_id. It works, but every “give me station X” query scans every point in the range instead of using the index. The opposite mistake is just as bad: using near-unique values as tags — raw timestamps, request UUIDs, session IDs.

Cardinality: the concept that decides performance

Series cardinality is the number of unique measurement + tag combinations. With 50 stations, 8 cities and 3 sensor types you have at most 1,200 series: no problem. Add a unique reading_id tag and cardinality explodes into the millions, and InfluxDB 2.x’s in-memory index suffers badly. InfluxDB 3 copes with far higher cardinality thanks to its columnar engine, but the rule of thumb still holds: tags describe stable entities, not individual events.

InfluxDB 2.x or InfluxDB 3: which one in 2026?

Most older tutorials skip this question, and it changes a lot. InfluxDB 3 is a ground-up rewrite in Rust built on Apache Arrow, DataFusion and Parquet. The practical differences:

AspectInfluxDB OSS 2.xInfluxDB 3 Core
Query languagesFlux, InfluxQLSQL, InfluxQL (no Flux)
LicenseMITMIT / Apache 2.0
Web UIBuilt in (Data Explorer, dashboards)Separate (InfluxDB 3 Explorer) or Grafana
High cardinalityWeak spotHandled well (columnar engine)
Built-in automationFlux tasksProcessing Engine with Python plugins
DeploymentSingle nodeSingle node (HA and clustering in Enterprise)

The key point: Flux is in maintenance mode. InfluxData still ships security patches and critical fixes for it on 1.x and 2.x, but no new features, and recommends SQL or InfluxQL for new code. My take: if you already run 2.x with Flux, there’s no rush to migrate. If you’re starting a new project today, seriously consider InfluxDB 3 with SQL — it’s where the product is heading, and SQL is a skill your team reuses everywhere. For teaching, 2.x is still convenient thanks to its built-in UI, and it’s what the course projects use.

Writing data: from sensor to bucket

The official influxdb-client Python library (for InfluxDB 2.x) supports synchronous and batched writes. The difference is huge: synchronous means one HTTP request per point; batching buffers points and ships them in blocks. In the project’s technical notes this is explicit — the Kafka → InfluxDB consumer must use batch mode.

from influxdb_client import InfluxDBClient, Point, WriteOptions

client = InfluxDBClient(url="http://localhost:8086", token="***", org="flowygo")
write_api = client.write_api(write_options=WriteOptions(
    batch_size=5_000, flush_interval=10_000  # 5,000 points or 10 seconds
))

def on_reading(r):  # called for each Kafka message
    p = (Point("sensors")
         .tag("farm", r["farm"]).tag("plot", r["plot"]).tag("sensor_type", r["type"])
         .field("soil_temp", float(r["soil_temp"]))
         .field("soil_moisture", float(r["soil_moisture"]))
         .time(r["timestamp"]))
    write_api.write(bucket="agro", record=p)

# when done: write_api.close() flushes the buffer

Notice the explicit float(). If the first value written to a field is an integer and the next is a decimal, InfluxDB rejects the second with a type conflict — a very common failure when importing CSVs. For the initial load of millions of historical rows, always use bulk writes (or influx write from the CLI), never single inserts.

Database consulting

Is your database slowing down or struggling to keep up with growth? We start from the data model and find the right solution, relational or NoSQL.

Discover database consulting →

On InfluxDB 3, the Python client is influxdb3-python and it accepts the same line protocol; the mental model is identical.

Querying: Flux vs SQL

Here’s the same question — hourly average soil temperature per plot over the last 7 days — in both languages.

Flux (InfluxDB 2.x)

from(bucket: "agro")
  |> range(start: -7d)
  |> filter(fn: (r) => r._measurement == "sensors" and r._field == "soil_temp")
  |> group(columns: ["plot"])
  |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)

Flux is a pipeline language: each |> hands its output to the next step. It’s powerful — in the smart agriculture project we use it to compute Growing Degree Days, the seasonal heat accumulation that drives vine development, with a simple map() summing degrees above 10 °C — but it has its own learning curve.

SQL (InfluxDB 3)

SELECT
  plot,
  date_bin(INTERVAL '1 hour', time) AS hour,
  avg(soil_temp) AS avg_soil_temp
FROM sensors
WHERE time >= now() - INTERVAL '7 days'
GROUP BY plot, hour
ORDER BY hour;

If you know SQL, you can read it at a glance. date_bin() does what aggregateWindow() does: it buckets points into regular intervals.

InfluxDB Cloud Data Explorer with a SQL query and a chart of hourly average soil temperature by plot
This article’s SQL query in the InfluxDB Cloud Data Explorer: soil temperature for five plots, hourly average over 7 days

Retention and downsampling: keep only what you need

Keeping a 30-second reading forever is expensive and pointless. The standard strategy has two tiers, and it’s what the healthcare monitoring project requires: a vitals_raw bucket with 7-day retention for high-frequency vitals, and a vitals_hourly bucket with 5-year retention holding only hourly averages. In InfluxDB 2.x the move between tiers is a scheduled Flux task:

option task = {name: "downsample_vitals", every: 1h}

from(bucket: "vitals_raw")
  |> range(start: -task.every)
  |> filter(fn: (r) => r._measurement == "vitals")
  |> aggregateWindow(every: 1h, fn: mean)
  |> to(bucket: "vitals_hourly")

Expired data is dropped automatically — no manual DELETE, no locks. On InfluxDB 3 you get the same result with per-database retention and a scheduled Processing Engine plugin written in Python.

InfluxDB in the pipeline: Kafka, Telegraf and Grafana

InfluxDB rarely works alone. In the stack we’re building in this series it has a precise job: receive data from the stream and serve it to visualization.

IoT pipeline: sensors to Kafka, consumer or Telegraf to InfluxDB, then Grafana dashboards and alerts
Where InfluxDB sits: between Kafka streaming and Grafana dashboards
  • Kafka → InfluxDB. Two routes: a Python consumer with batched writes like the one above, or Telegraf with the kafka_consumer plugin, no code required. As we saw in Kafka Part 1, the log means that if InfluxDB is down for maintenance, data waits in the topic and nothing is lost.
  • InfluxDB → Grafana. Grafana has a native InfluxDB data source supporting Flux, InfluxQL and SQL. Watch the config: with 2.x you must explicitly select Flux, otherwise Grafana expects InfluxQL and queries fail. It’s the single most common snag, which is why the course asks students to document it in their README.
  • Alerts. Critical thresholds — PM10 above WHO limits, soil temperature below 2 °C, oxygen saturation below 94% — are configured in Grafana Alerting on top of InfluxDB queries.

Three real use cases

Three of the seven capstone projects in my Advanced Data Modeling course at Politecnico di Torino use InfluxDB, each with a different challenge:

ProjectWhat it measuresTagsMain challenge
Smart CityPM10, PM2.5, NO₂, O₃, temperature, noise every 5 min from 50 stations (~5M points/year)station_id, city, sensor_typeStreaming ingest from Kafka with batched writes
Smart AgricultureAir/soil temperature, moisture, rain, radiation every 15 minfarm, plot, sensorGrowing Degree Days in Flux and frost alerts
Healthcare monitoringHeart rate, SpO₂, blood pressure, respiratory rate every 30 s (synthetic data)ward, bed, patient_pseudo_idTiered retention and clinical alerts

In the healthcare project, 10 patients × 4 vitals × one reading every 30 seconds × 7 days comes to about 80,000 points — trivial for InfluxDB, but enough to show students why retention must be designed up front, not after the fact.

Best practices from day one

  • Design tags before writing the first point. Ask: what will I filter by? Those are tags. Everything else is a field.
  • Avoid high-cardinality tags: timestamps, UUIDs, request IDs, free text.
  • Always write in batches, both for streaming and historical imports.
  • Pin field types (a float is always a float) to avoid conflicts.
  • Use the right timestamp precision: seconds are plenty for an environmental sensor.
  • Define retention and downsampling at design time, not when the disk is full.
  • New project in 2026? Write queries in SQL or InfluxQL, not Flux.

Next step: orchestrating it all

You now have the three pieces of the real-time data layer: Kafka transports, InfluxDB stores, Grafana displays. One thing is missing: who coordinates the batch work — the nightly CSV import, the KPI recalculation, the sync with PostgreSQL? Next in the series: Apache Airflow, and the full pipeline for the smart agriculture project.

If your organization has sensors, machines or systems producing time-stamped data and you’re not sure where to put it — or it’s sitting in a spreadsheet that barely opens anymore — request a data and AI analysis. We’ll start from your real data volumes and pick a proportionate tool, whether that’s InfluxDB, TimescaleDB or something even simpler.