← Back to index

Redis — Internal Architecture

A developer's guide to how Redis actually works under the hood: the single-threaded event loop, the data structures and their encodings, the RESP protocol, expiry and eviction, persistence, replication, and how it scales out with Sentinel and Cluster.

Student edition · understand, apply, verify

Understand isolated commands, in-memory state, expiry, persistence, and asynchronous failover as separate mechanisms.

What you should be able to do: Explain why a Redis operation can be atomic yet lose acknowledged state or fail a multi-step business invariant.

Assumes basic programming and HTTP. This is an architecture-level tutorial, not a release-specific implementation contract. Defaults, file formats, APIs, and guarantees depend on the deployed release and configuration; use the linked product documentation before production changes. Snippets marked pseudocode are not runnable integrations.

Redis is an in-memory data structure server. Unlike a traditional database that keeps data on disk and caches hot pages in memory, Redis keeps the entire dataset in RAM and treats disk only as a durability backstop. Its second defining choice is that command execution is single-threaded: one logical thread runs commands one at a time, which removes locks and makes every individual command atomic. Those two decisions — memory-resident data and a single-threaded execution model — explain almost everything else about how Redis behaves, from its microsecond latencies to the way it persists, replicates, and shards. This guide walks through the internals a developer needs to reason about Redis confidently.

Contents

  1. Design Goals and Core Ideas
  2. Architecture & the Event Loop
  3. Data Structures & Encodings
  4. RESP Protocol & Command Flow
  5. Expiry & Eviction
  6. Persistence: RDB & AOF
  7. Replication
  8. High Availability & Scale
  9. Summary
  10. Concepts explained
  11. Worked example
  12. Practice and verification

Concepts explained in this guide

Read these definitions first, or use the linked terms as you go. The examples and limitations are local to this guide; companion pages are optional.

Isolation is not rollback

Isolation prevents another operation from observing intermediate state. Rollback undoes failed work; durability preserves acknowledged work under specified faults.

Example. Redis MULTI/EXEC may continue after a runtime command error. Script failure can leave earlier writes applied.

Boundary. A read outside MULTI followed by writes is still racy without WATCH/retry or a conditional script. Async failover can lose state.

Snapshots, AOF, and power-loss durability

Snapshots capture recoverable state; an append-only log records later operations. fsync/equivalent and crash-safe ordering define power-loss survival.

Example. An every-second AOF policy targets a small flush gap; disk stalls/delayed flush can make actual risk exceed a strict second.

Boundary. fork/COW can pause and amplify memory. Supporting a format does not mean persistence is enabled by default.

Event loops and nonblocking I/O

One loop handles ready sockets/callbacks instead of one waiting thread per connection. Blocking or long CPU work delays other clients.

Example. Ten thousand sockets share a loop, but their file descriptors, state, and buffers still consume memory.

Boundary. Readiness is not a whole request. Preserve partial read/write parser state; background threads and defaults depend on release/configuration.

Wall and monotonic clocks

A wall clock maps to civil/epoch time and can jump because of correction. A monotonic clock measures elapsed time locally without wall-clock jumps, but its origin is not a shared global epoch.

Example. Use an epoch timestamp to label an event and a monotonic deadline to bound how long a process waits for the next usable timestamp.

Boundary. Neither establishes causal order across machines. Monotonic readings cannot be compared between hosts or assumed to survive reboot with the same origin.

Cache-aside and invalidation

A read checks a cache, reads the authoritative store on a miss, then fills the cache. A write commits to the store and invalidates or refreshes the cached value. TTL means time to live, not a freshness proof.

Example. A reader fetches version 1, a writer commits version 2 and invalidates, then the delayed reader caches version 1. DB-first invalidation alone does not prevent this refill race.

Boundary. Use invalidation-aware fill leases or version fences for stronger behavior; a TTL only bounds one entry's residence under additional assumptions. Authorization and revocation need an explicit stale-data policy.

Quorum

A set of replicas whose responses are sufficient for an operation. In a fixed set of N replicas, read size R and write size W overlap whenever R+W>N. A majority is one particular quorum, not the definition of every quorum.

Example. With N=3, writes to {A,B} and reads from {B,C} share B. If the write completed durably and replicas preserve a comparable version, B carries that version forward.

Boundary. Overlap alone does not provide linearizability: concurrent or failed writes, sloppy replica sets, and version selection matter. Consensus adds protocol rules to preserve decisions.

1. Design Goals and Core Ideas

Every internal decision in Redis traces back to a small set of goals. Keeping them in mind makes the rest of the architecture predictable.

Redis design goals
The whole dataset lives in RAM, a single logical thread executes commands one at a time, and the data structures are simple enough to keep operations in the microsecond range.
GoalHow Redis achieves it
Microsecond latencyAll data is in memory; operations are O(1) or O(log n) on purpose-built data structures, with no disk seek on the hot path.
Simplicity and atomicityOrdinary keyspace commands execute under an isolated execution model; separate commands can still race as a sequence, and internal/background work may use synchronization.
Rich data types as a serverRedis is not just a string cache. Lists, hashes, sets, sorted sets, and streams let the server do work that would otherwise round-trip to the client.
Predictable performanceMemory layout and encodings are tuned so common operations cost the same regardless of dataset size.
Optional durabilityPersistence (RDB, AOF) is configurable. You choose the trade-off between speed and how many recent writes you can afford to lose.
Single-threaded does not mean slow. A single core doing nothing but in-memory pointer work and avoiding lock contention can serve hundreds of thousands of operations per second. The bottleneck is usually the network and the kernel, not Redis itself — which is exactly why I/O threads (covered next) were added rather than a multi-threaded command executor.

2. Architecture and the Event Loop

At its core Redis is a single process running an event loop. It uses the operating system's efficient I/O readiness mechanism (epoll on Linux, kqueue on BSD/macOS) to wait for many client sockets at once and wake up only when one of them has data ready. There is no thread per connection; one thread multiplexes thousands of clients.

Single-threaded event loop
The main thread waits on epoll, dispatches and executes one command at a time, then builds the reply. I/O threads (6.0+) only offload reading and writing the socket bytes — execution stays single-threaded.

One iteration of the loop does the following:

I/O threads (Redis 6.0+). Profiling showed that on busy servers a large share of CPU was spent in the kernel reading request bytes and writing reply bytes, not in executing commands. Redis 6 added an optional pool of I/O threads that parallelize only the socket read/parse and reply-write steps. The actual command execution remains single-threaded, so atomicity and the no-locks model are preserved while throughput on multi-core machines improves.

while server_running:
  events = epoll_wait(fds)            # block until sockets ready
  for fd in events.readable:
    buf = read(fd)                    # (optionally on an I/O thread)
    cmd, args = parse_resp(buf)
    reply = dispatch(cmd, args)       # executes on the MAIN thread, atomically
    queue_output(fd, reply)
  for fd in events.writable:
    write(fd, output_buffer[fd])      # (optionally on an I/O thread)
  run_time_events()                   # expiry, rehashing, timeouts

3. Data Structures and Internal Encodings

Redis exposes a handful of data types — string, list, hash, set, sorted set, and stream — but each type can be stored internally in more than one way. Redis picks a compact encoding when a value is small and transparently upgrades to a general encoding once it grows past a configurable threshold. The command behavior is identical; only memory layout and performance characteristics change.

Data structures and encodings
Every type starts in a memory-efficient encoding for small data and switches to a general-purpose structure once it crosses a size or element-count threshold.

The key encodings are:

TypeEncodingsHow it works
Stringint, embstr, rawA value that is a valid integer is stored as a boxed long (int). Short strings (≤ 44 bytes) use embstr, which allocates the header and the bytes in one contiguous block. Longer or modified strings use raw.
Listlistpack → quicklistA small list is a single listpack (a flat, packed byte array). As it grows it becomes a quicklist: a doubly linked list whose nodes are each a listpack, balancing memory density against fast end operations.
Hashlistpack → hashtableFew small field/value pairs are packed into one listpack and scanned linearly. Past the threshold it becomes a real hash table (dict) for O(1) field access.
Setintset / listpack → hashtableA set of only integers uses a sorted intset. A small mixed set uses a listpack. Large sets become a hash table (keys only).
Sorted Setlistpack → skiplist + dictSmall zsets use a listpack. Large ones combine a skip list (ordered by score, for range queries) with a dict (member → score, for O(1) lookups).
Streamradix tree of listpacksAn append-only log keyed by time-ordered IDs. Entries are grouped into listpack-packed nodes indexed by a radix tree, with consumer-group state tracked alongside.
The thresholds are tunable (for example hash-max-listpack-entries, set-max-intset-entries). The trade-off is always the same: compact encodings save memory and have great cache locality but are O(n) to scan, so they are only used while n is small. Once a structure is large, Redis pays the per-element overhead of a general structure to keep operations cheap.

4. RESP Protocol and Command Flow

Clients talk to Redis using RESP (REdis Serialization Protocol) — a simple, line-oriented, binary-safe protocol that is easy to parse and human-readable on the wire. A request is an array of bulk strings: the command name followed by its arguments. The reply is a single RESP value whose type depends on the command.

RESP protocol and command flow
A request arrives as a RESP array, gets parsed, the command is looked up in the command table and executed, and the reply travels back over the same socket as a RESP value.

RESP uses a one-byte type prefix to tell the parser what follows:

PrefixTypeExample
+Simple string+OK\r\n
-Error-WRONGTYPE ...\r\n
:Integer:1000\r\n
$Bulk string (length-prefixed, binary-safe)$3\r\nbar\r\n
*Array*2\r\n...\r\n...\r\n

The full flow for a single command is short, which is the point — minimal per-command overhead:

# Client sends: SET foo bar  ->  *3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
function handle_request(socket):
  bytes = socket.read()
  argv  = parse_resp(bytes)            # ["SET", "foo", "bar"]
  cmd   = command_table.lookup(argv[0])
  if cmd is None:
    return reply_error("unknown command")
  if not cmd.arity_ok(argv):
    return reply_error("wrong number of arguments")
  result = cmd.proc(argv)              # mutate the keyspace, atomically
  return encode_resp(result)           # "+OK\r\n"

Pipelining falls out of this design for free: a client may send many requests back to back without waiting for each reply. Redis reads them all, executes them in order, and sends the replies in order, which amortizes network round-trip time across many commands. MULTI/EXEC transactions go one step further by queuing commands and executing the whole batch as one uninterrupted unit on the single thread.

5. Expiry and Eviction

Expiry removes an entry after its configured time; eviction makes room under a memory policy. These are different. Lazy expiry on access and active sampling reclaim expired keys, but TTL does not prove the underlying source value is fresh. A recently cached stale value can still be within its TTL.

Expiry uses authority clocks/configuration; wall clocks can jump. Replication and command type affect observed behavior. Eviction policies such as approximate LRU/LFU choose candidates under their supported algorithm, not an exact universal ordering. Limits, buffers, overhead, persistence/fork memory, and non-evictable state also affect memory pressure.

noeviction rejects appropriate memory-growing work rather than guaranteeing all commands succeed. Use actual policy and workload measurements; approximate algorithms and defaults depend on release/configuration.

6. Persistence: RDB and AOF

RDB snapshots provide a recoverable point-in-time image. fork/copy-on-write lets a child write while the parent continues, but fork can pause and copied pages can consume substantial extra memory. Writes since the last valid snapshot are not preserved by that snapshot alone.

AOF records operations under its configured flush/sync policy. always requests frequent durable completion; everysec targets periodic fsync, but disk stalls/scheduling/errors can exceed a strict one-second loss bound. no leaves more to operating-system behavior. Persistence may be disabled; format defaults do not imply it is enabled.

Modern AOF designs can use multiple files/manifests and snapshot preambles. Rewrite, file publication, recovery ordering, storage behavior, and backup validation are release-specific. Do not equate 'appended' with power-loss durable or promise zero blocking during rewrite.

7. Replication

Replication is normally asynchronous: acknowledged primary state may be absent from a promoted replica. Backlog/PSYNC handles supported partial resynchronization; falling outside retained history requires a supported full sync. Replication IDs/offsets and the snapshot boundary must agree.

WAIT can wait for replication acknowledgements for prior writes on the relevant connection, reducing some risks. It does not turn Redis into a consensus/linearizable durable store or guarantee the failover procedure selects a copy with every acknowledged write. Durability-related commands/options such as supported AOF-wait policies have their own release-specific scope.

Sentinel coordinates failover; Redis Cluster has its own cluster/failover protocol and is not literally Sentinel wrapped around each shard. Both retain declared failure assumptions. Hard quota/financial invariants need a store/protocol whose loss and ownership contracts actually enforce them.

8. High Availability and Scale

Replication by itself gives you read scaling and a warm copy, but a human still has to react when the master dies, and the whole dataset must fit on one machine. Redis solves the first problem with Sentinel and the second with Cluster.

Sentinel and Cluster
Sentinel adds automatic failover to a single master/replica set. Cluster shards the keyspace across many masters using 16384 hash slots and a gossip bus, redirecting clients with MOVED/ASK.

Sentinel: automatic failover

Sentinel is a separate process (run in odd numbers, typically three or five, for quorum) that monitors a master and its replicas. When enough sentinels agree the master is unreachable, they elect a leader among themselves, promote a suitable replica to master, reconfigure the other replicas to follow it, and tell clients where the new master is. Sentinel provides high availability but not sharding — the entire dataset still lives on one master at a time.

Cluster: sharding the keyspace

Redis Cluster partitions the keyspace across many master shards. The mechanism is a fixed map of 16384 hash slots. Every key is assigned to a slot by CRC16(key) mod 16384, and every slot is owned by exactly one shard. To distribute data you simply distribute slot ownership; each shard typically also has replicas for HA, so Cluster folds in the Sentinel-style failover too.

ConceptWhat it does
Hash slots (16384)A coarse, fixed partitioning of the keyspace. Moving data between shards means reassigning slots, not rehashing every key.
CRC16 hashingslot = CRC16(key) mod 16384 maps a key to a slot deterministically on every client and node.
Gossip busNodes exchange cluster state (who owns which slots, who is up) over a separate cluster bus port, so every node converges on the full topology without a central registry.
MOVED redirectIf a client sends a key to the wrong shard, the node replies MOVED <slot> <addr>; the client retries against the correct shard and caches the mapping.
ASK redirectDuring a slot migration, a key may have moved already. The owner replies ASK to send that one request to the new shard without invalidating the client's whole slot map.

Resharding moves slots from one shard to another online, key by key, while the cluster keeps serving traffic. The combination of a fixed slot count, deterministic CRC16 hashing, and MOVED/ASK redirects lets the cluster rebalance without a coordinator and without taking the keyspace offline.

# client-side routing in a Redis Cluster
function cluster_route(key):
  slot = crc16(hash_tag(key)) % 16384  # {tag} forces co-location
  node = slot_map[slot]                # cached topology
  resp = send(node, command)
  if resp.is_moved():                  # wrong shard, topology changed
    slot_map[resp.slot] = resp.addr    # update cache
    return send(resp.addr, command)
  if resp.is_ask():                    # slot mid-migration
    return send(resp.addr, ASKING + command)  # one-shot redirect
  return resp
A multi-key command in Cluster only works if all its keys live in the same slot. The hash tag convention — putting a {...} substring in the key, e.g. user:{42}:profile and user:{42}:sessions — makes only the braced part feed CRC16, guaranteeing related keys share a slot and can be operated on together.

Worked example: a command is atomic but a pair is racy

Two clients read the same counter and each then writes an increment.

a command is atomic but a pair is racyRead concurrently: Both GET calls return 10. Each command is isolated; the whole read/decide/write sequence is not.. Write separately: Both SET calls write 11, losing an increment. Use a supported atomic command/script/conditional transaction for that local invariant.. Fail after acknowledgement: An asynchronous replica may not contain the write when promoted. WAIT is not a complete consensus/failover durability protocol.. Expire a cached key: Expiry prevents that key being returned after its authority's TTL rule; it does not prove the cached source data was fresh before expiry.1Read concurrently2Write separately3Fail after acknowledgement4Expire a cached key
Sequence for this example. The table explains the evidence and decision at each step.
StepEvidence, decision, and expected result
Read concurrentlyBoth GET calls return 10. Each command is isolated; the whole read/decide/write sequence is not.
Write separatelyBoth SET calls write 11, losing an increment. Use a supported atomic command/script/conditional transaction for that local invariant.
Fail after acknowledgementAn asynchronous replica may not contain the write when promoted. WAIT is not a complete consensus/failover durability protocol.
Expire a cached keyExpiry prevents that key being returned after its authority's TTL rule; it does not prove the cached source data was fresh before expiry.
Check your understanding: Does MULTI/EXEC provide rollback on runtime errors?

No. Distinguish queueing errors from execution errors; some effects may already apply. Validate before mutation and keep durability/failover semantics separate.

Run and change the example

Download guide_labs.py. Python 3.10+, standard library only. In its directory, run:

python3 guide_labs.py transactions
python3 guide_labs.py test

The SQLite teaching model contrasts atomic local effects with a deliberately racy read/modify/write sequence; it is not a Redis server.

The program tests its stated finite contracts. It is an educational model, not an implementation of the named production product or a proof of unmodeled distributed faults.

Sources and further verification

9. Summary

The whole system is built from a few reinforcing ideas:

ConcernMechanism
Why is it fast?Entire dataset in RAM; simple O(1)/O(log n) structures; no disk on the hot path.
Why are commands atomic?A single thread executes one command at a time; no locks, no inter-command races.
How does it stay multi-core friendly?Optional I/O threads parallelize socket read/write only; execution stays single-threaded.
How is memory kept small?Compact encodings (listpack, intset, embstr) for small values, upgraded to general structures past a threshold.
How are clients served?The simple, binary-safe RESP protocol over an epoll event loop, with free pipelining.
How is memory reclaimed?Lazy + active TTL expiration; sampled LRU/LFU eviction at the maxmemory limit.
How is data made durable?RDB snapshots (fork + copy-on-write) and the AOF log, combined in a hybrid format.
How does it survive failure?Async replication (full/partial resync via PSYNC) plus Sentinel for automatic failover.
How does it scale out?Cluster shards the keyspace into 16384 CRC16 hash slots, redirecting clients with MOVED/ASK.
The recurring theme: keep the execution model dead simple — one thread, in-memory, atomic — and push every source of complexity (durability, replication, sharding) into layers that wrap that core without compromising it.

⌂