Memory Management and Eviction Policies
Redis stores every key and value in RAM, which is what makes it so fast — but it also means the data you keep in Redis can never exceed the memory available to the server. Left unmanaged, a growing keyspace will eventually exhaust that memory and crash the process or trigger the operating system’s out-of-memory killer. Redis addresses this with a configurable memory ceiling (maxmemory) and a set of eviction policies that decide which keys to remove once that ceiling is reached. Understanding how Redis tracks memory, encodes values compactly, and expires or evicts keys is essential for running it reliably in production.
Overview: How Redis Uses Memory
Every key costs more than just the bytes of the value you gave it. Internally, each key lives in a hash table (the main dict) mapping the key name to a Redis object that wraps the value, plus bookkeeping for its type, its encoding, and — if one is set — its expiration time. That per-key overhead is why storing millions of tiny keys can use far more memory than the raw payload size suggests.
Redis also chooses different internal encodings for the same logical type depending on size, to save memory. A short string that looks like an integer uses an int encoding (no character buffer at all); a short string under 44 bytes uses a compact embstr encoding allocated in one memory block; anything larger falls back to a raw encoding with a separately allocated buffer. Lists, hashes, sets, and sorted sets follow the same idea: small collections use compact encodings such as listpack, and Redis automatically switches to the standard, more memory-hungry encoding once a collection crosses a size threshold. You can inspect a key’s current encoding with OBJECT ENCODING and estimate the memory a specific key occupies with MEMORY USAGE.
By default Redis has no memory limit — it keeps allocating until the OS refuses more, at which point the process can be killed and unsaved data lost. In production you set a hard ceiling with maxmemory. Once used_memory (visible in INFO memory) reaches that ceiling, behavior is governed by maxmemory-policy: either Redis refuses further writes with an out-of-memory error, or it evicts existing keys using an approximation of least-recently-used (LRU) or least-frequently-used (LFU) order — an approximation because exact tracking would cost extra memory and CPU on every access. This is distinct from, and often confused with, TTL-based expiration: a key with EXPIRE set is removed lazily (checked and deleted the next time something accesses it) or actively (a background cycle samples keys with TTLs several times per second and deletes any that have expired), regardless of whether maxmemory is even configured.
Syntax
Memory limits and eviction behavior are set with CONFIG SET (or persistently in redis.conf):
CONFIG SET maxmemory <bytes>
CONFIG SET maxmemory-policy <policy>
maxmemory— the memory ceiling in bytes; suffixes like100mbor2gbare accepted.0means no limit (the default).maxmemory-policy— what Redis does oncemaxmemoryis reached. See the table below.
Commands you’ll use constantly when working with memory:
| Command | Purpose | Time Complexity |
|---|---|---|
MEMORY USAGE key |
Estimated bytes a key (value + overhead) occupies | O(1) for a plain string, O(N) for aggregate types |
OBJECT ENCODING key |
Internal encoding used for that key’s value | O(1) |
DBSIZE |
Number of keys in the current database | O(1) |
TTL key |
Seconds until expiry (-1 no TTL, -2 key doesn’t exist) |
O(1) |
EXPIRE key seconds |
Attach a TTL to an existing key | O(1) |
INFO memory |
Server-wide memory stats (used_memory, fragmentation ratio, etc.) |
O(1) |
Eviction Policies
| Policy | Behavior once maxmemory is hit |
|---|---|
noeviction |
Evicts nothing; writes return an OOM error, reads still work. The default. |
allkeys-lru |
Evicts the least-recently-used key across the whole keyspace. |
allkeys-lfu |
Evicts the least-frequently-used key across the whole keyspace (Redis 4.0+). |
allkeys-random |
Evicts a random key across the whole keyspace. |
volatile-lru |
Evicts the least-recently-used key among keys that have a TTL set. |
volatile-lfu |
Evicts the least-frequently-used key among keys that have a TTL set. |
volatile-random |
Evicts a random key among keys that have a TTL set. |
volatile-ttl |
Evicts the key with the nearest expiration among keys that have a TTL set. |
Every volatile-* policy behaves like noeviction once no key with a TTL remains — you can still hit OOM errors under a volatile-* policy if nothing is expirable.
Examples
Example 1: Inspecting how Redis encodes a value
SET counter:visits 100
OBJECT ENCODING counter:visits
SET user:1001:short "hello"
OBJECT ENCODING user:1001:short
SET user:1001:bio "A pioneer of computing, often regarded as the first programmer in history."
OBJECT ENCODING user:1001:bio
Output:
OK
"int"
OK
"embstr"
OK
"raw"
The integer-looking value gets the most compact int encoding. The short greeting fits under the 44-byte embedded-string limit and gets embstr, allocated in a single block alongside the object header. The long bio exceeds that limit, so Redis falls back to raw, a separately allocated buffer — slightly more overhead, which matters when you have millions of such keys.
Example 2: Checking memory footprint
SET user:1001:name "Ada Lovelace"
MEMORY USAGE user:1001:name
SET session:abc123 "active" EX 3600
TTL session:abc123
DBSIZE
Output:
OK
(integer) 56
OK
(integer) 3600
(integer) 2
MEMORY USAGE reports the approximate bytes Redis has allocated for that single key, including overhead — useful for hunting down surprisingly large keys. DBSIZE confirms both keys landed in the keyspace.
Example 3: How SET interacts with an existing TTL
SET cache:page:home "homepage snapshot v1"
EXPIRE cache:page:home 120
TTL cache:page:home
SET cache:page:home "homepage snapshot v2"
TTL cache:page:home
EXPIRE cache:page:home 120
SET cache:page:home "homepage snapshot v3" KEEPTTL
TTL cache:page:home
Output:
OK
(integer) 1
(integer) 120
OK
(integer) -1
(integer) 1
OK
(integer) 120
The plain SET that overwrote the key with “v2” silently cleared the TTL back to -1 — a very common source of accidental memory leaks. Re-applying EXPIRE and then updating with SET ... KEEPTTL refreshes the value while leaving the existing TTL intact.
Example 4: Configuring a maxmemory ceiling and eviction policy
CONFIG SET maxmemory 100mb
CONFIG SET maxmemory-policy allkeys-lru
CONFIG GET maxmemory
CONFIG GET maxmemory-policy
Output:
OK
OK
1) "maxmemory"
2) "104857600"
1) "maxmemory-policy"
2) "allkeys-lru"
This is a server-wide setting, normally placed permanently in redis.conf rather than issued ad hoc — here it caps the instance at 100MB and tells it to evict the least-recently-used key from anywhere in the keyspace once that ceiling is hit.
How It Works Step by Step
When a write command arrives and maxmemory is configured, Redis does roughly this before executing it, all on its single command-processing thread:
- Check current
used_memoryagainstmaxmemory. - If under the limit, execute the command normally.
- If at or over the limit and the policy isn’t
noeviction, sample a small pool of candidate keys (size tunable viamaxmemory-samples, default 5) from either the whole keyspace (allkeys-*) or only keys with a TTL (volatile-*), score them by the policy’s rule, and evict the best candidate — repeating until enough memory is freed. - If memory still can’t be freed, the write is rejected with an OOM error; reads keep working.
This is why Redis’s LRU and LFU are called approximated: rather than maintaining a perfectly ordered global list of every key by recency, which would cost memory and CPU on every access, Redis keeps a lightweight per-key timestamp (or, for LFU, a probabilistic counter) and only compares a small random sample at eviction time. A larger sample gets closer to true LRU/LFU at the cost of more CPU per eviction.
TTL expiration runs independently of this eviction path. Redis expires keys two ways: lazily, checking a key’s expiration the moment any command touches it and deleting it on the spot if overdue, and actively, via a background cycle running roughly ten times a second that samples a batch of keys with TTLs and deletes any that have expired, repeating immediately if more than 25% of the sample was expired. This guarantees expired keys are eventually reclaimed even if nothing ever reads them again.
Common Mistakes
Mistake 1: Running with no maxmemory limit
Leaving maxmemory at its default of 0 means Redis will keep growing until the host runs out of RAM, at which point the kernel’s OOM killer can terminate the process outright, or Redis can start swapping and become unusably slow. Always set an explicit ceiling sized with headroom below total available RAM (see Example 4).
Mistake 2: Assuming EXPIRE on a missing key does something
GET user:9999:name
EXPIRE user:9999:name 60
EXISTS user:9999:name
Output:
(nil)
(integer) 0
(integer) 0
EXPIRE returns (integer) 0, not an error, when the target key doesn’t exist — it’s easy to miss this and assume a TTL was set. Always check the return value, or create the key first, before relying on the timer.
Mistake 3: Forgetting to set a TTL on cache-style keys
SET cache:report:daily "large computed report payload"
TTL cache:report:daily
Output:
OK
(integer) -1
-1 means this key has no expiration and will live forever unless explicitly deleted or evicted. If it’s genuinely a cache entry, it should carry a TTL from the start: SET cache:report:daily "large computed report payload" EX 3600. Otherwise every cache write accumulates permanently and slowly consumes all available memory.
Mistake 4: Using noeviction on a pure cache workload
After configuring a 100MB ceiling with noeviction, once used_memory reaches that ceiling every further write is rejected instead of making room:
SET cache:session:99999 "payload"
Output:
(error) OOM command not allowed when used memory > 'maxmemory'.
noeviction is the right choice for data you can never silently lose, but it’s the wrong choice for a pure cache — there, the application starts throwing write errors the moment the cache fills, instead of Redis quietly dropping the coldest entries. Switch to allkeys-lru or allkeys-lfu for cache-only databases.
Best Practices
- Always set an explicit
maxmemoryin production — never rely on the default of unlimited growth. - Match the policy to the workload:
allkeys-lru/allkeys-lfufor a pure cache where any key can be safely dropped;volatile-lru/volatile-ttlwhen the same instance mixes must-keep data with expirable cache data;noevictiononly when losing a key is worse than rejecting a write, paired with real capacity monitoring. - Prefer LFU over LRU when access patterns are skewed toward a small set of hot keys — LRU can evict a very popular key just because it wasn’t the single most recent access.
- Monitor
used_memory,maxmemory, andevicted_keysfromINFO memory/INFO statsso eviction pressure is visible before it causes problems. - Set a TTL on every key that represents a cache entry, session, or other transient data — don’t rely on eviction alone to bound memory.
- Use
SCAN, neverKEYS *, to inspect or iterate keys on a production instance —KEYSblocks the single-threaded server for the entire scan. - Use
MEMORY USAGEto find unexpectedly large individual keys rather than guessing where memory is going. - Remember persistence interacts with memory too: RDB snapshotting forks the process and can temporarily increase memory usage under heavy write load, so size
maxmemorywith headroom rather than right up to available RAM.
Practice Exercises
- Set three keys representing a shopping cart, a page-view counter, and a password reset token. Give only the ones that should expire a TTL, then use
TTLon each to confirm which ones have no expiration and which will clean themselves up. - Store a short string and a long (over 44 character) string as two different keys, then run
OBJECT ENCODINGon both. Explain in your own words why Redis picked a different encoding for each. - Decide which
maxmemory-policyyou’d choose for: (a) a session store where losing a session just forces a re-login, (b) an inventory count that must never silently disappear, and (c) a leaderboard cache rebuilt from a database every few minutes. Write out theCONFIG SET maxmemory-policycommand for each.
Summary
- Redis keeps its entire dataset in RAM, so memory is a resource you must budget explicitly with
maxmemory. - Redis uses compact encodings (
int,embstr,listpack) for small values and switches to larger encodings as data grows — check withOBJECT ENCODING. maxmemory-policydecides what happens once the ceiling is hit: reject writes (noeviction) or evict keys using approximated LRU, LFU, TTL-nearest, or random selection.volatile-*policies only ever evict keys that have a TTL set;allkeys-*policies can evict anything.- TTL-based expiration (lazy + active) is a separate mechanism from maxmemory eviction and runs regardless of whether a memory limit is configured.
- A plain
SETon an existing key clears its TTL unless you addKEEPTTL. - Use
MEMORY USAGE,INFO memory, andSCAN— neverKEYS *— to observe and manage memory safely in production.
