RizTech Academy logo
RizTech Academy
Key-Value Stores and CachingLesson 5 of 630 min

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 stats reports evicted_keys and keyspace_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 memory shows used_memory and maxmemory; MEMORY USAGE key shows one key's footprint. used_memory creeping toward maxmemory with 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

  1. Set a key with no TTL and read TTL (-1). Delete it and read TTL (-2). Explain the difference.
  2. Set, EXPIRE, then PERSIST a key, watching the TTL. Set another with PX and read PTTL.
  3. Read the default maxmemory and maxmemory-policy on a fresh instance. Explain why the default maxmemory is dangerous for a cache.
  4. Set maxmemory to a few megabytes and allkeys-lru. Insert far more data than fits and confirm DBSIZE stays bounded while evicted_keys climbs.
  5. Switch to noeviction, fill it, and confirm writes start failing with an OOM error.
  6. Switch to volatile-lru, give only some keys a TTL, fill memory, and confirm the no-TTL keys survive.
  7. With volatile-lru, remove all TTLs, fill memory, and observe it refuse writes like noeviction.
  8. Insert keys with the same TTL and watch used_memory briefly stay high after they expire. Explain why (lazy plus active expiry).
  9. Read evicted_keys and keyspace_misses from INFO stats and describe what a rising eviction rate tells you.
  10. For a cache you would run, write down the maxmemory, the policy, and why.

Official documentation

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