Skip to content

Eviction Policies

Verified on Redis 8.10.2 (redis:8.10.2-alpine), ioredis 6.0.0, Node 24, Apple-silicon Mac. The # cli/... transcript is replayed by the course harness; the eviction runs are an @run script with maxmemory set to 4 MB (an empty 8.10.2 server already uses about 2.4 MB, so 4 MB leaves room for roughly 17,000 small keys); the container OOM-kill test ran in my own throwaway containers with a 64 MB Docker limit. Reference: Key eviction.

maxmemory caps the memory Redis uses for data. The default is 0, meaning no limit. When a write would push memory over the cap, the maxmemory-policy decides what happens: evict something, or refuse the write. Both can be changed at runtime with CONFIG SET, and an unknown policy tells you the valid list, which is eleven long on 8.10.2:

cli/persistence-expiration-eviction-policies-1.redis
127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "0"
127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"
127.0.0.1:6379> CONFIG GET maxmemory-samples
1) "maxmemory-samples"
2) "5"
127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lru
OK
127.0.0.1:6379> CONFIG SET maxmemory 4mb
OK
127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "4194304"
127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lrm
OK
127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-nonsense
(error) ERR CONFIG SET failed (possibly related to argument 'maxmemory-policy') - argument(s) must be one of the following: volatile-lru, volatile-lfu, volatile-random, volatile-ttl, volatile-lrm, allkeys-lru, allkeys-lfu, allkeys-random, allkeys-lrm, noeviction
127.0.0.1:6379> CONFIG SET maxmemory 0
OK
127.0.0.1:6379> CONFIG SET maxmemory-policy noeviction
OK
config/persistence-expiration-eviction-policies-conf.conf
maxmemory 512mb
maxmemory-policy allkeys-lru
maxmemory-samples 5

maxmemory-samples 5 is the default: LRU and LFU are approximations that look at 5 random keys and evict the best candidate among them, not a global ordering. More samples means a closer match to true LRU for more CPU per eviction (docs). One accounting detail: with replication or persistence some buffers do not count toward the limit (mem_not_counted_for_evict in INFO memory), so leave headroom below the container limit.

PolicyBehaviour
noevictionRefuse writes that need memory (default); reads and DEL still work
allkeys-lruEvict the least recently used (read or written) key
allkeys-lrmEvict the least recently modified key (8.x): reads do not protect a key
allkeys-lfuEvict the least frequently used key
allkeys-randomEvict a random key
volatile-lru / volatile-lrm / volatile-lfu / volatile-randomThe same four, but only among keys with a TTL
volatile-ttlEvict the key with the shortest remaining TTL (only keys with a TTL)

The old version of this table listed eight policies; allkeys-lrm and volatile-lrm are accepted by 8.10.2 (see the transcript above) and are in the docs.

All of this comes from one script. It sets maxmemory to 4 MB and writes 100-byte values. Results are reported per pipeline entry (in ioredis a refused write is an error tuple, not a thrown exception):

src/persistence-expiration-eviction-policies-run.ts
// @run
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
const val = 'x'.repeat(100);
async function reset(policy: string) {
await redis.flushall();
await redis.config('SET', 'maxmemory', '0');
await redis.config('RESETSTAT');
await redis.config('SET', 'maxmemory-policy', policy);
await redis.config('SET', 'maxmemory', String(4 * 1024 * 1024)); // 4 MB: the empty server already uses ~2.4 MB
}
const evicted = async () => Number((await redis.info('stats')).match(/evicted_keys:(\d+)/)![1]);
// pipeline in batches; results are [error, value] tuples, so a refused write shows up as an error entry, not a throw
async function fill(prefix: string, count: number, ttl?: number, touch?: string) {
let written = 0;
let firstError: string | undefined;
for (let start = 0; start < count; start += 500) {
const p = redis.pipeline();
for (let i = start; i < Math.min(count, start + 500); i++) ttl ? p.set(`${prefix}${i}`, val, 'EX', ttl) : p.set(`${prefix}${i}`, val);
if (touch) p.get(touch); // a READ of one key per batch
for (const [err] of (await p.exec())!) err ? (firstError ??= err.message) : written++;
if (firstError) break;
}
return { written, firstError };
}
// 1) noeviction: reads and DEL still work, new writes fail
await reset('noeviction');
const full = await fill('k:', 100000);
console.log('noeviction:', full.written, 'writes ok, then:', full.firstError);
console.log(' GET still works:', (await redis.get('k:0')) === val, '| evicted_keys:', await evicted());
// 2) allkeys-*: 500 "hot" keys are READ constantly and never rewritten; 500 "cold" keys are equally old and never touched
async function survivors(prefix: string) {
let n = 0;
for (let i = 0; i < 500; i++) n += await redis.exists(`${prefix}${i}`);
return n;
}
// the LRU clock has a 1-second resolution, so the fill is spread over ~6 seconds: reads must happen at different seconds than the writes
for (const policy of ['allkeys-lru', 'allkeys-lrm', 'allkeys-lfu']) {
await reset(policy);
for (let i = 0; i < 500; i++) await redis.set(`hot:${i}`, val).then(() => redis.set(`cold:${i}`, val));
for (let stage = 0; stage < 6; stage++) {
await new Promise((r) => setTimeout(r, 1050));
const p = redis.pipeline();
for (let i = 0; i < 500; i++) p.get(`hot:${i}`); // reads only, every stage
for (let i = 0; i < 7000; i++) p.set(`k:${stage}:${i}`, val);
await p.exec();
}
console.log(policy.padEnd(12), 'evicted', String(await evicted()).padStart(5), '| hot left', await survivors('hot:'), '/ 500 | cold left', await survivors('cold:'), '/ 500');
}
// 3) volatile-lru with NO ttl anywhere behaves like noeviction
await reset('volatile-lru');
const noTtl = await fill('k:', 100000);
console.log('volatile-lru, no TTLs:', noTtl.written, 'writes ok, then:', noTtl.firstError, '| evicted', await evicted());
// 4) volatile-lru with 1000 persistent keys and many expiring ones: only expiring keys are candidates
await reset('volatile-lru');
await fill('keep:', 1000);
const tmp = await fill('tmp:', 60000, 3600);
let kept = 0;
for (let i = 0; i < 1000; i++) kept += await redis.exists(`keep:${i}`);
console.log('volatile-lru mixed: error', tmp.firstError ?? 'none', '| evicted', await evicted(), '| persistent keys left', kept, 'of 1000');
// 5) volatile-ttl: the shortest remaining TTLs go first
await reset('volatile-ttl');
await fill('short:', 2000, 100);
await fill('long:', 2000, 100000);
await fill('fill:', 40000, 50000);
let short = 0, long = 0;
for (let i = 0; i < 2000; i++) { short += await redis.exists(`short:${i}`); long += await redis.exists(`long:${i}`); }
console.log('volatile-ttl: EX 100 keys left', short, '/ 2000 | EX 100000 keys left', long, '/ 2000');
// leave the server as we found it
await redis.config('SET', 'maxmemory', '0');
await redis.config('SET', 'maxmemory-policy', 'noeviction');
await redis.flushall();
await redis.quit();

Real output (this Mac):

noeviction: 17032 writes ok, then: OOM command not allowed when used memory > 'maxmemory'.
GET still works: true | evicted_keys: 0
allkeys-lru evicted 26504 | hot left 500 / 500 | cold left 1 / 500
allkeys-lrm evicted 26537 | hot left 2 / 500 | cold left 1 / 500
allkeys-lfu evicted 26339 | hot left 500 / 500 | cold left 154 / 500
volatile-lru, no TTLs: 17111 writes ok, then: OOM command not allowed when used memory > 'maxmemory'. | evicted 0
volatile-lru mixed: error none | evicted 46653 | persistent keys left 1000 of 1000
volatile-ttl: EX 100 keys left 0 / 2000 | EX 100000 keys left 2000 / 2000

The exact counts move a little on every run because eviction samples random keys (across four runs of mine: LFU left between 11 and 154 cold keys, LRM between 0 and 7 hot ones); the shape did not change.

Reading it:

  • noeviction wrote about 17,000 keys and then every write got OOM command not allowed when used memory > 'maxmemory'. Reads worked, evicted_keys stayed 0. The database is not damaged, it just says no. (Numbers vary by a few hundred keys per run.)

  • allkeys-lru: 500 “hot” keys read at every stage survived (500 of 500) while 500 equally old “cold” keys were wiped out (0 or 1 left). This is the classic cache behaviour.

  • allkeys-lrm: the hot keys were only read, never modified, so they were evicted like everything else (0 to 7 of 500 left). Use LRM when “recently written” is what matters, not “recently read”.

  • allkeys-lfu: the frequently read keys survived (500 of 500), and among untouched keys some survived (154 of 500 here, 11 in another run): with equal frequency the choice among samples is arbitrary.

  • volatile-* with no TTLs behaves like noeviction. About 17,100 writes and then OOM, with evicted_keys 0: there was nothing eligible. This is a real production trap: a cache policy set to volatile-lru on keys that forgot their TTL fails writes instead of evicting.

  • volatile-lru with 1,000 persistent keys and many expiring ones evicted about 46,600 expiring keys and never touched the persistent ones (1000 of 1000 left).

  • volatile-ttl: with 2,000 keys at EX 100, 2,000 at EX 100000 and 40,000 fillers at EX 50000, every short-TTL key was evicted (0 of 2,000 left) and every long-TTL key survived (2,000 of 2,000).

allkeys-lru and -lrm only differ if the LRU clock can tell moments apart: it has one-second resolution, so my first, faster version of this test (everything inside one second) could not tell hot from cold at all (170 vs 154 survivors). The script therefore spreads the fill across about six seconds. Do not conclude anything about LRU from a sub-second test.

CONFIG GET maxmemory-policy
CONFIG SET maxmemory-policy allkeys-lru
CONFIG GET maxmemory
CONFIG SET maxmemory 256mb
CONFIG SET maxmemory 0
CONFIG SET maxmemory-policy noeviction

When maxmemory is unset: the OOM killer decides

Section titled “When maxmemory is unset: the OOM killer decides”

With no maxmemory Redis just keeps allocating. Inside a container with a memory limit that ends with the kernel killing the process, and all data with it. Same image, same 64 MB Docker limit (-m 64m), same fill loop of 1 KB values, with and without --maxmemory 40mb:

Terminal window
=== A: container limit 64m, maxmemory unset
chunk 4 -> 40000
chunk 5 -> 50000
chunk 6 -> Error: Server closed the connection
status=exited OOMKilled=true exit=137
=== B: same limit, --maxmemory 40mb (noeviction)
chunk 4 -> 40000
chunk 5 -> OOM command not allowed when used memory > 'maxmemory'. script: ...
status=running OOMKilled=false exit=0
DBSIZE: 40000, GET still works

In A the container was killed (OOMKilled=true, exit code 137) at about 50,000 keys and everything in memory was gone; in B the server refused the write, stayed up and kept serving the 40,000 keys it had. Set maxmemory below the container or machine limit (leave room for the fork of BGSAVE, replication buffers and fragmentation) so that Redis, not the kernel, chooses what fails.

An assistant containerises a cache. To “keep all data safe” it leaves eviction at the default and lets the orchestrator limit memory:

🐛 Spot the bug

services:
cache:
image: redis:8
deploy:
resources:
limits:
memory: 64M # no maxmemory configured, default policy
Show the answer

Nothing tells Redis about the 64 MB limit. Its maxmemory is 0, so it grows until the kernel OOM-kills it. In our run the container died with OOMKilled=true, exit 137 and lost all its keys, and a restart loop would repeat that forever. With --maxmemory 40mb the same load produced a clean OOM command not allowed error and the server stayed up. For a cache use an allkeys-* policy so writes succeed by evicting:

command: redis-server --maxmemory 48mb --maxmemory-policy allkeys-lru

maxmemory is the size of the room and the policy is what happens when it is full: throw out furniture (evict, and which piece: least recently used, modified, frequently used, soonest to expire) or turn people away at the door (noeviction). Without a room size, the building’s own limit applies, and that limit is not polite.

ChoiceBenefitCost
noevictionNever drops data; reads and DEL keep workingWrites fail with OOM once full; needs monitoring
allkeys-lruBest cache default when a subset is hot: 500/500 hot keptApproximate (samples 5), may evict something you wanted
allkeys-lrmEvicts what has not been written recentlyRead-hot keys get evicted (0/500 hot kept)
allkeys-lfuProtects frequently read keysUntouched keys tie; needs enough traffic to build counters
volatile-*Only expiring keys are candidates, persistent data is safeWith no TTLs it is noeviction in disguise
No maxmemoryNothing to configureThe kernel OOM-kills the process (exit 137)
  • “Redis has a memory limit by default.” maxmemory is 0 (unlimited).
  • “volatile-lru evicts something when memory is full.” Only keys with a TTL are eligible; with none, writes fail.
  • “LRU is exact.” It samples 5 keys by default, and the clock has one-second resolution.
  • “Reading a key protects it under every policy.” Not under allkeys-lrm/volatile-lrm.
  • “noeviction means Redis stops working when full.” Reads and DEL still work; only memory-growing writes fail.
  • A production cache on noeviction — every write errors once full.
  • Never setting maxmemory — the container or host runs out first.
  • volatile-* on keys that lost their TTL — writes fail instead of evicting (a SET without KEEPTTL removes the TTL).
  • Setting maxmemory equal to the container limit — fork copy-on-write, output buffers and fragmentation need headroom.
  • Judging LRU with a sub-second test — the clock resolution is one second.

Real-world example

Stack Overflow’s 2016 architecture write-up (Nick Craver, 2016) reports about 160 billion Redis operations a month, and a separate machine-learning Redis instance (“Providence”) on dedicated servers “due to memory”. Separate instances are what let each workload keep its own maxmemory and policy. The post is ten years old; the point is the separation, not the sizes.

Which eviction policy evicts the least recently used key from ALL keys?
What are the default maxmemory and maxmemory-policy?
You set volatile-lru but none of your keys has a TTL, and memory is full. What happens on a write?
The volatile-ttl policy evicts based on what criterion?
Under allkeys-lrm, what happens to a key that is read constantly but never modified?
Redis runs in a 64 MB container with maxmemory unset and keeps receiving writes. What ends the story?
You test allkeys-lru by writing everything inside half a second, reading one key throughout. Why is the result unreliable?