Downsample High-Frequency Sensor Data with Flux Tasks in InfluxDB 2.x
Downsample sub-second sensor data in InfluxDB 2.x using Flux tasks, hourly windowing, and retention pruning to control storage costs.
21 Sept 2026, 05:29 UTC

Raw sensor data is cheap to write and expensive to store
A temperature sensor reporting every second creates 86,400 points per day. Across a fleet, raw points remain indexed in the bucket long after dashboards stop needing sub-second detail. Most views only require hourly averages, yet the high-resolution data continues to consume disk and memory.
The practical fix is continuous downsampling: a scheduled Flux task reads recent raw windows, computes hourly averages, and writes them to a separate bucket. After you verify the downsampled data, you can shorten retention on the raw bucket. This post covers InfluxDB 2.x, where Flux is the default query language for tasks. Behavior in other versions differs and is not covered here.
Why Flux fits downsampling
Flux is a functional, pipe-style query language. A script passes tables through functions such as filter, aggregateWindow, and to. For downsampling, aggregateWindow groups points into fixed windows—such as one hour—and applies an aggregation like mean. Unlike fixed GROUP BY time() intervals, Flux lets you schedule the script with a task offset, giving late-arriving points time to land before the window is aggregated.
Key terms: a bucket is a named storage location with a retention period. A task is a scheduled Flux script. Downsampling writes aggregated points to a new bucket so the raw bucket can be pruned later.
Worked example: hourly temperature downsampling
Assumptions: InfluxDB 2.x, source bucket raw-sensors, destination bucket sensors-hourly, organization engineering. Replace these names with your own.
option task = {
name: "Downsample temperature to hourly",
every: 1h,
offset: 10m,
}
from(bucket: "raw-sensors")
|> range(start: -1h, stop: -10m)
|> filter(fn: (r) => r._measurement == "temperature" and r._field == "value")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> to(bucket: "sensors-hourly")
The task runs every hour, ten minutes past the hour. The range covers the previous complete hour, stopping ten minutes ago so late points have a short buffer. createEmpty: false prevents empty windows from being written when a sensor is silent.
Where to run: InfluxDB UI under Tasks > New Task, or the CLI on a machine with network access to the InfluxDB API.
Required permissions: an API token with read access to raw-sensors, write access to sensors-hourly, and permission to create tasks in the engineering organization.
Example command:
influx task create --file downsample-temperature.flux \
--host http://localhost:8086 \
--token "$INFLUX_TOKEN" \
--org engineering
Set INFLUX_TOKEN or replace "$INFLUX_TOKEN" with your actual token. The command returns a task ID. In the UI, confirm the task appears as scheduled with no recent failures.
Risk: if a run takes longer than every, the next scheduled run may start before the previous one finishes, depending on task concurrency settings. Test on a limited time range before enabling fleet-wide. This example covers one measurement and one field; multiple fields require additional filter logic or separate pipelines.
Verify the downsampled data
Run the same hourly aggregation against the raw bucket and compare a few sample hours to the downsampled bucket:
from(bucket: "raw-sensors")
|> range(start: -7d)
|> filter(fn: (r) => r._measurement == "temperature" and r._field == "value")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> limit(n: 10)
Then run the equivalent query against sensors-hourly and check that the mean values and timestamps align for the sampled hours. Also confirm points exist in the destination bucket:
from(bucket: "sensors-hourly")
|> range(start: -7d)
|> filter(fn: (r) => r._measurement == "temperature" and r._field == "value")
|> aggregateWindow(every: 1d, fn: count, createEmpty: false)
Do not prune raw data until the downsampled counts and averages match expectations.
Limitations
- Flux tasks require InfluxDB 2.x. InfluxQL scripts from 1.x do not run natively.
- Downsampling writes new points; it does not delete the source. Retention is enforced on a background schedule, so you must separately shorten the raw bucket's retention period to reclaim storage.
- Backfilling historical data is not automatic. A task processes data according to its schedule; older points need a separate backfill script or manual task runs.
- Deeply nested pipe operations can become hard to read. Keep downsampling scripts linear where possible.
Rollback
Because this creates a task and writes data, rollback means removing the scheduled task and, optionally, the downsampled bucket:
influx task delete --id "TASK_ID"
Replace TASK_ID with the ID returned when the task was created. Deleting sensors-hourly removes all downsampled points; the raw bucket is untouched unless you manually change its retention.
Closing
Start with one measurement and one bucket. Run the task on a short range, verify the hourly counts and averages, then tighten raw retention. Downsampling does not fix a poorly designed schema, but it is one of the most effective ways to keep storage costs proportional to what you actually query.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.