Topic 2.1
TTL and Expiration: Lazy, Active, and on Replicas
In one line
A TTL makes a key disappear after a set time. Redis deletes expired keys lazily (when you touch them) and actively (a background sampler), replicas wait for the primary's DEL, and sliding vs absolute expiry is a design decision you make per use case.
Think of it like this
Milk in a shop fridge with a best-before date. A shelf-stacker checks a few random cartons every few minutes and throws out expired ones (active expiry), and a shopper who picks up an expired carton hands it back instead of buying it (lazy expiry). Either way, nobody drinks expired milk, but some expired cartons may sit on the shelf for a while.
Key ideas
- 01
Commands:
EXPIRE/PEXPIRE(relative, seconds or ms),EXPIREAT/PEXPIREAT(absolute Unix time),TTL/PTTL(remaining;-1= no expiry,-2= no key),EXPIRETIME/PEXPIRETIME(7.0, the absolute time),PERSIST(remove TTL), and set-with-expiry forms:SET ... EX,GETEX,SETEX. Since 7.0,EXPIREtakesNX(only if no TTL),XX(only if there is one),GTandLT(only if the new TTL is greater or less). - 02
What resets a TTL: overwriting the key with
SET(unlessKEEPTTL),GETSET,DEL, orRENAMEonto it. What doesn't:INCR,LPUSH,HSETand other in-place modifications keep the TTL. A key's expiry is stored as an absolute Unix timestamp in ms, so it keeps counting down even while Redis is stopped: after a restart, keys whose time has passed are expired immediately. - 03
Lazy expiration: every key access checks the expiry first; an expired key is deleted and treated as missing. Active expiration: about 10 times a second (
hz 10), Redis samples keys that have TTLs, deletes expired ones, and repeats while a large share of the sample was expired, within a CPU time budget. So expired-but-untouched keys use memory for a while, and a mass expiry can cost noticeable CPU.active-expire-effort(1–10) trades CPU for faster cleanup. - 04
Replicas don't expire keys on their own clock; they wait for the primary to send
DEL(to keep the dataset consistent). Since Redis 3.2, reads on a replica still return "not found" for keys that are logically expired, so clients don't see stale values, but memory is only freed when the DEL arrives. Writable replicas with their own TTL keys are a special case you should avoid. - 05
Expiry precision is about 1 ms. Expired keys generate
expiredkeyspace events only when actually deleted, not at the exact TTL moment, so don't build precise timers on keyspace notifications (use a sorted set scheduler, Topic 12.6). - 06
Sliding vs absolute expiry: sliding (refresh TTL on each use) keeps active sessions alive but lets a stolen session live forever if used; absolute (fixed deadline) caps session lifetime. Real systems combine both: slide for idle timeout, plus a hard maximum lifetime stored in the value.
- 07
TTL jitter: if a million keys are written at once with the same TTL, they expire together, creating a CPU spike in Redis and a thundering herd on the database (cache avalanche, Topic 6.6). Add ±10–20% random jitter to TTLs.
Code & diagrams
127.0.0.1:6379> SET session:abc123 "..." EX 1800
OK
127.0.0.1:6379> TTL session:abc123
(integer) 1800
127.0.0.1:6379> EXPIRE session:abc123 60 LT # shorten only
(integer) 1
127.0.0.1:6379> EXPIRE session:abc123 3600 LT # would lengthen: ignored
(integer) 0
127.0.0.1:6379> EXPIRETIME session:abc123
(integer) 1727521860
127.0.0.1:6379> HSET session:abc123 lastSeen 1727520000
(integer) 1
127.0.0.1:6379> TTL session:abc123 # HSET keeps the TTL
(integer) 55
127.0.0.1:6379> PERSIST session:abc123
(integer) 1
127.0.0.1:6379> TTL session:abc123
(integer) -1
127.0.0.1:6379> TTL does-not-exist
(integer) -2
127.0.0.1:6379> INFO stats
...
expired_keys:18233
expired_stale_perc:0.42
expired_time_cap_reached_count:0Refresh the idle TTL, but never beyond the absolute deadline stored in the session.
-- KEYS[1] = session key, ARGV[1] = idle seconds, ARGV[2] = now (unix seconds)
local deadline = tonumber(redis.call('HGET', KEYS[1], 'absExpiry'))
if not deadline then return nil end -- no session
local now = tonumber(ARGV[2])
if now >= deadline then
redis.call('UNLINK', KEYS[1])
return nil -- hard lifetime reached
end
local ttl = math.min(tonumber(ARGV[1]), deadline - now)
redis.call('EXPIRE', KEYS[1], ttl)
return redis.call('HGETALL', KEYS[1])Interview problem
The problem
Session expiration under heavy traffic
Sessions are stored at session:abc123 with a 30-minute TTL. Decide between sliding and absolute expiry, whether to refresh on every request, how it behaves under high traffic, what happens if Redis restarts, and what changes if the app reads from replicas.
You're given
- 50K requests/sec
- Idle timeout 30 min
- Security requires re-login every 12 h
- Replicas used for reads
The interviewer follows up
Can a key's TTL be different on the primary and a replica?
When it breaks
Millions of keys written in a batch job with the same TTL
What you see
They all expire in the same second: Redis CPU spikes in the active expire cycle, latency jumps, and the database is hit by a wave of cache misses.
Fix & prevent
Add random jitter (±10–20%) to TTLs; warm caches gradually; monitor expired_keys rate.
A code path re-writes cached values with plain SET
What you see
The TTL is removed; the key becomes permanent. Over weeks, memory fills with keys that never expire (TTL returns -1).
Fix & prevent
Always write cache entries with EX, or KEEPTTL on updates; add a periodic scan that samples cache keys and alerts on missing TTLs.
Explain it without notes
Explain lazy and active expiration, and why both exist.
Why don't replicas expire keys themselves, and what do they return for an expired key?
Practice
Set a key with a 5-second TTL, stop the container for 10 seconds, start it again (with persistence on) and check the key.
Use EXPIRE ... GT to implement "extend a trial but never shorten it".
Trade-offs
- ↔
Refreshing TTL on every access gives precise idle timeouts but doubles write traffic; threshold-based refresh is cheaper and slightly less precise.
- ↔
Higher
active-expire-effortfrees memory faster at the cost of CPU on the main thread.
Done when you can
I know every TTL command and which operations reset or keep a TTL.
I can explain lazy vs active expiry and replica behaviour.
I can design sliding plus absolute session expiry and add TTL jitter.