Every caching lesson so far has assumed the thing being cached exists. A surprising amount of real load comes from requests for things that DON'T — and caching that specific fact turns out to need its own deliberate design, not just "cache everything, including misses."
3 min read
def get_user(user_id):
cached = cache.get(f"user:{user_id}")
if cached is not None:
return cached
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
if user is None:
return None # NOT cached — every lookup for this ID hits the database again
cache.set(f"user:{user_id}", user, ttl=300)
return userA lookup for a user ID that doesn't exist pays the full database round trip every single time, with no benefit from the cache at all — the cache only ever stores found results. For IDs that are genuinely random or don't exist (a scraper probing sequential IDs, a client retrying a request against an account that was since deleted, a typo'd ID hit repeatedly by a buggy client), this silently defeats the entire point of caching for exactly the requests that keep recurring.
def get_user(user_id):
cached = cache.get(f"user:{user_id}")
if cached is not None:
return None if cached == "NOT_FOUND" else cached
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
cache.set(f"user:{user_id}", user if user else "NOT_FOUND", ttl=60) # shorter TTL than a real hit
return userNegative caching stores a sentinel value representing "this was looked up and confirmed not to exist," so a repeated lookup for the same missing ID hits the cache instead of the database — the same benefit caching already provides for real data, extended to cover the absence of it.
If a client requests a resource that doesn't exist yet, gets a negative cache entry, and then the resource is created a moment later, that same client can keep seeing "not found" until the negative entry's TTL expires — even though the resource genuinely exists now. This is a materially worse user-facing bug than ordinary staleness on an existing record, because it looks like the create operation silently failed, not like data is merely a few seconds old.
Because a negative result going stale looks like a broken create flow (rather than merely outdated information), negative cache TTLs are almost always set much shorter than positive ones — seconds, not minutes — trading away some of the caching benefit specifically to bound how long this particular failure mode can last. The right TTL for a negative entry is a direct trade-off between "how much database load are repeated not-found lookups actually creating" and "how long is it acceptable for a just-created resource to appear not to exist yet."
For an endpoint where "not found" almost always means a genuine typo or a one-off invalid request (never followed by that same ID suddenly becoming valid), negative caching is close to pure upside. For an endpoint in a create-then-immediately-read flow — a user creates an account and the very next request checks it — negative caching that create-then-read pattern can actively introduce a bug that didn't exist before caching was added at all, and deserves either a much shorter TTL or exclusion from negative caching entirely.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What is negative caching?
2. What is the real danger negative caching introduces?
3. Why do negative cache entries typically use a shorter TTL than positive ones?
Caching Strategies