# How Redis Actually Works

Redis has a reputation: it’s fast, it’s simple, it’s “just a cache.” Each of those is partly true and mostly misleading. Under the hood, Redis is a tightly engineered piece of C built around a single bet: **keep hot data in memory and execute commands serially, and much of the overhead of coordination disappears.**

This post walks through how that bet plays out: the event loop, the data structures, memory management, persistence, and scaling. By the end, you should be able to predict how Redis behaves, including when it behaves badly.

## The mental model

At its core, Redis maintains a dictionary mapping keys to typed values in RAM. Each value has an object header recording its type, internal encoding, and metadata used by mechanisms such as eviction. Networking, persistence, and replication are machinery built around that keyspace.

Hold onto that picture. Everything below is either a way to make it faster, smaller, safer, or bigger.

## One thread, one loop

Redis executes commands on a single thread. That sounds like a limitation. It is actually a major reason Redis is fast.

The server runs an event loop using mechanisms such as epoll on Linux and kqueue on BSD and macOS:

1.  Ask the OS which client sockets have data ready.
    
2.  Read and parse requests using RESP, Redis’s binary-safe serialization protocol.
    
3.  Execute each command to completion.
    
4.  Queue replies and write them back.
    
5.  Run periodic housekeeping: expiry, replication, and statistics.
    
6.  Repeat.
    

Why does this work? For small, inexpensive commands, avoiding locks, context switches, and coordination overhead can matter more than spreading execution across cores. Each command also runs without another command interleaving with it, making individual commands atomic relative to other clients on that instance.

Since Redis 6, optional I/O threads can handle socket reads and writes in parallel, but command execution remains single-threaded. You can scale across cores by running more instances or using Redis Cluster.

Because network round trips often dominate small commands, **pipelining** matters enormously. Sending 100 commands in one batch avoids the overhead of 100 separate request-response round trips. The actual throughput gain depends on the workload, hardware, and pipeline size.

The catch is just as important: **one slow command stalls everybody.** `KEYS *` on 50 million keys, `SMEMBERS` on a million-element set, or a long-running Lua script can prevent other clients from being served until that work finishes.

## Not just strings: data structures and encodings

Redis’s real trick is that each data type has internal representations optimized for different sizes.

-   **Strings** use a length-prefixed, binary-safe structure called SDS, so retrieving a length is O(1) and values can contain arbitrary bytes. Short strings can fit into a single allocation, and eligible integer values can use integer encoding.
    
-   **Small hashes and sorted sets** can use a _listpack_: a compact, contiguous byte array without a pointer per element. Linear scans can beat heavier structures for small collections because of better memory locality.
    
-   **Lists** use a _quicklist_, a linked structure whose nodes contain packed listpacks.
    
-   **Sets of small integers** can use an _intset_, a sorted array searched by binary search. Redis 7.2 and later can also use listpacks for eligible small sets.
    
-   **Larger hashes and sets** use hash tables.
    
-   **Larger sorted sets** use a _skip list_ plus a hash table. The skip list supports O(log N) range queries by score and rank, while the hash table provides efficient member lookups.
    

You can watch an encoding switch:

```text
> HSET user:1 name ada
> OBJECT ENCODING user:1
"listpack"
```

These thresholds are configurable. In Redis 7+, hashes default to a maximum of 512 entries and 64 bytes per element for listpack encoding; sorted sets default to 128 entries and 64 bytes. Exceeding the applicable limits triggers conversion to a more general representation. See the [Redis memory optimization documentation](https://redis.io/docs/latest/management/optimization/memory-optimization/).

Two more details matter. First, Redis resizes hash tables **incrementally**: during rehashing, it maintains old and new tables and migrates buckets progressively, reducing the risk of a long pause rather than eliminating latency spikes entirely. Second, jemalloc is the default allocator in common builds, helping manage fragmentation under constant allocation churn.

## Expiry and eviction

Redis cleans up expired keys in two ways. The **lazy** way: when you access a key, Redis checks its TTL and deletes it if it has expired. The **active** way: roughly ten times a second, Redis samples keys with expiration times, deletes expired ones, and repeats if more than a quarter of the sample was expired. This adaptive cleanup keeps stale memory bounded without scanning the entire keyspace on every cycle.

When memory reaches the configured `maxmemory` limit, **eviction** follows your policy: `allkeys-lru`, `allkeys-lfu`, `volatile-ttl`, or `noeviction`, which is the default policy. With `noeviction`, writes requiring additional memory fail rather than evicting keys. Redis’s LRU is deliberately _approximate_: instead of maintaining a costly global recency list, it samples candidate keys (five by default) and evicts a suitable least-recently-used candidate.

## Surviving restarts: RDB and AOF

Memory is volatile, so Redis offers two persistence mechanisms that work differently.

**RDB snapshots** are point-in-time dumps. Redis calls `fork()`, and the child process writes a snapshot using copy-on-write while the parent keeps serving traffic. Pages modified by the parent may need to be copied, increasing memory use during write-heavy snapshots. RDB files are compact and can restart quickly, but you lose writes made since the last snapshot. On large instances, the `fork()` call itself can cause a brief latency spike.

The **append-only file (AOF)** records write operations. Its durability depends on the fsync policy: `always` (safest, usually slowest), `everysec` (the common choice, with roughly a one-second loss window under normal conditions), or `no` (the OS decides). Because the log grows, Redis periodically rewrites it in the background, producing a more compact sequence of operations that recreates the current dataset. AOF is disabled by default.

Many production setups use both. Pure caches often use neither, which is a legitimate choice when losing the data is acceptable. The [Redis persistence documentation](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/) covers the trade-offs.

## Scaling out

**Replication** is asynchronous. A replica connects and sends `PSYNC` with its replication ID and offset. If it is new or too far behind, it gets a **full resync**: the master forks, generates an RDB snapshot, and sends it while buffering subsequent writes. If the replica's offset remains within the master's backlog, it can get a cheaper **partial resync** instead. Because replication is asynchronous, failover can lose recently acknowledged writes. Redis replication alone does not provide strong consistency.

**Sentinel** adds automatic failover. Sentinel processes monitor the master, use quorum to establish that it is down, and coordinate failover through an elected leader that promotes a replica and informs clients.

**Cluster** shards the keyspace. It has 16,384 hash slots, and each key maps to a slot using `CRC16(key) mod 16384`. Each master owns a subset of slots, and nodes exchange information to detect failures and coordinate ownership. If a client contacts the wrong node, it receives a `MOVED` redirect. Smart clients cache the slot map to avoid repeated redirects.

Multi-key operations generally require all involved keys to share a slot. Hash tags make that possible: `{user:1}:cart` and `{user:1}:profile` are hashed using `user:1` alone, so they land together. The [Redis Cluster specification](https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/) explains the details.

## Atomicity without locks

Because one thread executes commands, a `MULTI`/`EXEC` transaction runs without other clients' commands interleaving with its queued commands. Lua scripts and Redis Functions also execute atomically relative to other commands. This makes them useful for rate limiters and compare-and-set logic.

Two cautions: **there is no rollback** if a queued command fails at runtime; other commands in the transaction can still succeed. And scripts block the event loop while running, so they must finish quickly.

## What this means in practice

Once you understand the design, the folklore becomes actionable:

-   **Respect command complexity.** An O(N) command on a large collection can block the server. Prefer `SCAN` over `KEYS` and read large collections in chunks.
    
-   **Avoid giant keys.** A 500 MB hash can make commands, replication, and snapshots expensive. Split it into manageable pieces.
    
-   **Pipeline aggressively.** Network round trips, not just command execution, can limit throughput.
    
-   **Leave memory headroom.** Fork-based persistence and full replication syncs can increase memory use through copy-on-write. Plan for peak usage, not just the steady state.
    
-   **Choose durability deliberately.** Decide between RDB, AOF, both, or neither based on how much data you can afford to lose, not simply on the defaults.
    

## The takeaway

Redis is a lesson in picking a constraint and following it all the way down. Data in memory, single-threaded command execution, compact encodings that change with size, and fork-based tricks for persistence and replication: each choice reinforces the others. Once you understand that, Redis stops being a black box that’s mysteriously fast and becomes a system you can reason about and debug from first principles.

---

*Published via [ZyVOP](https://zyvop.com/how-redis-actually-works-1ei6a?utm_source=hashnode&utm_medium=crosspost&utm_campaign=syndication) — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium & Hashnode in 1 click.*
