In short. When a Redis instance reaches maxmemory it does one of two things, and you choose which with maxmemory-policy: it rejects every write with OOM command not allowed when used memory > 'maxmemory', or it throws away keys to make room for the new one. There is no third way. With noeviction and a limit of 2 MB I saw 6,358 writes out of 30,000 succeed and the other 23,642 fail, including those that had nothing to do with the cache. With allkeys-lru none failed, but out of 2,000 revocation keys with no expiry, 732, 340 and 247 survived in three rounds: Redis deleted the data nobody can rebuild. With volatile-lru, same composition, all 2,000, three rounds out of three. The right policy doesn't depend on load in the abstract: it depends on what coexists in the instance, and that is measured before choosing.
Method. Every command is from 8 November 2026, on throwaway instances started by me with --save "" --appendonly no: Redis 8.0.5 native and 7.4.7 from the image redis/redis-stack-server:latest. Where the two give different numbers I say so. The site's development instance was only read; I did not verify the production configuration, so when I talk about the site I talk about development.
What happens when memory runs out
The default limit is not a limit
The first check is whether a limit exists. On the site's development instance there is none, and the policy is the one that rejects everything:
$ redis-cli -p 6379 info server | grep -E '^redis_version'
redis_version:8.0.5
$ redis-cli -p 6379 config get maxmemory
maxmemory
0
$ redis-cli -p 6379 config get maxmemory-policy
maxmemory-policy
noeviction
$ redis-cli -p 6379 info memory | grep -E '^used_memory_human|^maxmemory_human'
used_memory_human:7.96M
maxmemory_human:0B
The documentation says that maxmemory at zero means no limit on the dataset, and it is the default on 64-bit; on 32-bit there is an implicit limit of 3 GB. Without configuration, then, Redis's limit is the machine's RAM, and the only mechanism left is the operating system's out-of-memory killer: it doesn't know which keys matter, nor that the process is a database.
With noeviction the instance becomes read-only
I set an absurdly low limit, 2 MB, so that the behavior arrives in a few seconds instead of weeks. Thirty thousand 100-byte SET in a pipe, on 8.0.5:
$ redis-cli -p 6395 config set maxmemory 2mb
OK
$ redis-cli -p 6395 config set maxmemory-policy noeviction
OK
$ V=$(printf 'v%.0s' $(seq 1 100))
$ for i in $(seq 1 30000); do echo "SET chiave:$i $V"; done | redis-cli -p 6395 --pipe | tail -1
errors: 23642, replies: 30000
$ redis-cli -p 6395 dbsize
6358
$ redis-cli -p 6395 info memory | grep -E '^used_memory_human|^maxmemory_human'
used_memory_human:2.05M
maxmemory_human:2.00M
$ redis-cli -p 6395 info stats | grep evicted_keys
evicted_keys:0
6,358 keys get in, 23,642 writes are rejected, no key is evicted. On 7.4.7 the same commands give errors: 26034 and dbsize 3966: fewer get in because that image loads modules that take memory at startup. It is not a comparison between versions, it is a comparison between installations; the behavior is identical.
Not all commands die, and the line is precise
Whoever reads "Redis is full" imagines a stopped service. With the instance saturated and noeviction:
$ for cmd in "set nuova x" "incr contatore" "append chiave:1 y" "expire chiave:1 100" "get chiave:1" "dbsize" "del chiave:1"; do printf '%-22s -> %s\n' "$cmd" "$(redis-cli -p 6395 $cmd | head -c 62)"; done
set nuova x -> OOM command not allowed when used memory > 'maxmemory'.
incr contatore -> OOM command not allowed when used memory > 'maxmemory'.
append chiave:1 y -> OOM command not allowed when used memory > 'maxmemory'.
expire chiave:1 100 -> 1
get chiave:1 -> vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
dbsize -> 6358
del chiave:1 -> 1
Everything that adds data fails, and the documentation says why: Redis checks memory when a client runs a command that adds data. Reading, deleting and giving an expiry to an existing key work. Hence the consequence that bites: if your Redis probe only does PING and a read, it stays green while the application can no longer write a session.
Is setting maxmemory equal to the free RAM safe?
No, and the gap is large: maxmemory is compared with used_memory, the bytes Redis requests from its allocator, not with the resident memory the operating system sees. In the measurement below an instance that declared 6.51 MB occupied 13.75 of real RAM, and the limit doesn't notice.
Fresh instance, 16 MB limit, thirty thousand keys, no eviction:
$ redis-cli -p 6395 info memory | grep -E '^used_memory_human|^used_memory_rss_human|^mem_fragmentation_ratio' # empty instance
used_memory_human:741.32K
used_memory_rss_human:8.07M
mem_fragmentation_ratio:12.61
$ for i in $(seq 1 30000); do echo "SET chiave:$i $V"; done | redis-cli -p 6395 --pipe | tail -1
errors: 0, replies: 30000
$ redis-cli -p 6395 info memory | grep -E '^used_memory:|^used_memory_rss:|^mem_fragmentation_ratio|^mem_allocator'
used_memory:6833192
used_memory_rss:14413824
mem_fragmentation_ratio:2.18
mem_allocator:jemalloc-5.3.0
Redis counts 6.51 MB against a limit of 16 and doesn't evict; the operating system sees 13.75 MB resident. The 2.18 is not transferable to an instance of several GB, because the empty instance already occupies 8.07 MB resident and here that fixed cost dominates: the INFO documentation warns that mem_fragmentation_ratio also includes code, shared libraries and stack.
The rule that stays true at every scale is another one: maxmemory is not a ceiling on the process's RAM, it is a ceiling on an internal accounting. Setting it equal to all the free memory means choosing the moment when the operating system will kill Redis.
The policies: eight that work, two that the page lists
The list your server really accepts
I did not copy the list from the documentation: I asked the server, one policy at a time, because it depends on the version.
$ for p in noeviction allkeys-lru allkeys-lfu allkeys-random volatile-lru volatile-lfu volatile-random volatile-ttl allkeys-lrm volatile-lrm; do printf '%-18s %s\n' "$p" "$(redis-cli -p 6395 config set maxmemory-policy $p 2>&1)"; done
noeviction OK
allkeys-lru OK
allkeys-lfu OK
allkeys-random OK
volatile-lru OK
volatile-lfu OK
volatile-random OK
volatile-ttl OK
allkeys-lrm ERR CONFIG SET failed (possibly related to argument 'maxmemory-policy') - argument(s) must be one of the following: volatile-lru, volatile-lfu, volatile-random, volatile-ttl, allkeys-lru, allkeys-lfu, allkeys-random, noeviction
volatile-lrm ERR CONFIG SET failed (possibly related to argument 'maxmemory-policy') - argument(s) must be one of the following: volatile-lru, volatile-lfu, volatile-random, volatile-ttl, allkeys-lru, allkeys-lfu, allkeys-random, noeviction
Eight accepted, two rejected, same outcome on 7.4.7. Yet allkeys-lrm and volatile-lrm are listed on the page that redis.io serves as "latest", complete with advice on when to use them: further down, in the same page, you read that LRM exists from Redis 8.6. That page documents several editions together, so the proof that a policy exists on your side is a CONFIG SET, not a paragraph.
Why does volatile-lru evict nothing without a TTL?
Because the volatile-* policies consider only keys with an expiry as eviction candidates: if there are none in the database, there is nothing to evict, and Redis rejects writes as it would with noeviction. It is the trap that costs an incident, because the configuration looks prudent and does the opposite of what it promises.
Thirty thousand keys without expiry, volatile-lru policy:
$ redis-cli -p 6395 config set maxmemory-policy volatile-lru
OK
$ for i in $(seq 1 30000); do echo "SET chiave:$i $V"; done | redis-cli -p 6395 --pipe | tail -1
errors: 23642, replies: 30000
$ redis-cli -p 6395 dbsize
6358
$ redis-cli -p 6395 info stats | grep evicted_keys
evicted_keys:0
The same 23,642 errors and the same 6,358 keys as noeviction, zero evictions. The documentation says it in a line that is easy to read without grasping its reach: the volatile-xxx policies behave like noeviction if no key has an associated expiry. Whoever configures it thinking "it only evicts expirable stuff, so it's safe" gets an instance that errors on every write and that declares an eviction policy in INFO without ever evicting.
On my development environment, two candidates out of 2,440
Before choosing a volatile-* you need to know how many keys really have a TTL. On my development instance, read-only:
$ redis-cli -p 6379 dbsize
2440
$ redis-cli -p 6379 --scan --count 1000 | sed -E 's/[0-9a-f]{6,}.*//; s/:.*//' | sort | uniq -c | sort -rn
2426 article
11 dataset
1 sdmx
1 open_meteo_archive
1 kb_fiscal_paese_eval_green
$ redis-cli -p 6379 --scan --count 1000 > /tmp/k.txt; sed 's/^/TTL /' /tmp/k.txt | redis-cli -p 6379 | sort | uniq -c | sort -rn
2438 -1
1 42708
1 1227342
$ redis-cli -p 6379 memory usage article:1416:en
2880
The 2,426 article:* keys are the per-article and per-language hashes of the Python microservice: five sampled sit between 2,880 and 3,052 bytes, and none has an expiry. Of the 2,440 total keys, 2,438 have no TTL: the only two expiries belong to cached responses from external services. Here a volatile-* would have two candidates out of 2,440 and would behave like noeviction. The shape of those hashes is in the article on data structures and what they cost; here what matters is that they take up almost all the memory and never expire.
Two populations with opposite natures
The composition changes as soon as the Java application runs, because it is the one that sets expiries: in RedisCacheConfig.java there are 27 caches declared one by one, each with its TTL, from one minute to seven days, with the prefix built at line 300 as cacheVersion + ":" + cacheName + "::".
- Application cache: it has a TTL, it can be rebuilt with a database query.
- Microservice indexes: no TTL, rebuildable only by redoing the indexing at the next startup.
- Security keys, like token revocation: they have a TTL, but losing them reopens an already revoked token.
- Where they live: in the same instance and in the same database, not hypothetically. The backend points to
${REDIS_HOST:localhost}on port${REDIS_PORT:6379}(application.yml, lines 79-80), the microservice hasredis_url: str = "redis://localhost:6379"(config.py, line 60).
The criterion that holds the first three items together is a single question: if this key disappears, who puts it back? The cache puts itself back on the first request, the indexes only at the microservice restart, the revocation keys never. The fourth item is what makes the problem real, because without the coexistence in the same database the first three wouldn't compete and the eviction policy wouldn't have to choose among them. It is from here that you decide, not from the traffic profile.
Who gets deleted: the measurement that decides the policy
Two thousand revocation keys against thirty thousand cache keys
I rebuild that composition in miniature: 2,000 revocation keys without TTL, then 30,000 cache keys with a one-hour TTL, 4 MB limit. Three rounds per policy, because a single round says nothing.
$ for POL in allkeys-lru volatile-lru allkeys-lfu allkeys-random; do for giro in 1 2 3; do
redis-cli -p 6397 flushall > /dev/null
redis-cli -p 6397 config set maxmemory 4mb > /dev/null
redis-cli -p 6397 config set maxmemory-policy $POL > /dev/null
redis-cli -p 6397 config resetstat > /dev/null
for i in $(seq 1 2000); do echo "SET jwt:revoked:$i 1"; done | redis-cli -p 6397 --pipe > /dev/null 2>&1
for i in $(seq 1 30000); do echo "SET cache:v1:$i $V EX 3600"; done | redis-cli -p 6397 --pipe > /dev/null 2>&1
printf '%-16s giro %s revoca %4s/2000 cache %5s/30000 sfratti %6s\n' "$POL" "$giro" \
"$(redis-cli -p 6397 eval "return #redis.call('KEYS','jwt:revoked:*')" 0)" \
"$(redis-cli -p 6397 eval "return #redis.call('KEYS','cache:v1:*')" 0)" \
"$(redis-cli -p 6397 info stats | grep '^evicted_keys:' | cut -d: -f2 | tr -d '\r')"
done; done
allkeys-lru giro 1 revoca 732/2000 cache 13566/30000 sfratti 22615
allkeys-lru giro 2 revoca 340/2000 cache 9010/30000 sfratti 25172
allkeys-lru giro 3 revoca 247/2000 cache 7105/30000 sfratti 24924
volatile-lru giro 1 revoca 2000/2000 cache 5764/30000 sfratti 24236
volatile-lru giro 2 revoca 2000/2000 cache 10757/30000 sfratti 20926
volatile-lru giro 3 revoca 2000/2000 cache 9102/30000 sfratti 24020
allkeys-lfu giro 1 revoca 261/2000 cache 7233/30000 sfratti 24511
allkeys-lfu giro 2 revoca 275/2000 cache 8360/30000 sfratti 25210
allkeys-lfu giro 3 revoca 219/2000 cache 6821/30000 sfratti 25908
allkeys-random giro 1 revoca 82/2000 cache 5923/30000 sfratti 25996
allkeys-random giro 2 revoca 146/2000 cache 7042/30000 sfratti 24812
allkeys-random giro 3 revoca 315/2000 cache 10025/30000 sfratti 21839
The result has two readings, to be kept separate because they have a different degree of certainty.
- Categorical:
volatile-lrukept all 2,000 keys without expiry in all three rounds. Not by luck: those keys are not even candidates. - Statistical: all the
allkeys-*policies deleted the great majority of the revocation keys, to a very variable extent from one round to the next. - On 7.4.7: the first reading holds identically (2,000 out of 2,000, three rounds out of three); with
allkeys-lru626, 14 and 589 remain, and the dispersion is the same.
Between the two readings the first weighs more, and not because the numbers are bigger: it is the only one that can be promised to whoever will use the system. "volatile-lru will not touch keys without a TTL" is a
guarantee that follows from the definition of the policy; "with allkeys-lru most of them are lost" is the average of a draw, and the round that left 14 out of 2,000 could just as well
have left zero. A guarantee and a statistic are not used in the same way when writing a requirement.
Why doesn't allkeys-lru always delete the same keys?
Because Redis's LRU is not a true LRU: the documentation declares it an approximation that samples a few keys at random and evicts the most inactive among the sampled ones, and the number of samples is maxmemory-samples, which on my instances is 5. The dispersion between one round and another is not measurement noise, it is the algorithm; LFU works the same way, with a probabilistic counter in place of time.
I tried to measure whether raising maxmemory-samples from 5 to 10 changed the outcome. I am not publishing that comparison: two runs of the identical configuration, samples at 5, gave me 740 and 82 surviving keys out of 2,000, and with this dispersion the difference between 5 and 10 is not distinguishable from chance. What remains is the conclusion that doesn't depend on tuning: with an allkeys-* you can't predict which keys you lose, only that you lose them.
volatile-ttl picks whoever was about to die anyway
One policy doesn't look at accesses but at the remaining expiry, and behaves in a much more predictable way. Two families of 15,000 keys, TTL of 60 and 86,400 seconds, 4 MB limit:
$ redis-cli -p 6395 config set maxmemory-policy volatile-ttl
OK
$ for i in $(seq 1 15000); do echo "SET breve:$i $V EX 60"; echo "SET lunga:$i $V EX 86400"; done | redis-cli -p 6395 --pipe | tail -1
errors: 0, replies: 30000
$ redis-cli -p 6395 eval "return #redis.call('KEYS','breve:*')" 0
1712
$ redis-cli -p 6395 eval "return #redis.call('KEYS','lunga:*')" 0
12006
$ redis-cli -p 6395 info stats | grep evicted_keys
evicted_keys:20708
Of the keys with one minute to live, 1,712 remain, of those with one day, 12,006: seven times as many. It is the right tool when the TTL already expresses how much you need that data, and the documentation adds a useful effect: by using expiries well you reach the limit less often, because keys often die before they have to be evicted. The same reasoning on a stream is the trimming with MAXLEN in Pub/Sub and Streams.
LFU keeps a counter, and you can read it
$ redis-cli -p 6397 config set maxmemory-policy allkeys-lfu
OK
$ redis-cli -p 6397 config get lfu-log-factor
lfu-log-factor
10
$ redis-cli -p 6397 set calda v; redis-cli -p 6397 set fredda v
OK
OK
$ for i in $(seq 1 1000); do echo "GET calda"; done | redis-cli -p 6397 --pipe > /dev/null
$ redis-cli -p 6397 object freq calda
20
$ redis-cli -p 6397 object freq fredda
5
$ redis-cli -p 6397 config set maxmemory-policy allkeys-lru
OK
$ redis-cli -p 6397 object freq calda
ERR An LFU maxmemory policy is not selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust.
A thousand reads took the counter from 5 to 20. It is not a count: the documentation describes it as a Morris counter, probabilistic and with decay, and publishes a table between logarithmic factor and accesses that for lfu-log-factor 10 and a thousand accesses gives 18. Over three runs I read 18, 20 and 21: close to the table, not equal, and it is the behavior a probabilistic counter promises. OBJECT FREQ serves to tell you whether your idea of a "hot key" is the same as Redis's, before entrusting it with the choice.
The warning on the last line should be taken literally: right after that policy change OBJECT IDLETIME returned 11,229,495 seconds on a key created a few seconds earlier. I can't explain that number and I draw no conclusions from it: I report it so that, if you see it, you know your key hasn't traveled through time.
The case that concerns me: a release that doubles the cache
The version prefix, and the orphan keys
The backend's release script computes the cache version as the abbreviation of the current commit (scripts/deploy/backend.sh, line 66); RedisCacheConfig uses it as the first segment of every key. The comment at line 63 declares the choice: no FLUSHALL, because the instance also holds things that don't belong to that cache manager, and the earlier census says which.
The mechanism is correct, and the article on Spring's cache tells it. Here what matters is the consequence on memory: after a release the keys with the old prefix are not deleted, they die of TTL. For a window equal to the longest TTL the instance hosts two entire caches.
The simulation, with noeviction
I simulate the composition with redis-cli, without starting the application: 2,426 values of 2,800 bytes without TTL in place of the indexes, 15,000 aaa0000 keys with a ten-minute TTL in place of the cache, 16 MB limit. Then I do what a release does: I rewrite the same 15,000 with the bbb1111 prefix. I use strings and not hashes because what matters is the occupation, not the structure.
$ redis-cli -p 6395 config set maxmemory 16mb; redis-cli -p 6395 config set maxmemory-policy noeviction
OK
OK
$ B=$(printf 'b%.0s' $(seq 1 2800))
$ for i in $(seq 1 2426); do echo "SET article:$i:it $B"; done | redis-cli -p 6395 --pipe | tail -1
errors: 0, replies: 2426
$ for i in $(seq 1 15000); do echo "SET aaa0000:article-metadata::$i $V EX 600"; done | redis-cli -p 6395 --pipe | tail -1
errors: 0, replies: 15000
$ redis-cli -p 6395 info memory | grep '^used_memory_human'
used_memory_human:14.45M
$ for i in $(seq 1 15000); do echo "SET bbb1111:article-metadata::$i $V EX 600"; done | redis-cli -p 6395 --pipe | tail -1
errors: 8215, replies: 15000
$ redis-cli -p 6395 eval "return #redis.call('KEYS','aaa0000:*')" 0
15000
$ redis-cli -p 6395 ttl aaa0000:article-metadata::1
599
$ redis-cli -p 6395 eval "return #redis.call('KEYS','bbb1111:*')" 0
6785
The new release wrote 6,785 keys out of 15,000 and crashed on the other 8,215, while the 15,000 of the previous one are all alive, with almost ten minutes of TTL ahead. The part that matters comes now, because a full instance doesn't distinguish a cache from a security datum:
$ redis-cli -p 6395 set jwt:revoked:9f3c 1
OOM command not allowed when used memory > 'maxmemory'.
$ redis-cli -p 6395 setex sessione 900 x
OOM command not allowed when used memory > 'maxmemory'.
$ redis-cli -p 6395 get aaa0000:article-metadata::1 | head -c 10
vvvvvvvvvv
$ redis-cli -p 6395 info commandstats | grep -E 'cmdstat_(set|setex):'
cmdstat_set:calls=24211,usec=57244,usec_per_call=2.36,rejected_calls=8216,failed_calls=0
cmdstat_setex:calls=0,usec=0,usec_per_call=0.00,rejected_calls=1,failed_calls=0
An ordinary release, which nobody considers a risky operation, made it impossible to record a token revocation and to write a session. There is no programming error: there is a policy that, chosen to protect the data that can't be rebuilt, also protected the old cache and sacrificed all the new writes without distinguishing them. On 7.4.7 the same experiment gives 4,892 keys written out of 15,000 and the same rejection on the revocation.
The same keys, with the two eviction policies
I redo the identical sequence changing only the policy, and look at who pays for the release:
# with allkeys-lru (8.0.5)
vettori in vita: 1569 su 2426
aaa0000 in vita: 14298 su 15000
bbb1111 in vita: 14389 su 15000
set jwt:revoked:9f3c 1 -> OK
evicted_keys:2170
# with volatile-lru (8.0.5), same sequence
vettori in vita: 2426 su 2426
aaa0000 in vita: 7561 su 15000
bbb1111 in vita: 7195 su 15000
set jwt:revoked:9f3c 1 -> OK
evicted_keys:16104
With allkeys-lru no write fails and the service stays up, but 857 of the 2,426 indexes are gone: the microservice rewrites them at startup, so they are not lost forever, but until then 857 articles are invisible to semantic search and nobody receives an error.
With volatile-lru the indexes are all in place and only the cache pays the bill, with seven and a half times the evictions: 16,104 against 2,170, because the candidates are only the cache keys. The eviction count adds up: evicting a 2,800-byte index frees fifteen times the memory of a 100-byte cache entry, so whoever can touch the indexes gets under the limit with far fewer deletions. On 7.4.7 the pair gives 1,968 against 2,426 out of 2,426. The index I am talking about is the one in the article on the RediSearch setup in production: there is how it is built, here what happens to it when space runs out.
Diagnosing a saturated instance
INFO memory: the four fields that matter
used_memory: the bytes requested from the allocator. It is this number that is compared withmaxmemory.used_memory_rss: whattopandpssee, that is the RAM the machine considers occupied.used_memory_dataset:used_memoryminus the overhead, that is how much of that memory is your data.mem_not_counted_for_evict: the transient replication and AOF buffers, which don't count for eviction.
The four fields don't weigh the same. The first is the only one that decides Redis's behavior; the other three serve to understand why that number is where it is. If only one goes into a dashboard, put
used_memory against maxmemory, because it is the comparison that generates the OOM error; if instead you are sizing the machine, what matters is used_memory_rss, because it is
the number the operating system looks at when it decides which process to kill.
Redoing the release sequence and reading INFO memory at the end, in a separate run:
$ redis-cli -p 6395 info memory | grep -E '^used_memory:|^used_memory_human:|^used_memory_dataset:|^maxmemory:|^maxmemory_human:|^mem_not_counted_for_evict:'
used_memory:16792680
used_memory_human:16.01M
used_memory_dataset:14027232
maxmemory:16777216
maxmemory_human:16.00M
mem_not_counted_for_evict:0
Redis stopped 15,464 bytes above the ceiling, that is practically on the limit, and of the 16.01 MB counted as used only 13.38 are data: the other 2.64 MB, a sixth of the total, are overhead. The documentation warns that a command that adds a lot of data in one go can exceed the limit temporarily and by a lot, so an instantaneous overshoot is not in itself a fault; here the writes were small and the gap stayed minimal.
INFO stats: three cases that look the same
$ redis-cli -p 6395 info stats | grep -E '^evicted_keys:|^expired_keys:|^current_eviction_exceeded_time:'
expired_keys:0
evicted_keys:0
current_eviction_exceeded_time:136
- Evictions at zero and
current_eviction_exceeded_timenon-zero: the instance is above the limit and is not evicting. You are innoevictionor involatile-*with no candidates, and someone is receiving errors. - Evictions growing: the cache works as a cache. The documentation suggests reading them together with
keyspace_hitsandkeyspace_misses: many evictions with few successful reads indicate that the policy throws away the wrong keys. expired_keyshigh and few evictions: your TTLs are doing their job. It is the situation you want.
What distinguishes the three cases is not how much memory is occupied, which can be identical: it is who is paying. In the first the client pays, with an error in the face; in the second the data pays,
disappearing without anyone knowing; in the third nobody pays. An alarm set only on used_memory makes them indistinguishable, and the first is the most urgent of the three, because it is the only one
in which someone is already receiving errors right now.
INFO commandstats: which command was rejected
This field closes the investigation, and I found it suggested by the eviction page itself: commandstats reports for each command how many calls were rejected, and serves precisely to find who runs into the limit. In the earlier measurement cmdstat_set declared rejected_calls=8216, which matches the 8,215 errors of the pipe plus my hand-typed SET. The value is not the total: a rejected_calls on setex and zero on get tells you which part of the application is losing writes, and that is a diagnosis, not a symptom.
Changing the limit hot, and why it doesn't stay written
$ redis-cli -p 6395 dbsize # with 16mb limit, allkeys-lru
30000
$ redis-cli -p 6395 config set maxmemory 4mb
OK
$ redis-cli -p 6395 dbsize
23440
$ redis-cli -p 6395 info stats | grep evicted_keys
evicted_keys:13616
$ redis-cli -p 6397 info server | grep '^config_file'
config_file:
$ redis-cli -p 6397 config rewrite
ERR The server is running without a config file
CONFIG SET reconfigures Redis at runtime without restarting the server: six thousand five hundred and sixty keys vanished in the time of one command, without any client having asked for anything. And the change lives only in memory: on an instance started without a configuration file CONFIG REWRITE has nowhere to write, and after a restart with the same command line I found maxmemory 16777216 and allkeys-lru, the startup values. It is the typical way a configuration adjusted during an incident at three in the morning dissolves at restart, and the incident comes back.
The trade-off: what I would choose, and what I discarded
After these measurements the choice is not between "safe" and "fast", because neither option is safe. noeviction protects the data that can't be rebuilt at the price of rejecting all writes when full, including the critical ones, and doing so silently for whoever looks only at a PING. An eviction policy protects the availability of writes at the price of deleting data without telling anyone.
With the composition I measured I would choose volatile-lru: it is the only configuration in which the two populations are treated differently, and the distinction coincides with the one that matters. I discarded allkeys-lru because it sacrifices the indexes without warning, and volatile-ttl because my cache's TTLs are chosen for freshness of the data, not for importance: making them a survival criterion would evict the most up-to-date caches first.
- Set an explicit
maxmemory, lower than the available RAM, bearing in mind that the replication and AOF buffers are not in the count. - Before choosing
volatile-*, count the keys with a TTL: without candidates it isnoevictionunder another name. - Don't deduce the policy from the load: deduce it from the keyspace census, asking yourself for each family what happens if it disappears.
- Alarm on
current_eviction_exceeded_time, not justused_memory: it distinguishes "full and evicting" from "full and rejecting". - If you change the configuration hot, write it where it survives restart:
CONFIG SETalone doesn't.
The five items are not in order of importance but of sequence, and the last three depend on the second, which is the only one that requires a measurement. If today you can do only one, do that one: counting the
keys with a TTL costs one command and tells you whether the policy you wrote in the configuration really exists or is noeviction under another name.
There is, however, a better solution than any policy, and the documentation suggests it: the volatile-* policies are mostly useful when you want to use a single instance both as a cache and for a set of persistent keys, and in that case it is worth evaluating two separate instances. That is my case to the letter. Choosing the policy is the intermediate developer's step; understanding that the problem is a wrong architectural decision upstream, two classes of data with opposite requirements in the same process, and that the policy is only the least bad way to live with it, is the step toward senior. The cost is not zero: two processes to watch, two configurations, two connections and a baseline consumption paid twice, a little over eight megabytes resident per empty instance on my measurements. For a site with a single machine it is a bill to work out, not a foregone conclusion; what you can't do is leave the decision to an algorithm that samples five keys at random.
What you can do now
You can tell the two possible behaviors of an instance at its limit and recognize them from INFO instead of from users' complaints, you know that a volatile-* policy is worth as much as the number of keys with a TTL and you can count them in one command, and you know that maxmemory doesn't limit the process's RAM. Above all you know that the question to start from is not "which policy is the best" but "what coexists in this instance, and what happens if I lose a piece of it": it is answered with a census, not with a preference.
For the shape of the data that takes up that space there is the article on data structures and what they cost; to make a non-atomic sequence atomic, Lua, EVAL and EVALSHA. The step before this one is what survives a restart between RDB and AOF, the step after is Spring Cache on Redis, with TTLs and a version prefix; to measure latency, SLOWLOG and memory per key there is the dedicated article. On a single, small machine the capacity context is that of low-cost VPS hosting, which shows limit and policy already chosen; how to rebuild everything after a disaster is instead in disaster recovery and backup strategy.
Do you have a Redis instance where cache and critical data coexist? Book a call →
Sources
- Redis, Key eviction - list of policies,
maxmemoryat zero and the implicit 3 GB limit on 32-bit, thevolatile-xxxpolicies that behave likenoevictionwithout keys with an expiry, the memory check on commands that add data, the approximated LRU withmaxmemory-samples, the Morris counter and thelfu-log-factortable (factor 10 and a thousand accesses: 18), LRM from Redis 8.6, the buffers excluded from the count, the advice of two separate instances,commandstatsto find the rejected commands. Consulted on 2026-11-08. - Redis, INFO - definitions of
used_memory,used_memory_rss,used_memory_dataset,mem_fragmentation_ratio(which also includes the process costs),mem_not_counted_for_evict,evicted_keysandrejected_calls. Consulted on 2026-11-08. - Redis, CONFIG SET - runtime reconfiguration without restarting the server. Consulted on 2026-11-08.
- Redis, Memory optimization - confirmation that occupation depends on the internal encoding and not on content alone. Consulted on 2026-11-08.
- Own measurements: all the commands in the blocks above, run on 8 November 2026 on Redis 8.0.5 native and 7.4.7 (
redis/redis-stack-server:latest); the development instance on port 6379 was only read. - Repository code, read line by line:
RedisCacheConfig.java(27 caches with their own TTLs, prefix at line 300,app.cache.versionat line 232),scripts/deploy/backend.sh(line 66 the version from the commit, line 63 the reason for noFLUSHALL),application.yml(lines 79-80),ml-service/app/config.py(line 60).
The first four items serve to check that I haven't misunderstood the documentation, the last two to redo the measurements from scratch. If you want to verify only one, verify the line on the volatile-*
policies in the eviction page: it is one sentence, and half the article rests on it.
Commenti
Caricamento commenti...
Sign in to comment