Redis can answer a simple command in microseconds, and it executes every command on one main thread. That design explains its speed. It also explains most Redis incidents: one slow command stalls every client, a fork adds memory pressure, and asynchronous replication can lose acknowledged writes during a failover. This guide walks through the Redis architecture layer by layer, with the settings and commands that expose each part.
- One main thread runs the event loop and executes every command, so each command is atomic and a slow one blocks everyone.
- I/O threads (
io-threads) offload socket reads, parsing, and writes. Command execution stays on the main thread. - Small hashes, sets, sorted sets, and lists use compact encodings (listpack, intset) until they cross configurable thresholds.
- RDB snapshots and AOF rewrites run in a forked child that shares memory pages with the parent through copy-on-write.
- Replication is asynchronous. Sentinel adds automatic failover for one primary; Cluster splits data across 16,384 hash slots.
Version scope: Defaults below are taken from the redis.conf shipped with Redis Open Source 8.10.2 (September 2026) and the Redis documentation. Valkey forked from Redis 7.2.4, so it shares most of this design, with its own changes since. Check CONFIG GET on your own version before you rely on a default.
The Redis architecture at a glance
A Redis server is one process. Inside it, a single main thread owns the dataset and runs every command. Around that thread sit optional I/O threads for network work, a few background threads for slow system calls, and, from time to time, a forked child process that writes persistence files. Replicas are separate Redis processes that receive a stream of write commands from the primary.
Every arrow into the main thread is serialized: only the main thread reads or changes the keyspace. Everything else exists to keep slow work off that thread.
How Redis processes a command
Redis is built on a small event library (ae) that waits on the operating system's readiness API: epoll on Linux, kqueue on macOS and BSD. The main thread sleeps until a socket is readable or a timer is due. It then handles the ready events, runs housekeeping before it sleeps again, and repeats. There are no locks around the data, because only one thread ever touches it.
Steps 2 and 6 can move to I/O threads. Steps 3 to 5 always run on the main thread, which is why command execution never overlaps.
Single-threaded execution gives Redis two properties that applications depend on. Each command is atomic: no other client sees a half-applied INCR or LPUSH. Transactions (MULTI/EXEC) and Lua scripts also run without interleaving. The cost is that the server is only as responsive as its slowest command.
Any O(N) command on a large key blocks every other client until it finishes: KEYS *, SMEMBERS on a huge set, DEL on a multi-gigabyte hash, or a long Lua script. Use SCAN, SSCAN, and HSCAN for iteration, and UNLINK instead of DEL for large keys.
Two built-in tools show when the main thread is stalling. The slow log records commands whose execution time exceeds slowlog-log-slower-than (in microseconds). The latency monitor records spikes from sources such as forks and expiry cycles once you set a threshold.
# Ten slowest recent commands (execution time only, not network time)
redis-cli SLOWLOG GET 10
# Record latency events above 100 ms, then ask for an analysis
redis-cli CONFIG SET latency-monitor-threshold 100
redis-cli LATENCY DOCTORI/O threads: what they parallelize and what they do not
For small commands, much of the CPU time goes to system calls and protocol handling, not to the data structures. Redis 6.0 added threaded I/O to spread that work across cores, and Redis 8.0 replaced it with a new implementation. When io-threads is greater than 1, client connections are assigned to I/O threads, which read from sockets, parse requests, and write replies. The main thread still executes every command.
# redis.conf: off by default (io-threads 1 uses only the main thread).
# The shipped comments suggest enabling it only on machines with at least
# 4 cores, leaving one core spare, and only when Redis is CPU-bound.
io-threads 4I/O threads help when many clients send small requests and the main thread is saturated by networking. They do not help when one expensive command is the bottleneck, because that command still runs alone on the main thread. If redis-cli --latency and the slow log point at specific commands, fix the commands first.
Pipelining: why round trips dominate
A request and response pair crosses the network once per command unless the client batches. Pipelining sends many commands before reading the replies, so the client pays one round trip per batch. It also reduces the number of read and write system calls on the server. For bulk loads and fan-out reads, pipelining usually matters more than any server setting. See the Redis pipelining documentation.
Background threads and the forked child
Some work is too slow for the main thread, so Redis hands it off in two ways:
- Background I/O threads close file descriptors, run
fsyncfor the AOF, and free memory for lazy deletes.UNLINK,FLUSHALL ASYNC, and thelazyfree-lazy-*settings use them. Those settings default tonoin Redis 8.10, so a plainDELstill frees memory on the main thread. - A forked child process writes RDB snapshots (
BGSAVE) and rewrites the AOF (BGREWRITEAOF). The fork call itself runs on the main thread and pauses it briefly; the pause grows with the size of the parent's page tables.INFO statsreports the last one aslatest_fork_usec.
How Redis stores data in memory
Each logical database is a hash table (a dict) that maps key names to value objects; in cluster mode, recent versions keep one table per hash slot. A second table maps keys that have a TTL to their expiry time. When a table needs to grow, Redis rehashes it incrementally, moving a few buckets on each operation, so a resize never blocks the server in one long step.
Compact encodings and when they convert
Each value object records its type and its internal encoding. Small collections use compact, contiguous encodings that save memory and are fast to scan at small sizes. When a collection crosses a threshold, Redis converts it to a general-purpose structure. The conversion is one way: a hash that grows past the limit stays a hash table even if it later shrinks.
| Type | Compact encoding | Converts to | Thresholds (Redis 8.10 defaults) |
|---|---|---|---|
| Hash | listpack | hashtable | hash-max-listpack-entries 512, hash-max-listpack-value 64 |
| Set | intset (integers only), then listpack | hashtable | set-max-intset-entries 512, set-max-listpack-entries 128, set-max-listpack-value 64 |
| Sorted set | listpack | skiplist plus hashtable | zset-max-listpack-entries 128, zset-max-listpack-value 64 |
| List | listpack | quicklist (a linked list of listpacks) | list-max-listpack-size -2 (8 KB per node) |
| String | int, embstr | raw | Chosen by value type and length |
You can watch a conversion happen with OBJECT ENCODING:
redis-cli HSET user:42 name "Asha" plan "pro"
redis-cli OBJECT ENCODING user:42 # "listpack"
redis-cli CONFIG GET hash-max-listpack-entries
redis-cli MEMORY USAGE user:42 # bytes, including overheadMany small hashes in listpack encoding often use less memory than the same data stored as separate string keys, because each top-level key carries its own overhead. Measure with MEMORY USAGE on a sample before you redesign a keyspace.
Expiry: lazy and active
Redis removes expired keys in two ways. Lazy expiry checks the TTL whenever a command touches a key and deletes it if it has expired. Active expiry runs inside the server's periodic task (hz, default 10 times per second). It samples keys that have a TTL, deletes the expired ones, and keeps going while it finds many, within a CPU time budget. On replicas, expired keys are deleted only when the primary sends a DEL, although replicas report them as missing on reads.
Eviction when maxmemory is reached
When used memory reaches maxmemory, Redis applies maxmemory-policy before running a command that needs more memory. The default is noeviction, which returns an error to write commands. That default suits a primary data store. For a cache, choose an eviction policy explicitly.
| Policy | Candidates | Chooses by |
|---|---|---|
noeviction (default) | None | Write commands fail with an OOM error |
allkeys-lru / volatile-lru | All keys / keys with a TTL | Least recently used (reads and writes) |
allkeys-lfu / volatile-lfu | All keys / keys with a TTL | Least frequently used |
allkeys-lrm / volatile-lrm | All keys / keys with a TTL | Least recently modified (writes only), listed in the Redis 8.10 redis.conf |
allkeys-random / volatile-random | All keys / keys with a TTL | Random choice |
volatile-ttl | Keys with a TTL | Nearest expiry time |
LRU, LFU, and LRM are approximations. Instead of tracking a global order, Redis samples maxmemory-samples keys (default 5) and evicts the best candidate it has seen. Raising the sample size makes eviction closer to true LRU at a small CPU cost. The volatile-* policies behave like noeviction when no key has a TTL, which surprises teams that mix cache and persistent keys in one instance. See the Redis key eviction documentation.
Memory allocator and fragmentation
On Linux, Redis is built with jemalloc by default. Deleting keys returns memory to the allocator, but not always to the operating system, so resident memory can stay high after a large delete. Compare used_memory with used_memory_rss and read mem_fragmentation_ratio in INFO memory. Active defragmentation (activedefrag yes) moves values into fuller pages in the background, using CPU time that you can bound with the active-defrag-cycle-* settings.
redis-cli INFO memory | grep -E "used_memory_human|used_memory_rss_human|mem_fragmentation_ratio|maxmemory_policy"Persistence: RDB snapshots and the AOF
Redis keeps the dataset in memory and offers two ways to write it to disk. They can be used together.
- RDB writes a compact point-in-time snapshot of the whole dataset. It restarts fast and suits backups. Writes made after the last snapshot are lost if the server crashes.
- AOF (append-only file) logs every write command. Since Redis 7.0 the AOF is a directory (
appenddirname) with a base file, incremental files, and a manifest. Withaof-use-rdb-preamble yes(the default), the base file is an RDB snapshot and the incremental files hold the commands since.
Both RDB saves and AOF rewrites use fork(). The child gets a copy of the parent's address space, but the kernel shares the physical pages between them. A page is copied only when the parent writes to it while the child is still running.
Extra memory during a save grows with the pages the parent writes while the child runs. A write-heavy instance can approach twice its dataset size in the worst case.
Plan memory headroom for forks. Redis recommends vm.overcommit_memory = 1 on Linux so that a fork does not fail on a busy host, and it logs a warning at startup when the setting is different. Transparent huge pages make copy-on-write copy 2 MB pages instead of 4 KB pages; Redis disables them for its own process by default (disable-thp yes).
The AOF fsync policy decides how much acknowledged data a crash can lose:
appendfsync | When Redis calls fsync | Trade-off |
|---|---|---|
always | After every write batch | Safest and slowest; disk latency becomes command latency |
everysec (default) | Once per second, on a background thread | About one second of writes at risk on a crash |
no | When the operating system flushes | Fastest; the loss window depends on kernel settings |
For RDB and AOF configuration in depth, including restore testing, read Redis persistence: RDB vs AOF.
Replication: how replicas stay in sync
A replica connects to a primary and receives the same stream of write commands the primary executes. Replication is asynchronous: the primary replies to the client without waiting for replicas. Each primary has a replication ID and an offset that counts the bytes of the stream. A replica tracks the ID and offset it has processed, which lets it resume after a short disconnect.
A backlog that is too small for your write rate turns every short network blip into a full resync, which forks the primary and copies the whole dataset.
Since Redis 7.0, repl-diskless-sync defaults to yes: for a full resync the primary streams the RDB straight to the replica socket instead of writing it to disk first. Size repl-backlog-size to cover your write rate across the longest disconnect you expect. The default 1 MB is small for busy primaries.
What asynchronous replication means for your data
Because the primary does not wait for replicas, a write acknowledged to a client can be lost if the primary fails before the write reaches a replica that is then promoted. Two tools narrow that window without removing it:
WAIT numreplicas timeoutblocks the client until that many replicas acknowledge the stream, andWAITAOF(Redis 7.2 and later) waits for fsync to the AOF locally or on replicas. The Redis documentation is explicit thatWAITdoes not make Redis strongly consistent.min-replicas-to-writeandmin-replicas-max-lagmake a primary refuse writes when too few replicas are connected and current. That limits how much a primary isolated by a network partition can accept.
High availability with Sentinel
Replication alone does not fail over. Redis Sentinel is a separate process that monitors a primary and its replicas. When enough Sentinels (the quorum) agree that the primary is unreachable, a majority of Sentinels must authorize a failover. One Sentinel then promotes a replica, repoints the other replicas, and publishes the new primary address. Clients ask Sentinel for the current primary instead of hard-coding an address.
Run at least three Sentinels on hosts that fail independently. Detection time follows down-after-milliseconds, which is 30 seconds in the sample sentinel.conf; tune it against your network rather than lowering it blindly, because false positives cause unnecessary failovers. For layouts and failure cases, read Redis Sentinel vs Cluster.
Scaling out with Redis Cluster
Sentinel protects one dataset that fits on one primary. Redis Cluster splits the keyspace across several primaries, each with its own replicas. Every key maps to one of 16,384 hash slots by CRC16(key) mod 16384, and each primary owns a set of slots. If a key contains a hash tag, a substring in braces, only the tag is hashed.
The slot ranges match what redis-cli --cluster create assigns to three primaries. Hash tags put related keys in one slot so multi-key commands work.
Hash tags matter because multi-key commands, transactions, and Lua scripts in Cluster must touch keys in one slot. Without a tag, related keys usually land on different nodes:
redis-cli CLUSTER KEYSLOT user:42:cart # (integer) 12984
redis-cli CLUSTER KEYSLOT user:42:profile # (integer) 9133
redis-cli CLUSTER KEYSLOT user:{42}:cart # (integer) 8000
redis-cli CLUSTER KEYSLOT user:{42}:profile # (integer) 8000A MGET of the first two keys fails with a CROSSSLOT error. The tagged pair works, because both keys hash to slot 8000. Use tags sparingly: one hot tag puts all of its traffic on one primary.
Cluster-aware clients cache the slot map and send each command to the owning node. A MOVED reply means the slot has a new owner, and the client refreshes its map. An ASK reply appears while a slot is migrating and applies to one request only. Nodes exchange state over the cluster bus and detect failures with cluster-node-timeout (15 seconds by default). When a primary fails, its replicas request votes, and a majority of the primaries must agree before one replica is promoted. By default (cluster-require-full-coverage yes), the cluster stops serving queries when any slot has no owner. Cluster mode supports database 0 only, so SELECT with another index fails.
For resharding and node operations, read Redis Cluster: sharding, replication, and HA.
Choosing a deployment architecture
| Standalone | Primary and replicas | Sentinel | Cluster | |
|---|---|---|---|---|
| Write capacity | One primary | One primary | One primary | One primary per shard |
| Read scaling | No | Replicas, possibly stale | Replicas, possibly stale | Replicas per shard (READONLY) |
| Automatic failover | No | No | Yes, by Sentinel quorum and majority | Yes, by a majority of primaries |
| Dataset limit | One host's memory | One host's memory | One host's memory | Sum of the shards |
| Client requirement | Any client | Read routing in the application | Sentinel-aware client | Cluster-aware client; same-slot multi-key operations |
| Fits | Development, disposable caches | Read-heavy caches with manual recovery | Datasets that fit one host and need failover | Datasets or write rates beyond one host |
Managed services such as Amazon ElastiCache and MemoryDB package these same topologies. They change who runs the failover, not how replication, eviction, or slot routing behave.
Production checklist
- Set
maxmemorybelow the host's memory, leaving room for fork copy-on-write, client buffers, and replication buffers. - Choose
maxmemory-policyon purpose:noevictionfor data you cannot lose, anallkeys-*policy for caches. - Ban
KEYSin application code and rename or restrict dangerous commands with ACLs. - Use
UNLINKfor large keys, or enable thelazyfree-lazy-*settings that match your workload. - Set
vm.overcommit_memory = 1and watchlatest_fork_usecafter each save. - Size
repl-backlog-sizefor your write rate and the longest disconnect you expect. - Run at least three Sentinels on independent hosts, or use Cluster with a replica per primary.
- Alert on
used_memory,mem_fragmentation_ratio,evicted_keys,connected_slaves, and replication offset lag. - Test a failover and a restore from your RDB or AOF files before you need either.
Frequently asked questions
Is Redis single-threaded?
Redis executes all commands on one main thread, so command execution is single-threaded. Since Redis 6.0 it can use I/O threads for socket reads, parsing, and writes, and it always uses background threads for tasks such as fsync and lazy freeing. Forked child processes write RDB snapshots and rewrite the AOF.
Why is Redis fast if it uses one thread for commands?
Redis keeps data in memory, uses data structures with predictable costs, and avoids locks because only one thread changes the data. An event loop multiplexes thousands of connections on that thread. Most latency comes from the network and from slow commands, which is why pipelining and avoiding O(N) commands matter.
What is the difference between Redis Sentinel and Redis Cluster?
Sentinel monitors one primary and its replicas and promotes a replica when the primary fails. The whole dataset still lives on one primary. Redis Cluster shards the keyspace across several primaries using 16,384 hash slots, and it handles failover for each shard itself.
Can Redis lose data during a failover?
Yes. Replication is asynchronous, so writes acknowledged by a failed primary can be missing on the replica that is promoted. WAIT, WAITAOF, and min-replicas-to-write reduce that window. They do not make Redis strongly consistent.
How many hash slots does Redis Cluster have?
Redis Cluster has 16,384 hash slots. Each key maps to a slot by CRC16 of the key modulo 16384, or of its hash tag when the key contains one, and each primary owns a range or set of slots.
Key takeaways
- Treat the main thread as a shared resource: one slow command delays every client.
- Enable I/O threads only when networking saturates the main thread, not to fix slow commands.
- Check value encodings and thresholds before redesigning a keyspace for memory.
- Budget memory for fork copy-on-write, and set an eviction policy on purpose.
- Plan for asynchronous replication: acknowledged writes can be lost in a failover.
- Pick Sentinel when the data fits one host, and Cluster when it does not.
Working with JusDB on Redis
JusDB reviews Redis and Valkey deployments: topology, memory and eviction settings, persistence, replication, and failover behavior. Your team approves production changes, and the service agreement records the work JusDB handles. If you are planning a move from Sentinel to Cluster, or chasing latency spikes, bring the INFO output and slow log you already have.
Explore Redis consulting | Request a Redis architecture review
Related reading: