TTLs, eviction policies and running out of memory
Redis lives in memory, and memory runs out. What happens when it does is the difference between a cache that quietly does its job and one that either starts refusing writes or throws away data you needed. Two mechanisms decide this — expiry (data you told to leave) and eviction (data Redis throws out to make room) — and they are not the same thing.
Expiry: TTLs, the ones you set
A TTL (time to live) marks a key to be deleted after a set time. You have met EXPIRE; here is
the full toolkit:
SET k1 v1 → TTL k1 = -1 (no expiry; lives until deleted)
EXPIRE k1 100 → TTL k1 = 100 (delete after 100 seconds)
PERSIST k1 → TTL k1 = -1 (remove the expiry again)
SET k2 v2 PX 500 → PTTL k2 = 437 (expiry in milliseconds, set atomically)
The TTL sentinels are worth memorising: -1 means "exists, no expiry"; -2 means "does not
exist". Confusing the two is a common bug — code that treats -2 as "no expiry" instead of
"gone".
How Redis actually expires keys is a detail that matters. It does not scan and delete on a timer. It uses two mechanisms together:
- Lazy — when a key is accessed, if it is past its TTL it is deleted then and not returned.
- Active — a background job samples random keys with TTLs a few times a second and deletes the expired ones it finds.
The consequence: an expired key still occupies memory until one of those catches it. A key that expired an hour ago but has not been touched, and has not been sampled, is still using RAM. Usually invisible, but it explains why "expired" and "freed" are not the same instant, and why a flood of same-time expiries can briefly bloat memory.
Eviction: what happens when memory fills
Expiry is data leaving on schedule. Eviction is Redis deciding, under memory pressure, to throw out data that has not expired — because it has no room for a new write. This is governed by two settings, and getting them wrong is how Redis surprises people.
maxmemory → 0 (the default: no limit — grow until the OS refuses)
maxmemory-policy → noeviction (the default)
The default of maxmemory 0 is a trap for a cache. No limit means Redis grows until it
exhausts the machine's RAM, at which point the OS's out-of-memory killer terminates the process —
your entire cache gone in an instant. A cache must have maxmemory set, to a value with
headroom below the machine's RAM. This is the single most important production setting in this
lesson.
Once maxmemory is set, maxmemory-policy decides what happens when it is reached.
The eight policies
All eight are real and all were accepted by Redis 7:
noeviction allkeys-lru allkeys-lfu allkeys-random
volatile-lru volatile-lfu volatile-random volatile-ttl
Read them as two choices crossed:
Which keys are candidates —
allkeys-*— any key may be evicted.volatile-*— only keys with a TTL may be evicted; keys without one are safe.
How to choose among candidates —
-lru— Least Recently Used: evict what has not been read in longest. The intuitive one.-lfu— Least Frequently Used: evict what is read least often. Better when some keys are perennially hot and others are one-hit — it does not evict a hot key just because it was quiet for a minute.-random— evict at random. Cheap; rarely what you want.volatile-ttl— evict the key expiring soonest anyway.
And noeviction: evict nothing — reject writes instead.
The two policies that actually matter, and their failure modes
For a pure cache, allkeys-lru (or allkeys-lfu) is almost always right: Redis holds the hot
working set and quietly discards the cold tail, which is exactly what a cache should do. Measured
on a 3 MB-capped instance, inserting 50,000 padded keys:
DBSIZE after the load: ~10,334 (only the recent keys survived)
evicted_keys: 45,479 (the rest were discarded to stay near the cap)
Redis held memory near the limit by throwing away least-recently-used keys, and every write succeeded. For a cache, that is the behaviour you want — losing cold entries is free.
noeviction does the opposite and it is the default, which is the dangerous part. When full,
it refuses every write:
OOM command not allowed when used memory > 'maxmemory'.
For a cache this is a self-inflicted outage: the cache fills, and instead of dropping cold data it
starts erroring on every SET, so nothing new can be cached and the errors surface in your
application. noeviction is correct only when Redis is a primary datastore you cannot afford to
silently lose data from — and then you must alert on memory and scale before it fills. Using a
Redis as a cache and leaving it on the default noeviction is a classic production incident.
The volatile-* policies are the nuanced middle: evict only keys that carry a TTL, and never
touch keys without one. This lets you mix roles in one instance — cache entries get a TTL and are
evictable; a few durable keys (a config blob, a critical counter) get no TTL and are protected. The
catch: if every key has a TTL, volatile-lru behaves like allkeys-lru; and if no evictable
(TTL-bearing) key exists when memory fills, it falls back to refusing writes like noeviction.
Know which keys have TTLs before choosing it.
The policy decision, condensed
| Redis is used as… | Set |
|---|---|
| A pure cache | maxmemory + allkeys-lru (or allkeys-lfu if some keys are always hot) |
| Mixed cache and a few durable keys | maxmemory + volatile-lru, TTLs only on the cache keys |
| A primary datastore | maxmemory + noeviction, and alert on memory |
| Anything in production | maxmemory set — never leave it at 0 |
Keeping memory under control
Beyond eviction, three habits:
- TTL almost everything. A cache entry without a TTL relies entirely on eviction to ever leave. Give every cache key a TTL and eviction becomes a safety net, not the primary mechanism.
- Watch the eviction rate.
INFO statsreportsevicted_keysandkeyspace_misses. A rising eviction rate means the working set no longer fits — the signal to add memory or reduce what you cache, before hit rate collapses. - Know your memory.
INFO memoryshowsused_memoryandmaxmemory;MEMORY USAGE keyshows one key's footprint.used_memorycreeping towardmaxmemorywith climbing evictions is the picture to recognise.
And the operational note from the data-structures lesson bears repeating: to inspect keys, use
SCAN, never KEYS * — the latter blocks the single-threaded server.
Check your work
Expiry versus eviction. Expiry is data you scheduled to leave (a TTL); eviction is Redis throwing out unexpired data under memory pressure.
The TTL sentinels. -1 exists with no expiry; -2 does not exist.
How Redis expires keys. Lazily on access, plus an active background sampler — so an expired key can still hold memory until one catches it.
The default maxmemory, and why it is a trap. 0 (no limit) — Redis grows until the OS kills
it; a cache must set maxmemory.
The default maxmemory-policy. noeviction.
The two axes of the policies. allkeys versus volatile (candidates), and lru/lfu/random/ttl (how to choose).
allkeys-lru for a cache. Holds the hot set, discards the cold tail; measured evicting ~45,000
of 50,000 keys to stay near a 3 MB cap while every write succeeded.
Why noeviction is dangerous as a cache default. When full it refuses every write with an OOM
error — a self-inflicted outage.
When noeviction is correct. When Redis is a primary datastore whose data you cannot lose —
paired with memory alerting.
What volatile-* protects, and its two catches. Only TTL-bearing keys are evictable; if all
keys have TTLs it acts like allkeys, and if no evictable key exists it refuses writes.
Two things to watch. evicted_keys (a rising rate means the working set no longer fits) and
used_memory versus maxmemory.
Practice
- Set a key with no TTL and read
TTL(-1). Delete it and readTTL(-2). Explain the difference. - Set,
EXPIRE, thenPERSISTa key, watching the TTL. Set another withPXand readPTTL. - Read the default
maxmemoryandmaxmemory-policyon a fresh instance. Explain why the defaultmaxmemoryis dangerous for a cache. - Set
maxmemoryto a few megabytes andallkeys-lru. Insert far more data than fits and confirmDBSIZEstays bounded whileevicted_keysclimbs. - Switch to
noeviction, fill it, and confirm writes start failing with an OOM error. - Switch to
volatile-lru, give only some keys a TTL, fill memory, and confirm the no-TTL keys survive. - With
volatile-lru, remove all TTLs, fill memory, and observe it refuse writes likenoeviction. - Insert keys with the same TTL and watch
used_memorybriefly stay high after they expire. Explain why (lazy plus active expiry). - Read
evicted_keysandkeyspace_missesfromINFO statsand describe what a rising eviction rate tells you. - For a cache you would run, write down the
maxmemory, the policy, and why.
Official documentation
- Redis — Key eviction — Every policy, the LRU/LFU approximation, and how to choose.
- Redis — EXPIRE — TTLs, and the lazy-plus-active expiry mechanism.
- Redis — Memory optimization — Keeping the footprint down.
- Redis — CONFIG SET maxmemory — Setting the limit and policy at runtime.
- Redis — INFO —
evicted_keys,used_memoryand the rest of the numbers to watch.
Next: persistence, and what a cache must never become.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship