Skip to content
Knowledge Hub

Redis expiration and eviction: did the key vanish because it expired or because memory ran out? Basic

Estimated reading time: 8 min read

An API reads orders from PostgreSQL and keeps a copy in Redis for 10 minutes. One afternoon database load rises because of more cache misses, and the key order:42 disappears before its expiry.

The hasty conclusion “Redis’s TTL is broken” is usually wrong: a key can vanish through two independent paths.

Expiration Eviction
Cause The key’s expiry time has passed Redis hit maxmemory and needs to make room
Can it happen before the TTL? No Yes
Metric expired_keys evicted_keys
Fix Tune the TTL, add jitter Add RAM, shrink big keys, change the policy

Raising the TTL while Redis is evicting because of memory pressure only makes things worse.

Setting Default Consequence
maxmemory 0 (unlimited, on 64-bit systems) Never evicts; Redis grows until the OS OOM killer steps in
maxmemory-policy noeviction When full: writes are rejected, reads still work
maxmemory-samples 5 LRU/LFU is only approximate

So a Redis used as a cache without maxmemory set never evicts, and setting maxmemory without changing the policy returns write errors when full. A pure cache needs both set.

TTL is a key’s remaining lifetime. SET key v EX 600 writes the value with an expiry; TTL returns -1 if the key exists without an expiry and -2 if the key does not exist.

An overwriting SET clears the key’s TTL (use KEEPTTL to keep it), while INCR, LPUSH, HSET leave the TTL intact.

  1. Lazy (passive): a key is deleted when a command accesses it and finds it expired.
  2. Active: per Redis’s latency documentation, a cycle runs 10 times per second; each cycle samples 20 keys with a TTL, deletes the expired ones, and repeats if more than 25% of the sample had expired.

Consequence: an expired key can keep occupying RAM for a while, and if a great many keys expire in the same second (25% or more of keys with a TTL), Redis can block while cleaning up. This is why TTLs need jitter.

The master deletes a key and then sends a DEL to replicas; a replica does not expire keys independently (but it keeps enough expiry information to take over when promoted to master). Consequences:

  • While waiting for the DEL to arrive, an expired key still occupies memory on the replica.
  • Per Redis’s replication documentation, replicas use a logical clock to report the key as nonexistent for reads, so clients reading from a replica usually don’t see expired values.

Conversely, per redis.conf, since Redis 5 replicas ignore their own maxmemory by default (replica-ignore-maxmemory yes); eviction happens on the master.

Policy Candidates Notes
noeviction (default) None Writes are rejected when full
allkeys-lru All keys Approximate LRU; Redis recommends it as the default choice absent other reasons
allkeys-lfu All keys Keeps frequently accessed keys; tune with lfu-log-factor (10), lfu-decay-time (1 minute)
allkeys-random All keys Fits when keys are accessed evenly
volatile-lru/lfu/random Only keys with a TTL Fits when one instance holds both cache and persistent keys
volatile-ttl Keys with a TTL Evicts keys closest to expiry first

Every volatile-* policy behaves like noeviction when no keys with a TTL remain. maxmemory-samples (default 5) is the number of keys sampled per eviction; raising it to 10 approximates true LRU better at a small CPU cost.

Workload Policy Reason
Pure cache, a few hot keys allkeys-lru (or allkeys-lfu) Exploits skewed access distribution
Pure cache, uniform access allkeys-random No “hot” keys to favor
Cache mixed with sessions/persistent keys in one instance Split instances; if unavoidable, volatile-* allkeys-* can evict sessions too
Queues, locks, data that must not be lost noeviction Better a write error than silent data loss
TTL and the memory limit are two different causes of lost keys
TTL and the memory limit are two different causes of lost keysStep 1/4: The application writes a cache key with a TTL. Redis stores the value and its expiration time.ClientRedisMemorySET EX
Client → RedisSET EX
Step 1/4

The application writes a cache key with a TTL. Redis stores the value and its expiration time.

Timeline of a key set with SET EX 2:

Point Observation Note
T0 Key has a value, TTL about 2
T0 + 1 s Still there, or already evicted under memory pressure From the application’s view, eviction looks like expiration
T0 + 2.3 s GET returns nil, TTL = -2 Whether the bytes are freed early or late is internal

The TTL is an upper bound on staleness, not a guarantee of fresh data within that window. The order can change in PostgreSQL right after the cache write; see cache-aside.

Loading diagram…

Commands to run:

Terminal window
redis-cli INFO stats | grep -E "expired_keys|evicted_keys|keyspace_hits|keyspace_misses"
redis-cli INFO memory | grep -E "used_memory_human|maxmemory_human|mem_not_counted_for_evict"
redis-cli CONFIG GET maxmemory-policy

Hit rate = keyspace_hits / (keyspace_hits + keyspace_misses). Don’t add expired_keys + evicted_keys and call it the miss count: an expired key nobody reads causes no miss, while one evicted hot key causes thousands.

Terminal window
docker compose -f examples/docker-compose.yml up -d redis
npx --prefix examples ts-node examples/src/redis-expiration/ttl-demo.ts
docker compose -f examples/docker-compose.yml stop redis

The script does three steps:

  1. Writes a key that lives 2 seconds, prints the value and TTL (before: cached-order, TTL near 2).
  2. Waits 2.3 seconds and reads it again (after: null TTL: -2).
  3. Prints the container’s maxmemory-policy, usually noeviction per the defaults above.

The demo only proves expiration; it does not push Redis to maxmemory. To see eviction, use a separate test Redis with a small maxmemory and read INFO stats; don’t CONFIG SET maxmemory on an instance serving real data.

What TTLs and policies give you

  • A TTL bounds a cache entry’s lifetime and automatically cleans up data nobody reads
  • Eviction lets Redis manage memory itself when the cache grows beyond expectations
  • Multiple policies let you favor hot keys or keys close to expiry

Limits to plan for

  • Long TTLs raise the risk of stale data; short TTLs raise misses and DB load
  • Eviction can delete keys that still have TTL left, dragging the hit rate down
  • The wrong policy for a mixed workload can evict important data or make writes fail
  1. Set maxmemory explicitly and leave RAM for replication/AOF buffers (see mem_not_counted_for_evict in INFO memory); these buffers do not count toward maxmemory.
  2. Set maxmemory-policy to match the workload; don’t leave the default noeviction on a pure cache.
  3. Don’t mix cache with data that must not be lost in one instance; split instances.
  4. Jittered TTLs for keys loaded in bulk.
  5. Alert on the growth rate of evicted_keys and on hit rate, not only on used_memory.
  6. Replicas: expired keys occupy memory until the master’s DEL arrives (reads on the replica already report them as nonexistent); monitor replica memory.
  7. Redis must not be the only store of business data if eviction is enabled.

Self-check: (1) evicted_keys rises while many keys still have TTL left: does raising the TTL help? (2) Why can volatile-ttl make writes fail even when keys without a TTL remain? (3) Why is an overwriting SET a TTL trap?