Lesson 3 · Senior

Failure modes & the real LSM read path

~7 min · visual-first · retrieval practice at the end

Published

Bloom filtersFailure modesData structuresFalse positiveProbabilistic

Why this, first. The senior gap is knowing how a Bloom filter earns its keep in a real database — and how it silently rots if you ignore load.

LSM-tree read: the Bloom filter guards the disk read(key) look up one SSTable check SSTable’s Bloom in RAM — nanoseconds “no” → SKIP the disk certain absent — no I/O at all “maybe” → READ disk then verify — may be a false positive no maybe Disk / SSD: the slow path microseconds–milliseconds, ×1000s No false negatives → a “no” is always safe to trust → skipping disk never loses data RocksDB & Cassandra put one Bloom filter per SSTable for exactly this
A per-SSTable Bloom filter turns most absent-key lookups into a free RAM check1.

Failure mode 1 — saturation: the FP rate is not constant

Size for n; keep inserting past it; bits fill, and the FP rate climbs toward 1.

More items → more bits set → FP rate rises load = items inserted ÷ design n FP rate 0.5× 1× design bits set FP rate designed FP → quietly multiplied as it fills
Past the design point, accuracy decays — the structure keeps answering, just worse2.
FPR rises
as n grows past design

(1−e−kn/m)k

FPR grows with n
no error
it degrades silently

Failure mode 2 — the LSM payoff, and when it breaks

Absent key
Bloom: “no”
skip disk — pure RAM cost
Present key
Bloom: “maybe”
read disk, then verify
Saturated
“maybe” for all
disk read on every miss

When the filter saturates, “no” nearly vanishes — the guard stops guarding and reads hit disk.

Failure mode 3 — bad hashes, no deletes, unbounded growth

Failure modeSymptomFix
Weak / correlated hashesFP rate worse than theory

Independent, well-mixed hashes 2

No-delete (standard filter)Deleted keys stay “present”

Periodic rebuild or counting variant 2

Unbounded growthSet outgrows the arrayRe-size / shard / scalable filter

A standard Bloom filter cannot remove a bit safely — clearing it could un-set a bit shared by a live key.

A Bloom filter’s accuracy decays with load — size for peak n and rebuild, or it silently rots.


Retrieval practice

Answer from memory — feedback is instant.

Q1. In an LSM read, a Bloom "no" lets the engine…

Q2. Skipping disk on "no" is always safe because…

Q3. As the filter fills past its design n, the FP rate…

Q4. A standard filter can't delete keys, so over time…

Rehearse out loud

Interviewer: "You're paging on a rising false-positive rate in a Bloom-fronted cache in production — diagnose and fix." ~60 seconds, from memory — then reveal and compare.


First hypothesis: saturation — the set grew past the n we sized for, so more bits are set and the FP rate climbed toward 1 (it’s

(1−e−kn/m)k

, not a constant). I’d confirm by comparing current item count and bits-set fraction against the design. Second: no-delete staleness — a standard filter can’t remove keys, so churned/deleted entries accumulate and keep matching. Fix: rebuild the filter from the live set on a schedule (or compaction), resize for the new peak n, and if deletes are frequent switch to a counting variant. I’d also verify the hashes are independent — correlated hashes push FPR above theory.

Why this scores: names saturation as primary cause, ties it to the load-dependent FPR formula, and adds the no-delete staleness angle with rebuild/resize/counting fixes.

I’m your teacher — ask me anything. Want the math behind the saturation curve, how RocksDB sizes its per-SSTable filters, or the next lesson (Staff: variants & scale)? Say so.

Primary source (read this next)

📖

RocksDB Wiki — Bloom Filter

1 . The per-SSTable filter and the disk-skip read path, in production.

Interview — pick your bar

Answer out loud in ~60s, then reveal. Core = recall · Senior = trade-offs & failure modes · Staff = synthesis under ambiguity · System Design = open design round (a different axis, not a harder level).

In an LSM-tree read, what does a Bloom "no" let the engine do, and why is that safe?
Hit these points: a "no" lets the engine skip the on-disk SSTable read entirely — pure RAM cost → it's safe because a Bloom filter has no false negatives: "not present" is certain → so skipping never loses a key that's actually there → that's the whole payoff: most absent-key lookups become a nanosecond RAM check instead of a disk/SSD seek → RocksDB and Cassandra put one filter per SSTable for exactly this.
What does a Bloom "maybe" cost in the LSM read path?
Hit these points: "maybe" means all k bits are set, so the engine must actually read the SSTable and verify the key → that read is the slow path (microseconds–milliseconds, orders of magnitude over the RAM check) → some "maybes" are true hits; a few are false positives that read disk and find nothing — wasted I/O bounded by the FP rate → so the filter doesn't make hits faster, it makes the common miss free → the value scales with how miss-heavy the workload is.
As a filter fills past its design n, what happens to the false-positive rate?
Hit these points: the FP rate is not constant — it's (1−e−kn/m)k, which rises with n → as you insert past the n you sized for, more bits get set, so more random lookups trip "all bits set" → the rate climbs steadily toward 1 → crucially nothing errors — the structure keeps answering, just less accurately → this is the saturation failure mode.
Why can't a standard Bloom filter delete keys, and what does that cause over time?
Hit these points: bits are shared, so clearing a bit to remove one key could un-set a bit a live key depends on — that would create false negatives → so deletes are unsupported → over time, churned/deleted entries keep matching: stale "present" answers pile up → the filter drifts away from the live set, raising effective FP load until you rebuild → fixes: periodic rebuild from the live set, or a counting variant.
You're paged on a rising false-positive rate in a Bloom-fronted cache in production. Diagnose and fix.
Hit these points: first hypothesis saturation — the set grew past the sized n, so bits-set fraction and the FP rate climbed (it's (1−e−kn/m)k, not a constant); confirm by comparing current item count and bits-set fraction to design → second no-delete staleness — churned/deleted keys accumulate and keep matching → fix: rebuild from the live set on a schedule (or on compaction), resize for the new peak n, switch to a counting variant if deletes are frequent → also verify hashes are independent — correlated hashes push FPR above theory → structure: name the primary cause, tie it to the load-dependent formula, then list rebuild/resize/counting fixes.
What happens to the LSM read path once the filter saturates — why is it operationally dangerous?
Hit these points: as the FP rate approaches 1, the certain "no" nearly vanishes — almost every lookup returns "maybe" → the guard stops guarding: reads that should have skipped disk now hit it → read amplification and latency spike, often suddenly and during peak load when the set is largest → it's dangerous because there's no error — throughput just collapses and the cause is non-obvious unless you monitor bits-set fraction / FP rate → the fix is capacity (rebuild/shard) before saturation, plus alerting on the FP rate, not on end-latency alone.
Your measured FP rate is worse than the formula predicts. What are the likely causes?
Hit these points: hash quality — weak or correlated hashes don't spread bits uniformly, so collisions exceed theory; use independent, well-mixed hashes (double-hashing done right) → saturation — actual n exceeds the design n, so the real bits-set fraction is higher than assumed → staleness — no-delete churn means the live set is smaller than the inserted set, inflating effective load → wrong k — too many hashes for the array fills it faster, raising FPR → the formula is also a slightly optimistic approximation (independence assumption), so expect a small gap even when everything's right → diagnose by measuring bits-set fraction and comparing to expected for the current n.
Why is the Bloom filter's silent degradation harder to operate than a loud failure?
Hit these points: there's no exception, no error code — the filter keeps returning plausible answers as accuracy decays → the symptom shows up far downstream (disk I/O, latency, cost), not at the filter, so it's easy to misattribute → it correlates with growth/load, so it tends to bite at the worst time (peak, large set) → you can't rely on the system to tell you; you must instrument it: track bits-set fraction, measured FP rate, and verify-path hit rate, with alerts before the rate is bad → the senior point: design observability in, because the structure won't fail loudly on your behalf.
Counting Bloom to enable deletes vs scheduled rebuild — make the principal call.
For counting: supports real-time deletes without rebuild, good when the set churns continuously and staleness can't be tolerated. Cost: ~4× the memory (counters instead of bits) and counter-overflow edge cases. For rebuild: keeps the compact 1-bit array, dead simple, and aligns naturally with compaction/time windows in many systems. Cost: staleness between rebuilds and rebuild CPU/IO. Synthesis: if deletes are rare or events expire on a window, rebuild (or roll filters) — cheapest and simplest; reach for counting only when continuous deletes with low staleness are a hard requirement and the 4× RAM is affordable; cuckoo if you want deletes and tight space.
How would you design alerting so a Bloom-fronted read path can't silently rot in production?
Hit these points: don't alert only on end-to-end latency — that fires after the damage → export filter-level metrics: bits-set fraction, current n vs design n, and a sampled measured FP rate (probe with known-absent keys) → alert when bits-set fraction crosses the design threshold or measured FP exceeds target, before disk reads spike → track verify-path hit rate: a falling ratio of true-hits among "maybes" signals saturation or staleness → tie alerts to an automated/runbook response: rebuild, resize, or shard → the staff move is making the silent decay observable and giving it a remediation path, so growth is a planned capacity event, not an incident.
An LSM store's read latency is creeping up over weeks. Walk your root-cause approach and where the Bloom filter fits.
Hit these points: separate causes: more SSTables/levels (compaction backlog), cache pressure, or filter degradation — don't assume the filter → check per-SSTable filter effectiveness: is the fraction of lookups skipping disk falling over time? → if absent-key reads increasingly hit disk, the filters are either under-sized for grown tables or saturating — compare bits-set fraction to design per level → also check for read-amp from delayed compaction (more tables to probe) which multiplies even a healthy filter's "maybe" cost → remediate the right cause: resize/rebuild filters on compaction, tune per-level bits/key (hotter upper levels get tighter filters), or fix compaction throughput → the staff framing: instrument disk-skip rate as a first-class metric so "the filter is rotting" is provable, not guessed, and weigh RAM-for-filters against the I/O it saves.
Design-round framework — drive any "Bloom in a read/dedup path under load" prompt through these, out loud:
  1. Map the path: where does "no" save the expensive work, and how often is the answer "absent"?
  2. Size from peak n and a p chosen from the cost of the slow path.
  3. Saturation plan: rebuild/shard thresholds on bits-set fraction, not on incidents.
  4. Churn plan: rebuild, time-windowed roll, or counting/cuckoo if deletes are real.
  5. Hash quality: independent, well-mixed hashes; validate measured vs theoretical FPR.
  6. Observability: bits-set fraction, measured FP rate, disk-skip / verify-hit rate.
  7. Failure modes: saturation, staleness, correlated hashes, read-amp from too many segments.
Design the Bloom layer for a read-heavy LSM key-value store so it stays healthy as data grows for years.
A strong answer covers: one filter per SSTable, built at flush/compaction from that table's keys — immutable tables mean no deletes and natural rebuilds on compaction, sidestepping staleness → size each filter from its key count and a per-level p (tighter for hot upper levels; numbers illustrative), so the FP rate per table stays at design even as total data grows → the certain "no" skips the SSTable read; "maybe" reads and verifies → guard against read-amp: as levels multiply, a query probes many filters, so monitor disk-skip rate and compaction backlog together → keep filters in RAM, spill/cache cold ones, budget aggregate bits/key across the fleet → observability: per-level bits-set fraction, measured FP, fraction of lookups skipping disk → trade-off: RAM for filters vs I/O saved on misses; tune per level rather than one global setting.
Design a Bloom-fronted dedup cache that won't silently rot — given the set churns and grows.
A strong answer covers: the two threats are saturation (growth) and staleness (churn/no-delete), so design for both up front → place the filter as a negative filter before the authoritative store: "no" → certainly new, fast path; "maybe" → verify against the store, never final → handle churn with time-windowed filters you roll and rebuild (entries age out) or a counting variant if continuous deletes are required → handle growth with scalable/sharded filters sized from peak per-window n plus headroom → instrument bits-set fraction, measured FP, and verify-hit rate, with alerts that fire before the FP rate is bad and trigger a rebuild/roll → pick p from the cost of the verify path × query rate → trade-off: rebuild simplicity + brief staleness vs counting's 4× RAM for live deletes; favor rolling rebuilds when entries naturally expire, counting only when they don't.
Sources
1. RocksDB Wiki, "RocksDB Bloom Filter" — per-SSTable filter; a negative skips the SST read, default ~10 bits/key ≈ 1% FP — github.com/facebook/rocksdb.
2. Broder & Mitzenmacher, "Network Applications of Bloom Filters: A Survey", Internet Mathematics 1(4), 2004 — FP rate (1−e−kn/m)k rises with n; counting Bloom filters for deletion; the textbook formula is an optimum approximation (cf. NIST, Christensen et al.) — harvard.edu, nist.gov.
Was this lesson helpful? Thanks — noted.

These notes reflect my current understanding and are updated as I learn, build, and discover better explanations.