Lesson 0003 · Caching & TTL
Two different ways a key disappears — one you ask for by time, one Redis forces by memory.
Last session you designed a "who's online" set and hit a wall: a plain set never forgets, so a crashed client stays online forever. Your instinct was to reach for expiry. Good instinct — but "expiry" in Redis hides two completely different mechanisms that beginners blur together, and interviewers love the gap. This lesson separates them: TTL (you mark one key to die at a time) versus eviction (Redis kills keys to survive memory pressure). Same outcome — a key vanishes — for opposite reasons.
A TTL (time-to-live) is a per-key countdown you attach. A key with a TTL is volatile; one without is persistent. The commands are O(1):
SET session:42 "..." EX 1800 # set + 1800s TTL in one atomic op
EXPIRE session:42 900 # (re)set TTL to 900s — O(1)
TTL session:42 # -> seconds left, -1 no TTL, -2 no key
PERSIST session:42 # remove the TTL — key now persistent
Those three TTL return values are worth memorizing: -2 = the
key doesn't exist, -1 = it exists but has no expiry.
— Redis Docs — TTL
A plain SET on an existing key wipes its TTL: "Any
previous time to live associated with the key is discarded on successful SET." Rewrite
a session value with a bare SET and it becomes immortal. Use
SET … KEEPTTL to preserve the countdown.
— Redis Docs — SET
Here's the surprise: a TTL is not a timer that fires at the exact second. Redis expires keys two ways, and neither is instant. — Redis Docs — How Redis expires keys
| Mechanism | When it runs | The gap it leaves |
|---|---|---|
| Passive (lazy) | On access — a client touches the key, Redis notices it's expired, deletes it, returns nil. | A key never accessed again would sit expired forever. |
| Active (sampling) | Periodically, Redis samples random keys that have a TTL and deletes the expired ones. | Runs in the background, so it closes the passive gap — but only approximately. |
The active loop is probabilistic: it samples keys with an expiry, deletes those already expired, and if more than 25% of the sample was expired it immediately samples again. So in the worst case up to ~25% of expired keys may still be in memory, not yet reclaimed. — Redis FAQ — expiration algorithm
A TTL means "logically dead at time T", not "memory freed at time T." The value is invisible to clients the instant it expires (passive check on read), but the bytes are reclaimed later, whenever active sampling or the next access gets to it. Never assume expiry frees memory on schedule.
Eviction is a different beast entirely. It has nothing to do with your
TTLs and everything to do with the maxmemory ceiling. When Redis is about to
exceed that limit, its maxmemory-policy decides whether — and which — keys
to drop to make room.
— Redis Docs — Key eviction
| TTL expiry | Eviction | |
|---|---|---|
| Trigger | Time (per key) | Memory pressure (global) |
| Scope | One key you marked | Whatever the policy picks |
| You control it via | EXPIRE / SET EX | maxmemory + maxmemory-policy |
| Means | "This datum is stale now" | "We're out of room" |
The policies split on two axes — which keys are candidates and how one is chosen:
| Policy | Candidate keys | Picks… |
|---|---|---|
noeviction | none | nothing — writes error with OOM (the default) |
allkeys-lru | all keys | least recently used |
allkeys-lfu | all keys | least frequently used |
allkeys-random | all keys | a random key |
volatile-lru | only keys with a TTL | least recently used |
volatile-lfu | only keys with a TTL | least frequently used |
volatile-random | only keys with a TTL | a random key |
volatile-ttl | only keys with a TTL | the shortest remaining TTL |
1. The default is noeviction. Point an app at a stock Redis
as a cache, fill memory, and writes start failing with OOM errors — it will not
silently make room. A cache usually wants allkeys-lru or allkeys-lfu.
2. volatile-* only touches keys that have a TTL. Choose
volatile-lru but forget to set TTLs, and there are no candidates — so it
behaves like noeviction and errors under pressure.
LRU and LFU are approximated, not exact — Redis samples a few keys and
evicts the best candidate among them, tuned by maxmemory-samples (default 5).
Same sampling philosophy as active expiry: good enough, cheap enough.
— Redis Docs — Key eviction
To keep replicas consistent, only the primary expires keys. When a key
expires there, the primary synthesizes a DEL and ships it to replicas and the
AOF. A replica will not expire a key on its own — it waits for that DEL.
So a read served by a replica can return a value that is already logically expired, until the
primary's delete arrives.
— Redis Docs — expires in replication
Answer from memory — effortful recall builds retention. Feedback is immediate.
TTL mykey returns -2. That means the key is:
A key's TTL just hit zero. What is guaranteed at that instant?
Stock Redis fills to maxmemory as a cache. Writes then:
You set volatile-lru but no keys have a TTL. Under pressure it acts like:
Overwriting a key with a bare SET (no KEEPTTL) does what to its TTL?
You need a local Redis and redis-cli.
redis-cli SET s "v1" EX 100
redis-cli TTL s # ~100
redis-cli SET s "v2" # bare SET...
redis-cli TTL s # -> -1 (TTL wiped!)
redis-cli SET s "v3" EX 100
redis-cli SET s "v4" KEEPTTL
redis-cli TTL s # still counting down
redis-cli CONFIG SET maxmemory 3mb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
redis-cli EVAL "for i=1,100000 do redis.call('set','k:'..i, string.rep('x',100)) end" 0
redis-cli DBSIZE # far fewer than 100000 — LRU evicted many
redis-cli INFO stats | grep evicted_keys # the eviction counter climbed
Before running step 2: will DBSIZE equal 100000? Now flip the policy to
noeviction and re-run the EVAL — what happens to the writes? Commit,
then try it.
Clean up: redis-cli CONFIG SET maxmemory 0 then redis-cli FLUSHALL.
Bring your evicted_keys number — and any OOM error text — to your teacher.
Redis Docs — Key eviction
is the authoritative page on maxmemory, every policy, and the approximated
LRU/LFU. Pair it with the short
expiration-algorithm FAQ
for the passive/active split and the 25% worst-case.