Negative Caching

Caching missing or empty results to prevent repeated wasted lookups, while avoiding subtle correctness problems when the underlying data changes.

Negative caching stores absence as a reusable answer. Instead of caching only successful reads, the system also caches “not found,” empty result sets, or other safe negative outcomes for a short period. This can dramatically reduce repeated waste when callers keep asking for something that genuinely does not exist or is temporarily absent.

The danger is that absence can change quickly. A user may be created moments after a previous lookup failed. A product may become available right after a cached empty answer was stored. That means negative caches usually need shorter TTLs and much more careful semantics than positive caches.

    sequenceDiagram
	    participant App
	    participant Cache
	    participant Store
	
	    App->>Cache: get(user:42)
	    Cache-->>App: negative hit (not found)
	    Note over App,Cache: repeated misses avoid origin load

Why It Matters

Negative caching is valuable because repeated misses can be surprisingly expensive. Attack traffic, buggy retries, repeated lookups for deleted resources, or polling for not-yet-created data can all hammer the origin pointlessly. A short negative cache can protect the origin and smooth behavior under load.

What Should And Should Not Be Negative-Cached

Good candidates include:

  • true “not found” results for stable identifiers
  • short-lived empty results where polling is excessive
  • upstream absence conditions that are safe to reuse briefly

Bad candidates often include:

  • transient infrastructure failures
  • authorization failures whose cause may change quickly
  • business states where creation or recovery is expected immediately

Caching a 404 is not the same as caching a timeout or a permission failure.

Example

This example stores a short-lived marker for a missing entity instead of repeatedly hitting the store.

 1type NegativeEntry = { kind: "not-found" };
 2
 3async function getUserOrNull(userId: string): Promise<User | null> {
 4  const key = `user:${userId}`;
 5  const cached = await cache.get(key);
 6
 7  if (cached) {
 8    const parsed = JSON.parse(cached) as User | NegativeEntry;
 9    return "kind" in parsed ? null : parsed;
10  }
11
12  const user = await userStore.find(userId);
13  if (!user) {
14    await cache.set(key, JSON.stringify({ kind: "not-found" }), 15);
15    return null;
16  }
17
18  await cache.set(key, JSON.stringify(user), 300);
19  return user;
20}

What to notice:

  • negative entries usually use shorter TTLs than positive ones
  • the cache explicitly distinguishes “not found” from a regular entity shape
  • absence is reused, but only for a bounded time

The Main Risk

The main risk is false absence. A negative cache entry may outlive the moment when the missing thing becomes available. That can produce confusing behavior:

  • recently created users appear missing
  • newly stocked products look unavailable
  • a recovered upstream dependency still appears empty

This is why negative caches should usually be conservative and event-aware when possible.

Common Mistakes

  • using the same TTL for negative and positive results
  • caching transient errors as if they were true absence
  • failing to invalidate negative entries when creation events happen
  • assuming negative caching is only an optimization and not a correctness trade-off

Design Review Question

Why should negative cache entries often have shorter TTLs than positive entries?

The stronger answer is that absence is frequently less stable than presence. The cost of a false negative can be high, so the system usually wants shorter reuse windows for “not found” answers than for established positive data.

Quiz Time

Loading quiz…
Revised on Thursday, April 23, 2026