Persistence, eviction and memory
The two questions behind every Redis production incident: what happens on restart, and what happens when it fills up. They have different answers and people routinely conflate them.
RDB and AOF
| RDB | AOF | |
|---|---|---|
| What it stores | periodic snapshot | every write command |
| Restart cost | fast load | slower replay |
| Worst-case loss | since last snapshot | 1 second (everysec) |
| File size | compact | larger, rewritten periodically |
| Cost while running | fork, copy-on-write | append, fsync |
# redis.conf — both, which is the usual production answer
save 900 1 # snapshot if ≥1 key changed in 15 min
save 300 10
appendonly yes
appendfsync everysec # always | everysec | noappendfsync always is the only setting that survives a power cut with no
loss, and it costs an fsync per write. everysec is the default because the
trade is right for almost everyone: bounded loss of one second.
Gotcha: the RDB fork is the surprise. Saving copies the page table and copy-on-write duplicates pages as they change, so a write-heavy 8 GB instance can spike well past 8 GB during a snapshot and get OOM-killed. Size the box for the fork, not for the dataset, or move snapshots to a replica.
None of this makes Redis durable in the database sense. Replication is asynchronous, so a failover promotes a replica that may be missing the last writes. Say that plainly — it is the answer to “can we use Redis as the source of truth” and the answer is no.
Eviction: only fires when maxmemory is set
maxmemory 4gb
maxmemory-policy allkeys-lruThe default policy is noeviction, and that is the single most common
production misconfiguration: memory fills, and writes start failing with OOM
errors while reads keep working. A cache should almost never be noeviction.
| Policy | Evicts |
|---|---|
noeviction |
nothing — writes error (default) |
allkeys-lru |
least recently used, any key |
allkeys-lfu |
least frequently used, any key |
volatile-lru |
LRU, only keys with a TTL |
volatile-ttl |
soonest to expire |
allkeys-random |
a random key |
Choosing between them:
allkeys-lrufor a pure cache. Safe default.allkeys-lfuwhen access is skewed — LFU keeps the genuinely hot keys instead of whatever a scan touched last.volatile-*only when the instance mixes cache and persistent data, and then the persistent keys must have no TTL. If nothing has a TTL, avolatile-*policy evicts nothing and behaves likenoeviction.
Redis’s LRU and LFU are approximate — sampled, not exact — which is a deliberate trade and worth knowing.
Expiry is lazy plus sampled
A key with a TTL is not deleted at that instant. It goes when something touches
it, or when the background cycle samples it. So INFO memory can show memory
held by keys that are logically gone, and DBSIZE can overcount.
Two consequences: never infer “the cache is full of live data” from memory alone, and set TTLs with jitter so expiry work is spread rather than bunched.
Watching the right numbers
redis-cli INFO memory | grep -E 'used_memory_human|maxmemory_human|fragmentation'
redis-cli INFO stats | grep -E 'evicted_keys|keyspace_hits|keyspace_misses'
# sampled: what is actually large
redis-cli --bigkeys
redis-cli MEMORY USAGE key| Signal | Means |
|---|---|
evicted_keys rising |
undersized, or TTLs too long |
| hit rate falling | eviction is throwing away useful data |
mem_fragmentation_ratio > 1.5 |
fragmentation; consider activedefrag |
used_memory ≈ maxmemory |
steady state for a cache — not an alarm |
The one to alert on is the hit rate, not memory. A cache at 100% of
maxmemory with a 95% hit rate is working exactly as intended.
Making the data smaller
- Shorten key names.
u:1:sagainstuser:1:sessionis real money at a hundred million keys, since the key is stored per entry. - Prefer a hash to many keys. Small hashes use a compact encoding
(
hash-max-listpack-entries), souser:1as one hash beats tenuser:1:fieldstrings. - Compress large values in the client, and store bytes.
- Set a TTL on everything. A key with no expiry is permanent by accident.
Related
Interview angle 7
- “RDB or AOF?” - usually both. RDB is a compact periodic snapshot that loads fast; AOF logs every write and bounds loss to a second at
everysec. AOF alone replays slowly on restart, RDB alone loses everything since the last snapshot. - “What’s the hidden cost of RDB?” - the fork. Copy-on-write duplicates pages as they are written, so a write-heavy instance can spike to nearly double its dataset size during a snapshot and get OOM-killed. Size for the fork or snapshot from a replica.
- “Is Redis durable?” - not in the database sense. Replication is asynchronous, so a failover promotes a replica that may be missing recent writes. It is a cache or a loss-tolerant store, not a system of record.
- “What’s the default eviction policy?” -
noeviction, which is the most common production misconfiguration: memory fills and writes start failing while reads keep working. A cache wantsallkeys-lru, orallkeys-lfuwhen access is skewed. - “When would
volatile-lruevict nothing?” - when no key has a TTL. Thevolatile-*policies only consider keys with an expiry, so on an instance where nothing expires they behave exactly likenoeviction. - “What do you alert on?” - the hit rate and
evicted_keys, not memory usage. A cache sitting at 100% ofmaxmemorywith a high hit rate is working correctly; a falling hit rate means eviction is discarding data you needed. - “Why does memory not drop when keys expire?” - expiry is lazy plus sampled, not immediate. A key goes when something touches it or the background cycle finds it, so memory and
DBSIZEboth lag reality.