skip to content

In a cache-aside deployment, an attacker (or a buggy client) repeatedly requests keys that are known not to exist in the database, e.g. random or sequential IDs. Since a cache-aside cache only stores things that were successfully loaded, why does this traffic pattern hit the database on every single request, and what technique addresses it?

level: principalimportance: nice to knowfreq 30%

answer

  1. cache penetration = misses never get cached
  2. negative caching / tombstone value
  3. shorter TTL for negative entries
  4. bloom filter as outer guard
  5. enumeration attack vector

basics

~20 s

The cache only remembers things it successfully found. If a key never exists, the database always says 'not found' and nothing ever gets saved to the cache, so every repeat request goes straight to the database again. The fix is to also cache the 'not found' answer for a little while.

solid answer

~50 s

This is called cache penetration: cache-aside only populates on a successful load, so a lookup that always returns 'not found' never produces anything to cache, and every repeated request for that same nonexistent key bypasses the cache entirely and reaches the database, exactly as if there were no cache. It's a cheap attack vector because it needs no valid data, just a stream of plausible nonexistent keys. The standard fix is negative caching: when a database lookup returns nothing, cache a small sentinel/tombstone value under that key, usually with a shorter TTL than normal entries. The next lookup hits the cache and returns 'not found' without touching the database. For very large or unbounded key spaces, this is often paired with a Bloom filter that rejects obviously-invalid keys before they reach the cache or database at all.

go deeper

for a junior

Not generally expected to know this on their own; credit for reasoning through it live if prompted.

for a middle

Should be able to reason that a naive cache never remembers a miss, so repeated identical misses always reach the database.

for a senior

Should independently propose negative caching (caching the 'not found' result) as the fix and note it needs a shorter TTL than positive entries.

for a principal

Should connect this to the broader attack surface (enumeration, cache exhaustion), and know a complementary technique like a Bloom filter for unbounded key spaces.

## Why a miss never gets cached Cache-aside, by construction, only ever populates the cache after a successful load from the database — the miss-then-populate flow described by the core pattern assumes the database lookup returns a real value to cache. When a client repeatedly requests a key that does not and will never exist in the database (a bogus user ID, a guessed order number, a randomly generated key from an attacker probing the system), every single request for that key follows the same path: 1. cache miss, 2. database query, 3. database returns "not found," 4. and — critically — nothing gets written into the cache, because there's no value to cache. The next request for that exact same nonexistent key repeats the identical cycle from scratch. This is called **cache penetration**: traffic that structurally cannot benefit from caching at all under a naive cache-aside implementation, because the cache only ever remembers hits, never misses. ## Why it matters This matters because it defeats the entire purpose of putting a cache in front of the database for that traffic pattern — the cache adds latency and complexity but provides zero load reduction for these requests, and if the request rate is high it can drive real, sustained load straight through to the database exactly as if there were no cache at all. That rate can be: - **organic**, like a broken client retrying a typo'd ID in a loop, or - **malicious**, like an attacker enumerating IDs specifically because they've noticed the cache doesn't shield the database from misses. It's a favorite vector in security write-ups precisely because it's cheap for an attacker to generate: no valid credentials or complex payloads needed, just a stream of plausible-looking but nonexistent keys. ## The standard fix — negative caching The standard fix is **negative caching**: when the database lookup for a miss comes back empty, the application caches that fact too — writing a small sentinel/tombstone value (e.g. a literal marker like `__NULL__`, or a JSON `{"exists": false}`) under the same key, usually with a shorter TTL than a normal positive entry. The next request for that same nonexistent key now hits the cache, sees the tombstone, and can return "not found" immediately without ever touching the database — converting what used to be guaranteed database traffic into an ordinary cache hit, just one that represents absence rather than presence. ## What negative caching costs Negative caching isn't free. - **Why the shorter TTL.** It uses a shorter TTL specifically because a "not found" result is more likely to flip (a record gets created later) than a "found" result is to change identity, and a stale negative entry means a newly-created record appears to not exist for up to that TTL window — annoying for a user who just signed up and immediately can't be found by their own new ID. - **It opens a new, smaller attack surface of its own.** If key space is unbounded (e.g. arbitrary strings), an attacker can still exhaust cache memory by generating enough distinct nonexistent keys to fill the cache with tombstones, which is why negative-cache TTLs are kept short and sometimes rate-limited. ## The outer guard — a Bloom filter A complementary technique used alongside or instead of negative caching, especially when the key space is huge and enumeration is a concern, is a **Bloom filter**: a compact, probabilistic structure built from all known-valid keys that can answer "definitely not present" or "possibly present" before ever touching the cache or database — a lookup that gets a definite "not present" from the Bloom filter is rejected immediately with no cache or database round trip at all, while only genuinely plausible keys proceed to the normal cache-aside path. This exact combination — cache-aside plus negative caching plus a Bloom filter as the outer guard — comes up repeatedly in large-scale system design discussions for services like URL shorteners and content platforms that are directly exposed to enumeration attempts, because the cost of a single unguarded miss-path query is cheap, but the cost of a sustained flood of them is not.

  • Why does a negative cache entry typically use a shorter TTL than a normal positive cache entry?
    Because 'not found' is more likely to become stale in a meaningful way than 'found' — a record can be created moments after being queried as missing, and a stale negative entry would then incorrectly hide a real, newly-existing record from callers for the rest of that TTL. A shorter TTL bounds how long that specific kind of wrongness can persist.
  • How does a Bloom filter help beyond what negative caching alone provides?
    Negative caching still requires at least one real database miss per distinct nonexistent key before it starts helping; a Bloom filter can reject a request for a key it knows was never valid before touching the cache or database at all, which matters when an attacker is generating a huge number of distinct nonexistent keys rather than repeating the same one.
  • What's a realistic downside of negative caching that a team should watch for?
    If the key space is effectively unbounded, an attacker (or a bug) generating many distinct nonexistent keys fills the cache with tombstone entries, consuming cache memory that would otherwise hold useful, frequently-accessed positive entries — so negative-cache entries usually need a short TTL and, for very large key spaces, a separate guard like a Bloom filter.

Like a receptionist who checks a full file cabinet every single time someone asks for a person who has never worked there, because nobody ever wrote down 'this person doesn't exist' anywhere the receptionist could check first.

saying these in an interview costs you the question

  • Thinks a cache miss for a nonexistent key gets cached the same way a hit does under plain cache-aside
  • Proposes fixing this purely by increasing TTL on positive entries
  • Doesn't recognize the security/enumeration angle of this traffic pattern
  • Suggests negative caching with no shorter TTL, ignoring the risk of hiding newly-created records

context