A TTL (time-to-live) is a countdown attached to a Redis key. When the TTL hits zero, the key is gone. This is how Redis behaves like a cache instead of a forever-store.
In simple language — think of it like a self-destructing message. You set the timer, and after that time elapses, Redis throws the key away. You don’t have to remember to delete it.
The basic commands
SET session:abc123 "user_42"
EXPIRE session:abc123 3600 # expires in 3600 seconds (1 hour)
# or do it in one shot
SET session:abc123 "user_42" EX 3600
TTL session:abc123 # → 3599 (seconds remaining)
PTTL session:abc123 # → 3599042 (milliseconds remaining)
PERSIST session:abc123 # removes the TTL — key now lives forever
TTL return values worth knowing:
-2 → key does not exist
-1 → key exists but has no TTL
N → key exists, expires in N seconds
You can also set an absolute expiry time (Unix timestamp):
EXPIREAT session:abc123 1735689600
SET token "xyz" EXAT 1735689600
How does Redis actually expire keys?
Here’s the cool part. Redis doesn’t run a timer per key — that would be insanely expensive with millions of keys. Instead it uses a hybrid strategy: lazy expiration plus active expiration.
When a client touches the key, Redis checks the TTL. Expired? Delete and return nil.
Cost: O(1) per access
10× per second, Redis samples 20 random keys with TTLs. Expires the dead ones. If >25% were dead, immediately repeat.
Probabilistic sweep
The active algorithm in pseudocode:
every 100ms:
loop:
sample 20 keys from the "keys with TTL" set
delete the expired ones
if more than 25% were expired:
continue (don't sleep, do another round)
else:
break
This means a key with a TTL might stick around in memory for a brief moment after its expiry — but no client will ever see it as alive, because the lazy check kicks in on access.
Common use cases
- Sessions —
SET session:<id> <data> EX 1800for 30-minute sessions - Rate limiting — counters that auto-reset
- OTP / verification codes —
SET otp:<phone> 123456 EX 300(5 min window) - Cache entries — keep hot data fresh, let stale data fall away
# rate limit: max 100 reqs/min per user
INCR ratelimit:user:42
EXPIRE ratelimit:user:42 60 NX # NX = only set TTL if there isn't one
NX on EXPIRE (Redis 7+) is handy — only sets the TTL if no TTL exists. So the first INCR sets the window, subsequent INCRs don’t reset it.
Gotchas
- Renaming a key with
RENAMEkeeps the TTL. Copying withCOPYdoes too (unless you pass options). SET key valuewithout EX clobbers any existing TTL. UseSET key value KEEPTTLto preserve it.- If you only check
TTL, you can’t tell “no key” from “no TTL” without combining withEXISTS. That’s why-2vs-1matters.
TTLs are how Redis stays small and stays cache-y. When memory pressure exceeds what TTLs can handle, eviction policies take over.