skip to content

Eviction & Expiration

You will learn how Redis decides what to delete: TTL mechanics, lazy versus active expiry, and the maxmemory eviction policies with their approximated LRU and LFU. Interviewers ask because a cache that evicts the wrong keys — or refuses writes at the memory ceiling — is the difference between a cache and an outage.

part ofRedisoverview, primer and where to startread it →
on this pageshow

explore

questions

25

A Redis key was written with a 60-second time-to-live and 60 seconds have passed. Is the key removed from memory at that exact moment? What can a client still observe about it afterwards?

level: juniorimportance: must knowfreq 58%

answer

  1. Deadline exact, reclaim lazy
  2. No timers — delete on access
  3. expires table = only keyed TTLs
  4. Sampler mops up cold keys
  5. INFO stats: expired_keys

basics

~20 s

No. Redis frees an expired key only when a command touches it (lazy delete) or when a background sampling cycle picks it. Reads never return an expired value, but the memory can stay allocated for a while.

solid answer

~50 s

Redis keeps no per-key timer. Expiry is logical first, physical second. - **Logically** the key is gone the instant its deadline passes: any command that looks it up (GET, EXISTS, TTL) treats it as missing. You can never read a value whose TTL elapsed. - **Physically** the entry and its value stay allocated until either a command touches the key and the lookup path deletes it on the spot (lazy expiration), or the background active-expiration cycle randomly samples keys that carry a TTL and reclaims the expired ones. Between deadline and reclaim the key still occupies memory, can still be counted by DBSIZE / INFO keyspace, and used_memory has not dropped. A cold key nobody reads again is reclaimed only by the sampling cycle. INFO stats -> expired_keys counts reclaims that actually happened, and the reclaim is also what propagates a DEL/UNLINK to replicas and the AOF.

code

text · 13 lines
text
127.0.0.1:6379> SET s v EX 5
OK
# ... wait 6 seconds, touch nothing ...
127.0.0.1:6379> DBSIZE
(integer) 1          # still counted: not yet reclaimed
127.0.0.1:6379> TTL s
(integer) -2         # logically gone
127.0.0.1:6379> GET s
(nil)                # this lookup performs the lazy delete
127.0.0.1:6379> DBSIZE
(integer) 0
127.0.0.1:6379> INFO stats
# expired_keys:1

go deeper

for a junior

Recall the core: no timers; delete on access plus a background sampler; reads never return expired data.

for a middle

Add the mechanics: the separate expires table, the fact that the reclaim propagates a DEL to replicas and the AOF, and why lazy alone would leak memory.

for a senior

Frame it as an operational fact: counters and memory lag TTLs, so alerting on dbsize or memory drop timing is unreliable; expiry also costs replication bandwidth.

for a principal

Position it as the single-threaded design tradeoff: exact logical semantics with amortized, bounded-cost reclamation, trading memory promptness for predictable latency.

## Two events, not one Saying a key "expired" conflates two things. The **deadline** is the absolute millisecond timestamp Redis stored when the TTL was set. The **reclaim** is the moment the entry and value are actually freed. Redis makes the deadline exact and lets the reclaim lag. Each database holds two tables: the main keyspace (key -> value) and a smaller *expires* table containing only keys that carry a TTL, mapping key -> deadline. Setting a TTL adds an entry there; PERSIST or overwriting the key with a plain SET removes it. Nothing schedules a callback: there is no timer wheel and no thread per key. That is a deliberate design choice, because Redis serves commands from a single thread and firing millions of timers on it would be ruinous. ## Lazy expiration: delete on access Every command that resolves a key runs a check first. If the key is in the expires table and its deadline is in the past, Redis deletes it right there and then serves the command as if the key never existed. So GET returns nil, EXISTS returns 0, TTL returns -2, and an INCR starts from zero. This is why the guarantee is airtight: the read path itself enforces it, so a stale value can never be served, no matter how late the physical reclaim is. The reclaim is also a write: Redis propagates an explicit DEL (or UNLINK when lazyfree-lazy-expire is enabled) to replicas and to the AOF, and fires the `expired` keyspace notification. Expiry is therefore not free — it produces replication traffic. ## Why lazy alone is not enough If a key is never touched again, lazy expiration never runs, and the key would leak forever. A cache full of one-shot keys would grow without bound. Hence the second mechanism: a background **active expiration cycle** that samples random keys from the expires table on a schedule, deletes the expired ones, and repeats while it keeps finding many. Together the two bound both correctness (lazy) and memory (active). ## What you can still observe Until the reclaim happens: - `used_memory` still includes the key and its value. - `DBSIZE` and `INFO keyspace` counters can still include it, so a database can report more keys than are logically readable. - A keyspace scan may still surface the name, while the very next GET on it returns nil. - The `expired` event has not fired yet, so anything triggered by that event has not run. This is normal and not a bug. It becomes a problem only when a large number of keys expire and nothing ever reads them, so memory stays high until the sampling cycle grinds through them — or, under maxmemory pressure, until the eviction path steps in for a different reason. ## The takeaway to say out loud "Redis never serves an expired value, but it does not free it on the tick. It deletes on access, plus a background sampler; memory and key counts can lag the TTL."

  • Can a client ever read a value whose time-to-live has already elapsed?
    No. Every key lookup checks the deadline before serving, so an expired key is reported as missing even if its memory has not been reclaimed. The only nuance is a replica that has not yet received the master's DEL: it still masks the key on reads, so clients never see the stale value there either.
  • If a key with a TTL is never accessed again, what eventually frees it?
    The active expiration cycle, which repeatedly samples random keys from the table of keys that have a TTL and deletes the expired ones. Under maxmemory pressure the eviction path can also remove it, but that is a different mechanism with different policy rules.

Like a fridge with dated food: the yoghurt is off the moment the date passes (nobody will eat it), but it only leaves the fridge when someone reaches for it or when you do a random shelf sweep.

saying these in an interview costs you the question

  • Claiming Redis sets a timer or callback per key that fires at the TTL
  • Claiming an expired key can still be returned by GET until it is reclaimed
  • Assuming used_memory or DBSIZE drops the instant TTLs elapse
  • Thinking the application must delete expired keys itself
  • Confusing expiration (TTL elapsed) with eviction (maxmemory pressure)

context

open as a page

What do the Redis commands `TTL` and `PTTL` return for a key that exists but has no expiry, versus a key that does not exist at all — and how do you remove an expiry without deleting the key?

level: juniorimportance: must knowfreq 58%

basics

~20 s

TTL returns the remaining seconds, -1 if the key exists with no expiry, and -2 if the key does not exist. PTTL is the same in milliseconds. PERSIST key removes the expiry and keeps the value, returning 1 if an expiry was removed, 0 otherwise.

open as a page

Redis's allkeys-lru eviction policy is documented as an *approximated* LRU. When Redis needs to free memory, how does it actually pick a victim key, and what does the maxmemory-samples setting control?

level: middleimportance: must knowfreq 52%

basics

~20 s

Redis keeps no global LRU list. When memory exceeds maxmemory it picks maxmemory-samples random keys (default 5), reads the last-access timestamp stored in each key's object header, and evicts the idlest. More samples means better accuracy and more CPU.

open as a page

Redis can publish an event whenever a key changes, but the feature ships disabled. How do you turn Redis keyspace notifications on, and what do the individual flag characters in the notify-keyspace-events configuration value mean?

level: middleimportance: must knowfreq 40%

basics

~20 s

Set notify-keyspace-events to a string of flags; empty means off. You must include K (keyspace channels) and/or E (keyevent channels) plus at least one event class such as g generic, $ string, l list, x expired, e evicted, or A for all classes. Example: Ex.

open as a page

Besides deleting a key when it is next accessed, Redis runs a background cycle over keys that carry a time-to-live. Walk through how that cycle decides how much work to do, and why its guarantee is probabilistic rather than exact.

level: middleimportance: must knowfreq 52%

basics

~20 s

It repeatedly samples about 20 random keys from the table of keys with a TTL, deletes the expired ones, and loops again if more than 25% of the sample was expired. Time-boxed per run, so it bounds stale keys statistically, not exactly.

open as a page

A Redis instance is using far more memory than expected. Which built-in commands and redis-cli modes would you use to find out where the memory went, and what does each of them actually measure?

level: middleimportance: must knowfreq 55%

basics

~20 s

INFO memory for the totals, MEMORY STATS for the breakdown and bytes-per-key, MEMORY DOCTOR for hints, MEMORY USAGE <key> for one key's bytes, and redis-cli --bigkeys (largest by element count) or --memkeys (largest by bytes) to find offenders. Never KEYS *.

open as a page

A Redis cache is configured with `maxmemory-policy volatile-lru`, but the application only sets a TTL on a small fraction of its keys. What happens when the instance fills up, and how do the `allkeys-*` and `volatile-*` eviction policy families differ in general?

level: middleimportance: must knowfreq 55%

basics

~20 s

volatile-* policies only consider keys that have a TTL. If too few keys are volatile, Redis runs out of eviction candidates and behaves like noeviction — writes fail with OOM errors even though most of the keyspace is untouched. allkeys-* policies consider every key.

open as a page

A Redis instance reaches the `maxmemory` limit configured in its config file, and the deployment never changed `maxmemory-policy` from its default. What happens to the commands clients send after that point, and what error do they see?

level: middleimportance: must knowfreq 68%

basics

~20 s

The default policy is noeviction: Redis deletes nothing. Reads still work, but any command that would use more memory is rejected with an error starting "OOM command not allowed when used memory > 'maxmemory'". Keys with TTLs still expire normally.

open as a page

In Redis, a key was given an expiry and the application later updates its value; sometimes the expiry survives and sometimes it silently disappears. What is the rule, and what does the `KEEPTTL` option of the `SET` command do?

level: middleimportance: must knowfreq 60%

basics

~20 s

Commands that replace the whole value — notably SET — clear the TTL, making the key permanent. Commands that modify a value in place (APPEND, INCR, LPUSH, HSET, SADD, ZADD) keep it. SET key value KEEPTTL (Redis 6.0+) replaces the value while retaining the existing expiry.

open as a page

An engineer proposes building a delayed-job scheduler by writing a Redis key with a TTL and running the job when its 'expired' keyspace notification arrives. What are the failure modes of that design, and what would you build instead?

level: seniorimportance: must knowfreq 46%

basics

~20 s

Keyspace notifications are fire-and-forget Pub/Sub: no persistence, no acknowledgement, no redelivery. A disconnected, slow, restarting or newly deployed consumer loses events permanently, and every subscriber gets a copy, so jobs are silently dropped or run twice. Use a sorted set of due timestamps polled atomically instead.

open as a page

Explain what actually goes wrong in a production Redis deployment when one key holds tens of millions of elements or a value of several hundred megabytes, and how you would remodel that data.

level: seniorimportance: must knowfreq 50%

basics

~20 s

Redis serves commands on one thread, so any whole-key operation on a giant key - reading it, deleting it, migrating it, replicating it - stalls every other client. It also unbalances shards and cannot be split. Fix by sharding the key and reading it in cursor-based chunks.

open as a page

Redis publishes key events on two families of channels, __keyspace@<db>__:<key> and __keyevent@<db>__:<event>. What is the difference between them, and which would you subscribe to in order to react to every key expiring in database 0?

level: juniorimportance: should knowfreq 30%

basics

~20 s

A keyspace channel is named after the key and its message is the event name; a keyevent channel is named after the event and its message is the key name. To catch every expiry in database 0 subscribe to keyevent@0:expired.

open as a page

Redis stores each value's recency in a 24-bit field inside the object header. What exactly is stored there, at what resolution, and what happens when that counter wraps around?

level: middleimportance: should knowfreq 26%

basics

~20 s

Under an LRU policy the 24-bit field holds a coarse clock value in seconds, taken from a cached server clock refreshed about once per second. Idle time is now minus that value. Twenty-four seconds-resolution bits wrap after roughly 194 days, and the idle-time calculation compensates for the wrap.

open as a page

Redis 7.0 added the `NX`, `XX`, `GT` and `LT` options to the `EXPIRE` command family. What does each one do, and which real problems do they solve that a plain `EXPIRE` cannot?

level: middleimportance: should knowfreq 34%

basics

~20 s

NX sets the expiry only if the key has none; XX only if it already has one; GT only if the new deadline is later than the current one; LT only if it is earlier. A key with no expiry counts as infinite, so GT never applies to it and LT always does.

open as a page

Since Redis 3.0 the eviction code keeps a persistent pool of candidate keys instead of simply evicting the best key of each random sample. How does that pool work, and what problem did it fix?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Redis keeps a small array of the best eviction candidates seen so far, sorted by idle time. Each round's random sample is merged into that pool, and the pool's best entry is evicted. The pool remembers good candidates across rounds, so quality no longer depends on one lucky sample.

open as a page

With the allkeys-lfu maxmemory policy, Redis tracks each key's access frequency in only 8 bits. How can 8 bits represent millions of accesses, and what does the lfu-log-factor setting control?

level: seniorimportance: should knowfreq 36%

basics

~20 s

The 8 bits hold a probabilistic logarithmic counter, not a hit count. Each access increments it only with probability decreasing as the counter grows, controlled by lfu-log-factor, so the counter saturates at 255 after millions of hits. OBJECT FREQ reads it.

open as a page

A teammate reports that the Redis 'expired' notification for a key with a 10-second TTL sometimes arrives minutes late. Why does that happen, and what determines the moment the event is actually published?

level: seniorimportance: should knowfreq 34%

basics

~20 s

The event is published when the key is actually deleted, not when its TTL logically elapses. Deletion happens either when something touches the key or when the background expiry cycle happens to sample it, so an untouched key in a large keyspace can wait far beyond its TTL.

open as a page

In a Redis primary-replica setup, how does a key whose time-to-live has elapsed actually get removed on the replica, and what surprising behaviour does that cause when you read from or measure that replica?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Replicas never expire keys on their own. They wait for the primary to send an explicit DEL/UNLINK when it reclaims the key. Meanwhile a replica read masks the key as missing, so its key count and memory can exceed the primary's.

open as a page

INFO memory on one Redis instance reports mem_fragmentation_ratio of 1.9, and on another it reports 0.7. Interpret each of those numbers and say what you would do about them.

level: seniorimportance: should knowfreq 42%

basics

~20 s

The ratio is resident memory divided by allocated memory. 1.9 means the process holds nearly twice what Redis uses - real fragmentation or transient overhead; consider activedefrag or a restart. Below 1, like 0.7, means part of the process is swapped to disk, which is a latency emergency.

open as a page

For a Redis instance acting as a cache, how do you choose between the `allkeys-lru`, `allkeys-lfu` and `allkeys-random` values of `maxmemory-policy`? Give a workload where each is the better pick.

level: seniorimportance: should knowfreq 52%

basics

~20 s

LRU suits recency-driven traffic but a batch scan can flush the hot set. LFU keeps stable popular keys and resists one-off scans, at the cost of adapting slower to shifting popularity. Random is cheapest and fine when access is near-uniform or keys are equally valuable.

open as a page

When would you use the Redis command `EXPIREAT` (or `PEXPIREAT`) instead of `EXPIRE`, and how does Redis represent a key's expiry internally with respect to time?

level: seniorimportance: should knowfreq 30%

basics

~20 s

EXPIREAT takes an absolute Unix timestamp rather than a relative duration. Redis stores every expiry as an absolute millisecond timestamp anyway — EXPIRE is converted internally — and replicates and persists it as PEXPIREAT, so deadlines survive restarts and never drift on replicas.

open as a page

One Redis instance holds both a disposable read cache and data the application cannot regenerate — login sessions, a dedupe set, and a pending-job list. How do you set `maxmemory` and `maxmemory-policy` for it, and what would you change about the architecture?

level: principalimportance: should knowfreq 38%

basics

~20 s

No single policy is safe for both: allkeys-* can delete sessions and queued jobs, noeviction turns cache growth into write outages. Split into separate instances. If you cannot, use volatile-* with enforced TTLs on cache keys only, and keep 25–30% RAM headroom.

open as a page

In a Redis primary–replica setup where both nodes have `maxmemory` configured, does a replica evict keys on its own when it reaches the limit? Explain what actually causes data to disappear from a replica.

level: seniorimportance: nice to knowfreq 28%

basics

~10 s

No. By default a replica ignores its own maxmemory for replicated data (replica-ignore-maxmemory yes). The primary evicts and propagates an explicit DEL/UNLINK down the replication stream; the replica just applies it, keeping datasets identical.

open as a page

How does Redis keep track of which keys carry an expiry, what does that bookkeeping cost, and can an individual field inside a hash expire on its own?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Each database holds a second dictionary, expires, mapping keys that have a TTL to an absolute millisecond deadline — so a volatile key costs an extra hash-table entry. Expiry is per top-level key; Redis 7.4 added HEXPIRE for per-field hash TTLs, and before that you emulated it.

open as a page

You run Redis as a cache with the allkeys-lfu policy and want eviction to follow the real access pattern rather than yesterday's. How do lfu-decay-time, lfu-log-factor and maxmemory-samples interact, and how would you tune them for a workload?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

lfu-decay-time (minutes) is how fast an untouched counter decays so once-hot keys can fall out; lfu-log-factor sets how many accesses fit on the 0-255 scale; maxmemory-samples sets how many candidates each eviction round inspects. Tune by sampling OBJECT FREQ on known hot and cold keys.

open as a page