[{"data":1,"prerenderedAt":6},["ShallowReactive",2],{"post-content-redis-maxmemory-policy-eviction-guide":3},{"content":4,"lastModified":5},"\u003Cp>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: \u003Ccode>OOM command not allowed when used memory &gt; 'maxmemory'\u003C\u002Fcode>.\u003C\u002Fp>\n\n\u003Cp>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.\u003C\u002Fp>\n\n\u003Cimg src=\"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1544197150-b99a580bb7a8?w=1200&q=80&auto=format&fit=crop\" alt=\"Network switch with ethernet cables plugged into ports in a server rack\" loading=\"lazy\">\n\n\u003Ch2>What the error actually means\u003C\u002Fh2>\n\n\u003Cp>Redis has a memory limit called \u003Ccode>maxmemory\u003C\u002Fcode>. When \u003Ccode>used_memory\u003C\u002Fcode> reaches it, Redis either evicts keys to make room or refuses writes. Which of those happens is decided by \u003Ccode>maxmemory-policy\u003C\u002Fcode>, and the default is \u003Ccode>noeviction\u003C\u002Fcode>. 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.\u003C\u002Fp>\n\n\u003Cp>One thing worth knowing: if you never set \u003Ccode>maxmemory\u003C\u002Fcode>, 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.\u003C\u002Fp>\n\n\u003Ch2>Check what you're actually running\u003C\u002Fh2>\n\n\u003Cp>First, look at the current state:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-bash\">redis-cli CONFIG GET maxmemory\nredis-cli CONFIG GET maxmemory-policy\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Mine said 536870912 bytes, so 512mb, and \u003Ccode>noeviction\u003C\u002Fcode>. Then the check most people skip, the one that decides everything:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-bash\">redis-cli INFO keyspace\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>The output looked like \u003Ccode>db0:keys=184203,expires=91102,avg_ttl=...\u003C\u002Fcode>. Roughly half my keys had no expiry set. Remember that number. It's about to matter.\u003C\u002Fp>\n\n\u003Ch2>The volatile-lru trap\u003C\u002Fh2>\n\n\u003Cp>My first fix was \u003Ccode>volatile-lru\u003C\u002Fcode>, 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.\u003C\u002Fp>\n\n\u003Cp>Here's the catch. \u003Ccode>volatile-lru\u003C\u002Fcode> 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 \u003Ccode>noeviction\u003C\u002Fcode>. Same OOM errors, new policy name. Every \u003Ccode>volatile-*\u003C\u002Fcode> policy has this failure mode: with memory held by keys without expiries, it silently degrades to refusing writes.\u003C\u002Fp>\n\n\u003Cp>So the real fix was never a policy change. It was admitting that half my \"cache\" keys were written with a plain \u003Ccode>SET\u003C\u002Fcode> and no \u003Ccode>EXPIRE\u003C\u002Fcode>, which is a code bug wearing a config problem's clothes.\u003C\u002Fp>\n\n\u003Ch2>Picking a policy on purpose\u003C\u002Fh2>\n\n\u003Cp>Redis has eight eviction policies and they all reduce to one question: when memory runs out, what are you allowed to lose?\u003C\u002Fp>\n\n\u003Cp>For a pure cache, \u003Ccode>allkeys-lru\u003C\u002Fcode> is the default answer for a reason. It evicts the least recently used key regardless of TTL, and the \u003Ca href=\"https:\u002F\u002Fredis.io\u002Flearn\u002Fredis-software-observability-playbook\" rel=\"noopener noreferrer\" target=\"_blank\">official Redis observability guide\u003C\u002Fa> 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.\u003C\u002Fp>\n\n\u003Cp>If a few very hot keys dominate your access, \u003Ccode>allkeys-lfu\u003C\u002Fcode> 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.\u003C\u002Fp>\n\n\u003Cp>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 \u003Ccode>allkeys-lru\u003C\u002Fcode> and one for data with \u003Ccode>noeviction\u003C\u002Fcode>. The \u003Ccode>volatile-*\u003C\u002Fcode> 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.\u003C\u002Fp>\n\n\u003Cp>Small print that matters: Redis LRU is not true LRU. It approximates by sampling keys, five by default. \u003Ccode>CONFIG SET maxmemory-samples 10\u003C\u002Fcode> 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.\u003C\u002Fp>\n\n\u003Ch2>Make it stick, then watch one number\u003C\u002Fh2>\n\n\u003Cp>The fix itself takes three commands and no restart:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-bash\">redis-cli CONFIG SET maxmemory 1gb\nredis-cli CONFIG SET maxmemory-policy allkeys-lru\nredis-cli CONFIG REWRITE\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>\u003Ccode>CONFIG SET\u003C\u002Fcode> applies immediately on a live server. \u003Ccode>CONFIG REWRITE\u003C\u002Fcode> 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 \u003Ccode>redis.conf\u003C\u002Fcode> look like this:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-bash\">maxmemory 1gb\nmaxmemory-policy allkeys-lru\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>Don't set \u003Ccode>maxmemory\u003C\u002Fcode> 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.\u003C\u002Fp>\n\n\u003Cp>After it's live, watch a single number:\u003C\u002Fp>\n\n\u003Cpre>\u003Ccode class=\"language-bash\">redis-cli INFO stats | grep evicted_keys\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Cp>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 \u003Ccode>keyspace_hits\u003C\u002Fcode> and cache patterns in my \u003Ca href=\"\u002Fposts\u002Fredis-caching-patterns-web-developers\u002F\">Redis caching patterns post\u003C\u002Fa>.\u003C\u002Fp>\n\n\u003Cp>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 \u003Ccode>EXPIRE\u003C\u002Fcode>, one config change, and it's been quiet since. The diagnostic commands from this post live in my \u003Ca href=\"\u002Fsnippetark\u002F\">Snippet Ark\u003C\u002Fa> library now, because at 11pm with a production error I don't want to reconstruct them from memory.\u003C\u002Fp>\n","2026-11-01",1793589940173]