Redis Architecture: Data Structures, Persistence, and High Availability
Redis architecture under the caching patterns: the single-threaded event loop, redis data structures, maxmemory eviction, rdb vs aof persistence, and the sentinel vs cluster HA ladder, part 5 of the Caching in System Design series.
Part 4 pointed every internals question here, and the series recorded the debts precisely. Part 1 promised that how a cache server implements eviction, persistence, and its data structures is this article’s subject. Part 2 (distributed caching) described Redis Cluster’s coordinator routing in a single line and left the machinery alone. Part 3 (cache failure modes) named this article as the replication and failover machinery behind a cold start. Part 4 (redis caching) handed over the eviction policies, the persistence question, and everything else that sits underneath a SET. This article pays those debts in order: the server model, the data structures, memory and eviction, persistence, and the high-availability ladder from replication to Sentinel to Cluster.
Redis architecture is the internal design of the server: an in-memory dataset with optional disk durability, a single thread that executes every command to completion in order, and (when one machine stops being enough) asynchronous replication with two different failover systems stacked on top. The design’s premise is that memory is fast and contention is slow: by executing commands one at a time, Redis buys atomicity and predictable latency at the price of never being allowed to block that one thread. Nearly every operational rule about Redis (avoid big values, avoid whole-keyspace commands, respect the memory ceiling) is that single rule restated: do not make the one thread wait.
The event loop: one thread, in memory, atomic
The core of the server is an event loop; one thread multiplexing thousands of client connections and executing each command to completion before it takes the next. Recent versions of Redis move the socket reading and writing onto a few additional threads, but command execution itself stays serialized on one thread, and that is where the design’s consequences live. With no two commands interleaving on the same core of execution, Redis needs almost no locks, and every single-key command is atomic for free, the property part 4’s counters leaned on when a SET ... NX became a lock and an INCR became a window without any further machinery.
The price of the design is that anything slow is slow for everyone. KEYS walks the entire keyspace and blocks the loop for its duration; a FLUSHALL does the same to delete it; a large Lua script or a SORT over a big collection is no different. The operational discipline is to keep the loop fed with small, bounded work: SCAN instead of KEYS, iterating the keyspace with a cursor in digestible pieces; UNLINK instead of DEL, freeing big values in a background thread; and a habit of asking the big-O of any command that takes a collection rather than a key.
Big values deserve their own warning because they fail quietly. A single multi-megabyte value makes every reader of that key wait for its serialization, makes eviction more expensive when the ceiling is reached, and distorts the memory accounting that capacity planning depends on. The fix is the one part 4 already gave (cache the response shape, keep values packet-sized) plus the client tooling that finds the outliers before an incident does. The rule of the server model, worth pinning before the data structures arrive: the dataset lives in memory, and the loop that serves it is one thread.
Data structures beyond strings
Strings are the workhorse of cache-aside (a serialized object under one key, a counter under INCR) but the type system exists because cached shapes are richer than blobs. The types a cache actually uses, and what each is for:
- Strings: whole serialized values, counters, and the part 4 pattern itself: one entity, one key, one TTL.
- Hashes, one cached entity with field-level access: read or update a single field without deserializing and rewriting the whole object.
- Lists; ordered, push and trim at the ends: latest-N feeds and recency windows via
LPUSHplusLTRIM. - Sets, unique membership: tags, feature flags per user, the negative-caching namespaces part 4 sketched, with
SADDandSISMEMBERdoing the work. - Sorted sets, members ordered by score: leaderboards, time-ordered activity with the score as a timestamp, and sliding windows that expire by score range.
Underneath the types, the server picks compact encodings for small values (integers stored as integers, small lists and hashes as packed sequences) and promotes to the larger structures as data grows. The cache-level consequence is that memory is charged per key as well as per byte: many small keys are not free, and neither is one giant one. The part 4 advice to cache the shape the caller needs is also, therefore, memory advice.
Two more features complete the picture for caching workloads. Pub/sub channels carry the invalidation events part 2 sketched for fleet fan-out. Keyspace notifications broadcast the server’s own events (expiries included) and look like an invalidation mechanism, but they are fire-and-forget: a subscriber that misses one has simply missed it, so a stale key survives until its TTL. Notifications are an optimization, never the correctness mechanism; the deliberate techniques are part 6’s subject, and this article only builds their transport.
Memory and eviction: the maxmemory decision
The memory ceiling is where architecture becomes policy. Redis holds the dataset in RAM under a configurable maxmemory, and when the ceiling is reached the server stops storing and starts choosing; the eviction policy decides what dies so that writes can keep living. The default, noeviction, refuses the write and returns an error: correct for a store, quietly load-bearing for a cache, because a full cache now fails requests instead of merely slowing them. The policies that make a cache behave like a cache:
allkeys-lru: evict the least recently used key, found by sampling rather than tracked exactly; the standard choice for a pure cache.allkeys-lfu; evict by frequency, keeping keys that are hit often even if not recently; better when scans and one-off traffic would mislead recency.volatile-lru/volatile-lfu: evict only among keys with TTLs, protecting anything the application marked permanent.volatile-ttl: evict the key nearest expiry, aligning the server’s victim choice with the staleness budget.allkeys-random/volatile-random, no ordering at all; rarely the right answer for a cache.noeviction, the default; writes fail at the ceiling; right only when the dataset is managed deliberately, never by TTLs alone.
Choosing for a cache follows the contract. A pure, flushable cache pairs allkeys-lru or allkeys-lfu with the part 4 TTL discipline; the TTL bounds staleness per key, the ceiling bounds memory for the whole dataset, and neither does the other’s job. The volatile family exists for the mixed instance, the one holding both cache keys and something the team decided must survive, and that instance is the drift part 4 warned about, arriving one convenience at a time. The honest configuration review asks a different question than which policy: why is there anything in this Redis that eviction is not allowed to touch?
One advanced note for the choice between recency and frequency: Redis’s LRU is an approximation (the server samples candidate keys rather than maintaining a perfect list) and its LFU counters decay over time, so yesterday’s hot key eventually loses to today’s. Neither is a flaw; both are the design saying exactness is not worth the memory. What the operator owes the ceiling is a deliberate answer, because the default answers nothing.
Persistence: RDB vs AOF
The persistence question arrives with the contract already signed. A cache is allowed to forget (that is what disposable means) so persistence is not a caching feature; it is the first feature of the store role, and part 4 deferred it here precisely because it changes what the component is. Redis offers two mechanisms, and they answer different questions about how much forgetting is tolerable.
RDB persistence writes a point-in-time binary snapshot of the dataset: the server forks, the child process writes the snapshot to disk while the parent keeps serving, and the result is compact and fast to load. The loss window is the time between snapshots; minutes or hours of writes, gone after an unclean restart. AOF instead appends every write command to a log, with an appendfsync policy deciding how often the log reaches the disk: everysec is the usual compromise, at most one second of loss against an fsync that stays off the request path, while always turns every write into a durability decision and no hands the timing to the operating system. The log grows, so a background rewrite periodically rewrites it as the minimal command sequence that rebuilds the same state.
| Dimension | RDB | AOF |
|---|---|---|
| What it is | Point-in-time binary snapshot via fork | Append-only log of write commands |
| Loss window on restart | Everything since the last snapshot | At most the fsync interval (one second, with everysec |
| File size | Compact | Larger, until the rewrite compacts it |
| Restart speed | Fast) one file to load | Slower; commands replayed on load |
| Cost while running | A fork per snapshot | An fsync policy and a rewrite to schedule |
| Best fit | Warm starts, cache-role Redis | Store-role Redis that must not forget |
The decision for a cache is usually none at all: persistence off, cold starts paid through part 3’s fail-open, and the database rebuilding the working set under the burst controls this series keeps insisting on. The exception is the warm start; an RDB loaded after a restart keeps a fleet’s worth of misses off the database, which is an availability decision, not a data one. What the decision must never be is accidental: AOF enabled because a restart was once painful is the moment a cache starts pretending to be a database, and the next incident will be sized accordingly.
High availability: replication, Sentinel, and Cluster
Replication is the bottom rung of the availability ladder: one primary serves writes, asynchronous replication streams them to replicas, and each replica can serve reads (part 2’s read tiers) and can outlive the primary. Asynchronous is the operative word: the primary acknowledges writes without waiting, so a replica lags by network and load, and a crash can lose the tail of the stream. Replication is therefore what it always was in this series (load-spreading and availability, not durability) and every read served from a lagging replica is eventually consistent in exactly the way consistency models describe.
What replication does not do is decide anything when the primary dies. The promotion question (who becomes primary, when, and who notices) is Sentinel’s job: a separate set of processes that watches the primary, agrees on its health with a quorum before declaring it down, then promotes a replica and publishes the new topology to the clients. The mechanics are failure detection and quorum wearing operational clothes, and the price of the design is measured in seconds: between the death and the promotion, writes are refused, and a cache deployed fail-open converts those seconds of misses into exactly the cold-start burst part 3 warned about. Sentinel is the right rung when the working set fits one machine’s memory and the only question is survival.
Redis Cluster is the answer when the working set does not fit: sharding built into the server. The keyspace is divided into 16,384 hash slots; a key’s slot is computed from its name; slots are assigned to nodes, and each node knows the map. A request that reaches the wrong node is redirected to the owner (MOVED, the coordinator routing part 2 described) and during a slot migration an interim ASK redirect bridges the handover. Smart clients learn the map and route directly, which restores the one-hop hot path part 4 required.
The honest nuance is that hash slots are not consistent hashing: slots are fixed partitions, moved in whole ranges when the topology changes, where consistent hashing minimizes how many keys move at all. The trade buys the redirect protocol (placement and routing live in the cluster itself) at the cost of more keys moving per reshard. The cache-relevant constraints are three: multi-key commands and transactions work only when every key hashes to the same slot, which is what hash tags ({user42} embedded in key names) are for; resharding moves ranges online rather than invisibly; and each shard carries its own replicas with failover decided inside the shard, which makes Cluster the upper rung of the ladder Sentinel starts.
Read as one sentence: a cache that fits one machine wants replication plus Sentinel; a working set that does not fit wants Cluster; and every rung climbed adds the moving parts this series has priced since part 2; monitoring, new failure modes, and the cold-start arithmetic of a fleet that can now fail in more interesting ways. What keeps the ladder a caching ladder rather than a database’s is the test this series keeps applying: any node can be flushed, and the system that survives it is still a cache.
FAQ
What are Redis’s data structures, and when does a cache use each?
Strings for single entities and counters under cache-aside; hashes when one field of a cached entity is read or updated independently; lists for latest-N and recency windows; sets for membership questions; sorted sets when score order matters: leaderboards, activity by timestamp, sliding windows. The type should mirror the shape the caller reads, which is also part 4’s memory advice.
What is the difference between RDB and AOF?
RDB is a point-in-time snapshot written by a forked child: compact and fast to load, losing everything since the last snapshot. AOF is an append-only log of write commands with an fsync policy, at most a second of loss with everysec, at the cost of a bigger file and a slower restart. A pure cache usually wants neither, or RDB alone for warm starts; the store role wants both.
What happens when Redis runs out of memory?
The maxmemory ceiling is reached and the eviction policy takes over. With noeviction (the default) writes fail with an error, which is correct for a store and wrong for a cache; with allkeys-lru or allkeys-lfu the server evicts to make room; the volatile family evicts only keys that carry TTLs. The choice should be deliberate, because the default is a decision to fail loudly.
Sentinel or Cluster, which one does a cache need?
They answer different questions. Sentinel keeps one Redis alive; monitoring, quorum, and promotion when the primary dies, and is right when the working set fits one machine. Cluster shards the keyspace across nodes with redirection and per-shard failover, and is right when it does not. The first question is capacity, the second is availability, and conflating them is how deployments end up on the wrong rung.
Is Redis single-threaded?
Command execution is serialized on one thread, which is where both its atomicity and its blocking dangers come from; recent versions move socket I/O onto additional threads and free memory in the background. The operational consequence does not change: one slow command (KEYS over a big keyspace, a giant value, a long script) is slow for every client at once.
Related articles
- Next read: cache invalidation strategies, part 6: the deliberate techniques; purges, versioned keys, write-through, whose transport this article built.
- redis caching; part 4: the patterns this machinery serves.
- caching in system design, part 1: the patterns, eviction, and hit-ratio math this series implements.
- distributed caching; part 2: the fleet architecture this server routes and replicates its way into.
- cache stampede and failure modes; part 3: the cold-start bill every failover sends.
- consistent hashing, the placement scheme hash slots trade against.
- fault-tolerant systems, the failure-detection and quorum vocabulary Sentinel implements.
- CAP theorem and consistency models, the vocabulary for what async replication promises.