Early Access: 87 spots left.

Claim

Key Technologies

Redis

Redis (REmote DIctionary Server) is an in-memory data store that keeps its entire dataset in RAM and answers most operations in well under a millisecond. Developers usually reach for it first as a caching layer, but that undersells it. Redis is a data structure server: it gives you strings, lists, sets, sorted sets, hashes, streams, and more as first-class server-side types, with atomic operations on each. That combination of speed and rich primitives is why the same system shows up as a cache, a rate limiter, a leaderboard, a session store, a job queue, and a message broker, often in the same application.

This guide walks through the parts that matter in design discussions and interviews: why in-memory makes it fast, the single-threaded execution model, persistence, eviction, replication and failover, scaling out with Redis Cluster, and the handful of patterns you'll be asked to reason about.

Why In-Memory Changes Everything

A read from main memory takes on the order of 100 nanoseconds. A read from an SSD is tens of microseconds, and a random read from a spinning disk is several milliseconds. By keeping everything in RAM, Redis skips the slowest part of a traditional database, the trip to disk, on every operation. That's the whole performance story in one sentence: it's fast because it doesn't touch disk on the hot path.

The cost of that decision is the constraint it imposes. Your working set has to fit in memory, and memory is more expensive and less abundant than disk. So Redis isn't a place to dump everything; it's a place to put the data that benefits most from being fast and small enough to hold. When people say Redis is a cache, this is what they mean: it stores a hot subset of data that you can afford to lose or rebuild.

Memory is the source of truth, disk is the backup

Redis does persist to disk (covered below), but persistence is for recovery, not for the read path. Reads and writes are served from memory; the disk only sees a background snapshot or an append-only log. This is the opposite of a disk-based database, where memory is a cache in front of disk. Keep that inversion straight and most of Redis's behavior follows from it.

More Than Key-Value: The Data Structures

The feature that separates Redis from a plain key-value cache like Memcached is that values are typed data structures, and the operations on them run on the server. Instead of reading a list into your application, modifying it, and writing it back (three round trips and a race condition), you send one command and Redis mutates the structure in place, atomically. Picking the right structure for the job is most of the skill in using Redis well.

TypeWhat it isTypical use
StringBytes up to 512 MB; also integers/floatsCached values, counters (INCR), feature flags
HashField-value map under one keyAn object's fields (user profile) without serializing the whole thing
ListOrdered, doubly linked listQueues and stacks (LPUSH/BRPOP), recent-items feeds
SetUnordered collection of unique membersTags, unique visitors, membership tests, set algebra
Sorted set (ZSet)Set where each member has a score, kept orderedLeaderboards, priority queues, time-ordered indexes
StreamAppend-only log with consumer groupsEvent streams and durable queues (a Kafka-lite)
Bitmap / HyperLogLogBit operations / probabilistic cardinalityDaily-active flags; counting uniques in a few KB
GeospatialSorted set of lat/long pointsNearby search (GEOADD, GEOSEARCH)

The core Redis data types and where each one earns its keep.

Take a leaderboard that ranks millions of players by score. In most stores that's awkward: you sort on read or maintain a separate index. In Redis it's a single sorted set. ZADD leaderboard 4200 player:42 records a score, ZINCRBY bumps it, ZREVRANK leaderboard player:42 returns the player's rank, and ZREVRANGE leaderboard 0 9 WITHSCORES returns the top ten. All of it's server-side, atomic, and fast, because the sorted set keeps members ordered as you write them.

Single-Threaded, and Why That Is a Feature

Redis executes commands one at a time on a single thread, in the order they arrive. This sounds like it should be a bottleneck, but in practice it's a large part of why Redis is both fast and easy to reason about. There are no locks, no mutexes, and no lock contention, because there's nothing to contend over. Every individual command is atomic for free: while INCR runs, no other command is running, so two clients incrementing the same counter can never lose an update. The CPU stays busy serving requests instead of coordinating between threads.

It works because the operations themselves are cheap (memory access, not disk) and because a single core can push hundreds of thousands of operations per second. The one thread doesn't sit waiting on the network either: it runs an event loop built on I/O multiplexing (epoll on Linux, kqueue on macOS and BSD), so the kernel hands it only the sockets that have a request ready and it never blocks on the thousands that are sitting idle. Modern Redis does use extra threads for network I/O and for some background work (snapshotting, freeing large objects), but the core rule still holds: command execution is serialized on one thread.

One slow command blocks the whole server

The flip side of single-threaded execution is that one slow command blocks every other client. A command like KEYS * scans the entire keyspace in O(N) and can freeze the server for seconds on a large database; the same goes for SMEMBERS on a huge set, a big SORT, or FLUSHALL on a large instance. In production, never run KEYS, use SCAN (which paginates with a cursor and doesn't block). Treat any O(N)-over-the-whole-dataset command as a foot-gun and watch for big keys (a single set or list with millions of members) that turn an innocent command into a stall.

On a multi-core machine, one instance uses one core

Single-threaded execution is per instance and only for command processing. A single Redis instance keeps one core busy on its command path and leaves the rest of the cores idle, so on an 8-core box one instance can't give you 8 cores of throughput. Redis still spreads other work across cores: the fork for RDB snapshots and AOF rewrites runs as a separate process, background (bio) threads handle fsync and the freeing of large objects, and threaded I/O (the io-threads option added in Redis 6, off by default) can parallelize socket reads and writes while command execution stays serialized. To actually use all the cores, run several Redis instances on the machine (roughly one per core) and shard across them; this is what Redis Cluster does even on a single host. Pin instances to cores for predictable latency and leave a core free for the OS and the snapshot fork.

Pipelining and Transactions

Every command is normally a round trip: the client sends one command, waits for the reply, then sends the next. The waiting is the problem. If the network round-trip time is half a millisecond, a thousand commands issued one after another spend about half a second just waiting, even though Redis handles each one in microseconds. Pipelining removes that wait by sending a batch of commands at once and reading all the replies together. Redis does the same work; what disappears is the round-trip pause between commands, which is why a bulk load can drop from hundreds of milliseconds to single-digit milliseconds. Pipelining is purely a latency optimization, not a transaction: the batched commands aren't atomic, and another client's commands can still run in between yours.

A transaction is the tool when you do need a group of commands to run together with nothing interleaved. You start it with MULTI, queue your commands (each one replies QUEUED instead of running), and fire them with EXEC. Because Redis is single-threaded, once EXEC begins the whole batch runs to completion before any other client is served, so no other command can slip in. For a read-modify-write that has to be safe against concurrent changes, you add WATCH: you watch one or more keys before MULTI, and if any of them changes before EXEC, the EXEC aborts and returns nil so you can retry. That's optimistic locking, and it's how you do a safe check-then-set without holding a lock.

Client AClient BRedisWATCH balance:42WATCH balance:42MULTI, SET, EXECOK, committedbalance:42 now changedMULTI, SET, EXECreturns (nil)watched key changed, aborted
Both clients watch balance:42 and read it. Client A's MULTI/EXEC commits and changes the key. When Client B then runs EXEC, Redis sees that a watched key changed since the WATCH and aborts the whole transaction, returning (nil), so B knows to re-read and retry. No lock is ever held; the conflict is caught at EXEC time. That is optimistic concurrency control.

Redis transactions don't roll back

Coming from SQL, the instinct is that MULTI/EXEC is all-or-nothing. It isn't. If a command fails at runtime inside the transaction (say you run a list command on a key that holds a string), Redis still executes the other commands and EXEC simply reports the error for the one that failed. The only thing that aborts the whole batch is a syntax error caught while queuing, before EXEC. So a transaction gives you isolation (no interleaving) and a single round trip, but not the atomic rollback SQL gives you. When later commands need to use the result of earlier ones, or you want tighter control, a Lua script is usually the better tool: it also runs atomically on the single thread, but unlike a queued transaction it can read one command's result to decide the next.

Persistence: RDB and AOF

Because the data lives in RAM, a restart or crash would lose everything unless Redis wrote something to disk. It offers two mechanisms, and you can run either, both, or neither depending on how much data you can afford to lose.

RDB takes point-in-time snapshots: every so often (or on demand) Redis forks a child process that writes the whole dataset to a compact binary file. The fork relies on copy-on-write, so the parent keeps serving requests while the child writes a consistent picture of memory as it was at fork time. AOF (Append Only File) instead logs every write command as it happens, so the dataset can be rebuilt by replaying the log. The AOF is periodically rewritten into a compact form so it doesn't grow forever.

RDB (snapshot)AOF (append-only log)
What it storesFull dataset, periodicallyEvery write command, continuously
Data loss on crashEverything since the last snapshotAt most one second (with the default fsync everysec)
Restart speedFast (load one compact file)Slower (replay the log)
File sizeSmall and compactLarger; needs periodic rewrite
Cost during operationFork + write on snapshotContinuous appends, lighter per write

RDB and AOF make different trade-offs; many production setups run both.

The durability dial that matters most is the AOF fsync policy. With appendfsync always, Redis flushes every write to disk before acknowledging, which is the safest and the slowest. With appendfsync everysec (the default), it flushes once per second, so a crash can lose up to one second of writes in exchange for far better throughput. With appendfsync no, it lets the operating system decide when to flush, which is fastest and least safe. The common production choice is RDB for fast restarts plus AOF with everysec for bounded loss, and Redis can combine them so the AOF starts from an RDB preamble.

Forking, copy-on-write, and the memory you must reserve

Persistence isn't free, and the snapshotting fork is where teams get surprised in production. Under copy-on-write, if the workload is write-heavy during the fork, the parent can momentarily need close to double the memory as pages diverge. Size the host accordingly, and watch for latency spikes that line up with snapshot times. And even with AOF, durability stays local: a single Redis node that loses its disk loses its data. Real durability comes from replication plus persistence, not persistence alone.

Eviction and Expiration

Two different mechanisms remove keys, and they're easy to confuse. Expiration is something you ask for: you set a TTL with EXPIRE key 60 or SET key val EX 60, and the key disappears when its time is up. Eviction is something Redis does to you: when memory hits the maxmemory limit, it has to free space to accept new writes, and the maxmemory-policy decides which keys to sacrifice.

Expiration is enforced two ways at once. Lazy expiration removes an expired key the next time something touches it. Active expiration runs in the background, sampling a batch of keys with TTLs and deleting the expired ones, repeating if too many in the sample were expired. Together they keep expired keys from lingering without scanning the whole keyspace.

PolicyBehavior
noevictionReject writes with an error when full (the default)
allkeys-lruEvict the least recently used key from all keys
allkeys-lfuEvict the least frequently used key from all keys
volatile-lru / volatile-lfuSame as above, but only among keys that have a TTL
allkeys-random / volatile-randomEvict a random key (from all keys, or only those with a TTL)
volatile-ttlEvict the key with the shortest remaining TTL first

The maxmemory-policy options. allkeys-* consider every key; volatile-* only consider keys that have a TTL set.

noeviction is the default; a cache almost never wants it

The default policy is noeviction, which means a Redis used as a pure cache will start returning OOM errors on writes once it fills up instead of quietly dropping cold data. If you're using Redis as a cache, set an eviction policy on purpose, usually allkeys-lru or allkeys-lfu. The volatile-* policies only evict keys that have a TTL, so if you forget to set TTLs they have nothing to evict and you still hit OOM. LFU (least frequently used) is often the better cache policy because it resists a one-time scan flushing your genuinely hot keys, which plain LRU doesn't.

Replication and High Availability

A single Redis node is a single point of failure. Replication fixes the read side and the durability side: a master accepts writes and streams them to one or more replicas, which serve reads and stand ready to take over. Replication is asynchronous by default, so the master acknowledges a write to the client before the replicas have it. That keeps writes fast, but it means a master can fail with a few writes that never reached any replica, and those writes are lost on failover.

asyncasyncClient writesMasterReplica 1Replica 2
Writes go only to the master and are streamed asynchronously to replicas. Replicas serve reads and can be promoted if the master fails. Because replication is async, writes acknowledged by the master but not yet replicated are lost if the master dies before they propagate.

Replication alone doesn't give you automatic failover; something has to notice the master is dead and promote a replica. Redis Sentinel is that something: a set of Sentinel processes monitor the master and replicas, agree by quorum that the master is down, elect a replica, promote it, and tell clients about the new master. Sentinel handles failover for a single master with its replicas. When you also need to shard data across many masters, that's Redis Cluster, which has its own failover built in.

Async replication means failover can lose writes

Because replication is asynchronous, failover can lose recently acknowledged writes, and a network partition can briefly produce two nodes that both think they're master (split brain), with writes to the losing side discarded after the partition heals. The WAIT command lets a client block until a write reaches N replicas, which narrows the window but doesn't make Redis a strongly consistent store, and it costs latency. In an interview, the line to remember is that Redis replication favors availability and speed over strict consistency, so treat it as a fast cache, or as a store where a small, bounded loss on failover is acceptable.

Redis Cluster: Scaling Out

Replication scales reads, but every write still goes to one master and the whole dataset still has to fit on one machine. When the data or the write rate outgrows a single node, Redis Cluster shards the keyspace across several masters. It divides the key space into 16384 hash slots; every key is mapped to a slot by CRC16(key) mod 16384, and each master owns a contiguous range of slots. Add a node and you move some slots (and their keys) to it; this is how the cluster rebalances. Each master can have its own replicas, so the cluster keeps working when a master fails.

slotslotslotKeyCRC16 % 16384Node A 0-5460Node B 5461-10922Node C 10923-16383
Every key maps to one of 16384 hash slots via CRC16(key) mod 16384, and each master owns a contiguous slot range. If a client asks the wrong node, it gets a MOVED reply pointing at the node that actually owns the slot.

Clients can talk to any node. If a node doesn't own the slot for a key, it replies with MOVED (or ASK during a slot migration) telling the client where to go; cluster-aware clients cache the slot map and route directly after that. The catch is multi-key operations. A command that touches several keys (an MGET, a set intersection, a transaction) only works if all those keys live in the same slot, because no single node holds the whole keyspace.

Multi-key ops need the same slot: hash tags {...}

To force related keys into the same slot, use a hash tag: only the part inside curly braces is hashed, so {user:42}:profile and {user:42}:sessions both hash on user:42 and land on the same node, letting you run multi-key commands on them. Overuse it and you create hot slots that ruin the even distribution sharding was supposed to give you, so tag only keys that genuinely must be operated on together. This is the single most common surprise when migrating an app from a single Redis to a cluster: multi-key commands that worked suddenly throw CROSSSLOT errors.

Patterns You Will Be Asked About

Most Redis questions are really questions about one of a few patterns. The most common is the cache-aside (lazy-loading) cache: the application checks Redis first, and on a miss it reads the database, writes the value back into Redis with a TTL, and returns it. Subsequent reads hit the cache until the TTL expires or the key is evicted.

AppRedisDatabaseGET keymiss (nil)SELECT ... (load on miss)rowSET key val EX 300
The application owns the cache. On a hit it returns immediately; on a miss it reads the database, writes the value back into Redis with a TTL, and returns it. The database is never read again for that key until the entry expires or is evicted.

Rate limiting is the next-most common. A fixed-window limiter is just a counter per user per window: increment on each request and reject once the count crosses the limit, letting the key expire at the end of the window. The subtlety is that the counter and its expiry must be set together, or a crash between the two can leave a counter that never expires. Doing it in a Lua script makes the whole thing atomic on Redis's single thread.

Distributed locks are the pattern people get wrong most often. The basic idea is sound: SET lock:order:99 <random-token> NX PX 30000 acquires a lock only if it doesn't already exist (NX) and auto-expires after 30 seconds (PX) so a crashed holder can't hold it forever. Releasing the lock must be conditional: you only delete it if the token is still yours, or you risk deleting a lock that another client has since acquired. That check-and-delete has to be atomic, which again means a Lua script.

Redis locks are for efficiency, not correctness

A single-node Redis lock isn't a correctness guarantee. If the master fails after granting a lock but before replicating it, a promoted replica doesn't know the lock exists and can grant it again, so two clients hold it at once. The Redlock algorithm spreads the lock across several independent masters to reduce this, but it remains contested: Martin Kleppmann argued it's unsafe for correctness because timing assumptions (clock drift, GC or VM pauses, network delay) can let an expired lock holder still act, and the real fix is a fencing token that the protected resource checks. Antirez (Redis's creator) pushed back. In practice, use a Redis lock for efficiency (avoiding duplicate work), but when correctness depends on it (never two writers at once), back it with a fencing token or use a system built for consensus, rather than trusting the lock alone.

Two more worth naming. Leaderboards and any ranked or time-ordered index are sorted sets, as shown earlier. For messaging, Redis offers two very different tools: Pub/Sub is fire-and-forget (a message is delivered only to clients connected at that instant and is never stored), while Streams are an append-only, persistent log with consumer groups and acknowledgments, much closer to a real message queue. If you need messages to survive a disconnect or to be processed exactly once per group, use Streams, not Pub/Sub.

Cache penetration, stampedes, and avalanches

Three related failure modes come up together in interviews. The stampede (or thundering herd) is the classic one: when a popular key expires, every request that missed it hits the database at the same instant and overloads it. A few fixes help here: add a small random jitter to TTLs so they don't all expire together, let one request rebuild the value while the rest serve a slightly stale copy, or pre-warm the hottest keys. Penetration is different: requests keep asking for a key that doesn't exist in the database at all (a bogus user_id = -1, say), so there's nothing to cache and every request slips past Redis straight to the database. Stop it by caching the miss itself (a short-lived null marker) or by putting a Bloom filter in front, often the RedisBloom module, so a key that definitely doesn't exist is rejected before the database is touched. Avalanche is when a large batch of keys expire in the same second, usually because a job loaded them all with an identical TTL, so the database takes the whole load at once; the same TTL jitter fixes it, and since a cache cluster going down has the same effect, that's another reason to run replicas. One more cousin, the hot key, sends all of a single wildly popular key's traffic to one node; a small in-process cache or client-side caching in front of Redis is the usual relief.

When to Use Redis (and When Not To)

Redis fits when you need very low latency on a working set that fits in memory, and when the data is either rebuildable (a cache) or where a small, bounded loss on failover is acceptable. Caching, rate limiting, leaderboards, session storage, ephemeral counters, real-time queues, and presence (who is online) are all natural fits. It's the wrong tool when the dataset is far larger than affordable RAM, when you need rich queries and joins (use a relational database), when you need strong durability and strict consistency for a system of record (use a database designed for it), or when you need full-text search or analytical scans. Redis usually sits alongside a primary database rather than replacing it.

How This Shows Up in Interviews

Expect a handful of recurring questions. "Why is Redis fast?" (in-memory data, single-threaded so no lock contention, simple operations). "Redis versus Memcached?" (Redis has rich data structures, persistence, replication, and clustering; Memcached is a simpler multi-threaded cache, occasionally better for plain large-value caching). "How does Redis persist data?" (RDB snapshots and the AOF log, with the everysec fsync trade-off). "What happens when it runs out of memory?" (the maxmemory-policy decides; the default noeviction rejects writes). "How do you scale Redis?" (replicas for reads, Redis Cluster with 16384 hash slots for writes). "How would you build a rate limiter / leaderboard / distributed lock?" (the patterns above, and the lock-correctness caveat is the part that impresses). "Pipelining versus transactions?" (pipelining batches commands to cut network round trips and isn't atomic; MULTI/EXEC runs a batch as one uninterrupted unit). "Do Redis transactions roll back on an error?" (no, the remaining queued commands still run and EXEC reports the failure; you get isolation, not rollback). And "What are the consistency guarantees?" (asynchronous replication, so failover can lose recent writes; not a strongly consistent store).

Your team uses Redis purely as a cache and leaves the configuration at its defaults. Under sustained load the instance fills up and the application starts getting errors on cache writes. What is the most likely cause?

You implement a distributed lock with SET key token NX PX 30000 on a single Redis master with one replica. Why is this not safe to rely on for strict correctness (never two clients holding the lock)?

Key Takeaways

Redis is fast because it serves everything from memory and executes commands one at a time on a single thread, which removes lock contention and makes every command atomic. It's more than a cache because its values are typed data structures (strings, hashes, lists, sets, sorted sets, streams) with rich server-side operations, which is what makes leaderboards, rate limiters, and queues a few commands rather than an application subsystem. It survives restarts through RDB snapshots and the AOF log, trading data-loss window against throughput via the fsync policy. When memory fills, the maxmemory-policy decides what to drop, and the default noeviction is the wrong choice for a cache. It scales reads with asynchronous replicas and scales writes with Redis Cluster's 16384 hash slots, at the cost of weaker consistency on failover and constraints on multi-key operations.

For a design discussion, remember the core trade: Redis gives up durability and strict consistency for speed. Use it where that trade is the right one, as a fast layer in front of a durable primary store, and respect the operational edges (big keys and blocking commands, the noeviction default, data loss on failover, and lock correctness), because those are exactly where real systems and interviewers probe.

ON THIS PAGE