# Time series

> Named series of timestamped numeric samples with retention, labels, range queries, and bucketed aggregation.

URL: https://docs.hotkv.com/data-types/time-series/


**Availability:** HotKV Cloud: Performance and Scale plans. On-Premise: requires the `time-series` license feature.

HotKV'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`.

> **Caution: Not RedisTimeSeries**
> The `TS.*` commands share names with the RedisTimeSeries module, but they
> are HotKV's own commands with their own arguments and replies. Code written
> for RedisTimeSeries needs changes; see [Differences from
> RedisTimeSeries](#differences-from-redistimeseries) below.

## 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](https://docs.hotkv.com/commands/ts-delete/index.md) removes one, and
[TS.LIST](https://docs.hotkv.com/commands/ts-list/index.md) lists them.

## Create, add, read, delete

A series must be created with [TS.CREATE](https://docs.hotkv.com/commands/ts-create/index.md) 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 celsius
OK
127.0.0.1:6379> TS.ADD sensor:1:temp 1767225600000 21.5
(integer) 1767225600000
127.0.0.1:6379> TS.ADD sensor:1:temp 1767225660000 21.7
(integer) 1767225660000
127.0.0.1:6379> TS.ADD sensor:1:temp 1767225720000 22.4
(integer) 1767225720000
127.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 1
1) 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](https://docs.hotkv.com/commands/ts-add/index.md) 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](https://docs.hotkv.com/commands/ts-range/index.md) returns samples between two timestamps,
  both inclusive; `-` and `+` stand for the earliest and latest. `COUNT`
  caps the number of samples returned. Values come back as strings with
  six decimals.
- Timestamps are Unix time in milliseconds.

> **Note: Samples are kept in time order**
> A series only grows forward. A sample whose timestamp is older than the
> newest sample is stored at the newest sample's timestamp instead (`TS.ADD`
> replies with the timestamp it used). Several samples may share a
> timestamp; all of them are kept. If you backfill history, add it oldest
> first, before live data.

## Aggregation

[TS.AGG](https://docs.hotkv.com/commands/ts-agg/index.md) 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 avg
1) 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 count
1) 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

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 checkout
OK
127.0.0.1:6379> TS.RETENTION api:latency 604800000
OK
```

[TS.RETENTION](https://docs.hotkv.com/commands/ts-retention/index.md) changes the retention of an
existing series; 0 removes the limit.

## Labels and discovery

Labels are name-value pairs attached at creation. [TS.LIST](https://docs.hotkv.com/commands/ts-list/index.md)
returns every series name, or with `LABEL name value` only the series
carrying that label. [TS.INFO](https://docs.hotkv.com/commands/ts-info/index.md) 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 kitchen
1) "sensor:1:temp"
```

## Downsampling

[TS.DOWNSAMPLE](https://docs.hotkv.com/commands/ts-downsample/index.md) 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 86400000
OK
127.0.0.1:6379> TS.CREATE {sensor:1}:history RETENTION 2592000000
OK
127.0.0.1:6379> TS.DOWNSAMPLE {sensor:1}:raw {sensor:1}:history avg 60000
OK
```

Rules apply to samples added after the rule was created.

## Clusters, persistence, and limits

- In a [cluster](https://docs.hotkv.com/clustering/index.md), a series lives on the node that owns its
  name's hash slot, and commands for it are redirected like key commands.
  `TS.DOWNSAMPLE` needs both series in the same slot, so give them a common
  hash tag as in the example above. `TS.LIST` lists 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.
- `FLUSHALL` removes all series; `FLUSHDB` does not touch them.
- The number of series per server is capped by `max_series` under
  `[timeseries]` in the configuration (0, the default, picks a limit that
  suits the server's memory). `TS.CREATE` past the cap
  replies `ERR series already exists or limit reached`.
- `enabled = false` under `[timeseries]` switches the feature off on a
  licensed server.

## 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

| Command | What it does |
| --- | --- |
| [TS.CREATE](https://docs.hotkv.com/commands/ts-create/index.md) | Create a series with optional retention and labels |
| [TS.ADD](https://docs.hotkv.com/commands/ts-add/index.md) | Add a sample |
| [TS.RANGE](https://docs.hotkv.com/commands/ts-range/index.md) | Samples in a time range |
| [TS.AGG](https://docs.hotkv.com/commands/ts-agg/index.md) | Bucketed aggregation over a time range |
| [TS.INFO](https://docs.hotkv.com/commands/ts-info/index.md) | Details of a series |
| [TS.RETENTION](https://docs.hotkv.com/commands/ts-retention/index.md) | Change a series' retention |
| [TS.DOWNSAMPLE](https://docs.hotkv.com/commands/ts-downsample/index.md) | Copy a series' new samples into another series |
| [TS.LIST](https://docs.hotkv.com/commands/ts-list/index.md) | List series, optionally by label |
| [TS.DELETE](https://docs.hotkv.com/commands/ts-delete/index.md) | Delete a series |

## From your application

**Python**

```python
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 timestamp
client.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()
```

**Node.js**

```ts
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();
```

**Go**

```go
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)
```

**Java**

```java
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"));
}
```
