What Is Cache Eviction?
Cache eviction is the process of removing existing entries from a cache to make room for new ones. Because every cache has a fixed capacity, something has to be discarded once that capacity is reached, and the rule that decides what gets discarded is called the eviction policy.
The choice matters more than it first appears. Two caches of identical size, holding identical data under an identical workload, can produce very different hit rates depending on which policy they use. Since cache memory is expensive, getting more value out of the same footprint is usually cheaper than buying more of it.
How Does Cache Eviction Work?
A cache is a bounded store sitting in front of something slower. When the application writes a new entry and the cache is already at its limit, it has three options: reject the write, grow without bound, or remove something. Growing without bound is not really an option, and rejecting writes is only appropriate where losing data silently would be worse than failing loudly. For most caches, that leaves eviction.
The mechanism is straightforward. On each write, the cache checks whether adding the entry would exceed its configured limit. If it would, the eviction policy selects a victim, that entry is removed, and the new one takes its place. The evicted entry is simply gone from the cache. The next request for it produces a cache miss, and the application falls back to the database or origin service.
The bookkeeping a policy needs is not free: knowing which entry was least recently used means updating an ordering structure on every read, which costs both memory and CPU. This is why most production caches approximate their policies rather than implementing them exactly.
Eviction and loading are two halves of the same cycle: an evicted entry has to come back somehow. With read-through caching the cache reloads it through its loader on the next request, so eviction stays invisible to the application.
It is worth separating eviction from cache invalidation, which is a different problem with a similar-sounding name. Eviction is driven by capacity and is concerned with space. Invalidation is driven by correctness and is concerned with staleness: it removes an entry because the underlying data changed and the cached copy is now wrong.
Cache Eviction vs. Cache Expiration
Eviction and expiration are frequently confused, and the distinction is worth getting right because most production caches use both at once.
Expiration is time-based. You attach a time to live (TTL) to an entry, and once that period elapses the entry is no longer served. Expiration is predictable: you decide up front how long the data stays valid, which makes it the right tool for data with a known freshness window, such as a session token or a rate-limit counter. One wrinkle: "expired" and "freed from memory" are not always the same moment. Redis and Valkey reclaim expired keys lazily, either when the key is next touched or when a background cycle happens to sample it, so an expired entry can occupy memory for some time after it stops being readable.
Eviction is capacity-based. It happens only when the cache reaches its configured limit, and it removes entries that have not expired and are otherwise perfectly valid. Eviction is non-deterministic from the application's point of view: you cannot predict which entry will be dropped or when, because it depends on the access pattern and how full the cache happens to be.
| Cache expiration (TTL) | Cache eviction | |
|---|---|---|
| Trigger | Time elapsed | Cache is full |
| Predictable? | Yes | No |
| Removes | Stale entries | Valid entries |
| Configured by | TTL or max idle time | Size limit plus a policy |
| Purpose | Bound how stale data can get | Bound how much memory is used |
In practice you want both. TTLs cap how far the cache can drift from the source of truth, while an eviction policy keeps total memory within budget when traffic spikes. Neither substitutes for the other: a cache with only TTLs can still exhaust memory, and a cache with only a size limit can serve stale data indefinitely.
Common Cache Eviction Policies
| Policy | What it removes | Best suited to |
|---|---|---|
| LRU (Least Recently Used) | The entry untouched for the longest time | Recency-driven workloads |
| LFU (Least Frequently Used) | The entry accessed the fewest times | Stable, heavily skewed hot sets |
| FIFO (First In, First Out) | The oldest entry, regardless of use | Simple pipelines where insertion order approximates usefulness |
| Random | An arbitrary entry | Very large caches where tracking overhead outweighs the benefit of precision |
| TTL-based | The entry closest to expiring | Caches where every entry already carries an expiry |
| No eviction | Nothing; writes fail instead | Stores where silently losing data is worse than rejecting a write |
The first two are the ones most caches actually offer, and choosing between them is a workload question rather than a correctness one: recency-driven traffic favours LRU, while a small hot set serving most requests favours LFU. LRU is the usual starting point — cheap to approximate, quick to adapt when the working set changes, though it degrades badly under sequential scans. Each has a characteristic weakness and there are newer policies that avoid both; see LRU cache and LFU cache for the full comparison, and Java LRU cache for how one is built in Java.
Cache Eviction in Redis and Valkey
Redis and Valkey handle eviction through two settings. maxmemory defines the memory ceiling and maxmemory-policy decides what happens when it is reached; neither LRU nor LFU is enabled by default. The available values differ between the two products, and both implementations are sampled approximations rather than exact orderings. For all of them compared side by side, the volatile-* trap and how to size maxmemory, see Redis eviction policy.
Cache Eviction in Java With Redisson
Redisson is a Java client for Redis and Valkey that provides more than 50 distributed objects and services. Among them are cache implementations that let you set an eviction policy per collection, rather than accepting one policy for the whole server.
RMapCache is a map that can be bounded by entry count, with the bound maintained by Redisson itself rather than by the server's maxmemory-policy:
RMapCache<String, SomeObject> map = redisson.getMapCache("anyMap");
// bound the map to 10,000 entries, evicting in LRU order
map.trySetMaxSize(10_000);
// or bound it using LFU order instead
map.trySetMaxSize(10_000, EvictionMode.LFU);
// expiration works alongside eviction, not instead of it:
// TTL of 60 seconds and a max idle time of 30 seconds
map.put("key", new SomeObject(), 60, TimeUnit.SECONDS, 30, TimeUnit.SECONDS);
Applications that read far more often than they write can add a local cache, which keeps hot entries inside the JVM and avoids a network round trip on every read. The local tier has its own independent eviction policy:
LocalCachedMapOptions<String, SomeObject> options =
LocalCachedMapOptions.<String, SomeObject>name("anyMap")
.evictionPolicy(EvictionPolicy.LFU) // LRU, LFU, SOFT, WEAK or NONE
.cacheSize(10_000)
.syncStrategy(SyncStrategy.INVALIDATE)
.timeToLive(Duration.ofMinutes(10))
.maxIdle(Duration.ofSeconds(30));
RLocalCachedMap<String, SomeObject> map = redisson.getLocalCachedMap(options);
Alongside LRU and LFU, the local cache offers two policies with no server-side equivalent. SOFT holds entries through soft references, so the garbage collector releases them when the JVM is running low on memory. WEAK uses weak references, so entries are collected as soon as nothing else refers to them, at the next garbage collection cycle rather than under memory pressure. NONE disables size-based eviction while leaving TTL and max idle time active.
The local cache can also be backed by Caffeine rather than Redisson's own implementation. Redisson PRO adds data partitioning for local cached maps, distributing entries across cluster shards so the cache scales horizontally rather than being bounded by a single node.
The same eviction settings are available through Redisson's Spring Cache, Hibernate and JCache integrations, so you can configure eviction without changing how your application code reads and writes.
For the broader picture of running a cache across multiple nodes, see distributed caching in Java with Valkey and Redis.