Tell someone new to systems that the cache holding up half the internet is single-threaded and watch them assume it's a legacy wart waiting to be fixed. It isn't. Redis processes commands one at a time on a single core on purpose, and that decision is a big part of why it's both blazingly fast and refreshingly simple to reason about. Redis is full of choices like that β counterintuitive until you see the logic, then obviously right. Understanding a few of them turns Redis from "the thing we throw a cache at" into a tool you can wield deliberately.
Redis is an in-memory data structure store: it keeps everything in RAM and serves not just opaque key-value pairs but rich structures β lists, hashes, sets, sorted sets. I'll cover why single-threaded is fast, the data structures that make it more than a cache, how it persists data despite living in memory, and how it scales and stays available β plus the failure modes that bite teams.
Why single-threaded is a feature
Redis runs your commands on a single thread, in an event loop: it accepts connections from thousands of clients, and processes their commands one at a time, each to completion, before the next. That sounds like a bottleneck and turns out to be an advantage, for a few reinforcing reasons:
- No lock contention. Because only one command runs at a time, there are no locks, no mutexes, no race conditions in the data path. Multithreaded data stores spend real effort (and latency) coordinating access to shared structures; Redis simply doesn't have the problem.
- Everything is in RAM. There's no disk seek in the hot path, so a command rarely blocks β the thread is almost never idle waiting on I/O, so one core can push enormous throughput (hundreds of thousands of ops/second).
- Atomicity is free. Every command is atomic by construction β it runs to completion with nothing interleaved β which is why operations like
INCRare safe under concurrency without you doing anything.
graph TD
C1["Client 1"] --> Q["Event loop
(single thread)"]
C2["Client 2"] --> Q
C3["Client 3"] --> Q
CN["β¦thousands of clients"] --> Q
Q --> EXEC["Process one command
to completion (in RAM)
β atomic, no locks"]
EXEC --> Q
The single-threaded event loop. Many clients connect, but commands are serialized through one thread and each runs to completion against in-memory data. No locks, no races, and every command is atomic for free. (Redis 6 adds threading for network I/O β reading/writing sockets β but command execution stays single-threaded, preserving this model.)
The flip side: one slow command blocks everyone. Because there's a single thread, a single expensive command stalls every other client behind it. The classic outage is running KEYS * on a large database in production β it scans the entire keyspace synchronously and freezes the server for seconds while every other request waits. Same danger with big SORTs, large SMEMBERS, or a Lua script that loops. Use SCAN (incremental) instead of KEYS, keep commands O(1)/O(log n), and remember that in Redis, slow command means global pause.
Data structures: why it's more than a cache
The thing that separates Redis from a plain key-value cache is that a value isn't just a blob β it's a typed data structure the server understands and manipulates. This is the difference between storing a serialized list and being able to push, pop, and range over it atomically on the server:
| Structure | What it is | Typical use |
|---|---|---|
| String | Bytes, integer, or bitmap | Cache value, counter (INCR), flag |
| Hash | Field β value map | An object (user profile) without re-serializing the whole thing |
| List | Ordered, push/pop both ends | Simple queue, recent-items feed |
| Set | Unordered unique members | Tags, unique visitors, set intersections |
| Sorted set (ZSET) | Members ranked by score | Leaderboards, priority queues, rate limiting by time |
This is why Redis shows up far beyond caching β as a session store, a rate limiter (sorted sets keyed by timestamp), a leaderboard (sorted sets are made for this), a lightweight job queue (lists), and the low-latency online store behind a feature store, where a model needs a feature value in single-digit milliseconds. The server-side data structures mean the operation happens atomically in Redis rather than as a fragile read-modify-write in your app.
Persistence: durability for something that lives in RAM
Redis keeps data in memory, but "in memory" and "gone on restart" don't have to be the same thing. It offers two persistence mechanisms with different trade-offs, and the choice matters:
- RDB (snapshotting): periodically fork the process and dump the entire dataset to a compact binary file. Fast to load, small on disk, great for backups β but you lose everything written since the last snapshot if the server crashes. Point-in-time, not continuous.
- AOF (append-only file): log every write command to a file as it happens; on restart, replay the log to rebuild state. Far more durable (you can fsync every second, or every write), at the cost of a larger file and slightly slower writes. The AOF is periodically rewritten to compact it.
In practice many deployments run both β AOF for durability, RDB for fast restarts and backups. But the honest framing is that Redis durability is a spectrum you choose, from "pure cache, lose it all on restart" to "fsync every write." Decide deliberately based on whether Redis is a cache (losing data is fine β rehydrate from the source) or a system of record (it usually shouldn't be).
Replication, Sentinel, and Cluster
Three mechanisms take Redis from one node to a resilient, scalable deployment, and they answer different questions:
- Replication β a replica keeps a copy of a primary's data (asynchronously), giving you read scaling and a warm standby. Note asynchronous: a write acknowledged by the primary that crashes before the replica receives it can be lost.
- Sentinel β a monitoring system that watches primary and replicas, and on primary failure performs automatic failover (promotes a replica) and tells clients the new address. This is high availability for a single-shard setup.
- Redis Cluster β horizontal sharding. The keyspace is split into 16,384 hash slots distributed across primaries (each with replicas); a key's slot is
CRC16(key) mod 16384. Clients are redirected to the node owning a key's slot. This scales beyond one machine's RAM and throughput.
Cluster mode comes with a real constraint people trip over: multi-key operations only work if all the keys live in the same slot. A command touching several keys (a multi-key transaction, a set intersection) fails across slots. The fix is hash tags β putting {...} in your keys (user:{42}:profile, user:{42}:cart) so only the braced part is hashed, forcing related keys onto the same slot. Design your key names for this up front; retrofitting it onto a live keyspace is painful.
Eviction: what happens when memory fills
Because Redis lives in RAM, it has a maxmemory limit, and when you hit it the server must decide what to do. The eviction policy governs this: noeviction (reject writes β safe for a system of record), allkeys-lru (evict least-recently-used keys β the classic cache policy), allkeys-lfu (least-frequently-used, often better for skewed access), or variants that only evict keys with a TTL. Pairing eviction with per-key expiration (TTLs) is what makes Redis a well-behaved cache that never grows without bound.
Getting this wrong is a common production surprise: leave the default and a cache can fill memory and start rejecting writes, or evict keys you assumed were durable. Match the policy to the role β LRU/LFU with TTLs for a cache, noeviction with real persistence if Redis holds anything you can't lose.
What to carry away
Redis earns its speed and simplicity from a single-threaded event loop over in-memory data: no locks, no races, every command atomic β with the sharp edge that one slow command pauses everyone (never KEYS * in production). Its real differentiator is server-side data structures β hashes, lists, sets, sorted sets β that make it a session store, rate limiter, leaderboard, queue, and feature-serving layer, not just a cache. RDB and AOF give you a durability spectrum to choose deliberately; replication, Sentinel, and Cluster add read scaling, failover, and sharding (mind hash slots and hash tags); and eviction policies plus TTLs keep it bounded.
Treat Redis as what it is β an exceptional in-memory data-structure server β and the decisions are clear: pick persistence and eviction for its role, design keys for cluster slots, and keep commands cheap so the single thread stays fast. It's the low-latency layer in front of slower stores, and the natural online half of a feature store.