Probabilistic Structures & Time Series

This content was generated with the assistance of AI and should be verified against the official Redis documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Examples on this page target Redis Open Source 8.10 (8.10.2) with the Jedis 8 and Lettuce 7.7 client libraries. Probabilistic structures and time series are both distributed as Redis modules bundled into every modern Redis Open Source and Redis Software build — they trade a small, bounded amount of accuracy or memory for operations that would otherwise be expensive or outright impractical at scale: counting how many distinct visitors a site had without storing every visitor, testing set membership without storing the set itself, or querying millions of timestamped samples by time range and aggregation without hand-rolling that indexing.

Probabilistic data structures

Every structure in this section answers a narrower question than a general-purpose Set or Hash would, using sublinear (often constant) memory in exchange for a small, quantified error rate — exact for structures like Bloom and Cuckoo filters (a tunable false-positive rate, never a false negative), and statistical for cardinality- and frequency-estimating structures like HyperLogLog, Count-min sketch, Top-K and t-digest.

HyperLogLog: approximate cardinality

A HyperLogLog (HLL) estimates the number of distinct elements added to it, using a fixed ~12 KB regardless of how many elements go in, with a standard error of about 0.81%. It answers "how many unique X" without storing the X’s themselves, which makes it a good fit for unique-visitor counting where storing every visitor ID would be wasteful and pointless once only the count is needed.

  • PFADD key [element [element …​]] — adds one or more elements; the internal representation is a regular Redis string, so a HLL key can be read with GET/STRLEN like any other string, though only PF* commands interpret its contents meaningfully. See PFADD.

  • PFCOUNT key [key …​] — returns the approximated cardinality of one key, or, given several keys, the cardinality of their union computed on the fly without merging them into a new key. See PFCOUNT.

  • PFMERGE destkey sourcekey [sourcekey …​] — merges one or more source HLLs into destkey, useful for rolling several time-bucketed HLLs (one per day, say) into a weekly or monthly total. See PFMERGE.

127.0.0.1:6379> PFADD bookshop:visitors:2026-09-25 user:42 user:107 user:42
(integer) 1
127.0.0.1:6379> PFCOUNT bookshop:visitors:2026-09-25
(integer) 2

This derives from PFADD and PFCOUNT.

Bloom filter: approximate membership, no false negatives

A Bloom filter answers "have I possibly seen this before" with no false negatives and a tunable false-positive rate: if it says an item is absent, it is genuinely absent; if it says present, it might be a false positive at the configured rate. Items can never be removed from a classic Bloom filter.

  • BF.RESERVE key error_rate capacity [EXPANSION expansion] [NONSCALING] — creates the filter up front with an explicit error_rate (for example 0.01 for 1%) and expected capacity; skipping this and letting the first BF.ADD auto-create the filter uses server-default error and capacity instead. Without NONSCALING, a filter that fills past its capacity automatically adds a new sub-filter sized by EXPANSION (default 2\x) rather than rejecting further adds, at the cost of a slightly higher combined error rate and one extra sub-filter to check per lookup. See BF.RESERVE.

  • BF.ADD key item / BF.MADD key item [item …​] — adds one or one-per-call-many items, auto-creating the filter with default parameters if it doesn’t exist yet. See BF.ADD and BF.MADD.

  • BF.EXISTS key item / BF.MEXISTS key item [item …​] — tests membership of one or several items. See BF.EXISTS.

127.0.0.1:6379> BF.RESERVE bookshop:seen-isbns 0.001 1000000
OK
127.0.0.1:6379> BF.ADD bookshop:seen-isbns 9780000000001
(integer) 1
127.0.0.1:6379> BF.EXISTS bookshop:seen-isbns 9780000000001
(integer) 1
127.0.0.1:6379> BF.EXISTS bookshop:seen-isbns 9780000000099
(integer) 0

A one-in-a-thousand false-positive rate over an expected one million ISBNs sizes bookshop:seen-isbns for gating whether a scraped ISBN needs a fresh catalog lookup — an occasional unnecessary lookup on a false positive is cheap, while a false negative would silently skip books the catalog already has. This derives from BF.RESERVE, BF.ADD and BF.EXISTS.

Cuckoo filter: membership with deletion

A Cuckoo filter answers the same membership question as a Bloom filter, with two practical differences: items can be deleted, and, for a comparable false-positive rate, a Cuckoo filter is typically more memory-efficient at lower target error rates, though it can reject an add once its buckets are saturated (bounded by MAXITERATIONS) where a scaling Bloom filter simply grows.

  • CF.RESERVE key capacity [BUCKETSIZE bucketsize] [MAXITERATIONS maxiterations] [EXPANSION expansion] — creates the filter with an explicit expected capacity. See CF.RESERVE.

  • CF.ADD key item / CF.ADDNX key item — adds an item, with CF.ADDNX only adding if the item is not already (probably) present. See CF.ADD.

  • CF.EXISTS key item — tests membership. See CF.EXISTS.

  • CF.DEL key item — removes an item, the capability a Bloom filter lacks; deleting an item that was never added can, at the filter’s error rate, remove a different colliding item instead. See CF.DEL.

127.0.0.1:6379> CF.RESERVE bookshop:seen-isbns-cf 1000000
OK
127.0.0.1:6379> CF.ADD bookshop:seen-isbns-cf 9780000000002
(integer) 1
127.0.0.1:6379> CF.DEL bookshop:seen-isbns-cf 9780000000002
(integer) 1

A Cuckoo filter is worth choosing over a Bloom filter for the same bookshop:seen-isbns-style gate whenever entries also need to expire out of the set explicitly — delisted or returned books, for instance — rather than only ever growing. This derives from CF.RESERVE and CF.DEL.

Count-min sketch: approximate frequency

A Count-min sketch estimates how many times each item has been seen, in memory independent of the number of distinct items, at the cost of a one-sided error: an estimated count can be over-stated (never understated) when two items collide in the sketch’s internal rows.

  • CMS.INITBYPROB key error probability — creates the sketch sized from a desired error factor and the probability of exceeding it (an alternative to CMS.INITBYDIM, which takes the internal width/depth directly). See CMS.INITBYPROB.

  • CMS.INCRBY key item increment [item increment …​] — increments one or more items' estimated counts. See CMS.INCRBY.

  • CMS.QUERY key item [item …​] — returns the estimated count for each item. See CMS.QUERY.

127.0.0.1:6379> CMS.INITBYPROB bookshop:search-terms 0.001 0.01
OK
127.0.0.1:6379> CMS.INCRBY bookshop:search-terms "clean code" 1
1) (integer) 1
127.0.0.1:6379> CMS.QUERY bookshop:search-terms "clean code"
1) (integer) 1

This derives from CMS.INITBYPROB, CMS.INCRBY and CMS.QUERY.

Top-K: the most frequent items

Top-K tracks only the k most frequent items it has seen, discarding the long tail rather than trying to estimate every item’s frequency the way a Count-min sketch does — a better fit for "what are our best-selling titles right now" than for "how many times has this exact ISBN been searched."

  • TOPK.RESERVE key topk [width depth decay] — creates a Top-K tracker for the topk most frequent items; width/depth/decay tune the underlying sketch and its recency bias. See TOPK.RESERVE.

  • TOPK.ADD key item [item …​] — adds one or more observations, returning, for each, any item it displaced out of the top-k list. See TOPK.ADD.

  • TOPK.QUERY key item [item …​] — reports, for each item, whether it is currently in the top-k list. See TOPK.QUERY.

127.0.0.1:6379> TOPK.RESERVE bookshop:bestsellers 10
OK
127.0.0.1:6379> TOPK.ADD bookshop:bestsellers 9780000000001 9780000000001 9780000000002
1) (nil)
2) (nil)
3) (nil)

This derives from TOPK.RESERVE and TOPK.ADD.

t-digest: approximate percentiles

A t-digest estimates quantiles (percentiles, median) and rank over a stream of numeric values without storing every observation, staying accurate at the extreme tails (p99, p999) where a naive fixed-bucket histogram loses precision.

  • TDIGEST.CREATE key [COMPRESSION compression] — creates the digest; higher compression trades memory for accuracy. See TDIGEST.CREATE.

  • TDIGEST.ADD key value [value …​] — adds one or more observations. See TDIGEST.ADD.

  • TDIGEST.QUANTILE key quantile [quantile …​] — returns the estimated value at each requested quantile (0.5 for the median, 0.99 for p99, and so on). See TDIGEST.QUANTILE.

127.0.0.1:6379> TDIGEST.CREATE bookshop:checkout-latency-ms
OK
127.0.0.1:6379> TDIGEST.ADD bookshop:checkout-latency-ms 120 95 340 110
OK
127.0.0.1:6379> TDIGEST.QUANTILE bookshop:checkout-latency-ms 0.99
1) "340"

This derives from TDIGEST.CREATE and TDIGEST.QUANTILE.

Choosing a structure

Question you need answered Structure Typical sizing note

How many distinct visitors/ISBNs/sessions?

HyperLogLog

Fixed ~12 KB per key regardless of cardinality.

Have I possibly seen this ISBN/token before, and it never needs removing?

Bloom filter

Size from an expected capacity and target error_rate; scales automatically unless NONSCALING is set.

Same as above, but entries must sometimes be deleted?

Cuckoo filter

Similar sizing to Bloom, plus BUCKETSIZE/MAXITERATIONS to tune insert success under load.

How many times has each item roughly occurred, across a large item space?

Count-min sketch

Size from error/probability (or width/depth); memory is independent of the number of distinct items.

What are the current top k most frequent items?

Top-K

Fixed by topk, width, depth; tail items are simply not tracked.

What is the p50/p95/p99 (or median) of a numeric stream?

t-digest

Size via COMPRESSION; higher values cost more memory for better tail accuracy.

Time series

RedisTimeSeries stores each metric as its own key holding an ordered sequence of (timestamp, value) samples, with optional labels for multi-series filtering and downsampling rules for automatic compaction — a purpose built alternative to re-deriving the same indexing on top of a sorted set or a stream.

Creating a series

TS.CREATE key [RETENTION retentionPeriod] [ENCODING <COMPRESSED \| UNCOMPRESSED>] [CHUNK_SIZE size] [DUPLICATE_POLICY <BLOCK \| FIRST \| LAST \| MIN \| MAX \| SUM>] [LABELS label value [label value …​]] — RETENTION (milliseconds; 0 means keep forever) bounds how long samples are kept before automatic eviction, CHUNK_SIZE tunes the memory block size samples are grouped into, ENCODING COMPRESSED (the default) uses a delta-of-delta compression scheme that shrinks well-behaved series considerably versus UNCOMPRESSED, and DUPLICATE_POLICY decides what happens when a sample arrives for a timestamp that already has one — BLOCK rejects it, LAST/FIRST/MIN/MAX/SUM resolve it. LABELS attaches metadata used later by TS.MRANGE and TS.MGET filters. See TS.CREATE.

127.0.0.1:6379> TS.CREATE bookshop:ts:pageviews:home RETENTION 604800000 DUPLICATE_POLICY SUM LABELS app bookshop page home
OK

This derives from TS.CREATE.

Writing samples

  • TS.ADD key timestamp value [RETENTION retentionPeriod] [ENCODING enc] [CHUNK_SIZE size] [ON_DUPLICATE policy] [LABELS label value …​] — appends one sample, auto-creating the series with these same options if it doesn’t exist yet; timestamp can be * for "now". See TS.ADD.

  • TS.MADD key timestamp value [key timestamp value …​] — appends samples to several series in one round trip. See TS.MADD.

  • TS.INCRBY key value [TIMESTAMP timestamp] [RETENTION retentionPeriod] [UNCOMPRESSED] [CHUNK_SIZE size] [LABELS label value …​] — atomically adds value to the series' current (or a named) sample instead of replacing it, the natural fit for a running page-view counter; TS.DECRBY is its mirror. See TS.INCRBY.

127.0.0.1:6379> TS.INCRBY bookshop:ts:pageviews:home 1
(integer) 1758800000000
127.0.0.1:6379> TS.MADD bookshop:ts:pageviews:home * 1 bookshop:ts:pageviews:catalog * 1
1) (integer) 1758800000010
2) (integer) 1758800000010

This derives from TS.INCRBY and TS.MADD.

Reading and aggregating

  • TS.RANGE key fromTimestamp toTimestamp [LATEST] [FILTER_BY_TS ts …​] [FILTER_BY_VALUE min max] [COUNT count] [ALIGN align] [AGGREGATION aggregator bucketDuration [BUCKETTIMESTAMP bt] [EMPTY]] — reads a range of raw or aggregated samples; fromTimestamp/toTimestamp accept -/+ for "earliest" and "latest". AGGREGATION downsamples into fixed-size buckets using an aggregator such as avg, sum, min, max, range, count, first, last, std.p/std.s, var.p/var.s, or twa (time-weighted average). Since Redis 8.6 the countNaN and countAll aggregators count only the NaN samples, or every sample including NaN, in each bucket. AGGREGATION takes a single clause, but its aggregator argument can be a comma-separated list with no spaces (AGGREGATION min,avg,max 60000). Each bucket then comes back with one value per aggregator, so several aggregated views of a series arrive in one round trip. See TS.RANGE.

  • TS.MRANGE fromTimestamp toTimestamp [FILTER_BY_TS ts …​] [FILTER_BY_VALUE min max] [WITHLABELS \| SELECTED_LABELS label …​] [COUNT count] [ALIGN align] [AGGREGATION aggregator bucketDuration …​] [GROUPBY label REDUCE reducer] FILTER filterExpr [filterExpr …​] — the multi-series counterpart of TS.RANGE: FILTER selects series by label (for example app=bookshop), and GROUPBY/REDUCE combine matching series into one reduced result instead of returning each separately. See TS.MRANGE.

127.0.0.1:6379> TS.RANGE bookshop:ts:pageviews:home - + AGGREGATION sum 3600000
1) 1) (integer) 1758798000000
   2) "42"
127.0.0.1:6379> TS.MRANGE - + FILTER app=bookshop page=home
1) 1) "bookshop:ts:pageviews:home"
   2) (empty array)
   3) 1) 1) (integer) 1758800000000
         2) "1"

This derives from TS.RANGE and TS.MRANGE.

Multi-key point reads

TS.MGET [LATEST] [WITHLABELS \| SELECTED_LABELS label …​] FILTER filterExpr [filterExpr …​] returns just the last sample of every series matching the label filter — a cheap way to read current values across many series (every page’s latest view count, say) without a range query per key. See TS.MGET.

127.0.0.1:6379> TS.MGET WITHLABELS FILTER app=bookshop
1) 1) "bookshop:ts:pageviews:home"
   2) 1) 1) "app"
         2) "bookshop"
      2) 1) "page"
         2) "home"
   3) 1) (integer) 1758800000010
      2) "1"

This derives from TS.MGET.

Compaction rules

TS.CREATERULE sourceKey destKey AGGREGATION aggregator bucketDuration [alignTimestamp] links an existing destKey series as a standing downsampled view of sourceKey: every new sample written to sourceKey is folded into destKey’s current bucket using `aggregator, so destKey stays populated automatically instead of needing a separate job to backfill it. destKey must already exist (created with a plain TS.CREATE) before the rule is attached, and typically carries a longer RETENTION than the raw source series, since the whole point of a compaction rule is to keep an inexpensive, long-lived downsampled history once the raw samples age out. See TS.CREATERULE.

127.0.0.1:6379> TS.CREATE bookshop:ts:pageviews:home:1h RETENTION 31536000000 LABELS app bookshop page home resolution 1h
OK
127.0.0.1:6379> TS.CREATERULE bookshop:ts:pageviews:home bookshop:ts:pageviews:home:1h AGGREGATION sum 3600000
OK

This derives from TS.CREATERULE.

flowchart LR S["bookshop:ts:pageviews:home (raw samples, short retention)"] R["TS.CREATERULE AGGREGATION sum 3600000"] D["bookshop:ts:pageviews:home:1h (hourly sums, long retention)"] S -- "every TS.ADD / TS.INCRBY" --> R R -- "folds into current bucket" --> D

Redis 8.10: NRANGE, READ and QUERYLABELS

Redis 8.10 added three further read-side commands to the time-series command family. TS.NRANGE extends range reads with normalization/relative-range semantics beyond what TS.RANGE offers, TS.READ provides another read path over a series' samples, and TS.QUERYLABELS returns the distinct label/value pairs present across matching series (useful for discovering what label filters are actually available, rather than hoping downstream code already knows). All three are recent enough, and their exact flag syntax detailed enough, that the authoritative source is their own redis.io command pages rather than a paraphrase here: see TS.NRANGE, TS.READ and TS.QUERYLABELS.

Writing samples from Java

package com.example.bookshop.metrics;

import redis.clients.jedis.RedisClient;
import redis.clients.jedis.timeseries.TSCreateParams;

public class PageViewMetrics {

    private final RedisClient jedis;

    public PageViewMetrics(RedisClient jedis) {
        this.jedis = jedis;
    }

    public void recordView(String page) {
        String key = "bookshop:ts:pageviews:" + page;
        jedis.tsIncrBy(key, 1);
    }

    public void createSeries(String page) {
        String key = "bookshop:ts:pageviews:" + page;
        jedis.tsCreate(key, TSCreateParams.createParams()
                .retention(604800000L)
                .label("app", "bookshop")
                .label("page", page));
    }
}

This wraps TS.INCRBY and TS.CREATE.

Visualizing with Grafana

Both a raw series like bookshop:ts:pageviews:home and a compacted one like bookshop:ts:pageviews:home:1h are ordinary metrics from a dashboarding tool’s point of view, and RedisTimeSeries ships a Grafana data source that queries them directly — panels, template variables, and alerting all work the same way they would against a Prometheus-backed dashboard. Prometheus & Monitoring covers Grafana’s dashboard and alerting model in depth; the only Redis-specific piece is pointing that same Grafana instance at the RedisTimeSeries data source instead of (or alongside) Prometheus.

  • Streams, Pub/Sub & Keyspace Notifications — Streams, for an append-only, replayable log of discrete events rather than approximate counting or numeric time series.

  • Use Cases & Patterns — probabilistic structures and time series in the context of the broader patterns Redis is used for.

  • Prometheus & Monitoring — Grafana dashboards and alerting, referenced above for visualizing a RedisTimeSeries metric.