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 withGET/STRLENlike any other string, though onlyPF*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 intodestkey, 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
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 expliciterror_rate(for example0.01for 1%) and expectedcapacity; skipping this and letting the firstBF.ADDauto-create the filter uses server-default error and capacity instead. WithoutNONSCALING, a filter that fills past its capacity automatically adds a new sub-filter sized byEXPANSION(default2\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 expectedcapacity. See CF.RESERVE. -
CF.ADD key item/CF.ADDNX key item— adds an item, withCF.ADDNXonly 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 toCMS.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 thetopkmost frequent items;width/depth/decaytune 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; highercompressiontrades 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.5for the median,0.99for 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 |
Same as above, but entries must sometimes be deleted? |
Cuckoo filter |
Similar sizing to Bloom, plus
|
How many times has each item roughly occurred, across a large item space? |
Count-min sketch |
Size from
|
What are the current top k most frequent items? |
Top-K |
Fixed by |
What is the p50/p95/p99 (or median) of a numeric stream? |
t-digest |
Size via |
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;timestampcan 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 addsvalueto the series' current (or a named) sample instead of replacing it, the natural fit for a running page-view counter;TS.DECRBYis 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
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/toTimestampaccept-/+for "earliest" and "latest".AGGREGATIONdownsamples into fixed-size buckets using an aggregator such asavg,sum,min,max,range,count,first,last,std.p/std.s,var.p/var.s, ortwa(time-weighted average). Since Redis 8.6 thecountNaNandcountAllaggregators count only theNaNsamples, or every sample includingNaN, in each bucket.AGGREGATIONtakes 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 ofTS.RANGE:FILTERselects series by label (for exampleapp=bookshop), andGROUPBY/REDUCEcombine 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"
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.
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));
}
}
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.
Related pages
-
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.
References
-
Redis probabilistic data types — overview of HyperLogLog, Bloom, Cuckoo, Count-min sketch, Top-K and t-digest.
-
RedisTimeSeries — overview, labels, retention, compaction rules and aggregation.