Skip to main content
Cache Eviction & Stampedelesson 7 of 7 · 3 min read

Negative Caching

The answer nobody stores

Standard caching has a blind spot. It only remembers answers, and not found is not an answer it stores.

Trace a request for something that does not exist. Miss the cache, query the database, get nothing back, cache nothing. The next identical request repeats the entire trip.

Read the consequence: every lookup for a row that is not there is a guaranteed miss and a guaranteed database query, forever.

Treat it as background noise most of the time, deleted users and dead links. It becomes an attack surface the moment your request patterns concentrate on missing data.

Picture the concentrated versions. A scraper walking sequential identifiers. A mobile bug requesting a deleted resource in a retry loop. Or somebody deliberately requesting random keys that do not exist.

All three bypass your cache completely and hit your database at full rate. The deliberate version has a name in the literature, and the accidental version causes just as many incidents.

Cache the absence

Cache the absence, and that is the fix. When your database returns no row, store an empty marker under that key with a short lifetime, and 30 to 60 seconds is typical.

Keep that lifetime short, because absence changes. Somebody who signs up a second after you cached their nonexistence should not be invisible for an hour.

Remember to delete the marker in your creation path, the same discipline as any other invalidation.

Reach for a bloom filter when your keyspace is huge enough that even markers would bloat memory. It answers definitely not in the database in constant space, letting you reject unknown keys before any lookup at all.

Weigh it honestly. It allows rare false positives that harmlessly fall through to a real query, and it needs rebuilding as your data grows, so most teams start with markers and reach for the filter only when the numbers demand it.

DNS formalised all of this decades ago, caching negative answers with their own lifetimes. The internet learned early that asking the same server for the same nonexistent name millions of times is a waste everybody pays for.

the shape of it
RequestCacheDatabaseEmpty markershort TTL1. get key2. miss3. no row4. store absence
step 1 of 4
Storing the absence stops every lookup for a missing row reaching the database.

Worked example

Ines runs the profile API at a social app. One Friday, database load doubles with no traffic increase, and the slow query log shows millions of lookups for user IDs that do not exist. A partner's integration had shipped a bug: on any API error, it retried the same deleted user's profile in a tight loop, and it was deployed to 200,000 devices. Every request was a cache miss by definition, so Redis, sitting at a 97 percent hit rate overall, was helping not at all. Ines ships negative caching in an afternoon: a NOT_FOUND sentinel with a 60-second TTL whenever the database returns no row, plus a sentinel delete in the signup path. Database queries from the retry storm drop by 99.8 percent within minutes, and the partner fixes their loop the following week without any further pressure on her database.

Cache Eviction & Stampede: wrapping up

In the real world

  • 01Redis exposes eviction as maxmemory-policy (allkeys-lru, allkeys-lfu, volatile-ttl, noeviction), using sampled approximations of LRU and LFU rather than exact bookkeeping.
  • 02Facebook's memcache system uses leases, small per-key tokens that both prevent stampeding refills and stop stale values from being written back after an invalidation.
  • 03The HTTP Cache-Control extension stale-while-revalidate (RFC 5861), supported by CDNs like Fastly and Cloudflare, serves the expired copy while one background fetch refreshes it.
  • 04DNS negative caching is standardized in RFC 2308: resolvers cache NXDOMAIN answers with a TTL derived from the zone's SOA record, protecting authoritative servers from repeat lookups.
  • 05The XFetch algorithm (Vattani, Chierichetti, Lowenstein, 2015) implements probabilistic early expiry, weighting refresh probability by recompute cost to prevent stampedes without locks.

Questions people ask

Which eviction policy should I pick if I am not sure?

LRU. It matches the access patterns of most web workloads and it is the default in Redis and most caching libraries. Move to LFU only when you can show a stable set of very popular keys being evicted by bursts of one-time reads, and consider excluding scan-type traffic from the cache before switching policies.

Is adding TTL jitter really necessary if my keys are populated at different times?

Yes, because synchronized population sneaks in through deploys, cache flushes, warming scripts, and bulk jobs, all of which set thousands of keys within the same second. Jitter costs one line and a few percent of cache freshness, and it removes an entire class of outage that otherwise fires days after its cause.

How is a cache stampede different from a hot key problem?

A hot key is heavy read traffic on one key, which the cache handles fine while the entry is present; the pressure is on the cache node. A stampede is what happens at the moment that entry disappears, when the read traffic converts into duplicate database queries. Hot keys are a capacity concern; stampedes are a transition concern, and the defenses target the transition.

Quick review

LRU (Least Recently Used):
evict the item accessed longest ago. Best for temporal locality. Default in Redis
LFU (Least Frequently Used):
evict the item accessed fewest times. Better for skewed access patterns
TTL (Time-To-Live):
automatic expiry after fixed duration. Ensures freshness; causes periodic miss spikes
FIFO:
evict oldest-inserted item. Simple; ignores access recency
Cache stampede (thundering herd):
popular key expires → thousands of requests hit DB simultaneously
Stampede fixes:
mutex lock (one request fetches, others wait), probabilistic early expiry (expire before TTL with small probability), background refresh (async refresh before expiry)
Cache warming:
pre-populate cache after restart to avoid cold-start stampede
the trade-off

The eviction policy is a bet on the access pattern, and a wrong bet quietly halves the hit rate. LFU keeps counters LRU does not need, short TTLs buy freshness and pay in periodic miss spikes, and stampede protection adds work to every hit in order to protect the misses.

in the room

LRU for general caching. TTL always set to bound staleness. Add stampede protection for high-traffic keys.