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.

Executive Summary: Redis runs as a single-threaded, in-memory event loop, which explains both its speed and its risks. I cover its core data structures and what to cache in each, memory limits and eviction policies, RDB versus AOF persistence, and how Sentinel and Cluster provide replication and failover.

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. (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

As a result, 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. 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. 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.

How this plays out in practice

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. 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. 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. 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 LPUSH plus LTRIM.
  • Sets, unique membership: tags, feature flags per user, the negative-caching namespaces part 4 sketched, with SADD and SISMEMBER doing 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. 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. 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. 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.

In contrast, 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. That instance is the drift part 4 warned about, arriving one convenience at a time.

Trade-offs worth weighing

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. 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

As a result, 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. 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.

Failure cases to expect

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. 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. 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. 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. 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. 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. The price of the design is measured in seconds: between the death and the promotion, writes are refused.

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.

Design details that matter

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. 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. 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. 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.

C-005 system-design

Share this article

2 thoughts on “Redis Architecture: Data Structures, Persistence, and High Availability”

  1. Owen Blake

    Sentinel vs cluster decision table matches what we went through last year. Cluster won for key volume.

  2. Jyoti

    AOF every-second felt safer until disk IO became the bottleneck. RDB + AOF mix is where we landed.

Leave a Reply

Your email address will not be published. Required fields are marked *