Redis Time Series in Java

Last updated
August 10, 2026

"Can Redis do time series?" is a question with three correct answers, and picking the wrong one is expensive to undo.

There is a purpose-built module, RedisTimeSeries, with its own command family, automatic downsampling and label-based queries. There are plain sorted sets, which have been the pragmatic answer since long before the module existed. And there is Redisson's RTimeSeries, a Java collection that gives you a typed API over the primitive.

Most articles on this topic present the module and stop. That is fine until you discover that your managed provider will not load it, or that the Valkey cluster you migrated to for licensing reasons has no official build of it. This post covers all three options, what each actually gives you, and — the part usually left out — what to do when the module is not available to you.

Three Ways to Store Time Series Data in Redis

RedisTimeSeriesSorted setRedisson RTimeSeries
What it isA module (TS.* commands)A core data typeA Java collection over sorted sets
Automatic downsamplingYes — compaction rulesNoNo
Labels / cross-series queriesYesNoPer-entry label, single series
Server-side aggregationYesavg, sum, min, maxNoNo
Per-sample retentionSeries-level RETENTIONManualPer-entry TTL
Typed Java APIVia a module clientRaw scores and membersYes — generics, async, reactive
Runs on ValkeyNo official buildYesYes
Runs on ElastiCache / MemoryDBNoYesYes

The shape of the decision is visible in that table. The module is more capable; the primitives are more portable. Which one wins depends far more on where you deploy than on what your data looks like.

RedisTimeSeries: The Purpose-Built Module

RedisTimeSeries stores samples as timestamp/value pairs in a compressed, append-optimised structure and exposes its own command family. If you control your deployment and you need what it does, it is the right answer — nothing you build on sorted sets will match it.

The basics look like this:

> TS.CREATE sensor:temp:1 RETENTION 604800000 LABELS floor 2 unit celsius
OK
> TS.ADD sensor:temp:1 1786352400000 21.4
(integer) 1786352400000
> TS.RANGE sensor:temp:1 1786352400000 1786352520000
1) 1) (integer) 1786352400000
   2) "21.4"

Labels and Cross-Series Queries

This is the feature with no equivalent anywhere else in Redis. Series carry labels, and you can query across every series matching a filter in one command:

> TS.MRANGE - + FILTER floor=2 GROUPBY floor REDUCE max
> TS.QUERYINDEX unit=celsius

With sorted sets you would issue one read per series and combine the results in your application. For ten series that is a non-issue; for ten thousand it is the whole design.

Retention and Compaction

Two mechanisms keep a series from growing without bound. RETENTION expires samples older than a given age relative to the newest sample. Compaction rules aggregate raw samples into a coarser destination series automatically, as data arrives:

> TS.CREATE sensor:temp:1:1h
> TS.CREATERULE sensor:temp:1 sensor:temp:1:1h AGGREGATION avg 3600000

Raw samples land in sensor:temp:1, hourly averages accumulate in sensor:temp:1:1h, and you can set a short retention on the raw series and a long one on the compacted series. That pattern — keep minutes for a week, hours for a year — is the core of what a time series database does, and the module does it server-side with no application code at all.

Reads support aggregation over time buckets (TS.RANGE key - + AGGREGATION avg 60000), bucket alignment, and filtering by timestamp or value. Writes support a configurable duplicate-timestamp policy. From Redis 8.6, samples can hold NaN for missing readings.

Where the Module Is Actually Available

This is the part that decides the question for a lot of teams, and it is rarely stated plainly.

PlatformRedisTimeSeriesNotes
Redis Open Source / Redis StackYesBundled; AGPLv3 as of Redis 8
Redis Software / Redis CloudYesCommercial offerings
Azure Managed RedisYes, conditionallyMemory Optimized, Balanced, Compute Optimized. Not Flash Optimized. Must be enabled at creation time
AWS ElastiCache / MemoryDBNoAWS does not permit loading external or Redis modules on either service
ValkeyNo official buildMaintainers note a Redis 7.2-compiled module should load via ABI compatibility, but there is no Valkey-native release and no endorsed port

Two consequences worth sitting with. First, if you run on ElastiCache or MemoryDB, the module is not an option and no amount of configuration will make it one. Second, if you moved to Valkey when Redis changed its licence — and many teams did — then any design that depends on TS.* has quietly reintroduced a dependency on the thing you left. Check this before you build, not after.

Sorted Sets and Streams: The Primitives

Long before the module, the standard approach was a sorted set with the timestamp as the score:

> ZADD sensor:temp:raw 1786352400000 "21.4"
> ZRANGEBYSCORE sensor:temp:raw 1786352400000 1786352520000

Range queries are O(log(N)+M), which is genuinely good, and it works on every Redis-compatible server ever shipped. The catches are real but manageable: members must be unique, so two identical readings at different timestamps need the timestamp folded into the member; there is no aggregation, retention or downsampling — all three are your job; and memory per sample is considerably higher than the module's compressed encoding.

Redis Streams are the other primitive people reach for. Streams are an excellent fit for ingesting a firehose of timestamped events with consumer groups and acknowledgement, and they cap length with MAXLEN. They are a poor fit for the analytical read patterns time series work implies — no aggregation, no downsampling, and range queries by ID rather than by value. A common production shape is Streams for ingest, then a windowed stream processing step writing summaries into whichever store you picked. If you are weighing Streams as a transport, Redis Streams vs Kafka covers that comparison.

Redisson RTimeSeries: A Portable Java Collection

Working with sorted sets directly from Java means managing scores, encoding members, and writing your own expiry logic. Redisson's RTimeSeries wraps that in a typed collection.

Be clear about what it is: Redisson does not wrap the RedisTimeSeries module. There are no TS.* commands in Redisson. RTimeSeries is implemented as Lua scripts over sorted sets, which is precisely why it runs unchanged on Redis, Valkey, ElastiCache and MemoryDB. You are trading the module's server-side aggregation for portability and a Java-native API.

The interface takes two type parameters — the value type and the label type:

RTimeSeries<Double, String> ts = redisson.getTimeSeries("sensor:temp:1");

ts.add(1786352400000L, 21.4);
ts.add(1786352460000L, 21.9);
ts.add(1786352520000L, 22.3);

Double value = ts.get(1786352460000L);
Collection<Double> window = ts.range(1786352400000L, 1786352520000L);
int count = ts.size();

Timestamps are long milliseconds — note the L suffix, without which any realistic epoch value overflows int and will not compile.

Labels

The second type parameter attaches a label to each entry, retrieved through TimeSeriesEntry:

ts.add(1786352400000L, 21.4, "floor-2");

TimeSeriesEntry<Double, String> entry = ts.getEntry(1786352400000L);
long timestamp = entry.getTimestamp();
Double reading = entry.getValue();
String label    = entry.getLabel();

This is a per-entry annotation, not the module's cross-series index. You cannot query "every series where floor=2" — labels here travel with the sample so you can carry a unit, a source or a quality flag alongside the value.

Retention

Where the module sets retention per series, Redisson sets a time-to-live per entry, which is finer-grained:

ts.add(1786352400000L, 21.4, Duration.ofDays(7));
ts.add(1786352520000L, 22.3, "floor-2", Duration.ofDays(7));

Expiry is enforced in two places. Reads filter expired entries out, and Redisson registers each time series with a background eviction task that actually deletes them — so memory is reclaimed whether or not anyone reads the series. That task is adaptive rather than immediate: it starts on a 5-second interval, backs off towards 30 minutes while it finds nothing to remove, and speeds up again when it does. Both bounds are tunable through minCleanUpDelay and maxCleanUpDelay on Config. Expect reclamation within minutes, not milliseconds.

The older add(long, V, long, TimeUnit) overload still exists but is deprecated — prefer Duration.

Batch Ingest

Adding samples one at a time is one round trip each. addAll takes a map or a collection of entries:

Map<Long, Double> batch = new LinkedHashMap<>();
batch.put(1786352400000L, 21.4);
batch.put(1786352460000L, 21.9);
ts.addAll(batch, Duration.ofDays(7));

List<TimeSeriesEntry<Double, String>> labelled = List.of(
    new TimeSeriesEntry<>(1786352580000L, 22.7, "floor-2"),
    new TimeSeriesEntry<>(1786352640000L, 23.1, "floor-2"));
ts.addAll(labelled, Duration.ofDays(7));

Reads

Beyond range, the collection exposes reversed ranges, limits, entry-level reads, streaming iteration and destructive drains:

Collection<TimeSeriesEntry<Double, String>> entries =
    ts.entryRange(1786352400000L, 1786352520000L);

Collection<Double> newestFirst =
    ts.rangeReversed(1786352400000L, 1786352520000L, 100);

Double latest   = ts.last();
Long   latestAt = ts.lastTimestamp();
Collection<Double> last10 = ts.last(10);

Collection<Double> drained = ts.pollFirst(500);
int removed = ts.removeRange(0L, 1786352400000L);

The limit overloads matter more than they look. range without a bound will happily materialise a million samples into your heap — the same failure mode as an unbounded HGETALL. Bound your reads, or use stream(int) / iterator(int) to page through in batches.

Async and Reactive

Every method has an async counterpart returning RFuture, plus Reactive and RxJava3 variants through RedissonReactiveClient and RedissonRxClient:

RFuture<Void> added = ts.addAsync(1786352400000L, 21.4, Duration.ofDays(7));
RFuture<Collection<Double>> ranged =
    ts.rangeAsync(1786352400000L, 1786352520000L);

ranged.whenComplete((values, exception) -> {
    if (exception != null) {
        return;
    }
    System.out.println(values.size());
});

Note that the async methods are the ones suffixed Async. The synchronous add returns void, so assigning it to an RFuture will not compile.

What RTimeSeries Will Not Do

No compaction rules, no server-side aggregation, no cross-series queries. If you want hourly rollups you write them yourself:

RTimeSeries<Double, String> raw    = redisson.getTimeSeries("sensor:temp:1");
RTimeSeries<Double, String> hourly = redisson.getTimeSeries("sensor:temp:1:1h");

Collection<TimeSeriesEntry<Double, String>> bucket = raw.entryRange(from, to);
if (!bucket.isEmpty()) {
    double avg = bucket.stream()
                       .mapToDouble(TimeSeriesEntry::getValue)
                       .average()
                       .orElse(Double.NaN);
    hourly.add(from, avg, Duration.ofDays(365));
}

Run that on a schedule and you have downsampling — at the cost of pulling the bucket to the client, a job to operate, and no atomicity guarantee if it overlaps with a write. The module does this better. That is the trade, stated honestly: portability and a typed API, against server-side machinery you have to rebuild.

Choosing

Use RedisTimeSeries when you control your deployment or run on a platform that offers it, you have many series and need to query across them by label, you need automatic downsampling, and AGPLv3 is acceptable to you. Metrics and IoT fleets are its home ground.

Use sorted sets directly when you have one or a few series, need range queries and nothing more, and want zero dependencies. It is not a lesser choice — for a rolling window of recent readings it is the whole solution. The same primitive backs real-time leaderboards.

Use Redisson's RTimeSeries when you are on Java and want a typed API without hand-rolling score encoding and expiry; when you need per-entry TTLs; or — the common case — when you need one codebase to run against both Redis and Valkey, or you deploy on a managed service that will not load modules.

And be honest about the fourth answer: if you are ingesting millions of samples per second, keeping years of history, and running complex analytical queries, you want a dedicated time series database. Redis is an excellent fit for recent, hot, operational time series data. It is not a replacement for InfluxDB or TimescaleDB at that scale, and the module does not change that.

Frequently Asked Questions

Does Redisson Support the RedisTimeSeries Module?

No. Redisson implements no TS.* commands. Its RTimeSeries collection is a separate structure built on sorted sets via Lua scripts, which is why it works on servers where the module is unavailable. If you need the module's commands specifically, use a client that exposes them.

Can I Use RedisTimeSeries on Valkey?

There is no official Valkey build. Valkey maintainers have noted that a redistimeseries.so compiled for Redis 7.2 should load on Valkey 7.2 because of ABI compatibility, but this is not a maintained or endorsed port. For anything production-critical on Valkey, sorted sets or RTimeSeries are the safer choice.

Does ElastiCache Support RedisTimeSeries?

No. AWS does not permit loading external, custom or Redis modules on ElastiCache or MemoryDB. If you need module functionality on AWS you would have to self-manage Redis on EC2. Sorted sets and RTimeSeries work on ElastiCache without any of that.

Should I Use Sorted Sets or Redis Streams for Time Series Data?

Sorted sets for querying, Streams for ingesting. Sorted sets give you O(log(N)+M) range queries by timestamp, which is what time series reads look like. Streams give you consumer groups, acknowledgement and capped length, which is what reliable ingest looks like. Many systems use both.

How Much Memory Does Time Series Data Use in Redis?

RedisTimeSeries uses a compressed encoding and is substantially more compact per sample than a sorted set, which stores a full score and member for every point. Since everything lives in RAM, retention policy is the variable that actually controls cost — downsample aggressively and expire raw samples early.

What Is the Difference Between RTimeSeries and RScoredSortedSet?

Both are sorted sets underneath. RScoredSortedSet exposes the raw score-and-member model; RTimeSeries adds a timestamp-oriented API, per-entry TTL, optional labels via TimeSeriesEntry, and read methods shaped for time ranges. Use RTimeSeries when the score is a timestamp.

Next Steps

For the underlying data type, see Redis sorted sets; for the ingest side, Redis Streams for Java. Redis data structures in Java maps the rest of the Redis types onto Redisson interfaces, and the Redisson collections documentation covers every RTimeSeries method in detail.

If portability across Redis and Valkey is what brought you here, Valkey vs Redis covers the licensing change and what it means in practice. For workloads that need local caching or data partitioning on top of the same API, Redisson PRO adds them without changing your code.