Redis
The in-memory store you'll reach for first when latency matters
Use Cases
Architecture
Most engineers working on backend systems that need to be fast have already used Redis. It started as a caching layer, but these days it sits at the center of session management, rate limiting, real-time analytics, and a dozen other use cases. The reason it stuck around while other tools came and went is simple: it delivers sub-millisecond latency with data structures that actually match real problems, not just key-value pairs.
That said, Redis is not a database replacement. Treating it as one ends badly, usually at 3 AM during a traffic spike.
How It Works Internally
Redis runs a single-threaded event loop. It uses epoll on Linux and kqueue on macOS for I/O multiplexing. No locks, no context switches. A single instance on modern hardware pushes 100,000 to 200,000 ops/sec without breaking a sweat. Since Redis 6.0, I/O threading offloads socket reads and writes to background threads while command execution stays single-threaded, roughly doubling throughput on multi-core machines.
The data structures are where Redis gets interesting. Sorted sets use a skip list for O(log N) range queries alongside a hash table for O(1) point lookups. Small hashes and lists use ziplist encoding, which packs data into contiguous memory and cuts pointer overhead. Strings under 44 bytes use embedded SDS (Simple Dynamic Strings) to skip a separate allocation. The practical takeaway: memory efficiency varies a lot based on how the data is shaped. A hash with 100 small fields uses far less memory than 100 individual string keys. Consider the data model before writing keys.
Persistence works in two flavors. RDB snapshots fork the process and serialize the dataset using copy-on-write, producing compact point-in-time backups. AOF (Append-Only File) logs every write command. Fsync can be configured per command, per second, or never. Most production setups run both: AOF with appendfsync everysec for durability, plus periodic RDB snapshots for fast restores and offsite backups. Neither is perfect. RDB can lose up to a few minutes of data. AOF files grow large and need periodic rewrites. Pick the tradeoff.
Production Architecture
Redis Cluster partitions the keyspace into 16,384 hash slots spread across primary nodes. Each primary replicates asynchronously to one or more replicas. When resharding is needed, the MIGRATE command atomically moves keys between nodes while clients follow MOVED and ASK redirections. It works, but it adds operational weight. Redis Sentinel handles automatic failover for non-clustered setups by monitoring primaries and promoting replicas when health checks fail. Failover typically completes within 30 seconds.
Here is practical advice from running this in production. Deploy at least three Sentinel instances across separate failure domains. Set replica-read-only yes and send read traffic to replicas if the workload skews read-heavy. Watch replication lag through INFO replication. And set min-replicas-to-write to stop a partitioned primary from accepting writes that will vanish once the partition heals. This one setting has saved me from data loss more than once.
Capacity Planning
A single Redis instance on an r6g.xlarge (32GB RAM) handles about 200K ops/sec for simple GET/SET. Budget 2x to 3x the dataset size in RAM. That headroom is needed for fragmentation, replication buffers, and copy-on-write overhead during RDB saves. People consistently underestimate this. Instagram, as a reference point, stores over 300 million key-value mappings in Redis for media-ID-to-user-ID lookups and keeps it around 1GB through aggressive hash encoding optimizations.
Keep an eye on used_memory_rss versus used_memory. If the fragmentation ratio crosses 1.5, significant memory is being wasted. Track evicted_keys, keyspace_misses, and connected_clients as the primary health signals. For pure cache workloads, set maxmemory-policy to allkeys-lru. For mixed workloads where some keys need to stick around, volatile-ttl is usually the better choice.
Failure Scenarios
Scenario 1: Split-brain during a network partition. The primary gets cut off from Sentinel and its replicas but stays reachable by some app servers. It keeps accepting writes while Sentinel promotes a replica on the other side. When the partition heals, the old primary demotes itself and resynchronizes. Every write it accepted during the split is gone, permanently. Limit the damage by setting min-replicas-to-write 1 with min-replicas-max-lag 10 so the isolated primary rejects writes once it loses contact with its replicas. Monitor master_link_down_since_seconds on replicas to catch this early.
Scenario 2: Memory exhaustion from missing TTLs. Someone ships a feature that writes session data without TTLs. Memory climbs slowly. Nobody notices until maxmemory fills up and the eviction policy starts deleting cache keys. The result is a stampede of cache misses hammering the database. I have seen this take down a production database backend during a traffic peak. The fix: alert when used_memory passes 80% of maxmemory, audit key namespaces regularly with MEMORY USAGE and OBJECT IDLETIME, and enforce TTL policies at the application layer for every cache key. No exceptions.
Pros
- • Sub-millisecond latency for reads and writes
- • Rich data structures: lists, sets, sorted sets, hashes, streams
- • Built-in replication and Lua scripting
- • Persistence options (RDB snapshots, AOF)
- • Cluster mode for horizontal scaling
Cons
- • RAM-bound, so your entire dataset must fit in memory
- • Single-threaded command execution
- • Cluster mode adds real operational complexity
- • No query language for complex lookups
When to use
- • You need sub-millisecond reads and writes
- • Caching hot data in front of a slower database
- • Real-time counters, leaderboards, or rate limiters
- • Session management across multiple app servers
When NOT to use
- • Your dataset is much larger than available RAM
- • You need full ACID transactions with joins
- • Primary long-term storage for data you cannot lose
- • Complex relational queries
Key Points
- •The single-threaded event loop hits 100K+ ops/sec by dodging context switches and lock contention entirely
- •RDB takes point-in-time snapshots via fork() and copy-on-write. AOF logs every write for durability, but at the cost of disk I/O and periodic rewrite overhead
- •Cluster mode splits the keyspace into 16384 hash slots using CRC16. Slot migration enables live resharding without downtime
- •If the memory fragmentation ratio (used_memory_rss / used_memory) climbs above 1.5, jemalloc is struggling. Run MEMORY PURGE or restart with activedefrag
- •Pub/Sub is fire-and-forget with zero persistence. For consumer groups, acknowledgment, and replay, use Redis Streams instead
- •Sorted sets use skip lists internally for O(log N) insert and range queries. They are the backbone of leaderboards at places like Riot Games and Discord
Common Mistakes
- ✗Skipping maxmemory and eviction policy config. Redis will eat all available RAM, the OOM killer fires, and the process (maybe the host) goes down
- ✗Running KEYS in production. It scans the entire keyspace in O(N), blocking everything else. Use SCAN with cursor-based iteration
- ✗Ignoring memory fragmentation. High fragmentation wastes 30-50% of allocated memory. Monitor INFO memory and turn on activedefrag in Redis 4.0+
- ✗Not using pipelining for batch operations. Round-trip latency kills throughput. Pipelining delivers 5-10x improvement on bulk workloads
- ✗Storing values larger than 100KB. Big values block the event loop during serialization and transfer. Break them into smaller keys or use hashes