Common Redis Mistakes to Avoid: A Practical Guide

Common Redis Mistakes to Avoid: A Practical Guide

Common Redis Mistakes to Avoid: A Practical Guide

Common Redis Mistakes to Avoid: A Practical Guide

Redis can handle over 100,000 operations per second on modest hardware, which is exactly why teams reach for it so quickly. It is also why so many teams get burned. Speed hides problems until traffic spikes, and by then the mistakes are baked into production. The errors below show up again and again, in code reviews, incident reports, and postmortems. None of them are exotic. All of them are avoidable.

Running KEYS in production code

The `KEYS` command scans the entire keyspace in a single blocking operation. On a database with a few million keys, that scan can stall the server for seconds. Every other client waits. The fix is `SCAN`, which walks the keyspace incrementally with a cursor and never blocks for long. A surprising number of teams still ship `KEYS` in a debug endpoint or an admin script and then forget it exists. Search the codebase for it before it finds you at 3 a.m.

The same logic applies to `SMEMBERS` on large sets and `HGETALL` on huge hashes. Any command that returns an unbounded result set deserves a hard look.

Believing Redis is truly single-threaded

Believing Redis is truly single-threaded

The phrase “Redis is single-threaded” gets repeated so often that people stop questioning it. Since Redis 6.0, released in May 2020, the server uses threaded I/O for networking while command execution stays on one main thread. That distinction matters. Network throughput improved, but a slow command still blocks everything behind it. A long Lua script, a heavy `SORT`, or a big `DEL` will freeze the whole instance regardless of how many I/O threads are running. (Redis)

Treat the main thread as a shared resource with no mercy. If a command takes more than a millisecond or two, it does not belong in the hot path.

Assuming persistence is on by default

Default persistence in Redis is RDB snapshotting at configured intervals. If the server crashes between snapshots, every write since the last snapshot is gone. Teams that treat Redis as a durable primary database without understanding this get a rude education during their first failover. (Redis Documentation)

The append-only file (AOF) offers finer-grained durability, and Redis 7.0 introduced multi-part AOF files in April 2022, which changed how AOF rewriting works. The trade-off is real: AOF costs performance, and even `appendfsync always` is not a substitute for a proper database with transactions and backups. The honest framing is that Redis is a cache and a data structure server first, and a durable store second, with caveats that depend on configuration.

Persistence option What it protects against What it costs
RDB snapshots Restart with recent-ish data Loss of writes since last snapshot
AOF (everysec) Most crashes, up to ~1s of writes Extra disk I/O, larger files
AOF (always) Nearly all crashes Significant write latency
None Nothing Total data loss on restart

Leaving maxmemory unset

Without a `maxmemory` limit, Redis will happily consume every byte of RAM on the host until the operating system kills the process. The default eviction policy is `noeviction`, which means once memory fills up, writes start failing with errors instead of old keys being removed. Neither outcome is pleasant.

Set `maxmemory` to a sensible fraction of available RAM, leaving headroom for copy-on-write during RDB saves and for fragmentation. Then choose a policy deliberately. `allkeys-lru` works well for pure caches. `volatile-lru` only evicts keys with a TTL, which is safer when some keys must never disappear. The policy is a design decision, not a default to accept.

Never setting TTLs

Keys without expiration accumulate forever. A cache that only grows is not a cache; it is a slow-motion memory leak. Every key written as a cache entry should generally carry a TTL, and the TTL should reflect how stale the data can afford to be.

One subtlety: `EXPIRE` marks a key for deletion, but a key holding a very large value still occupies memory until it is actually removed. Redis uses a mix of lazy expiration (on access) and active expiration (a background cycle sampling keys), so memory is not reclaimed the instant a TTL passes. Sizing memory plans around “keys will vanish exactly on time” leads to unpleasant surprises.

Storing oversized values and creating hot keys

Multi-megabyte blobs inside Redis values create latency spikes, slow down replication, and make snapshots enormous. Values should stay small; exact recommended ceilings vary by workload and source, so treat any specific number with suspicion and measure your own. If a value is measured in megabytes, object storage is usually the better home, with Redis holding a pointer.

Hot keys are the other side of the same coin. A single key receiving a disproportionate share of traffic becomes a bottleneck on one shard in a Cluster deployment. The usual mitigations are key splitting (sharding a counter into N sub-keys) and local caching in the application.

Using DEL on large collections

`DEL` on a set, hash, or list with millions of members is synchronous. The server frees the entire structure before moving on, and everything else waits. `UNLINK`, available since Redis 4.0 in 2017, removes the key from the keyspace immediately and frees the memory in a background thread. For large collections, `UNLINK` is almost always the right call.

Opening a connection per request

Redis handles many concurrent connections, but that does not make per-request connections a good idea. Each connection carries handshake overhead, and unbounded connection growth can exhaust `maxclients`, at which point new clients get rejected. Use a connection pool from your client library, size it to match your concurrency, and reuse connections across requests.

Ignoring security defaults

Historically, Redis had no authentication by default and could bind to all network interfaces. Exposed instances have been widely exploited for cryptomining and worse. Modern versions ship with `protected-mode` enabled and support ACLs, added in Redis 6.0, but managed services and older deployments differ. Verify your `bind` directive, confirm `protected-mode` is on, and never expose port 6379 to the public internet. A firewall rule is not paranoia; it is table stakes.

Treating replication as a backup

Asynchronous replication means a failover can lose the most recent writes. That is acceptable for a cache and unacceptable for anything resembling a system of record. Sentinel and Cluster both have specific consistency caveats worth reading before you rely on them. A replica is not a backup, and a backup is not a replica. Keep real backups, test restores, and know your recovery time objective.

A quick checklist

Before shipping any Redis-backed feature, run through this list:

  • Search the codebase for `KEYS`, `SMEMBERS`, and `HGETALL` on unbounded collections.

  • Confirm `maxmemory` and an eviction policy are set explicitly.

  • Verify TTLs on every cache write path.

  • Check that large values have an exit plan.

  • Confirm connection pooling is in use.

  • Review `bind`, `protected-mode`, and ACL configuration.

  • Confirm persistence settings match the durability the feature actually needs.

None of these take long. Each one has prevented an outage somewhere.

The pattern behind all these mistakes is the same: Redis is so easy to start that it is easy to stop thinking about. A five-minute configuration review beats a five-hour incident review. If you are documenting your setup for the team, you may also want a quick reference for capturing dashboards and config screens during handoffs, such as this guide on how to take a screenshot on windows or the equivalent for taking a screenshot on a mac. Small operational habits compound.

Conclusion

Redis rewards teams that respect its boundaries. The blocking commands, the persistence defaults, the memory limits, and the security posture are all documented, and all routinely ignored. The fix is not a rewrite. It is a handful of deliberate decisions: `SCAN` instead of `KEYS`, `UNLINK` instead of `DEL` on big collections, an explicit `maxmemory` policy, TTLs on cache entries, pooled connections, and a firewall that keeps the port private. Make those decisions once, write them down, and the next traffic spike becomes a non-event instead of a postmortem.

FAQ

Is Redis single-threaded or multi-threaded?

Both, depending on what you mean. Command execution runs on a single main thread, so a slow command blocks everything behind it. Since Redis 6.0, networking uses additional I/O threads. The practical takeaway is unchanged: keep individual commands fast.

Why is KEYS dangerous in production?

`KEYS` scans the entire keyspace in one blocking operation. On large datasets it can stall the server for seconds while every other client waits. Use `SCAN`, which iterates incrementally with a cursor and never blocks for long.

Does Redis lose data on restart?

It can. With default RDB snapshotting, any writes since the last snapshot are lost on a crash. AOF persistence reduces the window, and Redis 7.0 changed how AOF files are structured, but no configuration makes Redis a fully durable database without trade-offs. Match persistence settings to what the data actually needs.

What happens if maxmemory is not set?

Redis keeps allocating memory until the operating system runs out and kills the process. The default eviction policy, `noeviction`, means writes start returning errors once memory is full rather than evicting old keys. Always set `maxmemory` and choose a policy deliberately.

What is the difference between DEL and UNLINK?

`DEL` frees memory synchronously, so deleting a large collection blocks the server. `UNLINK`, available since Redis 4.0, removes the key immediately but reclaims memory in a background thread. For big sets, hashes, or lists, `UNLINK` is the safer choice.

Should every Redis key have a TTL?

Every key used as a cache entry generally should. Keys without expiration accumulate indefinitely and turn a cache into a memory leak. Exceptions exist for coordination keys and small reference data, but those should be deliberate choices, not accidents of omission.

Final thought

The checklist above is deliberately boring, and that is the point. None of these mistakes are exotic; they are the defaults you inherit when nobody on the team has made an explicit decision. A new service pointed at a fresh Redis instance gets `noeviction`, RDB snapshots, no TTLs, and whatever `bind` the package shipped with. It will work perfectly in staging and fail in production under conditions the defaults were never designed for.

The single highest-leverage habit is to write the Redis configuration down as part of the service’s design document, the same way you would document a database schema or an API contract. Which keys exist, which carry TTLs, what the eviction policy is, whether persistence matters, and who can reach the port. When those answers live in one place, a code reviewer can spot a `KEYS` call or a missing TTL in seconds, and an on-call engineer at 3 a.m. does not have to reverse-engineer intent from a running instance.

It also helps to treat Redis metrics as first-class signals rather than an afterthought. Watch `used_memory` against `maxmemory`, `evicted_keys`, `keyspace_hits` versus `keyspace_misses`, and `latency` percentiles. A rising eviction count on a cache is often fine; a rising eviction count on a store that was supposed to be durable is an alarm. Command-level slow logs catch the blocking commands before users do.

Finally, accept that Redis is a tool with sharp edges and a narrow, excellent purpose. It is superb at what it was built for: fast in-memory data structures, caching, rate limiting, queues, and coordination at modest scale. It is a poor fit for workloads that demand strict durability, unbounded value sizes, or complex multi-key transactions across shards. Teams that pick it for the right jobs, configure it on purpose, and revisit the configuration when the workload changes rarely end up writing postmortems. The ones that treat it as a magic box that “just works” eventually do. Choose the first path, and the list above becomes a five-minute habit instead of a cautionary tale.

on this page