Lesson 0003 · Caching & TTL

Key expiration: TTL vs eviction

Two different ways a key disappears — one you ask for by time, one Redis forces by memory.

~10 minRetrieval quiz at the endCheat sheet: here

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.

Part 1 — TTL: the expiry you set

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

The KEEPTTL footgun

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

Part 2 — How a TTL actually deletes the key

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

MechanismWhen it runsThe 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

The takeaway

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.

Part 3 — Eviction: the expiry Redis forces on you

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 expiryEviction
TriggerTime (per key)Memory pressure (global)
ScopeOne key you markedWhatever the policy picks
You control it viaEXPIRE / SET EXmaxmemory + 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:

PolicyCandidate keysPicks…
noevictionnonenothing — writes error with OOM (the default)
allkeys-lruall keysleast recently used
allkeys-lfuall keysleast frequently used
allkeys-randomall keysa random key
volatile-lruonly keys with a TTLleast recently used
volatile-lfuonly keys with a TTLleast frequently used
volatile-randomonly keys with a TTLa random key
volatile-ttlonly keys with a TTLthe shortest remaining TTL
Two traps that bite in production

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

Interview-grade nuance: replicas don't expire on their own

Why a replica can return a "dead" key

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

Check yourself

Answer from memory — effortful recall builds retention. Feedback is immediate.

TTL mykey returns -2. That means the key is:

-2 = the key does not exist. -1 = the key exists but has no TTL. A positive number = seconds remaining.

A key's TTL just hit zero. What is guaranteed at that instant?

Passive expiry hides it from reads immediately, but the bytes are reclaimed later (active sampling / next access), and replicas wait for the primary's DEL. Logically dead ≠ memory freed.

Stock Redis fills to maxmemory as a cache. Writes then:

The default policy is noeviction: it refuses to drop keys and returns OOM errors on writes. A cache usually wants allkeys-lru or allkeys-lfu instead.

You set volatile-lru but no keys have a TTL. Under pressure it acts like:

volatile-* policies only consider keys that have a TTL. With zero candidates there is nothing to evict, so it behaves like noeviction — and errors.

Overwriting a key with a bare SET (no KEEPTTL) does what to its TTL?

A successful SET discards any previous TTL — the key becomes persistent. Pass KEEPTTL to retain the countdown when rewriting the value.

Hands-on: watch both mechanisms fire

You need a local Redis and redis-cli.

1 · TTL and the KEEPTTL trap

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

2 · Force an eviction

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
Predict first

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.

Primary source — read this next

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.