Two things. First, memory: you are storing entries that carry no data. Second, it does not stop keys that keep changing: an attacker can send countless different keys that do not exist, and each one still reaches the database once before its empty marker is cached, so the cache fills up with useless entries. Empty markers suit a small, fixed set of missing keys. Against a deliberate flood, pair them with a Bloom filter and keep the marker's TTL short.
Stop 4 · Cache pitfalls & consistency
A cache is fast because it keeps a second copy of your data. This stop covers when that copy stops matching the database, what happens when it suddenly fails, and how to defend against both.
Add a cache and you now have a second copy to keep correct
A cache turns a slow query into one memory read. The price is a second copy of the data that lives outside the . That copy can fail in four ways: it never gets built ( always misses), one hot entry expires and many requests rebuild it at the same moment, a large batch of entries disappears together, or it holds a value the database has already changed. These four problems — penetration, breakdown, avalanche, and cache consistency — come up in almost every backend interview that touches Redis, and they are a common cause of production incidents. Open each tab to see what the failure looks like and how to prevent it.
The requested key exists nowhere, so the cache can never hold it
cache penetration · a key that exists nowhere
Requests keep asking for data that does not exist — a user id that was never created, for example. The cache does not have it (a ), so the request falls back to the , which does not have it either. There is no value to write back, so the cache for that key stays empty forever. Every one of these requests passes through the cache and reaches the database, and the cache cannot stop a single one of them. This is rare in normal traffic. It becomes serious when someone sends a large volume of made-up keys on purpose: the cache absorbs none of them, and the full request rate lands on the database.
Even when the database returns nothing, write an empty marker into the cache — an empty string or a sentinel value — with a short . The next request for the same key reads the marker and stops there, so the database is not queried again.
Load every key that really exists into a Bloom filter and ask it first. If it answers "not present", the key definitely does not exist, so you can reject the request without touching the cache or the database.
Reject obviously invalid parameters at the API edge — out-of-range ids, malformed input. Then add authentication and rate limiting so one client cannot send a flood of made-up keys.
It is a bit array plus a few hash functions, so it is small and fast. Its useful property is one-sided error: it can report "maybe present" for a key that is absent (a small false-positive rate), but it will never report "absent" for a key that is present. In short: "not present" is certain, "present" is only probable. So it stops the large majority of penetration traffic and never rejects real data. The cost is that a few made-up keys still get through, which the cache and database handle as ordinary requests. Downside: a standard Bloom filter only supports adding, not removing — use a counting Bloom filter or rebuild it periodically.
When you insert the record, delete or overwrite the empty marker in the same write path. Otherwise reads keep returning the stale "absent" marker and never see the new row. This is the second reason to keep the marker's TTL short: if the invalidation is missed, the wrong answer still has a bounded lifetime.