Using Redis as a Data Store: Memory Policy, Persistence and Its Limits as a Queue
Redis arrives as a cache and ends up holding sessions and the job queue. Choosing maxmemory-policy, the cost of RDB versus AOF, what async replication loses, and lists versus streams.
Redis usually enters a project as a cache. A few months later it also holds sessions, counters, locks and the job queue. That transition is rarely a deliberate decision; it just happens. Then the first incident reveals that a system designed on the assumption that "the cache can disappear, it does not matter" is in fact the only place some data lives. The question is not whether Redis is good or bad. The question is which guarantee it offers, and whether that guarantee matches what the product needs.
Memory policy: the first source of silent data loss
If maxmemory is not set, the process grows until it exhausts memory and the kernel kills it. If it is set, what happens at the limit is decided by maxmemory-policy, and the options behave very differently.
noeviction discards nothing and rejects write commands with an error. For a Redis instance used as a data store, that is the right choice: data does not vanish quietly, the application gets an error. The price is that the application must actually handle that error. A code path that ignores the return value of SET still loses data under this policy; the blame simply moves from Redis to your code.
allkeys-lru and allkeys-lfu are the right choice for a cache, evicting the least used keys. In an instance used as a data store, the same policy makes counters and locks evictable too.
The most dangerous option is the volatile-lru family, which evicts only keys that carry a TTL. It sounds sensible, and one usage pattern quietly paralyses it. If most keys have no TTL, there are no eviction candidates, and Redis starts behaving like noeviction: a system you believed to be a cache begins returning write errors. Measure what fraction of your keys carry a TTL instead of assuming. If the expires count in INFO keyspace is tiny next to the total key count, a volatile-* policy is the wrong setting.
Persistence: two mechanisms, two different costs
RDB writes a full point in time snapshot at intervals. It is cheap, the file is compact, restores are fast. The loss is the window: every write after the last snapshot is gone in a crash.
AOF appends every write command to a file. The appendfsync everysec default means at most one second of loss; always syncs on every command and cuts throughput significantly. Because the AOF file grows, it needs periodic rewriting, which costs resources of its own.
Running both is the right arrangement in most cases: AOF shrinks the loss window, RDB gives fast restores and a portable backup artifact.
The trap that bites hardest here is about memory. Both the RDB snapshot and the AOF rewrite are produced through fork. Pages written during the copy are duplicated, so memory usage can briefly approach double the data size. On a write heavy instance, a process that looks like it is using 60 percent of memory gets killed precisely while taking a backup. This is why vm.overcommit_memory=1 is recommended, and why you plan physical memory at close to twice the dataset size. For the same reason, the last rewrite duration and rdb_last_bgsave_status in INFO persistence are fields worth monitoring.
What replication does not guarantee
Redis replication is asynchronous. The primary acknowledges a write to the client and then ships it to replicas. If the primary dies right after acknowledging and a replica is promoted, acknowledged writes that never reached the replica are lost. That is not a bug, it is a design trade off, and the speed comes from it.
The WAIT command lets you block until a write has reached a given number of replicas. It reduces loss, does not eliminate it, and adds latency. Deploying Sentinel or Cluster does not close the gap either; they manage availability, not durability. For data whose loss costs money, such as a funds transfer, a stock decrement or an idempotency record, the right home is an ACID database. Redis can stay in that flow as a speed layer, but it should not be the record itself.
Redis as a queue: lists versus streams
The most common way to turn Redis into a job queue is the LPUSH and BRPOP pair. It is simple and fast. Its flaw: BRPOP removes the job from the list, so if the worker dies mid job, the job exists nowhere. There is no notion of acknowledgement.
Streams close that gap. Each entry read through a consumer group stays in a pending list until XACK arrives; the jobs of a dead worker are visible through XPENDING and can be reassigned with XCLAIM. So Streams give you the material to build at least once delivery, but they do not build it for you: you write the code that notices a dead worker, claims pending entries and moves poison messages to a dead letter stream.
When choosing among the three, I would draw the line like this. If jobs can be lost and regenerated (cache warming, thumbnail generation, a repeated notification), a Redis list is sufficient and needs the least code. If you need delivery guarantees at moderate volume and you already run a relational database, keeping the queue there is the least surprising path for most teams: you enqueue in the same transaction, so no job is lost. If you need very high volume, replay and several independent consumer groups, a durable messaging system is the right tool. Redis Streams sit in between as a reasonable middle ground that leaves part of the operational burden to your application.
Traps
Calling KEYS in production scans the entire keyspace and blocks the single threaded server for its duration; iterate with SCAN instead. A single large key, a list with millions of elements or a value of a hundred megabytes, slows both the commands touching it and any resharding; hunt them with --bigkeys.
Forgetting TTLs is the most common cause of memory growth in an instance that was never meant to be durable storage. Fragmentation is visible through mem_fragmentation_ratio in INFO memory; values above roughly 1.5 justify active defragmentation, which in turn costs CPU.
Finally, the replica output buffer: a replica being fed a large dataset over a slow link can exceed the buffer limit on the primary, the connection is dropped, and the sync restarts from the beginning, in a loop. Size client-output-buffer-limit against your dataset.
When not to make Redis your data store
If the only copy of some data lives in Redis, you are accepting a loss window measured in seconds. If that is not acceptable, solve it with architecture rather than persistence flags: let a durable database own the record and let Redis carry read speed and ephemeral state. Using Redis as a fast database works right up until a node dies unexpectedly, and on that day the size of the loss was already decided by the word you typed on the maxmemory-policy line months earlier.