Probabilistic structures
HotKV has three structures that trade a small, known amount of accuracy for a fixed and small memory footprint. They are built in: every HotKV server has them, with no module to load and no license needed.
| Structure | Answers | Can be wrong how |
|---|---|---|
| Bloom filter | “Have I seen this item?” | May say yes for an item never added (a false positive); never says no for one that was added |
| Count-min sketch | “How often have I seen this item?” | May overestimate a count; never underestimates |
| Top-K | “Which items do I see most often?” | May miss or misorder items whose counts are close |
For approximate counts of distinct items, see HyperLogLog.
Each structure is a key of its own type: TYPE reports MBbloom--,
CMSk-TYPE or TopK-TYPE, the same names Redis gives them. They are saved
in snapshots and the AOF, replicated, and can be given an expiry like any
other key. The commands, arguments, replies and error messages follow
those of Redis 8.
Bloom filters
Section titled “Bloom filters”A Bloom filter remembers that items were added, in a fraction of the space the items would take, so you can skip work for items that are certainly new. Typical uses: “has this user already seen this article?”, “is this username taken?” before a database lookup, or “have we crawled this URL?”.
BF.ADD creates a filter with default settings (room for 100 items at a 1% false-positive rate) if the key is missing:
127.0.0.1:6379> BF.ADD seen:urls "https://example.com/a"(integer) 1127.0.0.1:6379> BF.ADD seen:urls "https://example.com/a"(integer) 0127.0.0.1:6379> BF.EXISTS seen:urls "https://example.com/a"(integer) 1127.0.0.1:6379> BF.INFO seen:urls 1) Capacity 2) (integer) 100 3) Size 4) (integer) 240 5) Number of filters 6) (integer) 1 7) Number of items inserted 8) (integer) 1 9) Expansion rate10) (integer) 2BF.ADD replies 1 when the item is new and 0 when the filter may
already contain it. BF.EXISTS replies 1 for “probably added” and 0
for “definitely not added”. Under RESP3 these are booleans.
For real workloads, size the filter up front with BF.RESERVE: the false-positive rate you can accept, and how many items you expect.
127.0.0.1:6379> BF.RESERVE users:taken 0.001 1000000OK127.0.0.1:6379> BF.MADD users:taken "ada" "grace" "ada"1) (integer) 12) (integer) 13) (integer) 0127.0.0.1:6379> BF.MEXISTS users:taken "ada" "linus"1) (integer) 12) (integer) 0127.0.0.1:6379> BF.CARD users:taken(integer) 2127.0.0.1:6379> BF.INFO users:taken CAPACITY1) (integer) 1000000What to know about sizing:
- The error rate must be between 0 and 1 (exclusive) and the capacity from 1 to 1073741824. Lower error rates and higher capacities take more memory.
- A filter that fills up grows: once its newest layer holds the items it
was sized for, the next new item adds a layer
EXPANSIONtimes larger (2 by default, up to 32768) with a tighter error rate, so the filter as a whole keeps to the rate you asked for. A good capacity estimate keeps a filter to one layer. - A
NONSCALINGfilter never grows: once full, adding a new item fails withERR non scaling filter is full. - No single layer may exceed 4 GiB, and a filter is not created or grown
when the server lacks the memory for it
(
ERR Insufficient memory to create filter). - Items cannot be removed from a Bloom filter. To start over, delete the key.
BF.INSERT adds items and, if needed, creates the
filter with the options you give; with NOCREATE it refuses to create one.
BF.SCANDUMP and
BF.LOADCHUNK copy a filter out and back in, in
chunks, for example to move a large filter to another HotKV server.
Count-min sketch
Section titled “Count-min sketch”A Count-min sketch counts how often each item occurs in a stream of events, in a fixed-size table no matter how many different items there are. Use it for request counts per IP or per API key, word frequencies, or “how many times was this product viewed” when you track far more items than you want to store counters for.
Create a sketch with explicit dimensions (CMS.INITBYDIM: width and depth) or from the accuracy you need (CMS.INITBYPROB: the acceptable overestimate as a fraction of the total count, and the probability of exceeding it). Wider sketches overestimate less, deeper sketches are more likely to stay within the bound.
127.0.0.1:6379> CMS.INITBYDIM page:views 2000 5OK127.0.0.1:6379> CMS.INCRBY page:views /home 3 /pricing 11) (integer) 32) (integer) 1127.0.0.1:6379> CMS.QUERY page:views /home /pricing /about1) (integer) 32) (integer) 13) (integer) 0127.0.0.1:6379> CMS.INFO page:views1) width2) (integer) 20003) depth4) (integer) 55) count6) (integer) 4- CMS.INCRBY takes non-negative increments and replies with each item’s new estimated count. The key must exist; there is no automatic creation.
- CMS.QUERY returns estimates; an item never added reads as 0 unless it shares every cell with busier items.
- CMS.MERGE adds several sketches (optionally weighted) into an existing destination of the same width and depth, for example to roll hourly sketches into a daily one.
countin CMS.INFO is the total of all increments.
A Top-K list keeps the k items seen most often (the heavy hitters) as
events stream in, without keeping a counter for every item. Use it for
trending hashtags or products, the most active users or IPs, or the most
frequent errors.
TOPK.RESERVE creates the list with its k, and
optionally the width, depth and decay of the counting table behind it
(8, 7 and 0.9 by default). A larger table counts more accurately when many
items compete.
127.0.0.1:6379> TOPK.RESERVE trending 2 1000 7 0.9OK127.0.0.1:6379> TOPK.INCRBY trending shoes 3 hats 21) (nil)2) (nil)127.0.0.1:6379> TOPK.ADD trending socks1) (nil)127.0.0.1:6379> TOPK.LIST trending WITHCOUNT1) "shoes"2) (integer) 33) "hats"4) (integer) 2127.0.0.1:6379> TOPK.ADD trending socks1) "hats"127.0.0.1:6379> TOPK.LIST trending1) "shoes"2) "socks"127.0.0.1:6379> TOPK.QUERY trending shoes hats1) (integer) 12) (integer) 0- TOPK.ADD and TOPK.INCRBY
reply, for each item, with the item it pushed out of the list, or nil
when nothing was pushed out. In the session above, the second
sockspushedhatsout.TOPK.INCRBYtakes increments from 0 to 100,000. - TOPK.LIST returns the current top items, highest
first, with
WITHCOUNTfor their estimated counts. - TOPK.QUERY tells whether items are in the list right now; TOPK.COUNT estimates each item’s count, in the list or not.
- TOPK.INFO returns
k,width,depthanddecay. TOPK.ADDand the other commands need an existing list (TopK: key does not existotherwise).
Common patterns
Section titled “Common patterns”Cheap pre-check before an expensive lookup. BF.EXISTS first; only on
1 do you query the database. A 0 is always right, so most misses never
reach the database.
Rate or abuse signals across millions of keys. CMS.INCRBY per event
keyed by IP or token, and act when CMS.QUERY passes a threshold. One
sketch per time window, with an expiry, gives a rolling picture.
Trending lists. One Top-K per time window (trending:2026-10-02T14),
fed with TOPK.ADD on each event and read with TOPK.LIST.
Compatibility
Section titled “Compatibility”Bloom filter, Count-min sketch and Top-K commands match the Redis 8
commands of the same names. HotKV does not have Cuckoo filters (CF.*) or
t-digest (TDIGEST.*). DUMP payloads of these three types restore
into HotKV only, not into Redis.
Commands
Section titled “Commands”| Command | What it does |
|---|---|
| BF.RESERVE | Create a Bloom filter with an error rate and capacity |
| BF.ADD / BF.MADD | Add items |
| BF.INSERT | Add items, creating the filter with given options |
| BF.EXISTS / BF.MEXISTS | Test items |
| BF.CARD | Number of items added |
| BF.INFO | Capacity, size, layers, items, expansion |
| BF.SCANDUMP / BF.LOADCHUNK | Copy a filter out and in, in chunks |
| CMS.INITBYDIM / CMS.INITBYPROB | Create a sketch |
| CMS.INCRBY | Count items |
| CMS.QUERY | Estimated counts |
| CMS.MERGE | Add sketches together |
| CMS.INFO | Width, depth, total count |
| TOPK.RESERVE | Create a Top-K list |
| TOPK.ADD / TOPK.INCRBY | Count items |
| TOPK.LIST | Current top items |
| TOPK.QUERY | Whether items are in the list |
| TOPK.COUNT | Estimated counts |
| TOPK.INFO | k, width, depth, decay |
From your application
Section titled “From your application”Send these commands with each client’s generic command method.
import hotkv
client = hotkv.HotKV(host="localhost", port=6379, decode_responses=True)
client.execute_command("BF.RESERVE", "users:taken", 0.001, 1000000)client.execute_command("BF.ADD", "users:taken", "ada")print(client.execute_command("BF.EXISTS", "users:taken", "ada"))
client.execute_command("TOPK.RESERVE", "trending", 10)client.execute_command("TOPK.ADD", "trending", "shoes", "hats")print(client.execute_command("TOPK.LIST", "trending"))client.close()import { HotKV } from "@hotkv/client";
const client = new HotKV({ url: "hotkv://127.0.0.1:6379" });
await client.command("BF.RESERVE", "users:taken", "0.001", "1000000");await client.command("BF.ADD", "users:taken", "ada");console.log(await client.command("BF.EXISTS", "users:taken", "ada"));
await client.command("CMS.INITBYDIM", "page:views", "2000", "5");await client.command("CMS.INCRBY", "page:views", "/home", "1");console.log(await client.command("CMS.QUERY", "page:views", "/home"));await client.close();client, err := hotkv.NewClient(&hotkv.Options{Addr: "localhost:6379"})if err != nil { log.Fatal(err)}defer client.Close()ctx := context.Background()
pipe := client.Pipeline()pipe.Do(ctx, "BF.RESERVE", "users:taken", "0.001", "1000000")pipe.Do(ctx, "BF.ADD", "users:taken", "ada")exists := pipe.Do(ctx, "BF.EXISTS", "users:taken", "ada")if _, err := pipe.Exec(ctx); err != nil { log.Fatal(err)}fmt.Println(exists.Val())import com.hotkv.HotKV;
try (HotKV client = HotKV.create("hotkv://localhost:6379")) { client.execute("BF.RESERVE", "users:taken", "0.001", "1000000"); client.execute("BF.ADD", "users:taken", "ada"); System.out.println(client.execute("BF.EXISTS", "users:taken", "ada"));
client.execute("TOPK.RESERVE", "trending", "10"); client.execute("TOPK.ADD", "trending", "shoes", "hats"); System.out.println(client.execute("TOPK.LIST", "trending"));}Was this page helpful?
Thanks for the feedback.