JusDB
Free DB Audit
NoSQL Databases

Redis architecture explained: From event loop to Cluster

How Redis works inside: one main thread and its event loop, I/O threads, memory encodings, eviction, RDB and AOF persistence, replication, Sentinel, and Cluster.

JusDB Team
Published October 2, 2026
16 min read

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.

TL;DR
  • 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.

Redis architecture overview Clients connect over TCP, TLS, or a Unix socket. Optional I/O threads read, parse, and write client sockets. One main thread runs the event loop, executes commands, and owns the keyspace. Background I/O threads close files, fsync the AOF, and free memory. A forked child writes RDB snapshots and rewrites the AOF. Replicas receive an asynchronous command stream. Persistence files live on disk. Clients RESP over TCP, TLS, or a Unix socket I/O threads (optional) read, parse, and write client sockets Main thread: the event loop Execute commands one at a time, in order Keyspace dicts and encodings Background I/O fsync, close, free Forked child RDB, AOF rewrite Replicas async command stream Disk: RDB file and AOF directory

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.

1main thread executes every command
16,384hash slots in Redis Cluster
5keys sampled per eviction by default
1 MBdefault replication backlog

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.

The life of a Redis command Six steps: the socket becomes readable, Redis reads and parses the RESP request, runs the command lookup with ACL, cluster, and memory checks, executes the command on the main thread, appends the reply to the client output buffer, and writes the reply to the socket. The event loop then waits for the next event. 1 Socket becomes readable epoll or kqueue wakes the event loop 2 Read and parse RESP main thread, or an I/O thread when enabled 3 Look up and check the command ACL, cluster slot, and maxmemory checks 4 Execute on the main thread one command at a time, so each one is atomic 5 Append the reply to the client output buffer 6 Write the reply to the socket before the loop sleeps, or on an I/O thread next event

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.

Warning

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.

bash
# 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 DOCTOR

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

ini
# 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 4

I/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 fsync for the AOF, and free memory for lazy deletes. UNLINK, FLUSHALL ASYNC, and the lazyfree-lazy-* settings use them. Those settings default to no in Redis 8.10, so a plain DEL still 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 stats reports the last one as latest_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.

TypeCompact encodingConverts toThresholds (Redis 8.10 defaults)
Hashlistpackhashtablehash-max-listpack-entries 512, hash-max-listpack-value 64
Setintset (integers only), then listpackhashtableset-max-intset-entries 512, set-max-listpack-entries 128, set-max-listpack-value 64
Sorted setlistpackskiplist plus hashtablezset-max-listpack-entries 128, zset-max-listpack-value 64
Listlistpackquicklist (a linked list of listpacks)list-max-listpack-size -2 (8 KB per node)
Stringint, embstrrawChosen by value type and length

You can watch a conversion happen with OBJECT ENCODING:

bash
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 overhead
Tip

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

PolicyCandidatesChooses by
noeviction (default)NoneWrite commands fail with an OOM error
allkeys-lru / volatile-lruAll keys / keys with a TTLLeast recently used (reads and writes)
allkeys-lfu / volatile-lfuAll keys / keys with a TTLLeast frequently used
allkeys-lrm / volatile-lrmAll keys / keys with a TTLLeast recently modified (writes only), listed in the Redis 8.10 redis.conf
allkeys-random / volatile-randomAll keys / keys with a TTLRandom choice
volatile-ttlKeys with a TTLNearest 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.

bash
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. With aof-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.

Fork and copy-on-write during BGSAVE After fork, the parent and the child share the same memory pages. The child writes the snapshot to dump.rdb. When the parent writes to a page during the save, the kernel copies that page, so extra memory grows with the number of pages written during the save. Parent process: keeps serving writes shared Forked child: writes the snapshot copied after the parent writes dump.rdb

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.

Important

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:

appendfsyncWhen Redis calls fsyncTrade-off
alwaysAfter every write batchSafest and slowest; disk latency becomes command latency
everysec (default)Once per second, on a background threadAbout one second of writes at risk on a crash
noWhen the operating system flushesFastest; 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.

Redis replication and resynchronization The primary streams write commands to the replica and keeps recent stream bytes in a ring buffer called the replication backlog. When a replica reconnects, it sends PSYNC with its replication ID and next offset. If the offset is still in the backlog, the primary replies CONTINUE and sends the missing bytes. Otherwise it replies FULLRESYNC and sends a full RDB snapshot. Primary replid 8f3c…a91e offset 1048900 Replica replid 8f3c…a91e offset 1048830 Replication backlog repl-backlog-size 1mb After a disconnect, the replica asks to resume PSYNC 8f3c…a91e 1048831 Partial resync offset still in the backlog +CONTINUE Full resync offset gone or replid unknown +FULLRESYNC, then RDB

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 timeout blocks the client until that many replicas acknowledge the stream, and WAITAOF (Redis 7.2 and later) waits for fsync to the AOF locally or on replicas. The Redis documentation is explicit that WAIT does not make Redis strongly consistent.
  • min-replicas-to-write and min-replicas-max-lag make 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.

Redis Cluster key to slot to node mapping The key user:{42}:cart has the hash tag 42. CRC16 of 42 modulo 16384 is slot 8000. Three primaries own slots 0 to 5460, 5461 to 10922, and 10923 to 16383, so slot 8000 lives on node B. A client that sends the key to the wrong node receives a MOVED redirect with the slot and the correct address. Nodes gossip on the cluster bus, by default the client port plus 10000. key user:{42}:cart CRC16("42") mod 16384 = 8000 Node A 0 to 5460 Node B 5461 to 10922 Node C 10923 to 16383 slot 8000 lives on node B -MOVED 8000 10.0.0.12:6379 wrong node: the client updates its slot map and retries Nodes gossip on the cluster bus (client port + 10000 by default)

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:

bash
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) 8000

A 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

StandalonePrimary and replicasSentinelCluster
Write capacityOne primaryOne primaryOne primaryOne primary per shard
Read scalingNoReplicas, possibly staleReplicas, possibly staleReplicas per shard (READONLY)
Automatic failoverNoNoYes, by Sentinel quorum and majorityYes, by a majority of primaries
Dataset limitOne host's memoryOne host's memoryOne host's memorySum of the shards
Client requirementAny clientRead routing in the applicationSentinel-aware clientCluster-aware client; same-slot multi-key operations
FitsDevelopment, disposable cachesRead-heavy caches with manual recoveryDatasets that fit one host and need failoverDatasets 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

  1. Set maxmemory below the host's memory, leaving room for fork copy-on-write, client buffers, and replication buffers.
  2. Choose maxmemory-policy on purpose: noeviction for data you cannot lose, an allkeys-* policy for caches.
  3. Ban KEYS in application code and rename or restrict dangerous commands with ACLs.
  4. Use UNLINK for large keys, or enable the lazyfree-lazy-* settings that match your workload.
  5. Set vm.overcommit_memory = 1 and watch latest_fork_usec after each save.
  6. Size repl-backlog-size for your write rate and the longest disconnect you expect.
  7. Run at least three Sentinels on independent hosts, or use Cluster with a replica per primary.
  8. Alert on used_memory, mem_fragmentation_ratio, evicted_keys, connected_slaves, and replication offset lag.
  9. 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

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:

Share this article

Database engineering notes

Articles like this one, in your inbox. No spam, unsubscribe anytime.

JusDB Team

Official JusDB content team

Keep reading

Moving a Multi-Database Valkey Instance into a 9.x Cluster

Move a Valkey instance that uses SELECT 1 to 15 into a 9.x cluster: keep or remap databases, copy each one, verify counts per database, and keep a way back.

Redis18 minNov 10, 2026
Read

Valkey Keyspace Notifications in Production: Expired Events, hexpired and Cluster Mode

Run Valkey keyspace notifications in production: why expired and hexpired events lag the TTL, how to subscribe on every cluster node, and when a stream is safer.

Redis20 minNov 10, 2026
Read

Valkey GLIDE in Production: AZ-Affinity Reads and Migrating from Legacy Clients

Configure Valkey GLIDE AZ-affinity reads on servers and clients, learn what GLIDE 2.5.3 does not support, and migrate from redis-py, Jedis, ioredis, or go-redis one service at a time.

Redis20 minNov 10, 2026
Read