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.
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.