Redis Memory Management and Eviction
The maxmemory limit, eviction policies, how approximated LRU/LFU behave, and the memory overhead behind Redis memory operations
Contents
When Redis reaches its memory limit it either picks keys to discard or refuses writes. The eviction policy decides which.
The moment memory hits the ceiling
Redis keeps data in memory rather than on disk. That makes it fast, but it also means there is nowhere left to put anything once memory fills. maxmemory sets the limit, and the eviction policy decides what to discard when the limit is crossed. Those two are therefore the control points that matter most in operations.
maxmemory sets the maximum memory Redis may use for data. Past that value Redis evicts keys or rejects writes. One trap is that replication buffers and AOF (append only file, the write log) buffers are often not counted against this limit. Actual process memory can therefore exceed maxmemory.
maxmemory 8gb
maxmemory-policy allkeys-lru
maxmemory-samples 10Those three lines are the skeleton of memory operations. maxmemory sets the limit, maxmemory-policy sets the discard criterion, and maxmemory-samples sets how accurate that criterion is.
How eviction proceeds
Every time a write command increases memory, Redis checks whether the limit has been crossed. If it has, Redis frees as many keys as needed, and this check repeats on every write.
The important part is that the last arrow returns to the check. Redis removes one key at a time and repeats until memory drops below the limit. Running right up against the limit therefore means eviction interleaves with every write and latency can rise.
The eight eviction policies
Policies split along two axes: whether the candidates are all keys (allkeys-) or only keys with an expiry (volatile-), and what criterion decides the discard. Expiry here refers to the TTL (time to live) set per key.
| Policy | Candidates | Recommended for | Behavior |
|---|---|---|---|
allkeys-lru | All keys | General caching (recommended default) | Discards least recently used keys first |
allkeys-lfu | All keys | Access concentrated on a few hot keys | Discards least frequently accessed keys first |
volatile-lru | Keys with a TTL | Mixed short-lived and long-lived data | Protects keys without a TTL |
volatile-lfu | Keys with a TTL | Hot data that carries a TTL | Frequency-based eviction |
volatile-ttl | Keys with a TTL | Explicit expiry times | Nearest expiry goes first |
volatile-random | Keys with a TTL | Rarely used | Random selection |
allkeys-random | All keys | Rarely used | Random selection |
noeviction | No eviction | Not recommended | Rejects writes past the limit |
Two things are worth remembering from the table. allkeys-lru is the default for a general-purpose cache, and volatile-* considers only keys with a TTL. If most keys have no TTL, volatile-* has no candidates to discard and effectively behaves like noeviction.
How approximated LRU and LFU work
LRU (least recently used) discards the key unused for the longest time. Sorting every key to find the true minimum is expensive, so Redis uses an approximated LRU rather than an exact one.
Approximated LRU samples a handful of keys instead of scanning all of them and discards the oldest among the sample. maxmemory-samples sets that sample size.
maxmemory-samples 5 # default. fast but less accurate
maxmemory-samples 10 # recommended. better accuracy, small CPU cost
maxmemory-samples 20 # high accuracy, higher CPU costA larger sample raises the chance of picking a genuinely old key but costs more CPU. Raising the default of 5 to 10 is a reasonable production choice: accuracy improves noticeably while the added cost stays small.
LFU (least frequently used) uses a different criterion. It discards the least frequently accessed key rather than the least recently used one. Supported since Redis 4.0, it beats LRU under a Pareto pattern where access concentrates on a subset of keys.
key1: accessed 1000 times
key2: accessed 5 times
key3: accessed 8 times
LRU -> evicts by least recent use (access count is irrelevant)
LFU -> evicts key2 and key3 first, keeps key1 to the endLFU stores frequency in an 8-bit counter per key. That counter is a Morris counter, a probabilistic logarithmic scheme, so it increments more slowly as access counts grow. One byte therefore covers a wide frequency range and saves memory.
Memory usage exceeds the data
Actual memory usage does not equal the size of the stored data. Allocator fragmentation and various buffers add to it. The window into that difference is INFO memory.
redis-cli INFO memoryused_memory: 10737418240 # memory used for data (about 10GB)
used_memory_rss: 11811160064 # memory the OS gave the process
mem_fragmentation_ratio: 1.10 # RSS / used. near 1.0 is normal
mem_allocator: jemalloc-5.3.0 # memory allocator
mem_not_counted_for_evict: 123456 # amount excluded from eviction (replication buffers and so on)
mem_replication_backlog: 2097152 # replication backlog (2MB)
mem_aof_buffer: 0 # AOF buffermem_fragmentation_ratio is the memory the OS handed out (RSS) divided by the data memory. Near 1.0 is normal, and above 1.5 is a sign of heavy fragmentation. Fragmentation comes from allocating and freeing memory in small pieces, which scatters free gaps.
When fragmentation is severe, active defrag compacts the pieces. It relocates memory incrementally while the server is running.
activedefrag yes
active-defrag-ignore-bytes 10mb # ignore fragmentation under 10MB
active-defrag-threshold-percentage 10 # run at 10% fragmentation or abovemem_not_counted_for_evict deserves attention alongside it. It represents memory excluded from eviction, such as replication and AOF buffers. When this value is large, the process as a whole uses more memory even while used_memory stays under the limit.
Reducing memory usage
Using less comes before discarding. The most effective step is keeping small collections in a compact encoding. Small hashes and lists are stored automatically in a dense representation called listpack (Redis 7.0 and later) or ziplist (6.2 and earlier).
hash-max-listpack-entries 512 # stays a listpack at 512 fields or fewer
hash-max-listpack-value 64 # stays a listpack when values are 64 bytes or less
list-max-listpack-size 8192 # lists under 8KB stay listpacksPast the threshold the structure converts to a regular hash table and memory grows. Grouping related fields into a single hash therefore beats scattering them across individual string keys. This approach can cut memory substantially (roughly 50 to 90%, estimated, depending on the data).
Monitoring serves the same goal. When redis_evicted_keys_total rises above zero, eviction is actually happening. Alert when memory utilization passes 80% or the fragmentation ratio passes 1.5, then review whether to adjust the limit or run defrag.
Choosing a policy
Most caches are fine starting with allkeys-lru. It works well under power-law or Pareto access patterns where a subset of keys takes most of the traffic, and it is the widely recommended production default. When hot keys are more pronounced and need to survive longer, switch to allkeys-lfu so that infrequent keys go first.
Pick volatile-* only when you want to discard keys with a TTL exclusively. That applies when cache and durable data share one instance and keys without a TTL must never be lost. Watch the ratio of keys carrying a TTL, though: too few candidates and writes can be blocked because there is nothing to discard.
noeviction suits queues or job stores where data must not be dropped. Past the limit it rejects writes instead of deleting keys, so the application has to be ready to handle that error.
Summary
Redis memory operations reduce to drawing a limit with maxmemory and deciding through the eviction policy what to discard once that limit is crossed. A policy is a combination of candidate set and criterion, with allkeys-lru as the starting point for a general cache and allkeys-lfu for preserving hot keys. Approximated LRU trades accuracy against CPU through the maxmemory-samples sample size, and LFU tracks frequency cheaply with an 8-bit Morris counter. Actual memory exceeds the data, so watch fragmentation and the replication and AOF buffers through INFO memory, and cut usage with listpack encoding and active defrag. Checking the TTL ratio and the write-rejection behavior of noeviction is what keeps you from getting unexpectedly blocked near the limit.