Skip to content
About
Main content

Negative caching and Bloom filters

Why is caching absence so cheap yet rare, and how does a Bloom filter reverse the trade?

Negative caching: the cheapest entry on the dearest path

The absent-key read is the most expensive read path a system has. A lookup that misses must rule out every possibility before concluding that nothing is there: every LSM level (an LSM-tree is a write-optimized storage structure with several sorted files on disk, all of which a read may have to check), the full index, possibly a whole partition scan, and in a distributed store possibly a scatter to every with a wait on the slowest. Meanwhile the entry costs nothing to create (you learned the absence for free while paying for the miss) and occupies almost no space.

So S is maximal and P is about 0: negative caching passes the more easily than any other mechanism here, at almost any reuse rate. It is also the only derived with a perfectly known, single-key dependency set, so exact invalidation is trivially available: inserting key k invalidates exactly the negative entry for k and nothing else. is one, and you know which key.

Given all that, it should be everywhere. Two things stop it, and neither is about .

Obstacle 1: the key space is unbounded

Existing keys are bounded by the data. Non-existing keys are bounded only by the key's type. A client retry loop, a scanner, a misconfigured integration or an attacker can generate negative entries without limit, and every one evicts a positive entry that was earning its place. In a single shared this is fatal. The mitigation is structural, not a tuning knob: negatives need their own capacity partition with its own cap, so the unbounded population cannot compete with the bounded one. This is the first hard argument here for partitioned capacity, and Making it fit finds others.

Obstacle 2: its staleness has the worst user experience for the shortest-lived entries

A negative entry is invalidated by a create, which is exactly the write whose author most expects . ("I just registered and it says I do not exist.") The technical fix is free, because the dependency is one known key. People fall back to short anyway for an organizational reason: the write path often does not know the exists. That is a coupling cost, not a technical limit, and it is worth naming as such.

The Bloom filter is the same mechanism with the trade reversed

A Bloom filter is a compact bit-array structure that answers "definitely absent" or "maybe present". It uses O(bits) per key, not O(entry). It is a complete negative cache: it never forgets an absence it was told about, but it has a false-positive budget (sometimes it says "maybe present" for a key that is absent). An LRU negative cache is an incomplete one: it may have evicted the absence, but it has no false positives. Each trades away exactly what the other keeps.

That duality tells you which one to use without benchmarking.

  • When the absent set is enormous and you can tolerate a fraction of wasted lookups, . This is why every LSM SSTable (a sorted file) carries a filter.
  • When the absent set is small and hot and a wasted lookup is unacceptable, take exactness and accept that you will sometimes forget.

The LSM read path runs both at once: per-table filter, block cache and row cache. That is the derivation ladder built into a single read.

In one sentence: the cheapest entry on the dearest path, with one known dependency key. Exact invalidation is therefore free; the key space is unbounded, so capacity must be partitioned; and a is the same thing with completeness and exactness swapped.