Eviction, and the Approximations That Make It Cheap

Topics Covered

What maxmemory Actually Does

The limit, and what it counts

Where the check happens

What happens when eviction cannot help

Client buffers, and the 7.0 accounting change

Replicas, and the limit they should carry

Scripts, transactions and the OOM check

maxmemory of zero, and the default

Setting it at runtime

What to check first on an unfamiliar instance

Eviction is the mechanism that turns Redis from a database that fills up and fails into a cache that discards what it can afford to lose, and it begins with a single limit whose exact behaviour is less obvious than the name suggests.

The limit, and what it counts

maxmemory is a byte count compared against used_memory, the allocator's report of what Redis has asked for. It is not compared against the resident set size, so fragmentation is outside the limit entirely, and an instance at its maxmemory can have a resident set substantially larger.

That is the single most important thing to know about the setting, because it means maxmemory does not protect the host. A limit of 8GB on a host with 10GB of memory is comfortable at a fragmentation ratio of 1.1 and fatal at 1.4, and nothing in Redis will warn about the difference. Sizing the limit means sizing against the resident set the workload actually produces, which is the previous lesson's subject arriving as a hard constraint on this one.

maxmemory is compared against used_memory, not the resident set, so fragmentation sits outside the limit. The check runs before each command and evicts in a loop until it is satisfied, which makes it reactive: a single large write overshoots and is corrected afterwards.

Where the check happens

The limit is enforced in the command loop, before each command executes. Redis compares its current usage against the limit and, if it is over, evicts keys in a loop until it is under or until it concludes it cannot get under. Only then does it run the command.

This is reactive rather than predictive, which has a consequence worth understanding: Redis does not know how much memory a command will need, so it cannot refuse a command for being too large. A single SET of a 500MB value on an instance 100MB under its limit will succeed and leave the instance 400MB over, and the overshoot is corrected by evictions before the next command. The limit is therefore a level the instance returns to rather than a ceiling it cannot cross.

What happens when eviction cannot help

If the policy is noeviction, or if the policy is a volatile one and no keys have expiries, or if everything evictable has already gone, Redis cannot free memory and must refuse the command. Write commands then return an error beginning OOM command not allowed when used memory > 'maxmemory', and read commands continue to work normally.

That asymmetry is deliberate and useful: an instance in this state is still serving reads, so a cache in front of a database keeps absorbing traffic even while it cannot accept new entries. It is also the state that produces the most confusing incidents, because the application sees write failures while every dashboard shows the instance responding normally with good latency. The error string is the thing to grep for, and INFO stats does not count these refusals directly, so they are visible in client logs and nowhere else.

Client buffers, and the 7.0 accounting change

Before 7.0, client output buffers counted toward used_memory and therefore toward maxmemory, which meant a few slow consumers could trigger eviction of perfectly warm data. Redis 7.0 introduced maxmemory-clients, a separate limit for the total memory held by client buffers, with its own eviction of clients rather than of keys.

The default is a percentage of maxmemory, and the effect is that a client whose buffers grow without bound is disconnected instead of being allowed to push keys out of the cache. This is one of the more valuable quiet improvements in recent versions, and it is worth knowing which side of it a given instance is on, because the older behaviour produces an eviction storm with no change in the keyspace, which is a genuinely baffling symptom.

Replicas, and the limit they should carry

A replica applies the evictions its primary performs, arriving as explicit DEL commands in the replication stream, exactly as expiry does. A replica with its own maxmemory set and its own policy will also evict independently, which breaks the consistency between it and its primary.

The guidance is to set a replica's maxmemory higher than its primary's, or to rely on replica-ignore-maxmemory which defaults to yes and makes a replica ignore the limit for eviction purposes while still reporting it. Setting equal limits is the mistake, because a replica legitimately uses more memory than its primary, for the reasons the expiry lesson gave, so an equal limit means the replica hits it first and starts evicting keys the primary still has.

Scripts, transactions and the OOM check

The OOM check happens once before a script or a MULTI block runs, based on whether the script is flagged as possibly writing. A script that starts under the limit and allocates heavily can therefore take the instance far over it, and there is no check partway through.

The practical advice is to avoid scripts that allocate unboundedly, which sounds obvious and is routinely violated by a script that builds a large intermediate table or returns a huge reply. The same applies to a pipelined batch: the check is per command, so a pipeline of ten thousand writes will be checked ten thousand times and will also keep the eviction loop busy between them, which is a real source of latency during a bulk load.

maxmemory of zero, and the default

A maxmemory of 0 means no limit, which is the default on 64-bit builds, and it is the configuration under which an instance grows until the host runs out of memory and the kernel kills it. 32-bit builds default to 3GB because of their address space.

Running with no limit is defensible for a database-shaped workload where losing data is unacceptable, and in that case the protection must come from monitoring and from the host, with eviction explicitly declined. It is indefensible for a cache, where the absence of a limit converts a memory pressure event from a graceful degradation into a process kill. If I inherit an instance with no limit, the first question is which of those two it is, and the answer is usually that nobody decided.

Setting it at runtime

CONFIG SET maxmemory 8gb takes effect immediately, including the eviction that follows if the new limit is below current usage. Lowering the limit on a live instance therefore triggers an eviction burst proportional to the difference, which is worth doing in steps rather than in one move.

CONFIG SET maxmemory-policy allkeys-lru likewise takes effect immediately, and changing from noeviction to an evicting policy on an instance already over its limit will evict hard until it is under. That is usually what is wanted in an incident and it is not gentle, so knowing the size of the gap before making the change is the difference between a controlled recovery and a cache emptied by surprise.

What to check first on an unfamiliar instance

Four values: maxmemory, maxmemory-policy, used_memory and used_memory_rss. Those four answer whether there is a limit, what happens at it, how close the instance is, and whether the resident set has room on the host. Everything else about eviction follows from them.

I ask for those four before any discussion of policy tuning, because the common case is not a badly chosen policy but an instance with no limit, or with noeviction on something that was always meant to be a cache, or with a limit set so close to host memory that fragmentation alone will kill it. Policy choice is the interesting part of this lesson and it is the last part that matters in practice.

Common Pitfall

maxmemory is compared against used_memory, not against the resident set size, so fragmentation sits entirely outside the limit. An 8GB limit on a 10GB host is comfortable at a fragmentation ratio of 1.1 and fatal at 1.4, and nothing in Redis will warn about the difference. The limit does not protect the host, so size it against the resident set the workload actually produces rather than against the memory the dataset nominally needs.