Engineering Atlas › System Design Interviews

Redis for system design interviews

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

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

  1. 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.
  2. One thread means no locks, so structures stay simple and cache-friendly and every command is atomic for free.
  3. Memory is expensive per byte, so small values use listpacks, trading O(n) scans over tiny n for fewer pointers.
  4. An fsync costs 0.1 to 10 ms, hundreds of times the command itself, so durability is opt-in.
  5. 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 --> C

Worked 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

Trade-offs

OptionStrengthsWeaknessesChoose when
Redis, no persistenceFastest, simplest, cheap opsData gone on restartPure cache that can be refilled
Redis with AOF everysec + replicaSub-ms, rich structures, survives restartsCan lose about a second, or more on failoverSessions, rate limits, leaderboards
MemcachedMultithreaded, simple, predictable memoryOnly strings, no persistence or replicationLarge flat cache on big multi-core boxes
DynamoDB or CassandraDurable, horizontally scalable writesSingle-digit ms latency, fewer server-side structuresData that must not be lost
PostgresTransactions, durability, SQL10x higher latency for hot counters, harder to scale writesSystem of record

Pitfalls in production

Leadership lens

Check yourself

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

  2. A SET returned OK, 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. WAIT and min-replicas-to-write shrink the window but cannot close it.

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

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

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →