---
topic: system-design
author: Crashtech Editorial
date: Aug 14, 2026 · read: 7 min
---

Bloom Filters: The One-Way Membership Test

A probabilistic data structure: zero false negatives, tunable false positives. Check membership in RAM with bits instead of database queries.

At Instagram’s peak, checking “is this username taken?” happened billions of times a day. A naive database lookup for each query would collapse I/O. Instead, Instagram and Medium use Bloom filters: a RAM-resident probabilistic data structure that definitively rejects unavailable usernames in a handful of CPU cycles—and only escalates to the database when a username might be available. The entire membership test fits in under 12 MB per million usernames, even with a 1% false-positive rate.

A Bloom filter bit array showing two lookups: a definitive MISS when one probed bit is 0 (no DB call), and a false positive when all bits happen to be set by other keys. Left: naive approach queries the database on every check. Right: Bloom filter checks bits in RAM; only a MISS (any probed bit is 0) or a FALSE POSITIVE requires a database call.

The Problem: Database Lookups at Scale

When you type a username into a signup form, the system must check whether it’s already claimed. A straightforward approach: query the user table for that username. Simple. Scalable if you have one user per second.

But at massive scale—Instagram getting 500 million signups yearly, Medium checking availability in real time during composition—database queries become a bottleneck. Each lookup incurs:

  • Locking: A read lock on the username index, serializing concurrent checks.
  • I/O latency: Even with an index, the query must reach disk or wait for cache misses.
  • CPU context switches: The application thread waits; the kernel schedules work; responses stack up.

A dedicated availability-check cache helps, but it requires cache-invalidation discipline and storage proportional to the user count. And if the cache is wrong, you accept duplicate usernames.

The real question: Can you answer the query without touching the database?

The Breakthrough: Probability Instead of Certainty

A Bloom filter trades a small false-positive rate for a guarantee: no false negatives. In other words:

  • If the filter says “no”: the username is definitely unavailable. Go home. No database call.
  • If the filter says “maybe”: the username might be available; check the database to be sure.

This asymmetry is the entire win. Most username checks fail fast (the majority of names are already taken). The false positives—the rare “maybes”—still hit the database, but they’re tuned to be rare (1%, or even 0.1%).

How It Works: Hashing and Bit-Setting

A Bloom filter is elegantly simple:

  1. Allocate a bit array of size m (e.g., 12 megabits for 1 million users at 1% FP rate).
  2. Choose k hash functions (typically 3–7, depending on array size and target FP rate).
  3. On insert: hash the key with all k functions, set the k bits to 1.
  4. On lookup: hash the key with all k functions. If any bit is 0, return “definitely not in set.” If all are 1, return “probably in set.”

Mechanism: A username is hashed through three functions, each computing a bit index; those bits are set to 1. On lookup, the same three hashes guide us to check those bits. Top: inserting “alice” hashes it three times and sets bits 3, 7, and 11 to 1. Bottom: querying “alice” hashes it the same way and checks those bits. All are 1, so “probably present.”

Why does this work? Because:

  • No false negatives: If a key was inserted (its bits were set), a lookup will hash to the exact same bits and find all of them set to 1.
  • False positives are possible: Other keys may have set those same bits. If all k bits happen to be set by unrelated insertions, a lookup wrongly reports presence.

The probability of a false positive depends on:

  • Array size m: Larger arrays have sparser bit distributions; fewer collisions.
  • Number of keys n: More insertions set more bits, increasing FP likelihood.
  • Number of hash functions k: Too few, bits don’t spread enough; too many, all bits fill up.

The optimal k is ln(2) × (m/n) ≈ 0.69 × (m/n). For a 12-megabit array storing 1 million keys, that’s about k = 8. With this tuning, the false-positive rate is approximately (1 - e^(-k*n/m))^k.

The Asymmetry Explained

This is the crucial insight many miss: Bloom filters are asymmetric.

False negatives: Impossible. If a key is in the set, its bits are set, and the lookup will always find them.

False positives: Possible. If a key is not in the set but happens to hash to bits that other keys have set, the lookup reports “probably in set” even though it isn’t.

This asymmetry is why Bloom filters work for username availability. A false positive (wrongly thinking a username is taken) sends the user back to the database—an extra round-trip they can tolerate. A false negative (wrongly thinking a username is free) would accept a duplicate, corrupting data. Unacceptable. Bloom filters guarantee that doesn’t happen.

Real-World Sizing

The relationship between false-positive rate and space is predictable:

  • 1% false-positive rate: 9.6 bits per element.
  • 0.1% false-positive rate: 14.4 bits per element.
  • 0.01% false-positive rate: 19.2 bits per element.

For Instagram’s scale (1 billion users), a 1% Bloom filter costs:

1 billion users × 9.6 bits = 9.6 billion bits = 1.2 GB

Fits in a single server’s RAM. A 0.1% filter is still under 2 GB. Compare to storing the full username string (average 15 bytes): 15 GB. The Bloom filter is 12–13× smaller.

Add redundancy (multiple servers, cross-datacenter replication), and you’re still looking at 10–30 GB across the cluster—orders of magnitude cheaper than a distributed cache or querying the database billions of times a day.

Where Bloom Filters Are Used

  • LSM-Tree lookups (RocksDB, Cassandra, LevelDB): SSTable headers include Bloom filters; a query checks the filter before reading the full table from disk.
  • CDN negative caching: Has this file ever been on our origin? A Bloom filter of all historical URLs avoids redundant origin requests for files that have never existed.
  • DNS and IP reputation: Does this IP appear in a known-malicious list? The filter rejects 99% of traffic instantly.
  • Duplicate detection in streams: Is this event ID new? Bloom filters deduplicate without storing every ID.
  • Username and email uniqueness: Instagram, Medium, and most platforms that check availability in real time.
Practical Tip

If your Bloom filter fills up and the false-positive rate rises too high, don’t resize the array—rebuild it. Switching from a full 1%-FP filter to a fresh 1%-FP filter (or a 0.1%-FP filter) is often cheaper than live compaction. Plan for periodic rebuilds as part of your maintenance cycle.

When NOT to Use Bloom Filters

Bloom filters excel at high-frequency membership testing, but they have limits:

  • You need deletions: Standard Bloom filters don’t support deletion without breaking correctness (unsetting a bit breaks other keys). Counting Bloom filters add per-bit reference counts but use 4–8× more space.
  • You need to enumerate membership: Bloom filters answer yes/no; they don’t return the stored data or list members.
  • False positives are unacceptable: If a single false positive is catastrophic (e.g., wrongly admitting a user), use a set or database.
  • Your dataset is tiny: For under 100 keys, a hash set is simpler and just as fast.
Bloom Filter trade exact membership for speed

Space-efficient probabilistic membership. Zero false negatives; tunable false positives. Millions of lookups/second in RAM.

Counting Bloom Filter support deletions

Per-bit reference counts allow deletion. ~4–8× larger than standard Bloom filters.

Cuckoo Filter better CPU locality

Similar false-positive rate, supports deletion, better CPU cache behavior. Slightly more complex to implement.

Hash Set exact membership

O(1) average-case lookup. Requires O(n) memory for keys (not bits). Best for small datasets or when false positives are forbidden.

Summary

Bloom filters answer a fundamental question: Is this key in the set? with remarkable efficiency.

A small bit array in RAM, a handful of hash functions, and a tuned false-positive rate let you reject 99% of membership queries in a few CPU cycles—avoiding database round-trips entirely. The asymmetry (no false negatives, possible false positives) is engineered precisely for systems that can tolerate rare re-checks but not rare misses.

At Instagram’s scale, Bloom filters save millions of database queries per second, each saving milliseconds of latency. That efficiency compounds across billions of signup checks, availability queries, and content-lookup misses. Understanding when and how to trade certainty for speed is the mark of systems-level thinking.

Advertisement

Frequently asked questions

How does a Bloom filter guarantee no false negatives but allow false positives?

A key is added by hashing it through k functions and setting k bits to 1. On lookup, if ANY of the k bits is 0, the key is definitively absent (no false negatives). But all k bits being 1 proves nothing—other keys may have set those same bits. False positives are possible.

How many hash functions and how large should the bit array be?

The optimal number of hash functions is ln(2) × (m/n), where m is array size and n is expected elements. For 1% false positives, you need roughly 9.6 bits per element; for 0.1%, roughly 14.4 bits. A 10 million element set with 1% FP rate needs only ~12 MB.

Can you delete items from a Bloom filter?

Standard Bloom filters don't support deletion—unsetting a bit would break membership for other keys that need it. Counting Bloom filters track per-bit reference counts instead, allowing safe deletion, but use more space. For most use cases, append-only is acceptable.

Is k-hash or k-independent truly required, or can I use one hash with k-fold output?

Theoretically, k-independent hashing is optimal. In practice, a single strong hash function with k different seeds or outputs per seed (via a counter or slice offset) works well and often performs better than k separate hash implementations due to CPU cache locality.

Why not just use a set or database index if disk and CPU are cheap?

Sets and indices work at application level but require memory (O(n) pointers) and serialize lookups through the software stack, causing context switches and cache misses. A Bloom filter in RAM answers with a single CPU op per hash function—millions per second with zero I/O, making it ideal for high-frequency checks like username uniqueness.

Sources & further reading

/* Comments */