Redis AOF, maxmemory, and the Isolation Trap
I wanted one practical answer to four Redis questions:
- What does AOF really protect?
- Does
maxmemory 4gbkeep 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:
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.
| Setting | The question it answers |
|---|---|
| AOF | What Redis can reconstruct after a restart |
maxmemory | When Redis should apply its memory policy |
noeviction | What Redis does when that threshold is reached |
| DB 0, DB 1, DB 2 | Which 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:
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:
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:
maxmemory 4gb
It is tempting to read that as:
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:
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:
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:
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:
SELECT 1
Redis URLs encode the same choice:
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:
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:
DB 0 -> Django cache DB 1 -> Celery broker DB 2 -> Celery result backend
Now add:
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:
| Workload | Desired behavior under pressure |
|---|---|
| Django cache | Evict old entries and keep serving |
| Celery broker | Do 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:
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:
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_memoryused_memory_rssmem_not_counted_for_evictallocator_frag_ratioandallocator_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:
maxmemoryis 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.
