# Low Level Design: Redis Internals

Source: https://www.techinterview.org/post/3233473071/lld-redis-internals/
Updated: 2026-04-18 · techinterview.org

## Single-Threaded Event Loop

Redis processes all client commands on a single main thread sequentially. This design eliminates the need for locking on core data structures — there is no concurrent mutation, so no mutexes are required. The event loop is built on **I/O multiplexing** via `epoll` (Linux), `kqueue` (BSD/macOS), or `select` as a fallback. The main thread blocks on epoll waiting for readable/writable file descriptors, then dispatches commands one at a time.

Since Redis 6, **I/O threads** handle network reads and writes in parallel while the main thread still executes all commands. Background threads handle persistence (AOF fsync, RDB child) and lazyfree (async key deletion). The single-threaded execution model means that a slow command (e.g., `KEYS *`, `LRANGE` on a huge list) blocks all other clients — this is why Redis exposes non-blocking alternatives like `SCAN`.

## Data Structure Implementations

Redis implements each data type with carefully chosen internal representations optimized for both memory and CPU:

- **String**: uses **SDS (Simple Dynamic String)** — a struct with `len`, `alloc`, and `buf[]`. Unlike C strings, SDS tracks length explicitly, making `strlen` O(1) and allowing binary-safe storage (embedded NUL bytes). Small integers are stored as actual integer types to save memory.

- **List**: implemented as a **quicklist** — a doubly-linked list where each node is a **listpack** (compact, length-prefixed encoding). This gives O(1) push/pop at both ends with good memory locality for moderate-sized elements.

- **Hash**: uses **listpack** for small hashes (below `hash-max-listpack-entries` and `hash-max-listpack-value` thresholds), upgrading to a **hashtable** with chaining for large hashes. The hashtable uses incremental rehashing — two hash tables coexist during resize, with entries migrated on each access.

- **Set**: uses **intset** (sorted integer array) when all elements are integers and the count is small, upgrading to a **hashtable** otherwise.

- **Sorted Set (ZSet)**: uses **listpack** for small sets, upgrading to a combined **skiplist + hashtable**. The hashtable gives O(1) score lookup by member. The skiplist gives O(log N) rank queries, range-by-score, and range-by-rank operations.

## Skiplist: The Sorted Set Engine

The **skiplist** is a probabilistic data structure with multiple levels of linked lists. Level 0 is a full sorted linked list; higher levels skip over increasing numbers of nodes. On average, each node participates in O(log N) levels. Insertions and lookups are O(log N). Redis chose skiplist over a balanced BST (like AVL or red-black tree) because skiplist range operations are simpler to implement and the memory overhead is comparable. The combined skiplist+hashtable allows ZSETs to serve both `ZSCORE` (O(1) via hashtable) and `ZRANGE` (O(log N + M) via skiplist) efficiently.

## Memory Encoding: Listpack

**Listpack** (replacing the older ziplist in Redis 7.0+) is a compact, contiguous memory encoding for small collections. Each entry stores: a "previous entry length" field (for reverse traversal), an encoding byte (type + length), and the actual data. There are no pointers — entries are packed end-to-end. This saves significant memory for small lists, hashes, and sorted sets compared to pointer-based structures. The tradeoff is O(N) access by index, which is acceptable for small collections.

## RDB Snapshots

RDB (Redis Database) persistence writes a compact binary snapshot of the entire dataset. When `BGSAVE` is triggered, Redis calls `fork()` to create a child process. The child serializes all data to a temp file using a compact binary format, then atomically renames it to `dump.rdb`. The parent continues serving clients uninterrupted.

**Copy-on-Write (COW)**: immediately after fork, parent and child share the same physical memory pages. When the parent modifies a page, the OS copies that page for the parent — the child still sees the original. This means memory usage can temporarily spike by up to 2x during BGSAVE if writes are heavy. RDB files load fast on restart, but data written after the last snapshot is lost on crash.

## AOF: Append-Only File

AOF logs every write command as it is executed, in Redis serialization protocol format. On restart, Redis replays the AOF to reconstruct state. Three **fsync policies** control durability vs. performance:

- **always**: fsync after every command. Maximum durability, significant performance cost.

- **everysec** (default): fsync every second via a background thread. At most 1 second of data loss on crash.

- **no**: let the OS decide when to flush. Fastest, but data loss window equals OS flush interval (typically 30s).

**AOF Rewrite**: over time the AOF grows large with redundant commands (e.g., 1000 INCR commands on a key that could be represented as one SET). Background AOF rewrite compacts the file by writing the current state as a minimal set of commands. Redis forks a child to write the new AOF, while an AOF rewrite buffer captures new writes. On completion, the buffer is appended and the file is atomically swapped.

## Replication

Redis uses **asynchronous master-replica replication**. On initial sync, the master runs BGSAVE and sends the RDB file to the replica; the replica loads it and then receives a backlog of buffered write commands. Ongoing replication streams every write command from master to replicas in real time.

The **PSYNC protocol** supports partial resynchronization: each master tracks a *replication offset* and a *replication ID*. If a replica disconnects briefly, it reconnects and sends its last known offset. If the master's backlog still contains the missing commands, it sends only the delta — avoiding a full resync. Replicas are eventually consistent: a write acknowledged by the master may not yet be on replicas.

## Redis Sentinel

Sentinel provides **high availability** for a single-master Redis setup. Sentinel processes monitor master and replica health via periodic PING commands. When a master is unreachable, Sentinels vote among themselves (quorum) to confirm the failure (avoiding split-brain). The elected Sentinel orchestrates **failover**: promotes a replica to master, reconfigures remaining replicas to replicate from the new master, and publishes the new master address. Clients use Sentinel to discover the current master address at connection time.

## Redis Cluster

Redis Cluster provides **horizontal sharding** across multiple nodes. The keyspace is divided into **16,384 hash slots**. Each key is mapped to a slot via `CRC16(key) mod 16384`. Each master node owns a subset of slots; its replicas hold copies. Clients can connect to any node — if a key belongs to a different node, the client receives a `MOVED` redirect.

Cluster nodes discover each other and propagate state via a **gossip protocol** — each node periodically pings random peers, exchanging cluster membership and slot assignment information. Resharding (moving slots between nodes) can happen without downtime using `CLUSTER SETSLOT` commands. Hash tags (e.g., `{user}.session`) force keys to the same slot, enabling multi-key commands on co-located keys.

## Memory Optimization and Eviction

When Redis reaches `maxmemory`, it applies an **eviction policy** to free space:

- **noeviction**: return errors on writes (default).

- **allkeys-lru** / **volatile-lru**: evict least-recently-used keys (all keys or only keys with TTL).

- **allkeys-lfu** / **volatile-lfu**: evict least-frequently-used keys (Redis 4.0+). LFU uses a logarithmic counter with decay to estimate frequency.

- **allkeys-random** / **volatile-random**: random eviction.

- **volatile-ttl**: evict keys with shortest remaining TTL first.

LRU and LFU in Redis are approximate — Redis samples a small number of keys (configurable via `maxmemory-samples`) and evicts the best candidate from the sample rather than maintaining a full LRU list, keeping overhead low.

## Lua Scripting

Redis embeds a **Lua interpreter** (via the `EVAL` command) that executes scripts atomically on the server. While a Lua script runs, no other command can execute — the event loop is blocked. This enables complex **check-and-set** operations and multi-step atomic workflows without WATCH/MULTI/EXEC transaction boilerplate. Scripts are cached on the server by their SHA1 hash (`EVALSHA`) to avoid re-sending script text on every call. Scripts can call any Redis command via `redis.call()` or `redis.pcall()` (the latter traps errors rather than raising them). Since Redis 7.0, **Redis Functions** (stored server-side Lua or other engine scripts) replace ad-hoc EVAL scripts for production use.

See also: [Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence](https://www.techinterview.org/post/3233460274/netflix-interview-guide-2026-streaming-architecture-recommendation-systems-and-engineering-excellence/)

See also: [Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering](https://www.techinterview.org/post/3233460272/meta-interview-guide-2026-facebook-instagram-whatsapp-engineering/)

See also: [Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering](https://www.techinterview.org/post/3233460275/scale-ai-interview-guide-2026-data-infrastructure-rlhf-pipelines-and-ml-engineering/)

See also: [Anthropic Interview Guide 2026: Process, Questions, and AI Safety](https://www.techinterview.org/?p=3233460282)
