Redis System Design Interview: Building a Single-Threaded In-Memory Data Store That Serves Millions of Ops per Second
A single Redis process on commodity hardware can serve on the order of 100K to over 1,000,000 simple operations per second, because it keeps all data in RAM and executes commands one at a time on a single thread. Large deployments run Redis Cluster across dozens or hundreds of primary nodes to hold datasets far bigger than one machine's memory. The interesting part is not the raw speed. It is how a single-threaded design stays fast, and what you give up on durability and consistency to get there. Throughput numbers vary by command, pipelining, and payload size, so treat any specific figure as an estimate.
Redis is an in-memory data structure store that people reach for as a cache, but it is really a small, fast database with server-side data types: strings, hashes, lists, sets, sorted sets, streams, bitmaps, and HyperLogLog. The core is a single-threaded event loop that processes commands sequentially, which removes lock contention and makes every operation effectively atomic without the programmer thinking about it. Because data lives in RAM, latency is dominated by the network round trip rather than disk. Durability is optional and tunable through two mechanisms: point-in-time RDB snapshots and an append-only file (AOF) that logs every write. Availability at scale comes from asynchronous primary-replica replication plus either Redis Sentinel for automatic failover of a single shard, or Redis Cluster, which shards the keyspace across 16384 hash slots and gossips membership between nodes. The hard design questions are all trade-offs: how much data you are willing to lose on a crash or failover, how you handle multi-key operations once keys live on different shards, and whether you can tolerate reading slightly stale data from a replica. Redis leans AP: it favors staying available and fast over guaranteeing that every replica is perfectly in sync.
Asked at: Asked at Amazon, Google, Meta, Uber, Twitter, and most companies that run a large caching or real-time tier. It shows up both as a pure infrastructure design question and as a component inside larger designs like rate limiters, leaderboards, and session stores.
Why this question is asked
Interviewers like Redis because it forces a candidate to reason about the layers most system design questions gloss over: memory versus disk, single-threaded versus multi-threaded execution, synchronous versus asynchronous replication, and strong versus eventual consistency. There is no way to hand-wave it. You have to explain why single-threaded is a feature rather than a limitation, what actually happens to in-flight writes when a primary dies, and why a distributed lock built on Redis is genuinely controversial. It also rewards honesty, because the correct answers involve admitting that Redis can lose writes and that some popular patterns like Redlock are debated by serious people.
Requirements
Always clarify these in the first 5 minutes of the interview. Do not start drawing boxes until both lists are agreed.
Functional requirements
- Store and retrieve values by key with sub-millisecond server-side latency for point lookups
- Support rich server-side data structures: strings, hashes, lists, sets, sorted sets, streams, bitmaps, HyperLogLog
- Provide atomic operations on those structures (increment, push, pop, add-to-sorted-set) without client-side locking
- Support key expiration with TTLs and configurable eviction when memory is full
- Offer optional durability so data survives a process restart or crash
- Replicate data to one or more replicas for read scaling and failover
- Shard a keyspace larger than one machine's memory across many nodes
- Support pub/sub messaging and durable stream consumption with consumer groups
- Allow atomic multi-step logic through transactions (MULTI/EXEC) and server-side Lua scripts
Non-functional requirements
- Very low and predictable latency, dominated by network round trip rather than compute
- High throughput per node, on the order of 100K+ operations per second (estimate, workload dependent)
- High availability of each shard through automatic failover in seconds
- Tunable durability, from no persistence to fsync on every write
- Horizontal scalability by adding shards without downtime (online resharding)
- Memory efficiency, since RAM is the expensive resource
- Operational simplicity: cluster membership and failure detection without an external coordinator
Back-of-envelope scale estimates
Show your math. Pulling numbers from thin air signals you have not thought about the load.
Ops per second per node
100K to 1M+ (estimate)
Single-threaded command execution on modern hardware handles roughly 100K simple GET/SET per second without pipelining and can exceed 1M with pipelining, per widely published redis-benchmark results. Exact numbers depend on command type and payload, so mark as an estimate.
Hash slots in a cluster
16384 (fixed)
Published in the Redis Cluster specification. Slot = CRC16(key) mod 16384. This caps the practical number of primary nodes near 16384, though the docs suggest keeping clusters on the order of ~1000 nodes.
Memory for 100M small keys
~10 to 20 GB (estimate)
Derived. A small string key plus value plus Redis object and dict overhead is roughly 100 to 200 bytes. 100M * 150 bytes averages ~15 GB, before considering hash-field packing or larger values. Real usage varies widely, so this is an estimate.
Replica read fan-out
1 primary : N replicas
Each shard is one primary with N asynchronous replicas. Reads that tolerate staleness scale linearly by adding replicas; writes stay bottlenecked on the single primary of that shard.
Data loss window on default AOF
up to ~1 second (estimate)
With appendfsync everysec (the default), Redis flushes the AOF to disk about once per second in a background thread, so a crash can lose up to the last second of writes. This is a documented behavior, the exact loss depends on timing.
Failover detection time
seconds (config dependent)
Sentinel and Cluster declare a primary down after a configurable timeout (for example a few seconds), then run election and promotion. End-to-end failover is typically single-digit seconds but is fully tunable.
High-level architecture
A client opens a TCP connection and speaks the RESP protocol, sending a command like GET user:42. On a single node, that command lands in the socket buffer and the event loop picks it up on the next iteration. The event loop is single-threaded for command execution: it reads the request, looks up the key in a global hash table, runs the command against the in-memory data structure, and writes the reply back, all before moving to the next client. Because one thread runs commands to completion, every operation is atomic by construction and there are no data-structure locks. If persistence is enabled, the write is also recorded: appended to the in-memory AOF buffer and periodically flushed to disk, and captured by the next RDB snapshot. In a replicated setup, the primary streams the write to its replicas over an asynchronous replication link, so the client is acknowledged before the replicas confirm. When you outgrow one machine, Redis Cluster maps each key to one of 16384 slots using CRC16, and every primary owns a range of slots. The client library learns the slot-to-node map and sends each command directly to the node that owns the key's slot; if it guesses wrong, the node replies with a MOVED redirect telling it the correct node. Nodes gossip health and configuration to each other over a separate cluster bus, so when a primary fails, its replicas detect it and one is promoted without any external coordinator. Sentinel is the alternative for non-sharded setups: a separate set of processes that monitor a single primary-replica group and orchestrate failover.
In a real interview, sketch this on the whiteboard before diving into any single box.
Core components
Walk through each service. The interviewer wants to hear what each one owns, not just the names.
Event loop and command dispatcher
The single-threaded core built on an event loop (ae) that multiplexes many client sockets. It reads a command, executes it to completion against the in-memory dataset, and writes the reply. Modern Redis offloads slow parts like network I/O and some fsync work to helper threads, but the command execution itself stays single-threaded, which is what keeps semantics simple and atomic.
Keyspace and data structures
A top-level dictionary maps keys to values, where each value is one of the server-side types (string, hash, list, set, sorted set, stream, and so on). Each type has multiple internal encodings; small collections use compact representations like listpack and switch to hash tables or skiplists as they grow. Putting the data structure on the server is what lets a client do an atomic ZADD or LPUSH in one round trip instead of read-modify-write.
Persistence engine (RDB + AOF)
RDB forks the process and writes a compact point-in-time snapshot of the whole dataset to a .rdb file. AOF appends every write command to a log that can replay the dataset on restart. They can run together, and AOF supports rewrite/compaction so the log does not grow forever. Durability is a spectrum you choose per deployment.
Replication link
A primary streams its write stream to replicas asynchronously. A new or reconnecting replica performs a full or partial resync; partial resync uses a replication backlog and an offset so a briefly disconnected replica can catch up without a full snapshot transfer. Replicas serve reads and stand by to be promoted.
Redis Sentinel
A separate distributed monitoring system for classic primary-replica setups. Sentinel processes agree that a primary is down, elect a leader among themselves, promote a replica, and reconfigure the other replicas and clients. It provides automatic failover and service discovery without sharding.
Redis Cluster and the cluster bus
The sharding layer. It owns the 16384-slot map, routes clients with MOVED and ASK redirects, and gossips node health and configuration over a dedicated binary cluster bus port. Failure detection and replica promotion happen through this gossip and a voting process among the primaries, with no external coordinator like ZooKeeper.
Eviction and expiration manager
Enforces the maxmemory limit using policies such as approximated LRU, approximated LFU, random, or TTL-based eviction. It also removes expired keys through a mix of lazy expiration (on access) and periodic active expiration sampling, so memory is reclaimed even for keys nobody touches.
Data model
Pick the right store per table. Justify each choice with the access pattern, not by reflex.
keyspace (global dict)keyvalue_ptrtypeencodinglru_or_lfu_metaThe main hash table. Each entry points to a Redis object carrying the type, current internal encoding, and eviction metadata (idle time for LRU or a frequency counter for LFU). Lookups are O(1) average.
expires (per-db)keyexpire_at_msA separate dictionary holding absolute expiration timestamps for keys that have a TTL. Kept apart from the main keyspace so the active-expiry cycle can sample it cheaply.
sorted set (per key)memberscoreskiplist_nodehash_entryBacked by both a hash table (member to score) and a skiplist ordered by score. This dual structure gives O(1) score lookup and O(log N) range and rank queries, which is why leaderboards map onto it so well.
stream (per key)entry_idfield_value_pairsconsumer_grouplast_delivered_idpending_entries_listAn append-only log of entries keyed by monotonic IDs (ms-sequence), stored in a radix tree. Consumer groups track last delivered ID and a pending entries list (PEL) so multiple consumers can share a stream with at-least-once delivery and explicit acks.
cluster slot mapslot_rangeprimary_node_idreplica_node_idsconfig_epochThe authoritative assignment of slot ranges to nodes. config_epoch is a logical version used to resolve conflicting claims during failover, so the most recent configuration wins.
replication backlogreplidmaster_repl_offsetbacklog_bufferreplica_ack_offsetA circular buffer of recent writes plus a replication ID and byte offset. Lets a reconnecting replica ask for only the bytes it missed (partial resync) instead of a full RDB transfer, as long as the gap is still in the buffer.
Deep dives
These are the conversations the interviewer is steering you toward. Practice each one until you can talk through it without notes.
Why single-threaded is fast, not slow
The instinct is that one thread must be slower than many, but for an in-memory store the bottleneck is rarely CPU. Redis holds everything in RAM, so a command is mostly a hash lookup and a pointer chase, which is nanoseconds of work. The real cost is the network round trip and memory bandwidth. By running commands one at a time on a single thread, Redis avoids locks, mutexes, and cache-line contention that would eat most of the benefit of extra cores. It also makes every command atomic for free, which is a huge simplification: an INCR or a ZADD cannot interleave with another client's operation. To use multiple cores you run multiple Redis processes (or cluster shards) on the same box. Newer versions do use threads for network I/O and background tasks like fsync and freeing large objects, but the command execution that touches your data stays serial on purpose.
RDB versus AOF and the durability dial
RDB snapshots the entire dataset by forking and writing a compact binary file. It is great for backups and fast restarts, but between snapshots you can lose everything written since the last one. AOF logs every write command; on restart Redis replays the log to rebuild state. The key knob is appendfsync: 'always' fsyncs on every write (safest, slowest), 'everysec' fsyncs about once a second in a background thread (the default, and a good balance, with a worst case of losing roughly the last second), and 'no' lets the OS decide (fastest, least safe). AOF grows over time, so Redis periodically rewrites it into a minimal set of commands, and can use a hybrid format that embeds an RDB preamble for faster loading. In practice many teams run both: RDB for cheap periodic backups and AOF everysec for a small loss window. The honest framing for an interview is that Redis lets you pick your point on the performance-versus-durability curve, and that 'everysec' means Redis is not a fully durable database out of the box.
Asynchronous replication and the write-loss window
Replication in Redis is asynchronous by default. The primary acknowledges a write to the client and then streams it to replicas; it does not wait for replicas to confirm. This keeps writes fast but creates a failure mode: if the primary crashes after acking a write but before the replica received it, and a replica is then promoted, that write is gone. The WAIT command lets a client block until N replicas acknowledge, which narrows but does not fully close the window, and it is not the same as synchronous commit. Replicas reconnect using a replication ID and offset; if the gap is still in the primary's replication backlog they do a partial resync (just the missing bytes), otherwise they do a full resync from a fresh RDB. The design lesson is that read scaling via replicas is cheap and effective, but replicas are eventually consistent and promotion can drop the most recent writes.
Sentinel failover and the risk of lost acknowledged writes
For a non-sharded primary-replica group, Sentinel provides automatic failover. Several Sentinel processes monitor the primary; when enough of them mark it subjectively down and reach the configured quorum, they agree it is objectively down, elect a Sentinel leader, pick the best replica (most up to date, right priority), promote it, and repoint the other replicas and clients. Two real risks matter. First, because replication is async, the promoted replica may be missing the primary's last writes, so acknowledged writes can be lost across a failover. Second, split-brain: if the old primary was only network-partitioned rather than dead, it may keep accepting writes from clients that can still see it until it is reconfigured, and those writes are discarded when it rejoins as a replica. min-replicas-to-write can reduce this by refusing writes when too few replicas are connected. Sentinel gives you availability, not a guarantee that no write is ever lost.
Redis Cluster: 16384 slots, routing, and resharding
Cluster shards the keyspace into exactly 16384 hash slots. The slot for a key is CRC16(key) mod 16384, and each primary owns a contiguous or arbitrary set of slots. Clients cache the slot-to-node map and talk directly to the owning node; if they are stale they get a MOVED redirect (permanent, update your map) or an ASK redirect (temporary, for a slot currently migrating). Resharding moves slots between nodes one key at a time while staying online: the source marks the slot MIGRATING, the target marks it IMPORTING, keys are moved with MIGRATE, and in-flight requests are steered with ASK. 16384 was chosen deliberately: it is small enough that the per-node bitmap of owned slots stays tiny in gossip messages, but large enough to distribute data evenly across up to thousands of nodes. Nodes discover each other and share slot ownership and health through gossip on the cluster bus, so there is no separate coordination service to run.
Multi-key operations and the cross-slot limitation
Because keys are scattered across slots and nodes, a command that touches multiple keys only works if all those keys live in the same slot. Operations like MGET, MSET, transactions, and Lua scripts that reference several keys will fail with a CROSSSLOT error if the keys hash to different slots. The escape hatch is hash tags: if a key contains braces, only the substring inside the first {...} is hashed, so user:{42}:profile and user:{42}:sessions land in the same slot and can be operated on together. This is a genuine design constraint that shapes your key schema in a sharded deployment: you have to co-locate keys you intend to touch atomically. It also means cluster does not give you cross-shard transactions; if you need to update data on two different shards atomically, Redis will not do it for you.
Eviction, expiration, and memory pressure
Redis holds everything in RAM, so what happens at the memory ceiling is a first-class design question. When used memory hits maxmemory, the configured eviction policy kicks in: noeviction (reject writes), allkeys-lru or volatile-lru (evict least recently used), allkeys-lfu or volatile-lfu (evict least frequently used), allkeys-random, volatile-random, or volatile-ttl (evict soonest to expire). LRU and LFU are approximated, not exact: Redis samples a handful of keys and evicts the best candidate from the sample, which is far cheaper than maintaining a perfect global ordering and is accurate enough in practice. Expiration is separate and also two-pronged: lazy expiration removes a key when a client accesses it and finds it expired, and active expiration runs a periodic cycle that samples the expires table and deletes a fraction of expired keys, repeating if too many were still alive. Together they bound the amount of stale, expired-but-not-yet-collected memory.
Distributed locks and the Redlock controversy
A common pattern is a distributed lock: SET a key with a unique token, NX (only if absent), and PX (a timeout), then delete it (checking the token) to release. On a single Redis instance this is simple and works, but the instance is a single point of failure. Redlock is the multi-node algorithm that tries to fix that by acquiring the lock on a majority of independent Redis primaries. This is where the honest answer matters: Martin Kleppmann published a well-known critique arguing Redlock is unsafe for correctness because it relies on timing assumptions and has no fencing token, so a process that pauses (GC, scheduler) can act on a lock that already expired. Antirez, Redis's creator, responded defending it for its intended use and pointing out that fencing can be layered on. The practical takeaway for an interview: a Redis lock is fine for efficiency (avoid duplicate work) but you should not rely on it alone for correctness where two holders would be catastrophic. If you need correctness, add a fencing token that the protected resource checks, or use a system built for consensus.
Trade-offs to discuss
Every senior interviewer expects you to surface at least 3 of these. Pick the decisions, state the alternatives, and justify your choice.
In-memory storage versus disk-based storage
Keeping the working set in RAM is what delivers sub-millisecond latency, but RAM is expensive and finite, so dataset size is capped by memory and cost, and durability requires an extra persistence mechanism. You accept a memory budget and the risk of losing recent writes in exchange for speed.
Single-threaded execution versus multi-threaded
One thread removes locking and makes commands atomic and predictable, which is simpler and often faster for in-memory work, but a single slow command (a big KEYS scan or an O(N) operation on a huge collection) blocks everyone. You scale across cores with more processes or shards, not more threads.
Asynchronous replication versus synchronous
Async keeps writes fast and does not couple client latency to replica health, at the cost of a window where acknowledged writes can be lost on failover. WAIT can trade some latency for stronger guarantees, but true synchronous durability is not the default.
AOF everysec versus fsync always
everysec gives near-full throughput with a worst case of losing about a second of writes; always gives per-write durability but drastically lower write throughput. Most systems pick everysec because the second-of-loss is acceptable for a cache-like workload.
Redis Cluster versus a single primary with Sentinel
Cluster scales capacity and write throughput horizontally but imposes the cross-slot limitation and more operational complexity. Sentinel keeps the simple single-primary model with automatic failover but caps you at one machine's memory and one write path per group.
Availability (AP) versus strong consistency (CP)
Redis leans AP: during partitions it favors staying up and serving possibly stale reads from replicas over guaranteeing linearizable writes. If you need strong consistency or cross-key transactions across shards, Redis is the wrong primary store and you pair it with a system that provides those guarantees.
How Redis actually does it
The details above track the official Redis documentation and Salvatore Sanfilippo's (antirez) own writeups. The Redis Cluster specification defines the 16384-slot model, the CRC16 hashing, MOVED/ASK redirection, and gossip-based failure detection. The persistence documentation describes RDB snapshots, the AOF log, and the appendfsync policies including everysec as the default. Replication and Sentinel are documented as asynchronous by design with automatic failover, and the docs are explicit that failover can lose writes that had not yet reached the promoted replica. The distributed lock discussion is a real, public debate: Redis publishes the Redlock pattern, Martin Kleppmann wrote a detailed critique arguing it is unsafe for correctness without fencing tokens, and antirez responded defending it. Any interview answer that presents Redis as a fully durable, strongly consistent database is wrong, and the sources make that clear.
Lessons to study before this interview
If any of these topics are fuzzy, the interviewer will catch it. Each lesson is 15 to 60 minutes with diagrams, code, and a quiz.
Related system design interview questions
Practice these next. They lean on the same core building blocks as Redis.