Redis AOF, maxmemory, and the Isolation Trap banner

Redis AOF, maxmemory, and the Isolation Trap

Section: DevOps

I wanted one practical answer to four Redis questions:

  • What does AOF really protect?
  • Does maxmemory 4gb keep the Redis process below 4 GB?
  • What exactly happens with noeviction?
  • Is DB 1 actually isolated from DB 0?

The individual answers are not difficult. The dangerous part is how easily they combine into a setup that looks safe.

Take this configuration:

Conf
appendonly yes
appendfsync everysec
maxmemory 4gb
maxmemory-policy noeviction

At first glance, it reads like this: persist everything, stay below 4 GB, and never delete data.

That is not what it guarantees.

Four Knobs, Four Different Jobs

The cleanest mental model is to stop treating these settings as one safety package.

SettingThe question it answers
AOFWhat Redis can reconstruct after a restart
maxmemoryWhen Redis should apply its memory policy
noevictionWhat Redis does when that threshold is reached
DB 0, DB 1, DB 2Which logical key namespace a connection uses

None of them creates a hard operating-system memory limit. None of them gives one workload its own resources. AOF does not even give you a clean point-in-time backup.

That distinction explains most of the weird behavior later.

AOF is a Recovery Log, not a Backup

Redis keeps its working dataset in memory. With AOF enabled, it also records commands that change that dataset:

Text
SET user:42 "Jasur"
SET counter 1
INCR counter

After a restart, Redis replays the persisted operations and rebuilds the in-memory state. Normal reads still come from RAM; AOF does not turn every GET into disk I/O.

The useful configuration for many systems is:

Conf
appendonly yes
appendfsync everysec

appendfsync everysec is the usual balance between throughput and durability. In a catastrophic failure, roughly the latest second of writes may still be lost. always is safer but slower; no leaves flushing to the operating system and can lose a larger window.

So AOF improves durability. It does not promise zero data loss.

It also grows as writes arrive. If a counter changes 100,000 times, Redis does not need 100,000 commands forever just to restore its final value. An AOF rewrite produces a smaller representation of the current state. Since Redis 7, this uses a base file, incremental AOF files, and a manifest rather than one endlessly growing file.

The backup distinction matters more than the implementation detail. If the application deletes a key or runs FLUSHALL, that destructive change is part of the history too. Depending on timing and whether a rewrite happened, manual recovery from the AOF may be possible, but that is not a backup strategy.

If the data matters, keep independent snapshots or backups outside the Redis machine and test that they can be restored. Redis' own documentation recommends combining persistence approaches when stronger data safety is required.

maxmemory Is not a Process Limit

Suppose the server has 8 GB of RAM and Redis is configured like this:

Conf
maxmemory 4gb

It is tempting to read that as:

Text
Redis RSS <= 4 GB

That assumption is wrong.

maxmemory is the threshold Redis uses when deciding whether to apply its configured memory policy. The process still needs memory for things such as client buffers, AOF and replication buffers, internal structures, and allocator fragmentation. Background persistence can add more pressure through copy-on-write pages while the parent keeps accepting writes.

A perfectly possible snapshot looks like this:

Text
dataset and counted memory    3.9 GB
allocator fragmentation      0.4 GB
client and AOF buffers        0.2 GB
other overhead               0.1 GB
-----------------------------------
process RSS                  ~4.6 GB

Redis exposes mem_not_counted_for_evict in INFO memory for transient replica and AOF buffers that are excluded from the eviction calculation. used_memory_rss shows what the operating system sees, which can be noticeably higher than used_memory.

This is why maxmemory 8gb on an 8 GB server is not aggressive optimization. It is an invitation for swapping or the OOM killer.

How much headroom to leave depends on write volume, persistence, replication, client buffers, fragmentation, and whatever else runs on the machine. There is no honest universal percentage. Measure the real workload and leave room for its peaks, not just its calm average.

noeviction Chooses Explicit Failure

Once Redis crosses maxmemory, the maxmemory-policy decides what happens next.

For a disposable cache, an eviction policy such as allkeys-lru or allkeys-lfu can remove less valuable keys so new data fits. The application misses the cache, queries the real data source, and rebuilds the value. That is normal cache behavior.

With this configuration:

Conf
maxmemory 4gb
maxmemory-policy noeviction

Redis does not sacrifice existing keys to make room. Commands that require more memory start returning an OOM error. Reads still work, and commands that do not need more memory may work too. It is more precise to say "memory-growing writes fail" than "Redis becomes read-only."

TTL expiration also continues. These are separate events:

Text
expiration: the key reached its configured lifetime
eviction:   Redis removed a key early because of memory pressure

noeviction disables the second behavior, not the first.

The policy is reasonable when silently losing an existing key would be worse than rejecting a new write. Job queues, coordination data, and important application state often fit that description.

But explicit failure is still failure. If the application ignores Redis OOM errors, noeviction has not saved the system. It has only made the failure detectable.

For a pure cache, I would usually prefer eviction. Turning cache pressure into failed requests defeats much of the point of having a cache.

Logical Databases Separate Names, not Resources

A standalone Redis Open Source server can expose numbered logical databases. Clients start in DB 0 and can select another one:

Text
SELECT 1

Redis URLs encode the same choice:

Text
redis://redis:6379/0
redis://redis:6379/1

DB 0 can contain user:42 while DB 1 contains another user:42. FLUSHDB can clear the selected database without clearing the others. That is convenient namespacing.

The separation stops there.

Every logical database still shares the same Redis process, RAM, maxmemory, eviction policy, persistence files, CPU, and failure domain. SELECT is also connection state, so clients must restore the selected database after reconnecting. Mature libraries handle this, but it is still hidden state during debugging.

Redis Cluster makes the long-term tradeoff even clearer: it supports DB 0 only and does not allow SELECT.

For organization inside one workload, I usually prefer explicit prefixes:

Text
myapp:cache:user:42
myapp:session:8f2c
celery:task:9ab1

They work with Cluster, show their purpose in tooling, and remove the "was cache DB 2 or DB 3 here?" problem.

Non-zero databases are not useless. They can be handy in local development or when closely related data needs convenient FLUSHDB separation. They are just a weak boundary, and weak boundaries become expensive when people mistake them for strong ones.

The Django + Celery Trap

Consider one Redis server split like this:

Text
DB 0 -> Django cache
DB 1 -> Celery broker
DB 2 -> Celery result backend

Now add:

Conf
maxmemory 2gb
maxmemory-policy noeviction

The Django cache can consume almost the entire 2 GB. When Celery tries to enqueue another task in DB 1, Redis can reject the write. DB 1 does not have a reserved slice of memory just because it has another number.

Switch the policy to allkeys-lru and the failure changes shape. Redis can now evict keys across the instance, including keys that belong to workloads where silent deletion is unacceptable.

The problem is not DB 0 versus DB 1. The problem is mixing two incompatible contracts:

WorkloadDesired behavior under pressure
Django cacheEvict old entries and keep serving
Celery brokerDo not silently discard queued work

One Redis instance has one global memory policy. No DB number fixes that.

This does not mean every project needs five Redis servers. If the workloads have similar durability and eviction needs, sharing can be completely reasonable. Split them when their failure policies conflict, not because a diagram looks cleaner with more boxes.

What I Would Actually Deploy

For a disposable cache:

Conf
maxmemory 4gb
maxmemory-policy allkeys-lru

I would consider allkeys-lfu if access frequency is a better signal than recency and the metrics support it. Persistence may be unnecessary when the cache is cheap to rebuild.

For important queue or coordination state:

Conf
appendonly yes
appendfsync everysec
maxmemory 1gb
maxmemory-policy noeviction

I would give that workload its own Redis instance or service, then make the application handle OOM errors and alert before the limit is reached. everysec still leaves a small loss window. If losing even one accepted job is unacceptable, Redis AOF alone is not enough; use a stronger delivery design such as a transactional outbox or a broker whose guarantees match the requirement.

For key organization inside one workload, I would stay on DB 0 and use prefixes. I would use a non-zero DB only when its FLUSHDB convenience has a concrete benefit and Redis Cluster compatibility is irrelevant.

The decision rule is straightforward:

  • Same failure policy and similar durability needs: sharing one instance can be fine.
  • Different eviction or durability needs: use separate instances.
  • Need only namespacing: use prefixes first.
  • Need recoverability: AOF plus tested, independent backups.

What I Would Monitor

The configuration file tells you what should happen. Metrics tell you what is already going wrong.

From INFO memory:

  • used_memory
  • used_memory_rss
  • mem_not_counted_for_evict
  • allocator_frag_ratio and allocator_frag_bytes

From INFO stats:

  • evicted_keys
  • cache hits and misses
  • error replies, plus application-level Redis OOM errors

From INFO persistence:

  • whether AOF is enabled
  • last AOF write and rewrite status
  • whether a rewrite is running or scheduled

I would also alert on host memory, swap activity, disk space, and Redis latency. AOF cannot help much if the disk is full, and a healthy maxmemory number does not cancel an unhealthy RSS number.

The two lines I would keep in my head are these:

maxmemory is not a hard cap on the Redis process.

Logical databases are namespaces, not isolation.

Once those are clear, the rest of the configuration becomes much harder to misuse.

References