Time series
time-series licenseHotKV’s time series store numeric samples (a millisecond timestamp and a value) in named series. You append samples as they arrive, read them back by time range, and aggregate them into buckets (averages per minute, maximums per hour, and so on) on the server. Use them for metrics, sensor and IoT readings, prices, and any numeric value you track over time.
Time series are an enterprise feature. On HotKV Cloud they are included in
the Performance and Scale plans; on HotKV On-Premise they need the
time-series license feature. Without it, every TS.* command replies
-NOLICENSE time-series requires a HotKV license. Contact sales@hotkv.com.
Check with LICENSE.CHECK timeseries.
Series are not keys
Section titled “Series are not keys”A series lives in its own namespace, next to the keyspace rather than in
it. A series named sensor:1:temp is not a key: TYPE, EXISTS, DEL,
EXPIRE, KEYS and SCAN do not see it, and a regular key with the same
name can exist alongside it. Use the TS.* commands to manage series:
TS.DELETE removes one, and
TS.LIST lists them.
Create, add, read, delete
Section titled “Create, add, read, delete”A series must be created with TS.CREATE before samples are added. It takes an optional retention in milliseconds (0, the default, keeps samples forever) and optional labels.
127.0.0.1:6379> TS.CREATE sensor:1:temp LABELS room kitchen unit celsiusOK127.0.0.1:6379> TS.ADD sensor:1:temp 1767225600000 21.5(integer) 1767225600000127.0.0.1:6379> TS.ADD sensor:1:temp 1767225660000 21.7(integer) 1767225660000127.0.0.1:6379> TS.ADD sensor:1:temp 1767225720000 22.4(integer) 1767225720000127.0.0.1:6379> TS.RANGE sensor:1:temp - +1) 1) (integer) 1767225600000 2) "21.500000"2) 1) (integer) 1767225660000 2) "21.700000"3) 1) (integer) 1767225720000 2) "22.400000"127.0.0.1:6379> TS.RANGE sensor:1:temp 1767225660000 + COUNT 11) 1) (integer) 1767225660000 2) "21.700000"127.0.0.1:6379> TS.ADD sensor:2:temp * 19.0(error) ERR series not found- TS.ADD takes exactly a series, a timestamp and a
value, and replies with the timestamp it stored. Pass
*(or 0) to use the server’s current time. - TS.RANGE returns samples between two timestamps,
both inclusive;
-and+stand for the earliest and latest.COUNTcaps the number of samples returned. Values come back as strings with six decimals. - Timestamps are Unix time in milliseconds.
Aggregation
Section titled “Aggregation”TS.AGG splits a time range into buckets of a fixed length and returns one aggregated value per bucket that has samples. Each result is the bucket’s start time and the value.
127.0.0.1:6379> TS.AGG sensor:1:temp 1767225600000 + 120000 avg1) 1) (integer) 1767225600000 2) "21.600000"2) 1) (integer) 1767225720000 2) "22.400000"127.0.0.1:6379> TS.AGG sensor:1:temp 1767225600000 + 120000 count1) 1) (integer) 1767225600000 2) "2.000000"2) 1) (integer) 1767225720000 2) "1.000000"Buckets are counted from the start of the range you give, so pass a
rounded start time (or -, which starts at 0) to get buckets aligned to
whole minutes or hours. Empty buckets are left out.
| Aggregation | Result per bucket |
|---|---|
avg |
Mean of the values |
sum |
Sum |
min / max |
Smallest / largest value |
count |
Number of samples |
first / last |
Earliest / latest sample’s value |
range |
max minus min |
std.p (or stddev) |
Population standard deviation |
Write the aggregation in lower or upper case (avg or AVG).
Retention
Section titled “Retention”A retention removes samples once they are older than the retention period, measured back from the server’s current time. Old samples are removed whenever a new sample is added to the series (and when you change the retention), so a series that receives no new samples keeps its last ones.
127.0.0.1:6379> TS.CREATE api:latency RETENTION 86400000 LABELS service checkoutOK127.0.0.1:6379> TS.RETENTION api:latency 604800000OKTS.RETENTION changes the retention of an existing series; 0 removes the limit.
Labels and discovery
Section titled “Labels and discovery”Labels are name-value pairs attached at creation. TS.LIST
returns every series name, or with LABEL name value only the series
carrying that label. TS.INFO returns a series’
details: key, totalSamples (samples added over the series’ life),
firstTimestamp and lastTimestamp, lastValue, retentionMs,
memoryBytes, labels, and rules (the number of downsample rules).
127.0.0.1:6379> TS.LIST LABEL room kitchen1) "sensor:1:temp"Downsampling
Section titled “Downsampling”TS.DOWNSAMPLE links two existing series: from
then on, every sample added to the source is also added to the
destination. The aggregation and bucket length you give are recorded with
the rule, and you read the destination with TS.AGG. The usual setup is a
raw series with a short retention and a destination with a long one:
127.0.0.1:6379> TS.CREATE {sensor:1}:raw RETENTION 86400000OK127.0.0.1:6379> TS.CREATE {sensor:1}:history RETENTION 2592000000OK127.0.0.1:6379> TS.DOWNSAMPLE {sensor:1}:raw {sensor:1}:history avg 60000OKRules apply to samples added after the rule was created.
Clusters, persistence, and limits
Section titled “Clusters, persistence, and limits”- In a cluster, a series lives on the node that owns its
name’s hash slot, and commands for it are redirected like key commands.
TS.DOWNSAMPLEneeds both series in the same slot, so give them a common hash tag as in the example above.TS.LISTlists only the series of the node you ask. - Series are saved in snapshots and the AOF, and replicas keep the same series as their primary.
FLUSHALLremoves all series;FLUSHDBdoes not touch them.- The number of series per server is capped by
max_seriesunder[timeseries]in the configuration (0, the default, picks a limit that suits the server’s memory).TS.CREATEpast the cap repliesERR series already exists or limit reached. enabled = falseunder[timeseries]switches the feature off on a licensed server.
Differences from RedisTimeSeries
Section titled “Differences from RedisTimeSeries”If you are porting code written for the RedisTimeSeries module:
| RedisTimeSeries | HotKV |
|---|---|
A series is a key (DEL deletes it, TYPE shows it) |
A series is outside the keyspace; delete it with TS.DELETE |
TS.ADD creates a missing series and takes options |
Create the series first; TS.ADD takes only timestamp and value |
| Out-of-order samples are accepted or rejected by a duplicate policy | Late samples are stored at the newest timestamp; duplicates are kept |
TS.RANGE ... AGGREGATION |
Use TS.AGG |
TS.CREATERULE aggregates into the destination as data arrives |
TS.DOWNSAMPLE copies raw samples; aggregate with TS.AGG when reading |
TS.MADD, TS.GET, TS.MGET, TS.MRANGE, TS.REVRANGE, TS.INCRBY, TS.DECRBY, TS.ALTER, TS.DEL, TS.QUERYINDEX |
Not available. Use TS.LIST LABEL to find series, and TS.RANGE with COUNT |
Commands
Section titled “Commands”| Command | What it does |
|---|---|
| TS.CREATE | Create a series with optional retention and labels |
| TS.ADD | Add a sample |
| TS.RANGE | Samples in a time range |
| TS.AGG | Bucketed aggregation over a time range |
| TS.INFO | Details of a series |
| TS.RETENTION | Change a series’ retention |
| TS.DOWNSAMPLE | Copy a series’ new samples into another series |
| TS.LIST | List series, optionally by label |
| TS.DELETE | Delete a series |
From your application
Section titled “From your application”import hotkv
client = hotkv.HotKV(host="localhost", port=6379, decode_responses=True)
client.ts_create("sensor:1:temp", labels={"room": "kitchen"})client.ts_add("sensor:1:temp", 21.5, 1767225600000) # value, then timestampclient.ts_add("sensor:1:temp", 21.7) # timestamp defaults to "*"print(client.ts_range("sensor:1:temp", "-", "+"))print(client.ts_agg("sensor:1:temp", "-", "+", 60000, "avg"))client.close()import { HotKV } from "@hotkv/client";
const client = new HotKV({ url: "hotkv://127.0.0.1:6379" });
await client.tsCreate("sensor:1:temp", { labels: { room: "kitchen" } });await client.tsAdd("sensor:1:temp", 1767225600000, 21.5);await client.tsAdd("sensor:1:temp", "*", 21.7);console.log(await client.tsRange("sensor:1:temp", "-", "+"));console.log(await client.tsAgg("sensor:1:temp", "-", "+", 60000, "avg"));await client.close();client, err := hotkv.NewClient(&hotkv.Options{Addr: "localhost:6379"})if err != nil { log.Fatal(err)}defer client.Close()ctx := context.Background()
client.TSCreate(ctx, "sensor:1:temp", 0, map[string]string{"room": "kitchen"})ts := int64(1767225600000)client.TSAdd(ctx, "sensor:1:temp", &ts, 21.5)client.TSAdd(ctx, "sensor:1:temp", nil, 21.7) // nil timestamp: server time
samples, _ := client.TSRange(ctx, "sensor:1:temp", 0, math.MaxInt64, 0).Result()buckets, _ := client.TSAgg(ctx, "sensor:1:temp", 0, math.MaxInt64, time.Minute, "avg").Result()fmt.Println(samples, buckets)import com.hotkv.HotKV;import java.util.Map;
try (HotKV client = HotKV.create("hotkv://localhost:6379")) { var ts = client.timeSeries(); ts.create("sensor:1:temp", null, Map.of("room", "kitchen")); ts.add("sensor:1:temp", 1767225600000L, 21.5); ts.add("sensor:1:temp", null, 21.7); // null timestamp: server time System.out.println(ts.range("sensor:1:temp", "-", "+", null)); System.out.println(ts.agg("sensor:1:temp", "-", "+", 60000, "avg"));}Was this page helpful?
Thanks for the feedback.