What is a cache miss?
A cache miss occurs when an application looks for data in a cache and the data is not there, forcing the application to fetch it from a slower source such as a database, an origin server, or main memory. It is the opposite of a cache hit, where the requested data is found and returned immediately.
Cache misses are unavoidable, but their frequency is one of the most direct levers you have over application latency. This page covers the four types of cache misses, how to measure them with the cache hit ratio, what they cost, and how to reduce them — including the techniques that matter specifically in distributed caches built on Valkey and Redis.
Cache hit vs. cache miss
Every cache lookup ends in one of two outcomes. The difference between them is the entire reason caching exists. What happens on a miss depends on the caching pattern in use: under cache-aside the application loads from the database and populates the cache itself, while under read-through the cache does it.
| Cache hit | Cache miss | |
|---|---|---|
| What happens | Data is found in the cache and returned directly. | Data is absent. The application queries the underlying source, then usually writes the result into the cache. |
| Typical latency | Microseconds for an in-process cache; sub-millisecond for a remote cache on the same network. | Milliseconds to hundreds of milliseconds, depending on the source. |
| Load on the backing store | None. | One read, plus a cache write. |
A note on terminology: the Chrome browser error ERR_CACHE_MISS is a separate thing entirely. It usually means a browser is being asked to re-submit form data it no longer holds, and it has nothing to do with the caching concept described here.
The four types of cache misses
Cache misses are conventionally classified into four categories. This taxonomy comes from CPU cache architecture, where it is precise, and it carries over to application and distributed caches with some translation — noted below where the two differ.
1. Compulsory miss (cold miss)
Also called a cold start or first-reference miss. The data is being requested for the very first time, so it cannot possibly be in the cache. Compulsory misses are unavoidable by definition — you cannot cache something before it has ever been asked for.
You can only shift when they happen, not whether they happen. Prefetching and cache warming move compulsory misses off the critical path by loading likely-needed data before real traffic arrives. In a distributed cache, this is the class of miss that dominates immediately after a deployment, a failover, or a cache flush.
2. Capacity miss
The working set is larger than the cache. Data was present, got evicted to make room for something else, and is now being requested again. Capacity misses are the clearest signal that a cache is undersized relative to the workload — or that too much low-value data is being cached.
The fixes are to increase cache size, reduce what you store, or choose an eviction policy that better matches the access pattern.
3. Conflict miss (collision miss)
There is enough total capacity, but the cache's placement rules force an eviction anyway. In a CPU cache this happens because several memory addresses map to the same set in a direct-mapped or set-associative cache, and increasing associativity is the standard remedy.
Conflict misses in this strict sense are a hardware-level phenomenon. Distributed caches such as Valkey and Redis are effectively fully associative — any key can live in any slot — so they do not suffer classical conflict misses. The closest equivalent is uneven key distribution across a cluster, where a poorly chosen key scheme concentrates hot data on one shard while other nodes sit idle.
4. Coherence miss (invalidation miss)
The entry was present and valid, then something else changed the underlying data and the entry was invalidated. The next read misses.
This is the type that matters most in distributed systems, and the one where the CPU analogy is most useful. Whenever a cache is replicated across many application nodes, a write on one node has to invalidate copies held everywhere else. That invalidation traffic is the price of consistency, and tuning it is a real design decision — see cache invalidation for the strategies involved.
Cache hit ratio and miss rate
The cache hit ratio (also called the cache hit rate — the terms are used interchangeably) measures how often lookups succeed:
hit ratio = hits / (hits + misses)
miss rate = 1 - hit ratio
For example, a cache serving 9,500 hits and 500 misses over an hour has a hit ratio of 9,500 / 10,000 = 0.95, or 95%. Its miss rate is 5%.
What counts as a good cache hit ratio? There is no universal number, and treating one as a target is a common mistake. Read-heavy application caches are often cited as healthy in the 80–95% range, but the figure only means something in context:
- A 99% hit ratio achieved by caching a handful of keys that were never expensive to fetch delivers almost nothing.
- A 70% hit ratio on queries that each cost 400ms may be saving an enormous amount of database load.
- A ratio that is too high can indicate you are caching data that never changes and could be held closer to the application, or that your TTLs are too long and you are serving stale data.
Track hit ratio alongside miss penalty and backend load. A ratio on its own is not a performance metric.
What a cache miss actually costs
The time difference between a cache lookup and a fetch from the underlying source is the miss penalty. It is what makes misses expensive, and in a layered architecture it is not a single number.
A typical Java application backed by Valkey or Redis has at least two cache layers, and a miss in each costs something different:
- Local (near) cache miss — the entry isn't in the application's in-process cache. Cost: one network round trip to the remote cache. Small, but it happens on every miss and adds up under load.
- Remote cache miss — the entry isn't in Valkey or Redis either. Cost: a full database query, plus deserialization, plus the write back into both cache layers.
These differ by orders of magnitude, which is why a single hit-ratio number across the whole stack can hide the problem. Measure each layer separately.
How to reduce cache misses
Some misses are structural and some are self-inflicted. In rough order of how often they pay off:
- Size the cache to the working set. The most direct fix for capacity misses. Measure the working set rather than guessing.
- Match the eviction policy to the access pattern. LRU discards least-recently-used entries and suits recency-driven workloads. LFU discards least-frequently-used entries and suits workloads with a stable hot set. MRU, FIFO and LIFO exist for narrower cases. Test rather than assume.
- Tune TTLs deliberately. Aggressive expiry produces avoidable misses; long expiry produces stale reads. This is a trade-off to be set per data type, not globally.
- Warm the cache. Preload known-hot data on startup and after failover so that real traffic doesn't absorb the compulsory misses. A read-through cache can do this from its loader, since the component that fills entries on a miss can also populate them ahead of time.
- Design keys carefully. Overly granular keys fragment the cache and lower the hit ratio; overly coarse keys force large invalidations. In a cluster, key structure also determines whether load spreads evenly across shards.
Cache stampede
A single miss on a hot key is cheap. A hot key expiring under concurrent load is not: every request that wanted it misses at the same instant, and all of them query the database together. Because the effect turns one miss into thousands, it is worth treating as its own failure mode — see the thundering herd problem for what triggers it and the full set of mitigations, from distributed locking and TTL jitter to probabilistic early recomputation.
Negative caching
If your application repeatedly asks for keys that do not exist, every one of those lookups is a miss that reaches the database and returns nothing. Caching the absence of a value — negative caching — turns that repeated work into a single lookup.
Negative caching only helps once a key has been requested at least once. When the pool of nonexistent keys is effectively unbounded — the pattern usually called cache penetration — a Bloom filter holding every valid key is the stronger defence, because it rejects lookups for keys that were never valid before they reach the cache or the database at all.
Redisson exposes this directly on local caches through the storeCacheMiss option, which is disabled by default:
LocalCachedMapOptions options = LocalCachedMapOptions.defaults()
// store a cache miss in the local cache instead of
// re-querying for a key that is known to be absent
.storeCacheMiss(true)
.evictionPolicy(EvictionPolicy.LRU)
.cacheSize(10_000);
RLocalCachedMap<String, Integer> map = redisson.getLocalCachedMap("stock", options);
Use it where missing keys are common and cheap to represent — lookups by user-supplied identifiers, for example. Be aware that it consumes cache space to record absences, so it is not a default worth turning on everywhere.
Cache misses in Valkey and Redis
Redis and Valkey are in-memory data structure stores widely used to implement NoSQL key-value databases, caches, and message brokers. Both track hits and misses for you.
Running INFO stats returns two counters that let you compute the live hit ratio for the whole instance:
keyspace_hits:9500
keyspace_misses:500
The hit ratio is keyspace_hits / (keyspace_hits + keyspace_misses). These are cumulative since the server started, so sample them at intervals and compare deltas rather than reading the raw totals — a long-running instance's lifetime average will hide a problem that started this morning.
If misses are driven by eviction, the cache is too small for its working set. The maxmemory directive in redis.conf controls the memory limit:
maxmemory 100mb
Pair it with an appropriate maxmemory-policy so that the server evicts along the dimension you care about rather than rejecting writes outright.
Reducing misses in Java with Redisson
Valkey and Redis are not usable from Java out of the box, so Java applications reach them through a client. Redisson exposes them as familiar Java objects and collections, and implements client-side caching through the RLocalCachedMap interface — a near cache that answers reads in-process and avoids the network round trip entirely.
LocalCachedMapOptions options = LocalCachedMapOptions.defaults()
.evictionPolicy(EvictionPolicy.LRU)
.cacheSize(10_000)
.timeToLive(Duration.ofMinutes(10))
// invalidate this entry on every other node when it changes
.syncStrategy(SyncStrategy.INVALIDATE);
RLocalCachedMap<String, Integer> map = redisson.getLocalCachedMap("stock", options);
Integer previous = map.put("sku-123", 1);
Integer current = map.putIfAbsent("sku-323", 2);
Integer removed = map.remove("sku-123");
// use fast* methods when the previous value isn't needed
map.fastPut("sku-a", 1);
map.fastPutIfAbsent("sku-d", 32);
map.fastRemove("sku-b");
RFuture<Integer> putFuture = map.putAsync("sku-321", 5);
RFuture<Boolean> fastPutFuture = map.fastPutAsync("sku-321", 5);
The syncStrategy setting is where the coherence-miss trade-off becomes a concrete choice. INVALIDATE drops the entry on every other node when it changes, causing a miss on their next read. UPDATE pushes the new value out instead, avoiding that miss at the cost of more traffic. NONE avoids both and accepts staleness. See the cache API documentation for the full set of options, and distributed caching for the wider architectural picture.
Frequently asked questions
What is the difference between a cache hit and a cache miss?
A cache hit means the requested data was found in the cache and returned immediately. A cache miss means it was absent, so the application had to fetch it from a slower source such as a database or main memory, then typically store it in the cache for next time.
What are the four types of cache misses?
Compulsory (cold) misses, on data requested for the first time; capacity misses, when the working set exceeds the cache size; conflict misses, caused by placement restrictions in set-associative caches; and coherence misses, where an entry is invalidated because the underlying data changed elsewhere.
What is a good cache hit ratio?
It depends entirely on the workload. Read-heavy application caches are often healthy in the 80–95% range, but the number is only meaningful next to the cost of a miss. A lower ratio on expensive queries can be far more valuable than a high ratio on cheap ones.
How do you calculate cache hit ratio?
Divide the number of cache hits by the total number of lookups: hits / (hits + misses). The miss rate is 1 - hit ratio. In Valkey and Redis, the INFO stats command supplies both counters as keyspace_hits and keyspace_misses.
What causes a cache miss?
Four things: the data has never been requested before, it was evicted because the cache ran out of room, it was displaced by the cache's placement rules, or it was invalidated because the source data changed. Expired TTLs fall under eviction.
How do you reduce cache misses in Redis or Valkey?
Size the cache to the working set, choose an eviction policy that matches the access pattern, tune TTLs per data type, warm the cache after restarts and failovers, and add a near cache to eliminate network round trips. Guard hot keys against cache stampedes with a distributed lock.