Three Ways to Remember Without Storing Keys
Imagine a venue that must quickly reject people whose names are definitely not on a very large guest list. Keeping the complete list at every entrance is expensive. Instead, each entrance keeps a compact summary. The summary may occasionally say “possibly present” for a person who is absent, forcing a check against the authoritative list, but it must never say “absent” for a guest who was correctly inserted.
That is the contract of an approximate membership filter. A negative answer is definitive under the stated model; a positive answer may be a false positive. Bloom, cuckoo, and XOR filters all provide this asymmetric answer, but they organize evidence differently:
- A Bloom filter sets several shared bits for every key and checks that all corresponding bits are set.
- A cuckoo filter stores a short fingerprint in one of two candidate buckets and may relocate fingerprints during insertion.
- An XOR filter stores fingerprint values in an array constructed so that the XOR of three addressed cells reproduces every member's fingerprint.
This deep dive builds on the membership-filter prerequisite. The earlier chapter establishes hashes, false positives, and no-false-negative semantics. The unresolved engineering choice is which family fits a set that is static or changing, needs deletion or not, must build incrementally or offline, and is constrained by memory, latency, construction reliability, or adversarial exposure.
For a tiny Bloom example, use $m=8$ bits and $k=2$ hash locations. Inserting A sets positions 1 and 5; inserting B sets 2 and 5. A lookup for absent C that checks 1 and 2 returns “possibly present” because both bits happen to be set. Nothing was corrupted: the shared-bit representation deliberately permits this false positive. If C checked positions 0 and 7, either zero bit would prove absence.
The core metrics require precise denominators. Bits per key is total allocated filter storage divided by successfully represented keys, including slack and metadata when comparing implementations. Load factor is occupied capacity divided by allocated slots or cells, defined differently by family. False-positive rate is the fraction of absent queries answered positive for a declared workload. Construction covers insertions, relocations, peeling, retries, and failure handling. Deletion must preserve all remaining members; removing shared evidence without sufficient counts can create false negatives.
All three structures exchange exact key storage for occasional false positives, but their update and construction contracts are fundamentally different. A formula-sized filter is not production-sized until load slack, metadata, failed builds, hash behavior, and the actual negative-query workload are included.
| Concept | Intuitive meaning | Exact example | Non-example or boundary | Main assumption | Question answered |
|---|---|---|---|---|---|
| Bits per key | Whole filter footprint divided by represented keys | $9.6$ bits/key for a Bloom target near $1\%$ | Fingerprint width alone | Count slack, metadata, alignment, failed-build policy | What memory is actually paid? |
| Load factor | How full the allocated structure is | 950 occupied cuckoo slots of 1000 gives $0.95$ | Bloom fraction of one-bits called bucket load | Family-specific capacity definition | How close is construction to failure or slowdown? |
| False-positive rate | Absent queries incorrectly return positive | 100 positives among 10,000 absent probes is $1\%$ | Positives divided by all queries when many are members | Representative absent-query distribution | How much fallback work is created? |
| Construction | Work needed to create valid state | XOR peeling plus reverse assignment and retry | Lookup benchmark excluding failed builds | Hash seeds, capacity, retry policy fixed | Can the filter be built within budget? |
| Lookup | Probe filter and return absent/possibly present | Bloom checks $k$ bit positions | Treating positive as proof the key exists | Inserted state is valid and compatible | Can an expensive backing lookup be skipped? |
| Deletion | Remove one represented member safely | Cuckoo removes matching fingerprint under its contract | Clearing ordinary Bloom bits | No ambiguous or unsupported operation | Can the set change without false negatives? |
| Static versus dynamic | Whether keys change after construction | XOR filter for immutable SSTable | Rebuilding entire filter per single update called dynamic | Lifecycle and rebuild cadence declared | Which family matches mutations? |
Three Ways to Remember Without Storing Keys
Use an 8-bit Bloom filter with two locations per key. A maps to $(1,5)$ and B maps to $(2,5)$.
- Start with
00000000. - Insert A: set positions 1 and 5.
- Insert B: set positions 2 and 5; position 5 is shared.
- Query absent C at $(1,2)$. Both bits are one, so the answer is “possibly present”: a false positive.
- Query absent D at $(0,7)$. At least one bit is zero, so D is definitely absent.
- Try deleting A by clearing positions 1 and 5. Position 5 is also evidence for B, so B may become a false negative.
Expected result: insertion-only membership works, false positives are possible, and ordinary Bloom deletion is unsafe. Reflection: what additional state would allow safe decrements, and how would it change memory and overflow risk?
Bloom Bits, Cuckoo Buckets and XOR Peeling
For a Bloom filter with $m$ bits, $n$ inserted keys, and $k$ ideal independent hash positions, a given bit remains zero with probability $(1-1/m)^{kn}\approx e^{-kn/m}$. An absent query is a false positive when all $k$ queried bits are one, giving
$$ p_{\mathrm{Bloom}}\approx\left(1-e^{-kn/m}\right)^k. $$
Let $c=m/n$ be bits per key. Minimizing with respect to $k$ gives $k^*=c\ln2$, rounded to a practical positive integer. At this optimum, roughly half the bits are set and
$$ p^\approx(1/2)^{c\ln2}=e^{-c(\ln2)^2}, \qquad c=-\frac{\ln p^}{(\ln2)^2}. $$
For target $p=0.01$, $c\approx9.585$ bits/key and $k\approx6.64$, so a practical design tests nearby integers, usually 7, using the exact allocated $m$ and expected $n$.
A cuckoo filter stores an $f$-bit fingerprint in one of two candidate buckets, often with $b$ slots per bucket. The alternate bucket is derived from the current bucket and fingerprint so a fingerprint can be relocated without the full key. Lookup checks at most $2b$ slots. Under a simplified uniform-fingerprint model, the false-positive probability is approximately
$$ p_{\mathrm{cuckoo}}\approx1-(1-2^{-f})^{2b}\approx\frac{2b}{2^f}. $$
With $b=4$ and target near $1\%$, the approximation suggests $f\approx10$ bits. Actual bits/key are closer to $f/\alpha$ plus metadata, where $\alpha$ is achieved slot load. Insertions can trigger eviction chains and can fail near high load; bounded retries, stash policy, resizing, and atomicity are part of the contract.
An XOR filter is built for a known static key set. Each key selects three cells, forming a 3-uniform hypergraph. Construction repeatedly removes a key incident to a degree-one cell—the peeling phase. If every key peels, reverse order assigns cell fingerprints so the XOR of the three cells equals the key fingerprint. If a nonempty core remains, construction chooses a new seed or larger array and retries. A nonmember's reconstructed $f$-bit value matches its fingerprint with probability approximately $2^{-f}$ under the model. The space is roughly array-size factor times $f$ bits per key, commonly around $1.23f$ for a suitably engineered three-location design, before metadata and alignment.
These derivations are conditional. Correlated hashes change Bloom occupancy. Short fingerprints and nonuniform buckets change cuckoo FPR. Duplicate keys must be normalized for XOR construction. A peel failure is expected probabilistic behavior, not permission to publish an invalid filter. None of the three structures is cryptographically secure by default.
flowchart TD
A[Define n target FPR lifecycle] --> B{Set changes after build?}
B -->|insert only or rebuildable| C[Bloom bit array]
C --> D[Derive occupancy and optimize k]
D --> E[All k bits set means possible]
B -->|insert and delete| F[Cuckoo buckets and fingerprints]
F --> G[Place in one of two buckets]
G --> H[Relocate on collision]
H --> I[Lookup both buckets]
B -->|static batch| J[XOR three-cell hypergraph]
J --> K[Peel degree-one cells]
K --> L{All keys peeled?}
L -->|yes| M[Reverse-assign fingerprints]
L -->|no| N[Retry seed or resize]
A --> X{Hash and capacity assumptions valid?}
X -->|no| Y[Formula or construction guarantee invalid]
X -->|yes| B
classDef navy fill:#17365D,color:#fff,stroke:#17365D;
classDef teal fill:#147D84,color:#fff,stroke:#0D565B;
classDef purple fill:#6C3BAA,color:#fff,stroke:#472675;
classDef crimson fill:#C6284D,color:#fff,stroke:#861B35;
class A,B,C,F,J navy;
class D,G,H,K purple;
class E,I,M teal;
class L,N,X,Y crimson;
Bloom filters minimize $(1-e^{-kn/m})^k$ near $k=(m/n)\ln2$; cuckoo filters spend fingerprint bits plus load slack to gain dynamic deletion; XOR filters spend offline peeling and possible retries to obtain compact static lookup near $2^{-f}$ false positives. Each guarantee depends on its own hash, load, and lifecycle assumptions.
False Positives, Construction and Deletion Tradeoffs
Consider $n=1{,}000{,}000$ keys and target false-positive rate $p=1\%$. The optimal Bloom formula gives $m/n=-\ln(0.01)/(\ln2)^2\approx9.585$ bits/key and $k\approx6.64$. Choosing $k=7$ and allocating $m=9{,}585{,}059$ bits yields predicted
$$ p\approx\left(1-e^{-7n/m}\right)^7\approx0.01004. $$
The bit array alone is about $1.143$ MiB. Real storage rounds words or pages and carries seed, length, count, and version metadata.
For a cuckoo design with $b=4$, fingerprint width $f=10$, and achieved load $\alpha=0.95$, fingerprint payload is about $f/\alpha\approx10.53$ bits/key. The simple lookup approximation is $1-(1-2^{-10})^8\approx0.778\%$. Bucket rounding and metadata increase space; duplicate fingerprints, insertion retries, and maximum kick count affect behavior.
For a three-location XOR design with $f=8$, ideal FPR is about $1/256\approx0.391\%$. At array factor $1.23$, fingerprint payload is about $9.84$ bits/key. It looks excellent for a static set, but the cost comparison must include the input key set during construction, peel queues, retry work, seed metadata, and rebuild deployment strategy.
Deletion separates the families sharply. Ordinary Bloom cannot clear bits safely. A counting Bloom filter replaces bits with counters and supports deletion only when increments and decrements are correct and counters do not overflow or underflow. Cuckoo deletion removes a matching fingerprint, but an API deleting a key never inserted can remove another key's colliding fingerprint and create a false negative; deployments often require an authoritative membership check or strict delete provenance. Standard XOR filters are static and normally rebuilt for changes.
False Positives, Construction and Deletion Tradeoffs
Size three candidates for $n=1{,}000{,}000$ static keys and target FPR near $1\%$.
- Bloom: $c=-\ln(.01)/(\ln2)^2\approx9.585$ bits/key; $k=c\ln2\approx6.64$, choose 7. Predicted FPR is about $1.004\%$ with $m=9{,}585{,}059$.
- Cuckoo: with four-slot buckets and 10-bit fingerprints, simple FPR estimate is $1-(1-2^{-10})^8\approx0.778\%$. At $95\%$ load, payload is $10/0.95\approx10.53$ bits/key.
- XOR: with 8-bit fingerprints and array factor 1.23, payload is $9.84$ bits/key and ideal FPR is $0.391\%$.
- Record what is excluded: allocator rounding, headers, seeds, build workspace, retry probability, and CPU cache effects.
Perturbed workload: if the set receives continuous insertions and deletions, the static XOR candidate becomes operationally infeasible without rebuild layers. Bloom needs counting or generations; cuckoo becomes the natural candidate if insertion failures and delete provenance are controlled.
| Family | Best lifecycle | Approximate FPR model | Effective space drivers | Update/construction | Deletion | Main failure mode |
|---|---|---|---|---|---|---|
| Standard Bloom | Insert-only or rebuildable | $(1-e^{-kn/m})^k$ | Bit array, hash count, occupancy, metadata | Simple incremental inserts | No safe ordinary deletion | Overcapacity raises FPR; bit clearing creates false negatives |
| Counting Bloom | Dynamic with controlled deletes | Similar to Bloom before counter effects | Counter width times cells, overflow policy | Increment/decrement counters | Yes under valid provenance | Counter overflow/underflow or deleting absent key |
| Cuckoo | Dynamic insert, lookup, delete | $1-(1-2^{-f})^{2b}$ simplified | Fingerprint width, bucket size, achieved load, stash | Relocation chains and possible resize | Yes under fingerprint/delete contract | Insertion failure near load; ambiguous deletion |
| XOR | Static immutable set | Approximately $2^{-f}$ | Fingerprint width times array factor, seed/header | Batch peel, reverse assign, retry | No standard incremental deletion | Unpeelable core or publishing failed build |
| Blocked Bloom | Insert-only, cache-sensitive | Higher than ideal Bloom at same nominal bits | Block size and within-block occupancy | Incremental and cache-local | No ordinary deletion | Hot blocks and uneven occupancy |
| Layered/generational design | Changing set via immutable epochs | Composition of layer FPRs | Sum of layers plus compaction workspace | Append layer, periodically rebuild | By tombstone or rebuild policy | FPR accumulates and stale layers persist |
Filters Under Changing Production Workloads
In a teaching setting, Bloom filters make false positives visible with a small bit array. Cuckoo buckets then show how fingerprints enable relocation and deletion, while XOR peeling demonstrates that a static set can support faster, denser queries by moving complexity into construction.
In professional storage engines, immutable SSTables are an excellent fit for static filters. An XOR filter can be built alongside a newly flushed table and published atomically only after successful peeling and validation. A Bloom filter remains attractive when implementation maturity, streaming construction, or broad library support matters more than the last bits per key. The decision depends on measured negative-lookup latency, cache behavior, build throughput, retry rate, and recovery path—not payload arithmetic alone.
A dynamic cache-admission service has a different profile. It may need millions of inserts and deletes per second. A cuckoo filter offers deletion and typically few cache-line probes, but the system must monitor load factor, kick-chain tails, stash occupancy, failed inserts, and delete provenance. A scalable or layered Bloom design may be easier if deletes can be represented by epochs or rebuilds.
Adversarial use changes the threat model. An attacker who learns noncryptographic hash behavior may craft collisions, concentrate Bloom bits, trigger cuckoo relocation storms, or create repeated XOR build failures for chosen sets. A secret keyed hash can make targeting harder, but key rotation, cross-node compatibility, and denial-of-service limits still need design. Approximate filters must never authorize access: a positive is not proof of membership.
Evidence that invalidates a selection includes observed FPR above budget on production-shaped negatives, construction retry tails above deployment SLO, cuckoo load below planned capacity because of key distribution, false negatives after deletion, cross-language hash mismatch, memory above full-footprint budget, or p99 lookup regressions caused by cache misses.
flowchart TD
A[Define set lifecycle and negative-query workload] --> B{Need incremental deletion?}
B -->|yes| C[Cuckoo or counting Bloom candidate]
B -->|no| D{Static keys available before build?}
D -->|yes| E[XOR and Bloom candidates]
D -->|no| F[Bloom or layered candidate]
C --> G[Size load fingerprints and failure policy]
E --> H[Size FPR array factor and retry workspace]
F --> I[Size m n k and growth headroom]
G --> J[Benchmark construction updates lookup deletion]
H --> J
I --> J
J --> K{Full-footprint SLOs pass?}
K -->|no| L[Reject resize or change family]
K -->|yes| M[Deploy shadow verification and monitoring]
M --> N{FPR load retries or false negatives breach?}
N -->|yes| L
N -->|no| M
classDef navy fill:#17365D,color:#fff,stroke:#17365D;
classDef teal fill:#147D84,color:#fff,stroke:#0D565B;
classDef purple fill:#6C3BAA,color:#fff,stroke:#472675;
classDef crimson fill:#C6284D,color:#fff,stroke:#861B35;
class A,B,D navy;
class C,E,F,G,H,I purple;
class J,M teal;
class K,L,N crimson;
Filters Under Changing Production Workloads: Bloom, Cuckoo & XOR Filters Compared
Scenario. A storage service creates immutable files containing $50$ million keys, serves $95\%$ negative lookups, targets FPR below $0.5\%$, and has a 90-second build budget. Files never mutate after publication, but new versions appear hourly.
- Bloom candidate: target $p=0.005$ needs $-\ln(.005)/(\ln2)^2\approx11.03$ bits/key and about 8 probes. Payload is roughly $65.7$ MiB before rounding and metadata.
- XOR candidate: a 9-bit fingerprint gives ideal FPR about $0.195\%$ and, at factor 1.23, payload about $11.07$ bits/key—similar payload, usually fewer dependent operations per lookup, but with offline peel/retry work.
- Cuckoo candidate: deletion is unused, and load slack plus bucket probes may not justify its dynamic features. Keep it only if measured lookup or library constraints dominate.
- Protocol: build each candidate on identical key sets and hardware. Measure peak build memory, p50/p99 build time including retries, serialized bytes/key, negative and positive lookup latency, cache misses, FPR on production-shaped absent keys, and recovery from interrupted construction.
- Failure injection: force an XOR peel failure and confirm atomic retry; exceed planned Bloom cardinality and observe FPR drift; use incompatible seeds; corrupt headers; test duplicate keys and adversarially clustered inputs.
- Expected result: both Bloom and XOR meet membership semantics; XOR wins only if its end-to-end build reliability and lookup benefit offset retry/workspace complexity.
Selection decision: choose XOR for immutable files if p99 construction including retries stays below 90 seconds, peak workspace fits, and measured FPR/lookup wins persist. Otherwise choose the operationally simpler Bloom implementation. Reject cuckoo unless a future mutation requirement is concrete rather than hypothetical.
Benchmark the Right Membership Filter
The correct benchmark starts with the decision avoided by a negative answer. A filter that is marginally faster but produces more false positives may increase total latency if backing-store misses are expensive. Conversely, extra memory may be worthwhile when it prevents network or disk work. Measure the whole path with actual positive/negative mix, cardinality growth, CPU cache, allocator, construction, retries, and serialization.
This topic bridges backward to hashing and approximate membership guarantees. It leads forward to LSM-tree filter policies, learned filters, quotient and fuse filters, cache-aware layouts, adversarial hashing, and multi-level FPR budgeting. The durable skill is not memorizing a winner; it is mapping lifecycle and failure contracts to a measured design.
Practice: Benchmark the Right Membership Filter
Starter workload: $n=10{,}000{,}000$, target FPR $0.1\%$, $90\%$ negative queries, insert rate $200{,}000$/s, and two variants: immutable daily snapshot or continuously changing set.
Tier 1 — Bloom derivation
- Derive zero-bit probability and $p=(1-e^{-kn/m})^k$.
- Compute $m/n=-\ln(.001)/(\ln2)^2\approx14.38$ bits/key.
- Compute $k\approx9.97$ and test integer choices 9, 10, and 11.
Expected artifact: a table with $m$, $k$, predicted FPR, byte-rounded footprint, and occupied-bit fraction.
Tier 2 — Fingerprint sizing
For cuckoo buckets with $b=4$, find the smallest $f$ whose simplified $1-(1-2^{-f})^8$ meets target. Compare $f/\alpha$ at measured loads $0.90$ and $0.95$. For XOR, compare $2^{-f}$ and $1.23f$ payload for neighboring integer widths.
Debugging prompt: if fingerprint width alone is reported as bits/key, add load or array factor and metadata.
Tier 3 — Lifecycle selection
Choose Bloom, cuckoo, or XOR for immutable and dynamic variants. State construction, deletion, resizing, failed-build, duplicate-key, and crash-recovery behavior. Identify one observation that would reverse each choice.
Tier 4 — Verification and failure injection
Extend the Python experiment across load ratios $0.5$ through $1.5$ for Bloom. Implement or integrate production candidates and test no-false-negative behavior, representative absent queries, delete-absent misuse, cuckoo kick limits, XOR peel retries, rank/hash correlation, corrupted serialization, and cross-language golden vectors.
Completion criterion: zero false negatives in supported operations; FPR confidence interval within budget; every construction failure is explicit and recoverable; peak memory and p99 latency are reported.
Tier 5 — End-to-end economics
Model expected lookup cost as filter cost plus false-positive probability times backing-check cost. Sweep query mix, storage latency, bits/key, and cardinality drift. Select the lowest total-cost feasible candidate, not the lowest isolated nanoseconds.
Stretch goal: allocate a total FPR budget across several LSM levels so their combined fallback probability meets a service objective. Explain independence assumptions or use a conservative union bound.
Rubric (10 points): derivations 2; sizing constants 2; runnable verification 2; lifecycle/failure testing 2; end-to-end decision 2. Mastery requires at least 8 points and explicit separation of payload estimates from full production footprint.
Reflection: which measured cost dominates your system—memory, filter CPU, false-positive backing lookups, mutation complexity, or construction failure?
Decision checklist: define lifecycle and supported mutations, size from the correct FPR model, include load/array factors and metadata, benchmark construction plus lookup, preserve hash/version contracts, test false negatives and failure recovery, and price false positives end to end. No family is universally best: Bloom favors simplicity, cuckoo favors dynamic deletion, and XOR favors compact static sets when reliable offline construction is available.