PERFORMANCEENGINEERING ESSAY · 7 MIN READ

Bloom filters spend false positives

Notebook dates are an editorial chronology, separate from publication dates.

A Bloom filter exchanges memory for avoidable downstream work. Its positive answer means a key might exist, while its negative answer is useful only if the filter completely represents the set being queried. I would evaluate it as a cost model with an explicit maintenance contract, not as a smaller replacement for a set.

Separate the mathematical promise from the system

A conventional Bloom filter stores a bit array and sets several positions for each inserted key. A lookup reports absent if at least one required bit is clear; otherwise it reports possibly present. Redis's documentation describes this probabilistic membership behaviour. Under the usual assumptions, an inserted key will not be reported absent by that correctly maintained filter. That guarantee does not mean an application can never observe a false negative. A missing insertion, an incompatible serializer or a filter built from an incomplete snapshot can violate the relationship between the filter and the authoritative dataset.

This distinction matters when the filter sits in front of a database. A negative result can safely skip a database lookup only if every relevant database key is represented for the version of the data being served. A positive result still requires the authoritative lookup. I would express that invariant in the design and test it during loading, updates and recovery. If the system cannot establish completeness after a restart, bypassing the filter is a sensible degraded mode. Treating an uninitialised filter as an empty authoritative set can make real records disappear from the application.

References: [1] Redis Bloom filter documentation

Calculate the memory budget before choosing defaults

For a conventional Bloom filter with m bits, n inserted keys and k well-distributed hash positions, the common approximation for false-positive probability is p = (1 - exp(-kn/m)) raised to k. Choosing k near (m/n) times ln(2) gives an efficient balance. Rearranging yields m approximately equal to -n ln(p) divided by ln(2) squared. These are sizing approximations under a hashing model, not a universal exact guarantee for every blocked or implementation-specific variant. The units matter: m is bits, so converting to bytes requires division by eight.

For an illustrative one million keys and a target of one percent, the calculation produces about 9.585 million bits, or 1.198 million decimal bytes, with roughly seven hash positions. The following calculator rounds the bit count upward and selects an integer k, then evaluates the resulting approximation again. It also shows what happens if the actual population doubles without resizing: the error probability grows substantially. A nominal target in configuration is not a property that survives arbitrary growth. I would track represented population or occupancy and define when the filter must be rebuilt.

Conventional Bloom-filter sizing under independent, well-distributed hash assumptions; decimal bytes are printed. python
import math

n, target = 1_000_000, 0.01
bits = math.ceil(-n * math.log(target) / math.log(2) ** 2)
k = max(1, round(bits / n * math.log(2)))
def probability(items):
    return (1 - math.exp(-k * items / bits)) ** k
assert k == 7
assert 0.0100 < probability(n) < 0.0101
assert probability(2 * n) > 0.15
print(bits, math.ceil(bits / 8), k)
print(probability(n), probability(2 * n))

References: [1] Redis Bloom filter documentation

Price the false positives against the avoided work

Assume, illustratively, that a workload makes one million membership queries, ninety percent of keys are absent, and the filter's false-positive probability for absent queries is one percent. About nine thousand absent queries still reach the database, while about eight hundred ninety-one thousand absent queries avoid it. The one hundred thousand present keys also require authoritative reads. This is an expectation under the assumed workload, not a benchmark or a tail guarantee. If almost every query is for a present key, the same filter saves little while adding hashing and memory access to every request.

The relevant comparison includes the cost of the avoided operation. Skipping a remote object-store request may justify far more filter work than skipping a lookup in an already-hot in-process hash table. Cache behaviour also matters: several random bit probes can be expensive even when the total bit array looks small in a capacity spreadsheet. I would measure end-to-end latency, downstream request reduction and CPU use using the actual key distribution. A lower false-positive rate is not automatically better if the additional memory displaces a more valuable cache or increases lookup overhead.

References: [2] Apache Parquet Bloom filter specification

Storage formats can choose a different layout

Parquet's Bloom filter specification defines a split-block design intended to make membership checks useful for pruning column chunks. Its arrangement of blocks and hash-derived positions is a concrete storage-format contract, not just the abstract bit-array description above. This is why I would avoid copying the conventional formula and claiming it exactly predicts every Parquet implementation. The broader application pattern remains clear: use a compact auxiliary structure to avoid reading data that cannot satisfy a predicate, then use the underlying values to resolve possible matches.

A filter is only as useful as the predicate it can answer. Equality on a represented scalar key fits naturally; an arbitrary substring search or range predicate does not become efficient merely because a Bloom filter exists. Composite keys require an agreed encoding, including boundaries between fields and treatment of nulls. If the writer inserts a normalised form and the reader hashes a different form, the mathematical data structure can work correctly while the storage system gives an incorrect negative. Versioning the encoding is therefore part of the filter's correctness contract, not optional metadata.

References: [2] Apache Parquet Bloom filter specification

Deletion and replacement require a maintenance plan

Clearing the bits associated with a deleted key is unsafe in an ordinary Bloom filter because other keys may share those bits. Leaving deleted keys represented preserves the no-false-negative property for remaining keys but can increase unnecessary positive answers. Rebuilding from a complete snapshot is one answer; counting or other probabilistic structures offer different deletion tradeoffs. I would not switch structures solely because a delete method is available. Counter overflow, concurrent updates and recovery of the auxiliary state still need consideration for the chosen implementation.

A rebuild can introduce a race if a new filter is constructed while the authoritative set continues changing. A clean protocol needs a consistent base plus a way to include changes after that base, or an atomic version switch whose readers know which dataset version the filter covers. For append-only data, merging newly inserted keys may be straightforward. For mutable partitions, retaining the old filter until the replacement is complete can be safer than exposing partial construction. The important observable state is complete for version V, not merely build job finished without an exception.

References: [1] Redis Bloom filter documentation

Do not turn a probabilistic hint into authority

A positive answer should not grant access, charge an account or establish that a username is already owned. Those decisions require authoritative state. Even using a Bloom filter to suppress duplicate processing can be wrong if a false positive causes a genuinely new operation to be discarded. It can be acceptable for a workload whose contract explicitly tolerates that loss, but then the loss rate belongs in the product's error budget. I would describe the filter as a negative cache for a particular complete set, rather than a general proof of identity or prior execution.

A reasonable counterargument is that an exact set is simpler and memory is inexpensive. For modest populations or high consequence decisions, that can be the better design. Bloom filters earn their complexity when the set is large, negative queries are common and the avoided work is costly. Adversarial inputs also deserve attention: a workload chosen to exploit poor hash distribution or repeatedly hit false positives can remove the expected benefit. Random-looking average-case measurements should not be mistaken for a hard upper bound on downstream load during hostile or highly skewed traffic.

References: [1] Redis Bloom filter documentation[2] Apache Parquet Bloom filter specification

Verify completeness and measure the realised trade

I would test every inserted key against the constructed filter, test encoding edge cases, and compare a sample of negative decisions with the authoritative source. The first check validates an important local invariant; the last can detect integration mistakes that a pure data-structure test misses. During rebuilds, include writes immediately before and after the snapshot boundary and simulate interruption before publication. Corruption checks and version metadata help distinguish a legitimately saturated filter from one that was loaded with the wrong parameters or bytes.

Operationally, track bypasses, filter negatives, authoritative hits after positives and authoritative misses after positives. The observed false-positive estimate needs the correct denominator: absent queries, not all positive filter answers or all traffic. Sampled verification may be needed to learn about negatives without paying the full database cost. The goal is to show that a defined amount of memory avoids a useful amount of work while preserving the completeness invariant. That is a stronger engineering claim than announcing a theoretical one-percent error rate without saying what the application does with either answer.

References: [1] Redis Bloom filter documentation

Sources and further reading

  1. Redis Bloom filter documentation

    Primary documentation describes probabilistic membership and sizing behaviour. Calculations use an explicitly stated conventional model; maintenance protocols and workload assumptions are the essay's analysis.

  2. Apache Parquet Bloom filter specification

    The primary format specification defines split-block Bloom filters for Parquet. The essay distinguishes that layout from its conventional sizing example.

FROM THE NOTEBOOK.

Back to all notes