Redis for system design interviews
Open in the interactive app: practice, quiz, mock interview →In one minute
- Redis is an in-memory data structure server: strings, hashes, lists, sets, sorted sets and streams, each with atomic server-side operations answered in well under a millisecond.
- Commands execute on one main thread. Every command is atomic without locks, and one slow command (
KEYS *, a hugeZRANGE) stalls every client. - Durability is a dial: RDB snapshots via
fork(), an append-only file fsynced every write or every second, or nothing. Replication is asynchronous, so failover can lose acknowledged writes. - Choose it for caches, rate limiters, leaderboards, sessions and counters. Do not make it the only copy of data you cannot lose.
How it works
Data structures as the API
Redis stores a data structure under each key and offers operations on it: INCR, HINCRBY, LPUSH/BRPOP, SINTER, ZADD/ZRANGE, XADD/XREADGROUP. Because the server owns the structure, a leaderboard update is one atomic round trip rather than read, sort and write. Redis 8 (May 2025) folded the old Redis Stack modules into the core server, so JSON, time series, probabilistic structures, the query engine and the new vector sets ship with it.
One thread runs the commands
The server is an event loop: wait for ready sockets, read bytes, parse complete commands, execute each to completion, queue replies. Nothing interleaves, so MULTI/EXEC blocks and Lua scripts are atomic. Throughput per instance is bounded by one core, and everyone's latency by the slowest queued command.
Expiry, eviction, persistence, sharding
Keys with a TTL are deleted lazily on access and by a sampling background cycle. At maxmemory, maxmemory-policy either refuses writes (noeviction, the default per redis.conf) or evicts by approximate LRU, LFU, random, nearest TTL or, since 8.6, least recently modified. "Approximate" means Redis samples a handful of keys (maxmemory-samples, default 5) and evicts the best candidate from a small pool, rather than keeping a true LRU list (Redis docs). RDB writes point-in-time snapshots; AOF logs every write for replay. Replicas stream writes from a primary, and Sentinel or Cluster promotes one on failure. Redis Cluster maps each key to one of 16,384 slots with CRC16(key) mod 16384 (cluster spec); hash tags ({user42}) pin related keys to one slot so multi-key commands work.
Down the stack
Layer 1The contract
A command is atomic and linearizable on its primary. Across failover that weakens: a write acknowledged by the primary may not have reached any replica. WAIT numreplicas timeout blocks until N replicas acknowledge the replication offset, shrinking the loss window, but the docs are explicit that it does not make Redis a CP system and acknowledged writes can still be lost in a failover (Redis docs). WAITAOF numlocal numreplicas timeout (7.2+) goes one step further and waits until the write is fsynced to the AOF locally and on N replicas, with the same caveat (Redis docs). Single-node durability follows appendfsync: always loses nothing acknowledged at a heavy throughput cost, everysec (the default) can lose about a second of writes, no leaves it to kernel writeback, normally every 30 seconds on Linux (Redis docs). If the disk stalls, Redis delays the next write up to two seconds before writing anyway, so a slow disk turns into latency as well as risk (Redis latency guide). Server time for simple commands is tens of microseconds; clients see RTT plus queueing, typically 0.2 to 1 ms within one zone.
Layer 2The mechanism
Encodings. Small values use compact encodings. A sorted set with at most zset-max-listpack-entries (128) members of at most zset-max-listpack-value (64) bytes is a listpack: one contiguous byte array of member, score pairs, scanned linearly. Larger sets convert to a hash table plus a skip list. The dict gives O(1) ZSCORE; the skip list orders by (score, member). Each forward pointer stores a span, the number of elements it skips, so summing spans along the search path yields rank, which keeps ZRANK and ZRANGE logarithmic. Node levels grow with probability 0.25, capped at 32.
Keyspace. Each database is a dict with incremental rehashing: on growth, a few buckets move per operation and per cron tick, so no command pays for a full rehash.
RDB and AOF. BGSAVE forks; the child serialises every key (type byte, length-prefixed key, encoded value, expiry) while the parent serves traffic under copy-on-write. AOF appends each write in RESP form to a buffer flushed before the loop sleeps and fsynced by a background (bio) thread. Since 7.0 the AOF is multi-part (base file, incremental files, manifest), so a rewrite no longer splices in writes buffered during it.
Replication. A primary has a replication ID, a byte offset, and a circular backlog (repl-backlog-size, default 1 MB, allocated only once a replica connects). A reconnecting replica sends PSYNC <replid> <offset>. If the offset is still in the backlog, the primary replies +CONTINUE and streams the gap; otherwise +FULLRESYNC, a forked RDB transfer, then buffered commands. Replicas report offsets each second with REPLCONF ACK.
Cluster. Nodes gossip PING/PONG on a bus port (client port + 10,000 unless cluster-port overrides it) carrying slot ownership and failure flags. When a majority of primaries mark one as failed, its replicas hold an election and the winner claims the slots under a higher config epoch. During resharding, MIGRATE moves keys and clients get ASK redirects.
Layer 3The machine
The loop blocks in epoll_wait, then reads ready sockets into query buffers. With pipelining one read carries hundreds of commands, and throughput climbs almost linearly with pipeline depth to about 10 times the unpipelined rate, because syscalls and context switches dominate, not the hash lookup (Redis docs). io-threads (6.0+, off by default) moves socket reads, writes and parsing to helper threads while execution stays single-threaded. Valkey 8 reworked this so I/O threads also take over epoll_wait; on a c7g.16xlarge with 8 I/O threads and 650 clients, unpipelined SET throughput rose from 360,000 to 1.19 million per second (Valkey).
fork() copies page tables, not data. A 24 GB heap with 4 KB pages needs about 48 MB of page table, and copying it blocks the main thread. Redis's own measurements put this at roughly 9 to 13 ms per GB on physical hosts and VMware, but 239 ms per GB on old Xen-based EC2 instances (Redis latency guide), so a 25 GB node can stall for a quarter of a second or for several seconds depending on where it runs. With transparent huge pages, each post-fork write copies 2 MB instead of 4 KB, so disable THP. Write-heavy load during BGSAVE can nearly double resident memory. Memory comes from jemalloc; activedefrag fights fragmentation, and UNLINK or lazyfree-lazy-eviction yes frees large values on a background thread instead of blocking for hundreds of milliseconds.
Why it's built this way
- RAM access is about 100 ns and a network round trip about 100 us, so the network, not the CPU, bounds a memory store; one thread can saturate it for small commands.
- One thread means no locks, so structures stay simple and cache-friendly and every command is atomic for free.
- Memory is expensive per byte, so small values use listpacks, trading O(n) scans over tiny n for fewer pointers.
- An
fsynccosts 0.1 to 10 ms, hundreds of times the command itself, so durability is opt-in. - Scaling past one core means many single-threaded shards, so the cluster uses static slots and client-side routing instead of a coordinator hop.
Diagram
flowchart LR
C["Clients"] --> EP["epoll_wait"]
EP --> RD["read into query buffer"]
RD --> EX["execute command (main thread)"]
EX --> DS["dict + skiplist / listpack"]
EX --> AOFB["AOF buffer"]
EX --> BL["replication backlog"]
BL --> REP["replicas (PSYNC offset)"]
AOFB --> FS["bio thread: fsync every second"]
EX --> WB["reply buffer -> write"]
WB --> CWorked example
A game needs a global leaderboard for 20 million players with 50,000 score updates and 30,000 "my rank and 10 neighbours" reads per second.
One sorted set holds every player. At roughly 80 to 100 bytes per member with a 16-byte ID (hedge: measure with MEMORY USAGE), that is about 2 GB. ZADD and ZREVRANK each walk about 25 skip-list levels, a few microseconds. A read pipelines ZREVRANK then ZREVRANGE rank-5 rank+5 WITHSCORES in one round trip.
That is 110,000 commands per second on one key, so one core of one shard: feasible with pipelining and io-threads, but with little headroom. Reads go to a replica (milliseconds stale is fine), and authoritative scores live in a durable database so the set can be rebuilt in minutes. At 10x, split into score bands with a count per band; global rank is the sum of higher bands' counts plus rank within the band.
In the wild
- Twilio, 2013: a read-only Redis master double-charges customers. On 18 July a network blip made the billing replicas resync at once. A misdiagnosed restart of the master loaded a bad config, tried to recover from an AOF that did not exist, and left the master read-only with balances at zero. Auto-recharge then charged cards but could not record the new balance, so it charged again; 1.4% of customers were hit. Twilio stopped in-place master restarts and made billing refuse to charge when a balance is missing or unwritable (Twilio).
- Slack, about 2016: a Redis job queue fills up and cannot drain. Database contention slowed job workers until the Redis queue hit its memory limit. New jobs were refused, and dequeuing also needed spare memory to move a job onto an in-flight list, so the queue stayed stuck after the database recovered. At 1.4 billion jobs a day (33,000 per second at peak), Slack put Kafka in front of Redis as a durable buffer (Slack).
- Salesforce, 2026: hot keys pin single shards during a Memcached-to-Redis move. Migrating about 1.5 million cache events per second to Redis Cluster exposed keys hot enough to saturate one shard's command thread. Read replicas were a stopgap; the fix was a local in-process cache in front of Redis for those keys, plus planned count-min-sketch detection (Salesforce).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Redis, no persistence | Fastest, simplest, cheap ops | Data gone on restart | Pure cache that can be refilled |
| Redis with AOF everysec + replica | Sub-ms, rich structures, survives restarts | Can lose about a second, or more on failover | Sessions, rate limits, leaderboards |
| Memcached | Multithreaded, simple, predictable memory | Only strings, no persistence or replication | Large flat cache on big multi-core boxes |
| DynamoDB or Cassandra | Durable, horizontally scalable writes | Single-digit ms latency, fewer server-side structures | Data that must not be lost |
| Postgres | Transactions, durability, SQL | 10x higher latency for hot counters, harder to scale writes | System of record |
Pitfalls in production
- Slow commands stall everyone.
KEYS *,SMEMBERSon huge sets,DELof a giant hash. UseSCAN,UNLINK, and watchSLOWLOG. - Fork at peak.
BGSAVEor AOF rewrite on a large write-heavy node spikes latency and memory. Disable THP, keep headroom, snapshot on a replica. - Hot keys. One key pins one shard's core. Split it under several names or add a client-side cache.
- Loss on failover. Use
min-replicas-to-writeandWAITfor important writes; keep money elsewhere. noevictionon a cache. Writes fail with OOM when full. Useallkeys-lruorallkeys-lfuand alert onevicted_keys.- Mass expiry in one second. The active expire cycle repeats while more than 25 percent of sampled keys are expired, so millions of keys set with the same
EXPIREATcan block the server. Add jitter to TTLs (Redis latency guide).
Leadership lens
- Ask "what happens if this Redis is empty?" If the answer is data loss, the design treats a cache as a database.
- Decide durability per use case (
appendfsync, replicas,WAIT) and write it down. - Managed offerings remove failover toil; MemoryDB adds a durable multi-AZ log if Redis must be primary storage.
- Redis moved to RSALv2/SSPLv1 in March 2024, cloud providers backed the BSD-licensed Valkey fork, and Redis 8 added AGPLv3 as a third option in May 2025 (Redis). Check which one your platform team and cloud provider actually run.
- Budget 1.5 to 2 times dataset size for copy-on-write, fragmentation and replication buffers.
Check yourself
Why can a single-threaded Redis serve more than 100,000 operations per second?
Show answer
Each command costs microseconds in memory; the real cost is syscalls and network, which pipelining and I/O threads amortise, and there is no lock contention.
A
SETreturnedOK, the primary crashed, and after failover the value is gone. Why, and how do you reduce the risk?Show answer
Replication is asynchronous, so the promoted replica never received it.
WAITandmin-replicas-to-writeshrink the window but cannot close it.Why does a large sorted set keep both a skip list and a hash table?
Show answer
The hash table answers member-to-score in O(1); the skip list with spans keeps order for O(log n) rank and range queries.
Why should transparent huge pages be off on a Redis host?
Show answer
After
fork(), each write copies a whole page; 2 MB pages multiply copy-on-write memory and latency compared with 4 KB pages.
Sources and further study
Primary
- Kingsbury, Call me maybe: Redis (2013): Jepsen partition test of Sentinel failover that lost 56% of acknowledged writes.
- Redis source: server.h: the skip-list constants and core structs referenced above.
- Valkey: Unlock one million RPS: how the fork moved I/O and
epoll_waitoff the main thread while keeping single-threaded execution.
Systems docs
- Redis: Diagnosing latency issues: fork cost per GB, transparent huge pages, AOF fsync stalls and expire storms.
- Redis: WAITAOF: waiting for local and replica AOF fsync, and why it still is not strong consistency.
- Redis: Pipelining: why syscalls, not data structures, bound unpipelined throughput.
Books
- Designing Data-Intensive Applications (1st ed.), Martin Kleppmann, Ch 5 "Replication": asynchronous leader-follower replication and the writes it loses on failover.
- System Design Interview, Volume 2, Alex Xu and Sahn Lam, Ch 10 "Real-time Gaming Leaderboard": a sorted-set leaderboard sized and sharded end to end.
Other curricula
- donnemartin/system-design-primer: treats Redis and Memcached together as in-memory caches and lists Redis's extra features (persistence, sorted sets) without going into replication or durability.
- AWS Builders' Library: Caching challenges and strategies: frames an external cache as a dependency that can fail, and covers cold starts, request coalescing and serving stale data.
Watch
- System Design: Why is single-threaded Redis so fast? (ByteByteGo): the event loop, in-memory structures and I/O multiplexing in a few minutes.
Free resources
- Redis persistence: official explanation of RDB, AOF, fsync policies and the fork trade-offs.
- Redis replication: replication IDs, offsets, the backlog and partial resync.
- Redis Cluster specification: hash slots, gossip, redirects and the failover election in detail.
- Key eviction: each
maxmemory-policyand how approximated LRU and LFU work. - Sorted sets: commands and complexity for the structure behind leaderboards.