Command Palette

Search for a command to run...

Hectal
PHASE 2Intermediate ~17 min· topic 1 of 4

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.

0/4 · 0%

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

  1. 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, EXPIRE takes NX (only if no TTL), XX (only if there is one), GT and LT (only if the new TTL is greater or less).

  2. 02

    What resets a TTL: overwriting the key with SET (unless KEEPTTL), GETSET, DEL, or RENAME onto it. What doesn't: INCR, LPUSH, HSET and 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.

  3. 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.

  4. 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.

  5. 05

    Expiry precision is about 1 ms. Expired keys generate expired keyspace 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).

  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.

  7. 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

ttl.redisredis
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:0
expiry-paths.mermaiddiagram
Rendering diagram…
sliding-session.lualua

Refresh 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

01

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

01

Explain lazy and active expiration, and why both exist.

02

Why don't replicas expire keys themselves, and what do they return for an expired key?

Practice

01

Set a key with a 5-second TTL, stop the container for 10 seconds, start it again (with persistence on) and check the key.

02

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-effort frees 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.