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@runscript withmaxmemoryset 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.
Setting a memory limit
Section titled “Setting a memory limit”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:
127.0.0.1:6379> CONFIG GET maxmemory1) "maxmemory"2) "0"127.0.0.1:6379> CONFIG GET maxmemory-policy1) "maxmemory-policy"2) "noeviction"127.0.0.1:6379> CONFIG GET maxmemory-samples1) "maxmemory-samples"2) "5"127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lruOK127.0.0.1:6379> CONFIG SET maxmemory 4mbOK127.0.0.1:6379> CONFIG GET maxmemory1) "maxmemory"2) "4194304"127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lrmOK127.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, noeviction127.0.0.1:6379> CONFIG SET maxmemory 0OK127.0.0.1:6379> CONFIG SET maxmemory-policy noevictionOKmaxmemory 512mbmaxmemory-policy allkeys-lrumaxmemory-samples 5maxmemory-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.
The eleven policies
Section titled “The eleven policies”| Policy | Behaviour |
|---|---|
noeviction | Refuse writes that need memory (default); reads and DEL still work |
allkeys-lru | Evict the least recently used (read or written) key |
allkeys-lrm | Evict the least recently modified key (8.x): reads do not protect a key |
allkeys-lfu | Evict the least frequently used key |
allkeys-random | Evict a random key |
volatile-lru / volatile-lrm / volatile-lfu / volatile-random | The same four, but only among keys with a TTL |
volatile-ttl | Evict 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.
What each policy does when memory is full
Section titled “What each policy does when memory is full”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):
// @runimport { 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 throwasync 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 failawait 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 touchedasync 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 writesfor (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 noevictionawait 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 candidatesawait 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 firstawait 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 itawait 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: 0allkeys-lru evicted 26504 | hot left 500 / 500 | cold left 1 / 500allkeys-lrm evicted 26537 | hot left 2 / 500 | cold left 1 / 500allkeys-lfu evicted 26339 | hot left 500 / 500 | cold left 154 / 500volatile-lru, no TTLs: 17111 writes ok, then: OOM command not allowed when used memory > 'maxmemory'. | evicted 0volatile-lru mixed: error none | evicted 46653 | persistent keys left 1000 of 1000volatile-ttl: EX 100 keys left 0 / 2000 | EX 100000 keys left 2000 / 2000The 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:
-
noevictionwrote about 17,000 keys and then every write gotOOM command not allowed when used memory > 'maxmemory'.Reads worked,evicted_keysstayed 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 likenoeviction. About 17,100 writes and thenOOM, withevicted_keys0: there was nothing eligible. This is a real production trap: a cache policy set tovolatile-lruon keys that forgot their TTL fails writes instead of evicting. -
volatile-lruwith 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 atEX 100, 2,000 atEX 100000and 40,000 fillers atEX 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 noevictionWhen 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:
=== A: container limit 64m, maxmemory unsetchunk 4 -> 40000chunk 5 -> 50000chunk 6 -> Error: Server closed the connectionstatus=exited OOMKilled=true exit=137=== B: same limit, --maxmemory 40mb (noeviction)chunk 4 -> 40000chunk 5 -> OOM command not allowed when used memory > 'maxmemory'. script: ...status=running OOMKilled=false exit=0DBSIZE: 40000, GET still worksIn 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.
Reading AI-generated code
Section titled “Reading AI-generated code”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 policyShow 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-lruMental Model
Section titled “Mental Model”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.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
noeviction | Never drops data; reads and DEL keep working | Writes fail with OOM once full; needs monitoring |
allkeys-lru | Best cache default when a subset is hot: 500/500 hot kept | Approximate (samples 5), may evict something you wanted |
allkeys-lrm | Evicts what has not been written recently | Read-hot keys get evicted (0/500 hot kept) |
allkeys-lfu | Protects frequently read keys | Untouched keys tie; needs enough traffic to build counters |
volatile-* | Only expiring keys are candidates, persistent data is safe | With no TTLs it is noeviction in disguise |
No maxmemory | Nothing to configure | The kernel OOM-kills the process (exit 137) |
Common Misconceptions
Section titled “Common Misconceptions”- “Redis has a memory limit by default.”
maxmemoryis0(unlimited). - “
volatile-lruevicts 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. - “
noevictionmeans Redis stops working when full.” Reads andDELstill work; only memory-growing writes fail.
Common mistakes
Section titled “Common mistakes”- 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 (aSETwithoutKEEPTTLremoves the TTL).- Setting
maxmemoryequal 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
maxmemoryand policy. The post is ten years old; the point is the separation, not the sizes.