•5 min read

Redis OOM Errors: How to Pick the Right maxmemory-policy

Last Tuesday one of my side projects started failing in weird places. The checkout endpoint, session refresh, a background job that syncs inventory. Different routes, same line buried in the logs: OOM command not allowed when used memory > 'maxmemory'.

If you run Redis long enough, you will meet this error. It means something very specific, and the fix is usually a config change, not a fire drill. But the obvious fix, grabbing a policy name off a checklist, can quietly do nothing. I learned that at 11pm. Twice, because the first fix didn't take.

Network switch with ethernet cables plugged into ports in a server rack

What the error actually means

Redis has a memory limit called maxmemory. When used_memory reaches it, Redis either evicts keys to make room or refuses writes. Which of those happens is decided by maxmemory-policy, and the default is noeviction. Reads keep working, which is why the site looked mostly fine while anything that wrote to Redis got rejected. Sessions, counters, cache fills. All erroring.

One thing worth knowing: if you never set maxmemory, you won't see this error at all. Stock Redis runs with no limit until the Linux OOM killer shoots the process. Getting the polite error means someone set a limit. That someone was me, on a 2GB VPS, about a year ago, and I'd forgotten.

Check what you're actually running

First, look at the current state:

redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policy

Mine said 536870912 bytes, so 512mb, and noeviction. Then the check most people skip, the one that decides everything:

redis-cli INFO keyspace

The output looked like db0:keys=184203,expires=91102,avg_ttl=.... Roughly half my keys had no expiry set. Remember that number. It's about to matter.

The volatile-lru trap

My first fix was volatile-lru, because every guide calls it the safe choice: it only evicts keys that have a TTL, so permanent data can't be touched. Sensible, right? I applied it and writes kept failing.

Here's the catch. volatile-lru can only evict keys with an expiry. My memory was mostly held by keys with no TTL, so Redis had nothing it was allowed to remove and behaved exactly like noeviction. Same OOM errors, new policy name. Every volatile-* policy has this failure mode: with memory held by keys without expiries, it silently degrades to refusing writes.

So the real fix was never a policy change. It was admitting that half my "cache" keys were written with a plain SET and no EXPIRE, which is a code bug wearing a config problem's clothes.

Picking a policy on purpose

Redis has eight eviction policies and they all reduce to one question: when memory runs out, what are you allowed to lose?

For a pure cache, allkeys-lru is the default answer for a reason. It evicts the least recently used key regardless of TTL, and the official Redis observability guide recommends it when you expect a power-law in access patterns, which describes almost every web cache I've ever seen. A small set of hot keys gets read constantly, everything else is filler.

If a few very hot keys dominate your access, allkeys-lfu is worth a look. It counts frequency instead of recency, so a key that was read ten thousand times today won't get evicted just because something else was read five seconds ago. It's been available since Redis 4.0.

The awkward case is mixed data, one Redis holding both disposable cache and stuff you can't lose. The honest answer from the Redis docs is: run two instances, one for cache with allkeys-lru and one for data with noeviction. The volatile-* policies exist for exactly this mixed case on a single instance, but they only work if every cache key gets a TTL and every durable key doesn't. I clearly didn't. Two small instances beat one clever instance.

Small print that matters: Redis LRU is not true LRU. It approximates by sampling keys, five by default. CONFIG SET maxmemory-samples 10 gets closer to real LRU at some CPU cost. For most workloads the default is fine, but if eviction picks seem odd sometimes, that's why.

Make it stick, then watch one number

The fix itself takes three commands and no restart:

redis-cli CONFIG SET maxmemory 1gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
redis-cli CONFIG REWRITE

CONFIG SET applies immediately on a live server. CONFIG REWRITE writes the change back to your config file, which matters, because a restart would otherwise undo everything. If you prefer editing files, the lines in redis.conf look like this:

maxmemory 1gb
maxmemory-policy allkeys-lru

Don't set maxmemory to your full physical RAM. Persistence forks the process for RDB saves and AOF rewrites, and that fork needs copy-on-write headroom, plus replication buffers on top. I leave roughly 25 percent of RAM unclaimed and sleep fine.

After it's live, watch a single number:

redis-cli INFO stats | grep evicted_keys

Zero with no OOM errors means healthy. Climbing steadily means your cache is too small for its working set and Redis is eating your hit rate. That's your signal to raise the limit or shorten TTLs. If you're also tracking hit rate, I went deeper on keyspace_hits and cache patterns in my Redis caching patterns post.

And the original culprit in my story? The inventory sync job. It wrote cache keys with no expiry, they accumulated for months, and no eviction policy could save me from that. One added EXPIRE, one config change, and it's been quiet since. The diagnostic commands from this post live in my Snippet Ark library now, because at 11pm with a production error I don't want to reconstruct them from memory.