Redis in Production: Patterns That Work and Anti-patterns That Will Break You
Redis is fast until it isn't. Here are the caching patterns, data structure choices, and operational decisions that determine whether Redis is a performance multiplier or a fragile dependency.
Redis is the database most teams add without a meeting. You add it for caching, it works immediately, and six months later it's carrying session state, rate limiting, pub/sub messaging, distributed locks, and the leaderboard. The original cache is fine. The session store is fine. The distributed lock implementation is broken in a subtle way that only manifests under specific failure conditions.
The patterns matter. Here's what I've learned from running Redis at scale.
Data structure selection is architectural
Redis isn't a key-value store. It's a data structure server that happens to have strings. The data structure you choose affects performance, memory usage, and correctness.
Strings: for simple values, counters, and serialised JSON. Use INCR and INCRBY for counters — they're atomic. Don't store large JSON objects as strings if you frequently update single fields; you'll serialise/deserialise the whole object on every write.
Hashes: for objects with multiple fields where you update fields independently. A user profile stored as a Hash lets you HSET user:123 email new@email.com without touching the rest of the profile. Hashes are also memory-efficient for small collections — Redis uses a more compact encoding for hashes with fewer than 128 fields.
Sorted Sets: for leaderboards, priority queues, and range-by-score queries. ZADD and ZRANGEBYSCORE are O(log N). If you're building a leaderboard, a sorted set is the right structure — don't simulate it with strings and sorting in application code.
Sets: for unique collections, tag systems, and membership tests. SMEMBERS, SUNION, SINTERSTORE. The SINTERSTORE for computing the intersection of tag sets (e.g., "articles tagged both 'cloud' and 'finops'") is fast and idiomatic.
Lists: for queues and stacks. LPUSH / RPOP for a FIFO queue. Be careful using Redis Lists as a production job queue for anything critical — consider using a purpose-built system (Kafka, SQS, RabbitMQ) for durability requirements.
Streams: added in Redis 5.0, these are the right structure for append-only event logs with consumer group support. If you're using Lists as a poor man's message queue, evaluate Streams as an upgrade path.
TTL is not optional
Every key that isn't permanent state should have a TTL. This isn't about memory pressure (though it helps) — it's about correctness. A cached value without a TTL becomes stale data that persists indefinitely. A session key without a TTL becomes a session that never expires.
The most common mistake: setting TTLs on new keys but forgetting to reset them on update. If you cache a user's permissions with a 15-minute TTL and then update the permissions, the cached value still has whatever time remains on the original TTL. Use SET key value EX seconds to set the value and TTL atomically, or explicitly EXPIRE the key after every update.
The second most common mistake: all cache keys expiring at the same time. If you populate a cache at startup and set TTL=3600 on everything, one hour later your cache is empty and the database gets a spike of requests as everything misses simultaneously. Add jitter: TTL = base_ttl + random(0, base_ttl * 0.1).
The distributed lock problem
Redis-based distributed locks are widely used and frequently implemented incorrectly.
The pattern that looks correct but isn't:
SET lock:resource lock_id NX EX 30
NX means "set if not exists" — only acquires the lock if it's not already held. EX 30 means the lock expires after 30 seconds even if not released — prevents deadlocks if the holder crashes. The lock_id is a unique value generated by the lock holder — you only release a lock if the value matches (preventing releasing another process's lock).
The release is:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
The Lua script makes the check-and-delete atomic.
Why this is still not guaranteed safe: Redis is single-threaded but not instantaneous. Under partition scenarios (split-brain), two clients can both acquire the lock. For most applications this is an acceptable trade-off. For financial operations, inventory management, or any operation where double-execution has serious consequences — consider a different approach or use the Redlock algorithm (with understanding of its limitations).
The Redlock algorithm requires acquiring the lock on a majority of N independent Redis nodes. It's more robust against single-node failures but adds complexity. Martin Kleppmann's critique of Redlock is worth reading before implementing it.
Memory management: eviction policies and memory pressure
Redis stores everything in memory. When memory is full, the eviction policy determines what happens:
noeviction: return errors on write when memory is full. The right choice for a data store, never for a cache.allkeys-lru: evict the least recently used key across all keys. The right choice for a general cache.volatile-lru: evict the LRU key from keys with TTLs set. Preserves keys without TTLs (permanent data) while evicting cache entries.allkeys-lfu: evict the least frequently used key. Often better than LRU for access patterns that are skewed (hot keys accessed very frequently).
For cache use cases: allkeys-lru or allkeys-lfu. Set maxmemory to a value below the system memory limit (leave headroom for Redis's own memory management). Monitor used_memory_rss and used_memory — a large gap between them indicates fragmentation.
The hot key problem
Redis is single-threaded. A key accessed extremely frequently (a viral post's view count, a configuration value read on every request) can saturate a single Redis instance. Signs: CPU on the Redis node is high, latency for all operations increases, slow commands log shows fast commands taking milliseconds.
Approaches:
- Read replicas + read from replicas: valid for read-heavy hot keys, doesn't help write contention.
- Local cache with short TTL: cache the value in process memory for 100ms. Reduces Redis load by the request rate times 100ms.
- Key sharding: instead of one key
counter:popular_post_123, use multiple keyscounter:popular_post_123:shard_{0..9}and increment a random shard. Read by summing all shards. More complex, reduces write contention significantly.
Sentinel vs. Cluster vs. managed services
Redis Sentinel: high availability for a single-primary setup. Sentinel processes monitor the primary, detect failures, and promote a replica. Simple to operate; limited throughput scaling.
Redis Cluster: horizontal scaling across multiple nodes. Data is partitioned across 16,384 hash slots. Supports multiple primaries with their own replicas. Operations involving multiple keys must map to the same hash slot (use hash tags: {user:123}:profile and {user:123}:sessions both map to the same slot).
Managed services (AWS ElastiCache, Azure Cache for Redis, GCP Memorystore): operational overhead handled by the cloud provider. Automatic failover, patching, monitoring. If you're running on cloud infrastructure, there's rarely a good reason to run self-managed Redis — the operational cost of self-managing doesn't justify the control you get.
The choice between Sentinel and Cluster depends on throughput. If a single Redis instance handles your peak load comfortably with headroom, use Sentinel. If you're hitting single-instance CPU or memory limits, move to Cluster.
Diagnosing Redis performance issues or planning a migration from single-node to cluster? Happy to talk through the specifics.
Related
Capacity Planning in the Cloud Era: Headroom as an Engineered Property
Elastic infrastructure didn't kill capacity planning — it changed what gets planned. Demand modeling, the utilization-vs-headroom trade, quota and constraint archaeology, and the quarterly loop that replaces both waste and 3 AM scrambles.
API Design for Enterprise Systems: The Decisions That Age Well
An API that's easy to build against is easy to build wrong. The decisions around versioning, error models, pagination, idempotency, and authentication compound over time — here's how to make the ones that don't cause regret.
System Design Patterns Every Backend Engineer Should Know
CQRS, event sourcing, saga pattern, outbox pattern — the patterns that solve specific distributed systems problems, what trade-offs they introduce, and when the simpler option is actually the right one.
Working on this in production?
We do this work directly alongside engineering teams — architecture review, migration, and hands-on enablement.