A Bloom filter on Valkey looks free until the day BF.ADD returns ERR operation exceeds bloom object memory limit. In valkey-bloom 1.0.1, the module behind the BF.* commands, every filter is one key with a per-key memory cap of 128 MB. The first BF.ADD on a new key quietly creates a filter sized for 100 items, which then doubles each time it fills. Each doubling adds memory, adds hash checks to every lookup, and loosens the overall false-positive rate. A default filter stops growing at 26,214,300 items. A filter you sized yourself can hold several times that in less memory. This guide gives the sizing math, how scaling filters grow and stop, the NONSCALING and VALIDATESCALETO options, replication and persistence, and two patterns.
- A filter needs about 9.6 bits per item at a 1% false-positive rate and 14.4 bits at 0.1%. Multiply by the item count before you create it.
bf.bloom-memory-usage-limitdefaults to 134,217,728 bytes (128 MB) per filter. You can change it withCONFIG SET, and every create and scale-out is checked against the current value.- Scaling filters add a sub-filter twice the size of the last one, with half its false-positive rate. The combined rate can approach twice the rate you asked for, and every lookup that misses checks every sub-filter.
- If you know the item count, reserve it up front, with
NONSCALINGwhen the count is fixed. UseBF.INSERT ... VALIDATESCALETO nto reject a scaling filter that can never reachnitems. - Run valkey-bloom 1.0.1, which fixes CVE-2026-21864, and set the same memory limit on every node, because the RDB loader enforces it too.
Scope and review date: November 2026. This post covers valkey-bloom 1.0.1 on Valkey 8.1 and later (8.0 needs the module built with its valkey_8_0 feature flag), the version that valkey-bundle 9.1.4 (Valkey 9.1.3) ships. Defaults, error strings and commands were checked against the valkey.io documentation and the module's 1.0.1 source on 10 November 2026. The sizing figures are arithmetic from the module's own formulas, not measurements: the commands were not run in a lab, so confirm each number with BF.INFO on your own server.
How many bits and hash functions does a Bloom filter need?
A Bloom filter is a bit array plus k hash functions. Adding an item sets k bits, and a lookup reports "maybe present" only if all k bits are set. A filter can give a false positive, but never a false negative. The valkey.io Bloom filter topic describes the behaviour. The sizing comes from the Rust bloomfilter crate that valkey-bloom 1.0.1 builds on, which uses the textbook formulas:
- bits per item = −ln(p) ÷ (ln 2)², where p is the false-positive rate;
- bitmap bytes = ⌈n × −ln(p) ÷ (8 × (ln 2)²)⌉ for n items;
- hash functions k = round(bits per item × ln 2), with a minimum of 1.
# Worked arithmetic: bytes and hash count for n items at rate p
python3 -c '
import math
n, p = 50_000_000, 0.001
bits = -math.log(p) / math.log(2) ** 2
print(f"bits/item={bits:.3f} k={round(bits * math.log(2))}")
print(f"bitmap bytes={math.ceil(n * bits / 8):,}")'
# bits/item=14.378 k=10
# bitmap bytes=89,859,923Interpretation: 50 million items at 0.1% need about 85.7 MiB of bitmap and 10 hash functions. valkey-bloom adds a few hundred bytes of fixed overhead per filter: 264 bytes for a one-filter object, by the 1.0.1 struct sizes. That is why the documented default filter (100 items at 1%) reports a Size of 384 bytes: a 120-byte bitmap plus 264.
| False-positive rate | Bits per item | Hash functions in valkey-bloom | Bytes per million items |
|---|---|---|---|
| 1% (0.01) | 9.585 | 7 | 1,198,133 |
| 0.1% (0.001) | 14.378 | 10 | 1,797,199 |
| 0.01% (0.0001) | 19.170 | 13 | 2,396,265 |
Each tenfold cut in the false-positive rate costs another 4.8 bits per item. The RedisBloom BF.RESERVE page gives the same bits per item but rounds the hash count up, so it lists 14 hashes at 0.01% where valkey-bloom's crate rounds to 13.
What does BF.ADD create when the key does not exist?
BF.ADD, BF.MADD and BF.INSERT create a filter on first use from the module's defaults. The topic page lists them: capacity 100 (bf.bloom-capacity), false-positive rate 0.01 (bf.bloom-fp-rate), expansion 2 (bf.bloom-expansion), tightening ratio 0.5 (bf.bloom-tightening-ratio) and a random seed (bf.bloom-use-random-seed). Expansion 2 means every implicit filter is a scaling filter.
127.0.0.1:6379> BF.ADD seen:emails user@example.com
(integer) 1
127.0.0.1:6379> BF.INFO seen:emails CAPACITY
(integer) 100
127.0.0.1:6379> BF.INFO seen:emails MAXSCALEDCAPACITY
(integer) 26214300Interpretation: the filter starts with room for 100 items and can grow to 26,214,300 before the next sub-filter would break the 128 MB cap. Changing the defaults with CONFIG SET affects only filters created afterwards. Existing keys keep the capacity, rate and expansion they were created with.
Creating the filter explicitly with BF.RESERVE key error_rate capacity avoids the default. Add NONSCALING or EXPANSION n, but not both: valkey-bloom rejects the pair, while RedisBloom's syntax lists both options.
Reserving a filter for one million items, then reading BF.INFO
Illustrative output: the values are computed from the valkey-bloom 1.0.1 sizing code, not captured from a server
Read two fields: Size is the bytes held now, and Max scaled capacity is the ceiling under the current memory limit. The field layout follows the BF.INFO reference.
As of November 2026, the usernames example on the valkey.io Bloom topic (10,000 items at 0.001) shows a Size of 23,912 and a Max scaled capacity of 26,214,300. By the 1.0.1 formulas, those values should be about 18,236 and 20,470,000. The default-filter example also reports two items after one BF.ADD. Compute your own values, or read them from your server.
How does a scaling Bloom filter grow, and what does each step cost?
When the newest sub-filter is full, the next new item triggers a scale-out. valkey-bloom appends a sub-filter with capacity = previous capacity × expansion and false-positive rate = p × tighteningi, where i counts the sub-filters that already exist. With the defaults, the sub-filters hold 100, 200, 400 and so on items, at rates of 0.01, 0.005, 0.0025 and so on. Earlier sub-filters stay in the key and are checked on every lookup.
How a default scaling filter adds sub-filters
Capacity doubles and the false-positive rate halves at each step, until the next sub-filter would pass the 128 MB cap
Lead: the last sub-filter is half the key. Sub-filter 18 alone needs about 53 MiB, allocated by the single BF.ADD that triggers the scale-out.
Each sub-filter adds three costs:
- Memory per item rises. Each halving of the rate adds 1.44 bits per item, so sub-filter 18 uses about 34 bits per item against 9.6 in the first.
- Lookups get slower. A lookup checks the sub-filters in turn and stops at the first one that matches. An item that is not in the filter, the common case for a guard, checks all of them. The topic page puts add and exists at O(N × K) for this reason.
- The real false-positive rate drifts up.
BF.INFOstill reports the rate you set, 0.01. A miss can match any sub-filter, though, so once they fill, the combined rate approaches the sum 0.01 + 0.005 + 0.0025 + … , which stays just under 0.02. With tightening 0.5, the bound is twice the requested rate.
A scale-out can also be refused before the cap, if the next sub-filter's rate would round down to zero. valkey-bloom then returns ERR false positive degrades to 0 on scale out. With the default expansion, the memory cap comes first. In practice, you meet this error only with EXPANSION 1 or a very low tightening ratio.
Where is the 128 MB per-filter cap, and can you raise it?
The cap is the module configuration bf.bloom-memory-usage-limit, default 134,217,728 bytes, and it applies to each filter separately. valkey-bloom checks it when a filter is created and before each scale-out. An operation that would exceed it fails with ERR operation exceeds bloom object memory limit and changes nothing. It is not a server-wide budget: maxmemory still governs the instance, and the writing BF.* commands carry the deny-oom flag.
127.0.0.1:6379> CONFIG GET bf.bloom-memory-usage-limit
1) "bf.bloom-memory-usage-limit"
2) "134217728"
127.0.0.1:6379> CONFIG SET bf.bloom-memory-usage-limit 268435456
OKInterpretation: raising the limit to 256 MB takes effect immediately. The 1.0.1 code reads the current value on every create and scale-out, so a filter that was refused can grow again. A default filter under a 256 MB limit can reach 52,428,700 items in 19 sub-filters. The topic page says the limit exists to protect serialization performance.
To make it permanent, put it in valkey.conf after the loadmodule line. Keep comments on their own lines.
loadmodule /usr/lib/valkey/libvalkey_bloom.so
# Per-filter memory limit: 256 MB
bf.bloom-memory-usage-limit 268435456Interpretation: Valkey 9.1's config parser treats only lines that start with # as comments. For a module config, it joins every token after the name into the value. The valkey-bundle topic page's sample line bf.bloom-memory-usage-limit 134217728 # 128MB therefore passes 134217728 # 128MB as the value, which is not a number. The module path is valkey-bundle's.
A non-scaling filter is one bitmap, so its maximum size under a given limit is plain division. These figures are worked arithmetic from the 1.0.1 formulas.
| False-positive rate | Largest NONSCALING capacity under 128 MB | Example that fails at create time |
|---|---|---|
| 0.01 | 112,022,240 items | BF.RESERVE k 0.01 120000000 NONSCALING |
| 0.001 | 74,681,493 items | BF.RESERVE k 0.001 100000000 NONSCALING (needs about 171 MiB) |
| 0.0001 | 56,011,120 items | BF.RESERVE k 0.0001 60000000 NONSCALING |
Should you reserve a scaling or a NONSCALING filter?
The topic page's rule: use a non-scaling filter when the item count is known and fixed, and a scaling one when it is not. The panels load the same 26,214,300 items three ways, as worked arithmetic from the 1.0.1 formulas.
The same 26,214,300 items in three filters
Switch filters to compare memory, sub-filters, lookup cost and the real error rate at that fill level
Default (BF.ADD)
| Property | Implicit scaling filter |
|---|---|
| Created by | BF.ADD key item (capacity 100, expansion 2) |
| Sub-filters | 18 |
| Memory | 107,051,063 bytes (about 102 MiB) |
| Hash checks for a miss | Up to 279 (7 to 24 per sub-filter) |
| False-positive rate when full | Up to about 0.02 |
| Next new item | ERR operation exceeds bloom object memory limit |
Sized start
| Property | Scaling filter reserved at 1 million |
|---|---|
| Created by | BF.RESERVE key 0.01 1000000 |
| Sub-filters | 5 (holding up to 31,000,000) |
| Memory | 54,816,271 bytes (about 52 MiB) |
| Hash checks for a miss | Up to 45 (7 to 11 per sub-filter) |
| False-positive rate when full | Up to about 0.019 |
| Next new item | Accepted; the filter can grow to 63,000,000 |
NONSCALING
| Property | Fixed filter reserved at full size |
|---|---|
| Created by | BF.RESERVE key 0.01 26214300 NONSCALING |
| Sub-filters | 1 |
| Memory | 31,408,464 bytes (about 30 MiB) |
| Hash checks for a miss | Up to 7 |
| False-positive rate when full | 0.01 |
| Next new item | ERR non scaling filter is full |
Lead: reserving the right size up front uses less than a third of the memory of the default filter, with one fortieth of the hash checks and half the error rate.
A higher expansion does not always raise the ceiling. With a one-million start, EXPANSION 4 stops at 21,000,000 items, and the default expansion of 2 reaches 63,000,000. The bigger jumps reach a sub-filter that cannot fit sooner.
127.0.0.1:6379> BF.RESERVE dedup:2026-11-10 0.001 5000000 NONSCALING
OK
127.0.0.1:6379> BF.RESERVE seen:urls 0.01 1000000 EXPANSION 2
OK
127.0.0.1:6379> BF.RESERVE bad 0.01 1000000 NONSCALING EXPANSION 2
(error) ERRORInterpretation: the first command fixes the filter at five million items. The second scales from one million. The third fails because 1.0.1 accepts only one of the two options. The BF.RESERVE reference prose calls the option "SCALING <expansion rate>", but the keyword the module parses is EXPANSION.
How do you check that a filter can reach its target size?
VALIDATESCALETO n is an option of BF.INSERT, not BF.RESERVE. Before creating anything, valkey-bloom simulates the scale-outs for the capacity, rate, expansion and tightening on the command line. If n items cannot be reached under the current memory limit, or if the rate would degrade to zero first, the command is rejected and no key is created. It cannot be combined with NONSCALING, as the BF.INSERT reference states.
127.0.0.1:6379> BF.INSERT seen:events CAPACITY 1000000 ERROR 0.01 VALIDATESCALETO 63000001
(error) ERR provided VALIDATESCALETO causes bloom object to exceed memory limit
127.0.0.1:6379> BF.INSERT seen:events CAPACITY 1000000 ERROR 0.01 VALIDATESCALETO 63000000
(empty array)
127.0.0.1:6379> BF.INFO seen:events MAXSCALEDCAPACITY
(integer) 63000000Interpretation: by the 1.0.1 formulas, the ceiling for a one-million, 1% filter is 63,000,000 items in six sub-filters, so one item more is refused. The validation runs on the arguments you pass, not on an existing key. For a filter that already exists, read BF.INFO key MAXSCALEDCAPACITY, which recomputes the ceiling under today's limit. The other refusal reads ERR provided VALIDATESCALETO causes false positive to degrade to 0.
Sizing a Bloom filter before you create it
Seven steps, all safe to run on a live server; none needs a restart
-
Estimate the items per key
Count distinct items over the key's life: per day, per tenant or in total. Split the data across keys if one key would hold more than the cap allows.
-
Choose the false-positive rate
Price a wrong "maybe present". For deduplication it is a skipped item; for a guard it is one wasted database read.
-
Compute the bitmap size
Bytes are about n × −ln(p) ÷ 3.844. Add a few hundred bytes per sub-filter.
python3 -c 'import math; n,p=5e6,0.001; \ print(math.ceil(n*-math.log(p)/(8*math.log(2)**2)))' -
Compare it with the per-filter limit
Check the value on every primary and replica, not only the node you are connected to.
valkey-cli CONFIG GET bf.bloom-memory-usage-limit -
Validate a scaling filter's ceiling
Use a scratch key, then delete it. Skip this step for
NONSCALING.valkey-cli BF.INSERT tmp:check CAPACITY 1000000 \ ERROR 0.01 VALIDATESCALETO 50000000 valkey-cli DEL tmp:check -
Create the real filter explicitly
Reserve it before the first write, so no client creates a default filter first.
valkey-cli BF.RESERVE seen:events 0.01 1000000 -
Watch growth
Alert on sub-filter counts and on total Bloom memory in INFO BF.
valkey-cli BF.INFO seen:events FILTERS valkey-cli INFO BF
Main risk: a client that calls BF.ADD before step 6 creates a 100-item default filter, and BF.RESERVE then fails with ERR item exists. Delete that key and reserve again before traffic grows.
How do large filters behave under replication and persistence?
Four behaviours in the 1.0.1 release notes and source matter once filters reach tens of megabytes.
- Replication is deterministic. When a primary creates a filter, it replicates a
BF.INSERTcarrying the exact capacity, rate, tightening, expansion and seed, so the replica builds an identical bitmap. Adds that create nothing are replicated verbatim. Replicas skip the memory-limit check on replicated commands. - RDB loads do check the limit. The RDB loader recomputes each filter's size and refuses an object over the current
bf.bloom-memory-usage-limit, loggingFailed to restore bloom object: Object larger than the allowed memory limit.A replica or restore target with a lower limit than the primary can therefore fail its full sync or restore. - Snapshots carry the whole bitmap. RDB saves every sub-filter's bitmap, and an AOF rewrite emits one
BF.LOADper filter with the encoded object. Budget RDB, AOF and full-sync transfer size at roughly the filter'sSize. - One filter is one key in one slot. In cluster mode, a 100 MB filter sits on one shard and moves as one key when its slot is migrated. Split by tenant or time window if you need it spread across shards.
# On each node: confirm the module version, the limit, and Bloom memory
valkey-cli MODULE LIST
valkey-cli CONFIG GET bf.bloom-memory-usage-limit
valkey-cli INFO BF
valkey-cli MEMORY USAGE seen:eventsInterpretation: MODULE LIST shows the module as bf, with version 10001 for 1.0.1. INFO BF reports bf_bloom_total_memory_bytes, bf_bloom_num_objects and bf_bloom_num_filters_across_objects, plus defrag hit and miss counters. A rising ratio of sub-filters to objects means filters are scaling out more than you planned.
valkey-bloom 1.0.1, released in February 2026, fixes CVE-2026-21864 (GHSA-mc2g-h759-3qw2), a remote denial of service from a malformed RESTORE command. 1.0.0 is affected. valkey-bundle images 8.1.11, 9.0.7 and 9.1.4 all ship 1.0.1, per the bundle's versions.json.
Is valkey-bloom compatible with RedisBloom commands?
For the core commands, yes. The valkey-bloom README says it is API compatible with the Bloom filter command syntax of the Valkey client libraries and their Redis equivalents. The differences are at the edges.
| Area | valkey-bloom 1.0.1 | RedisBloom |
|---|---|---|
| Core commands | BF.ADD, MADD, EXISTS, MEXISTS, CARD, RESERVE, INFO, INSERT | Same names |
| Dump and load | BF.LOAD (used by AOF rewrite) | BF.SCANDUMP and BF.LOADCHUNK |
| Other probabilistic types | None shipped | Cuckoo, count-min sketch, top-k, t-digest |
NONSCALING with EXPANSION | Rejected | Both listed in the syntax |
BF.INFO extras | ERROR, TIGHTENING, MAXSCALEDCAPACITY | Not available |
| Data type name | bloomfltr | MBbloom-- |
Because the type names and hashing differ, filters move by re-inserting the source items, not by copying RDB files. The Redis Stack to valkey-bundle migration guide covers that cutover. For the wider server comparison, see Redis vs Valkey.
Which patterns suit a Bloom filter: deduplication or a cache-penetration guard?
A false positive means something different in each, and Bloom filters cannot delete items, so pick the rate and key layout to match.
Deduplication
BF.ADD is check-and-set in one command: it returns 1 if the item was new and 0 if it was probably seen. Use the return value directly. A separate BF.EXISTS followed by BF.ADD leaves a race between two workers.
# One key per day, sized for the day's volume, expiring after a week
127.0.0.1:6379> BF.RESERVE dedup:2026-11-10 0.001 5000000 NONSCALING
OK
127.0.0.1:6379> EXPIRE dedup:2026-11-10 604800
(integer) 1
127.0.0.1:6379> BF.ADD dedup:2026-11-10 order:81234:email-sent
(integer) 1
127.0.0.1:6379> BF.ADD dedup:2026-11-10 order:81234:email-sent
(integer) 0Interpretation: a 0 on a genuinely new item is a false positive, so that item is skipped. At 0.001, about one new item in a thousand is skipped once the day's filter is full. If a skipped notification or event is not acceptable, confirm a 0 against the source of truth before dropping the item. Time-windowed keys also give you deletion: the whole day expires together.
Cache-penetration guard
Requests for IDs that do not exist miss the cache every time and fall through to the database. A filter of every valid ID answers "definitely not" without a database read. The cache-aside and invalidation side of this design is covered in solving cache invalidation and stampede in Valkey.
import valkey
r = valkey.Valkey(host="10.0.0.5", port=6379)
def get_product(product_id: str):
# 0 means the ID was never added: skip the cache and the database
if r.execute_command("BF.EXISTS", "valid:products", product_id) == 0:
return None
cached = r.get(f"product:{product_id}")
if cached is not None:
return cached
return load_from_db_and_cache(product_id) # your existing path
def create_product(product_id: str, row):
insert_into_db(row)
r.execute_command("BF.ADD", "valid:products", product_id)Interpretation: every write path that creates an ID must also add it to the filter, or new rows will look absent. Deleted IDs stay in the filter as permanent "maybe" answers, and they pass through to the database as before. Rebuild the filter on a schedule under a new key, then switch readers with RENAME. A guard sees mostly misses, which check every sub-filter, so reserve it as NONSCALING or with a large starting capacity.
Frequently asked questions
What is the maximum size of a Bloom filter in Valkey?
By default, 128 MB per filter. valkey-bloom's bf.bloom-memory-usage-limit setting defaults to 134,217,728 bytes, and any create or scale-out that would exceed it fails with "ERR operation exceeds bloom object memory limit". You can raise it with CONFIG SET or in valkey.conf. Keep the same value on every node, because RDB loads enforce it too.
How many items fit in a default Valkey Bloom filter?
A filter created by BF.ADD starts at 100 items and doubles each time it fills. Under the 128 MB limit, it reaches 26,214,300 items in 18 sub-filters and about 102 MiB. A non-scaling filter at the same 1% rate holds about 112 million items under the same limit.
What does VALIDATESCALETO do in BF.INSERT?
It rejects the command if a scaling filter built from the given capacity, error rate and expansion could never hold the requested number of items. The failure can come from the memory limit or from the error rate degrading to zero. Nothing is created on failure. It cannot be combined with NONSCALING, and it checks the command's arguments, not an existing key.
Does a scaling Bloom filter keep the false-positive rate I set?
Not exactly. Each new sub-filter gets half the previous rate under the default 0.5 tightening ratio. A lookup can still match any of them, so once they fill, the combined rate approaches the sum of their rates. That sum stays under twice the rate you set. BF.INFO keeps reporting the original rate.
Can I move RedisBloom filters to valkey-bloom?
The commands carry over, but the data does not. RedisBloom stores filters as MBbloom-- and valkey-bloom as bloomfltr, with different hashing, so RDB files and SCANDUMP chunks cannot be loaded. Recreate each filter with BF.RESERVE and re-insert the source items from your database or event log.
Key takeaways
- Size before you create: bytes are about n × −ln(p) ÷ 3.844, which is 1.2 MB per million items at 1% and 1.8 MB at 0.1%.
- The per-filter cap is
bf.bloom-memory-usage-limit, 128 MB by default. It is checked on every create and scale-out, and on RDB load, so set it the same on every node. - Default filters from
BF.ADDstop at 26,214,300 items, use about 102 MiB and check up to 279 hash positions per miss. Reserve explicitly instead. - Use
NONSCALINGwhen the count is known, andBF.INSERT ... VALIDATESCALETOorBF.INFO ... MAXSCALEDCAPACITYwhen it is not. - Run valkey-bloom 1.0.1, and plan deletes with time-windowed keys or scheduled rebuilds, because filters cannot remove items.
Size and tune Valkey Bloom filters with JusDB
JusDB's Valkey performance tuning team can review your Bloom filter keys against their real growth. That covers capacity, error rate, sub-filter counts and the per-filter memory limit on every node. We can then help plan resized filters and the rebuild or cutover to move traffic onto them.
Bring the output of INFO BF, BF.INFO for your largest filters, and your expected item counts. Your team approves every production change.