How it works, what to say when an interviewer asks about it, and where it falls over.
Redis comes up in a strange number of interviews. Sometimes it's a direct question, like "what happens when a key expires?" More often it hides inside a system design round. You draw a cache box, the interviewer points at it, and asks what happens when it goes down.
The trouble with learning it is that most material is either a five-minute "Redis is an in-memory key-value store" intro or a reference manual. Neither is what you need. You need the middle: enough depth to explain why things work the way they do, and enough awareness to notice when an interviewer is walking you toward a trap.
This post is my attempt at that middle. It goes through how Redis works, the data types, expiry and eviction, caching patterns, persistence, replication and clustering, transactions and Lua, Pub/Sub, and the system design problems where Redis actually earns its place (locks, rate limiters, leaderboards, queues). It ends with when not to use Redis, which gets asked more often than you'd think. The details match Redis 7 and 8.
What Redis is and how it works
Redis stands for Remote Dictionary Server. It's an in-memory data structure store written in C. The words "data structure" matter more than "in-memory." A plain key-value store maps a key to a blob. In Redis the key is a string and the value is a real data structure: a list, a set, a sorted set, a hash. When you create a sorted set, it simply lives as the value under whatever key you picked. Nearly everything else about Redis follows from that.
People use it as a cache, a session store, a rate limiter, a leaderboard, a lightweight message broker, and sometimes as the main database for data they can afford to lose or rebuild.
Here's what happens when a client sends a command:
- The client opens a TCP connection to the server (port 6379 by default) and sends the command using a simple protocol called RESP. It's close enough to plain text that you can type commands into
redis-cliand see exactly what goes over the wire. - The server runs an event loop built on
epoll(Linux) orkqueue(BSD and macOS). One thread watches thousands of connections at once without needing a thread per client. - When a command is ready, the main thread parses it, runs it against the in-memory data, and writes the reply.
- The next command starts only after that one finishes.
That last point is the heart of Redis. Commands run one at a time, on a single thread, in the order they arrive.
A quick note on threads, because interviewers like to poke at "Redis is single-threaded." It's true for command execution. Since Redis 6 you can enable extra I/O threads that handle reading and writing network data. Background threads handle things like fsync for the append-only file and freeing large objects. Snapshots are written by a forked child process. But the commands themselves still run one after another on the main thread. To use more CPU cores, you run more Redis instances, which is what Redis Cluster does.
Why it's fast
Data lives in RAM, so there are no disk seeks on the read path. Running commands on one thread means no locks and no context switching inside the server, and it also means every single command is atomic for free. The event loop lets one thread serve many clients. And the operations themselves are cheap, mostly O(1) or O(log N).
A single node handles on the order of 100k operations per second. The command takes microseconds to run, and over a network you'll see sub-millisecond replies. That speed makes some habits that are terrible against SQL survivable here. Sending 100 small queries in a loop would hurt a database badly. Against Redis, each one is cheap, and you can batch them with pipelining (more on that later) to pay for one network round trip instead of 100.
What that design costs you
Everything has to fit in memory, and memory is the most expensive place to store data. One slow command blocks every other client, which is why KEYS * on a big database is a well-known way to take down production. Durability is weaker than in a relational database (the persistence section covers this). And there are no joins or ad-hoc queries. You decide how you'll read the data first, and design your keys around that.
Two comparisons you'll get asked
Redis vs Memcached: Memcached is a simpler cache. It's multi-threaded and stores plain strings. Redis adds rich data types, persistence, replication, Lua scripting and Pub/Sub. If all you need is a plain cache, either works. Once you want a sorted set or an atomic counter, Redis wins.
Redis vs a SQL database: they're rarely alternatives. Redis trades query flexibility and strong durability for speed and simplicity, so it usually sits in front of or next to a database instead of replacing one.
Data types and what's under them
Interviewers use data types as a shortcut for "does this person understand Redis or have they only used GET and SET?" You should know each type, its main commands, its cost, and a real use for it.
Strings are the base type. A string is binary-safe and can hold up to 512 MB, though you'll almost never go near that. SET and GET are the obvious commands. SET also takes options: EX or PX for a TTL, NX to set only if the key doesn't exist, XX to set only if it does. INCR and DECR treat the string as an integer and change it atomically, which is why counters are trivially safe in Redis. Cache values are usually strings, often JSON.
Hashes are a map of fields to values under one key, so they fit objects nicely. HSET user:42 name Asha city Pune creates one, HGET reads a field, HINCRBY bumps a numeric field. The classic interview question is hash versus a JSON string for an object. With a JSON string you read and rewrite the whole thing to change one field. With a hash you can update one field directly, and it's usually more memory-friendly for small objects. A JSON string is simpler if you always read and write the whole object.
Lists are ordered sequences, implemented as a linked list of compact blocks (called a quicklist). Pushing and popping at either end is O(1), while reaching into the middle is O(N). LPUSH, RPUSH, LPOP, RPOP do what you'd expect, and BRPOP blocks until an item arrives, which is how people built simple job queues before Streams existed. LRANGE reads a slice.
Sets hold unique, unordered strings. SADD, SISMEMBER, SCARD and SMEMBERS are the basics, all cheap except SMEMBERS on a huge set. The interesting commands are SINTER, SUNION and SDIFF, which give you things like mutual friends or users who share tags. Sets suit exact unique tracking, like "which users have already seen this notification."
Sorted sets are the type that comes up most in system design. Each member is unique and has a floating-point score, and the set stays ordered by score. Inside, Redis combines a skip list with a hash table. The skip list gives ordered traversal and rank lookups in O(log N), and the hash table gives O(1) lookup of a member's score. ZADD adds or updates a member, ZINCRBY bumps a score, ZRANGE (with REV for descending order) reads by rank, ZRANK and ZREVRANK give a member's position, and ZRANGEBYSCORE-style queries read by score. Leaderboards, rate limiters, priority queues and "delayed job" schedulers are all sorted sets in disguise.
Streams are an append-only log. Every entry gets an ID (a millisecond timestamp plus a sequence number) and holds field-value pairs. XADD appends, XREAD reads, and consumer groups (XREADGROUP, XACK) let several workers share a stream with acknowledgements. We'll come back to these in the queue section.
Bitmaps aren't a separate type. They're bit operations (SETBIT, GETBIT, BITCOUNT, BITOP) on a string. If you use a user ID as the bit offset, one bit per user per day costs about 12.5 MB for 100 million users. That's how you track daily active users cheaply.
HyperLogLog counts unique items approximately. PFADD adds an item, PFCOUNT returns the estimated number of distinct items. It uses about 12 KB per key no matter how many items you add, with a standard error around 0.8%. The catch is that you can't list what's inside or remove things. If an interviewer asks "how do you count unique visitors across billions of events," this is the answer, along with an honest note about the error.
Geo commands (GEOADD, GEOSEARCH) store coordinates and find points within a radius or box. Under the hood it's a sorted set where the score is a geohash of the coordinates. The search grabs candidates from grid-aligned boxes first, then filters to the exact radius.
Redis 8 also ships probabilistic structures like Bloom filters, plus JSON and time series support, in the core. On older versions these came from separate Redis Stack modules. A Bloom filter says either "definitely not in the set" or "probably in the set," never the other way round, and it will matter when we get to cache penetration.
One habit that's worth building early: name keys like user:42:profile or order:9001:items. Colons are just a convention, but every team uses them, and your key design is how you'll shard the data later.
Expiration and eviction
People mix these two up constantly, so keep them apart in your head. Expiration is about staleness: a key has a time to live and disappears when it runs out. Eviction is about memory: when Redis is full, it throws away keys to make room. They're separate mechanisms with separate settings.
Expiration. You set a TTL with EXPIRE key 60, or directly in SET key value EX 60. TTL tells you how many seconds remain, and PERSIST removes the TTL. One gotcha: a plain SET on an existing key wipes its TTL unless you pass KEEPTTL. That has caused real bugs.
Redis removes expired keys in two ways. When a client touches an expired key, Redis notices and deletes it on the spot (lazy expiration). Separately, a background task samples keys that have TTLs several times a second and removes the expired ones. So an expired key can sit in memory for a short while, but you'll never read it. Redis guarantees that you won't see a value after its TTL has passed.
Eviction. By default Redis has no memory limit on a 64-bit machine. You set one with maxmemory 2gb, and you choose what happens at the limit with maxmemory-policy. The default policy is noeviction, which means Redis refuses new writes once it's full. That surprises people who assumed a cache would just make room. For a cache, you pick something else:
-
allkeys-lruevicts the least recently used keys from the whole keyspace. It's a good default when some keys are much more popular than others, which is true of most workloads. -
allkeys-lfuevicts the least frequently used keys. It's better when a key that was popular for a long time shouldn't be dropped just because nobody touched it in the last minute. -
allkeys-randomevicts random keys. It suits workloads that scan through everything evenly. - The
volatile-versions (volatile-lru,volatile-lfu,volatile-random) only consider keys that have a TTL.volatile-ttlevicts keys with the least time left.
If no keys have a TTL, the volatile- policies behave exactly like noeviction. So if you pick one and forget to set TTLs, your cache will start rejecting writes. Newer versions also add "least recently modified" policies, but the ones above cover what interviews ask.
Redis's LRU is an approximation. It doesn't track the exact order of every key, which would cost memory. Instead it samples a handful of keys (the maxmemory-samples setting, 5 by default) and evicts the best candidate among them. LFU works similarly, using a small counter per key that decays over time.
A practical detail: eviction happens when a command that would add data arrives while memory is over the limit. And Redis needs headroom beyond maxmemory, for replication buffers, fragmentation, and the extra pages copied while a child process is writing a snapshot. Setting maxmemory to 100% of the machine's RAM is a common way to get the process killed by the operating system.
If you're asked "cache is full, what happens?" the answer is: it depends on maxmemory-policy, and the default rejects writes.
Caching patterns and how they fail
Caching is the most common reason Redis is in a diagram, and the part interviewers dig into.
The four patterns.
Cache-aside is the default. The application checks Redis first. On a miss it reads the database, writes the result to Redis (with a TTL), and returns it. The app talks to both systems, and the cache only ever holds data someone asked for.
Read-through looks the same from the outside, but the cache layer loads the data itself on a miss and the app only talks to the cache. Redis has no built-in loader, so read-through means a library or a small service that wraps Redis and your database.
Write-through sends every write to the cache and the database together. Reads stay fresh, writes cost a bit more.
Write-behind (also called write-back) writes to the cache and flushes to the database later, asynchronously. Writes are fast, but if Redis dies before the flush, those writes are gone, and you have to handle ordering and retries.
Invalidation. The three ways to keep cached data from going stale are a TTL, deleting the key when the underlying data changes, and write-through. Most systems use a TTL as a safety net and also delete on write.
On writes, deleting the cached key is usually safer than updating it. Two writers racing to update the cache can leave the older value winning. Even deleting has a race: a reader misses, reads the old row from the database, and gets delayed. Meanwhile a writer updates the database and deletes the key. Then the slow reader finally writes the old value into the cache. Now the cache is stale until the TTL runs out. It's rare, but it's why you always want a TTL, and why some teams delete the key a second time after a short delay.
The cache is also eventually consistent by nature. Reads from a replica can lag behind the primary too. If your product can't tolerate any staleness, say so in the interview and explain how you'd narrow it.
The three failure modes. These names are used inconsistently across blog posts. One popular article uses "penetration" for the hot-key case, while most sources use it for missing keys. Define the term in your own words before you answer, and you'll be fine.
Cache penetration is when lots of requests ask for keys that don't exist anywhere, so they miss the cache every time and hit the database every time. Someone scanning random IDs does this, deliberately or not. There are two standard fixes. You can cache the "not found" answer for a short time (a null value with a small TTL). Or you can put a Bloom filter in front holding every valid key, and reject requests that the filter says are definitely absent. The filter can have false positives, which just means an occasional wasted lookup.
Cache stampede (also called the thundering herd, or "breakdown") is when one very popular key expires and, in the split second before it's refilled, hundreds or thousands of requests all miss and all hit the database to rebuild the same value. Three fixes come up. The first is a mutex: the first request to miss grabs a lock with SET lock:key token NX PX 5000 and rebuilds the value, while the others wait a moment and retry, or serve a slightly stale copy. The second is probabilistic early expiration (sometimes called X-Fetch), where each request has a small, growing chance of refreshing the value shortly before it actually expires, so one request quietly refreshes it while everyone else keeps reading the old one. The third is never letting the hottest keys expire at all, and refreshing them from a background job.
Cache avalanche is many keys expiring at once, or the cache itself going down. If you filled the cache at 9:00 with a 60 minute TTL on everything, then at 10:00 the database gets the entire load at once. The fix is jitter: add a random amount to each TTL so expiry spreads out. For the "cache is down" case, you need a plan too: rate limit or shed load in front of the database, and maybe replicate Redis so one failure doesn't empty the cache.
Hot keys. Sometimes traffic doesn't spread evenly. Imagine a cluster of 100 nodes caching product data, and one product goes viral. The single node holding that key takes as much traffic as the other 99 combined. Adding nodes doesn't help, because the key lives on exactly one of them.
There are three usual remedies, each with a cost. A small in-process cache on each app server absorbs most reads for the hottest keys, at the price of data that can be stale for as long as that local cache's TTL. Key copies store the same value under several names (product:123:1 through product:123:10), which hash to different nodes, and readers pick one at random. Writes then have to update every copy. Read replicas add read capacity, but only if your clients are set up to read from replicas, and they do nothing for a key that's hot for writes.
Spotting a possible hot key in your own design, before the interviewer asks, is the kind of thing that stands out.
Persistence
Redis lives in memory, but it can write to disk so it survives a restart. There are two mechanisms, and you can use them together or turn both off.
RDB snapshots. Redis writes the whole dataset to a compact file (dump.rdb) at intervals. A config line like save 60 1000 means "snapshot if at least 1000 keys changed in the last 60 seconds." You can also trigger it with BGSAVE. To do this without pausing, Redis forks: the child process writes the file while the parent keeps serving clients, and copy-on-write means memory is only duplicated for pages that change during the snapshot.
RDB files are small, great for backups, and load fast on restart. The downsides are that you lose everything since the last snapshot on a crash (often minutes), and fork() on a very large dataset can pause Redis for a noticeable moment.
AOF (append-only file). Redis logs every write command, and on restart it replays the log to rebuild the data. AOF is off by default (appendonly yes turns it on). How much you can lose depends on the appendfsync setting:
-
alwaysflushes to disk on every write batch. It's the safest and by far the slowest. -
everysecflushes once a second, and it's the default. You can lose up to about a second of writes. -
noleaves flushing to the operating system, which is fast and least safe.
The log grows forever if you leave it, so Redis periodically rewrites it in the background into the shortest list of commands that produces the current data. Since Redis 7 the AOF is split into a base file plus incremental files, tracked by a manifest.
Which should you use? Redis's own guidance is to use both if you want durability comparable to a traditional database. If you can live with a few minutes of loss, RDB alone is fine. For a pure cache, you can use neither. If both are on and Redis restarts, it loads the AOF, because that's the most complete record.
The honest answer to "is Redis durable?" is "only as durable as you configure it, and never quite like a database that flushes every commit." With the default everysec, an acknowledged write can still be lost. And replication adds another window on top, which brings us to the next section.
Replication, high availability and scaling
One Redis node has two limits: if it dies you lose availability, and it can only hold as much as one machine's memory. Replication, Sentinel and Cluster each deal with a piece of that.
Replication. You point a replica at a primary (REPLICAOF host port). The replica does a full sync first: the primary produces an RDB snapshot and sends it over, followed by the writes that piled up during the transfer. After that the primary streams every write command to the replica. If the connection drops briefly, the replica reconnects and asks to continue from its last offset. If that offset is still in the primary's replication backlog (a fixed-size buffer), only the missing part is sent (a partial resync). Otherwise it's another full sync.
The thing to remember is that replication is asynchronous. The primary acknowledges your write before the replica has seen it.
So if the primary dies right after acknowledging a write, and a replica that hasn't received it gets promoted, that write is gone. This is the core reason Redis isn't a system of record, and it shows up again in the distributed lock section. The WAIT command makes a client block until N replicas confirm a write, which shrinks the window but doesn't eliminate it.
Replicas are read-only by default. You can spread reads across them, but every replica read can be a little behind the primary. That's fine for a product page and wrong for something like "did my payment go through."
Sentinel. Replication alone doesn't promote anyone when a primary dies. Sentinel is a set of separate processes that watch the primary and replicas and handle failover. You run at least three, on different machines. When a Sentinel can't reach the primary, it marks it as "subjectively down." Once enough Sentinels agree (the quorum you configured), it's "objectively down." The Sentinels then elect a leader, and that leader picks the best replica (by priority and how much data it has) and promotes it, and reconfigures the others. Clients ask Sentinel "who is the current primary?" instead of hardcoding an address.
Sentinel gives you high availability, and nothing more. All your data still lives on one primary, so it doesn't add capacity. It also can't bring back writes lost to asynchronous replication.
Cluster. When the data no longer fits on one machine, or one machine can't keep up with the traffic, you shard it, and Redis Cluster is the built-in way.
The keyspace is divided into 16,384 hash slots. A key's slot is CRC16(key) mod 16384. Each primary owns a range of slots, and each primary usually has one or more replicas for failover. Nodes exchange information with each other using a gossip protocol, so every node knows the full slot-to-node map.
Clients cache the slot map and send each command straight to the right node. If a slot has moved, the node answers with a MOVED error that includes the correct address, and the client refreshes its map. During a resharding operation you might see ASK, which is a one-time redirect for a slot that's midway through moving. Nodes don't forward requests for you, and there's no router that splits a request across nodes and merges the results.
That last point is what makes Cluster feel restrictive. Commands that touch several keys (MGET, SINTER, MULTI transactions, Lua scripts) only work when every key is in the same slot. To force that, you use hash tags: only the part of the key inside curly braces is hashed. {user:42}:posts and {user:42}:likes always land in the same slot. The takeaway is that with Cluster, how you name your keys is how you scale.
A few more things worth knowing. Failover in Cluster works without Sentinel: replicas get promoted when a majority of primaries agree a primary has failed. You need at least three primaries to have a majority. Cluster only supports database 0. And during a network partition, a primary stuck on the minority side can keep accepting writes for a short time before it stops, and those writes can be lost.
Interviewers sometimes ask how hash slots differ from consistent hashing. With consistent hashing, nodes and keys sit on a ring and a key belongs to the next node clockwise, so adding a node automatically takes over part of a neighbour's range. Redis uses a fixed number of slots and an explicit table of who owns which. Moving data means moving specific slots, which gives operators direct control, at the cost of that table being something the cluster has to keep consistent.
To choose between them: if everything fits on one machine and you want failover, primary plus replicas with Sentinel is enough (or a managed service that does the same). If you need more memory or write throughput than one node offers, you need Cluster.
Atomicity: transactions, Lua and pipelining
Because commands run one at a time, every single command is atomic. INCR can't lose an update the way GET followed by SET can, because two clients doing GET then SET can both read 5 and both write 6. That's why INCR exists.
The trouble starts when you need several steps to happen together. Redis has three tools that get confused with each other.
Transactions with MULTI and EXEC. MULTI starts queuing commands, and EXEC runs the queue. Nothing from any other client runs in the middle of it. But there's no rollback. If one command fails at runtime (say, INCR on a key holding text), the others still run. If a command is malformed and fails while being queued, the whole transaction is refused at EXEC. That's very different from a SQL transaction, and interviewers like to check that you know it.
For optimistic concurrency, use WATCH key before MULTI. If the watched key changes before EXEC, the transaction aborts, and you retry. It's the Redis version of compare-and-swap.
Lua scripts. EVAL sends a script to the server, and the whole script runs as a single command. Nothing else interleaves, and you can read a value, make a decision, and write, all atomically. That's the thing MULTI can't do, because inside MULTI you can't use an earlier result to decide a later command. Lua is how you build a safe lock release or a rate limiter. Two cautions: a long script blocks everybody, and in Cluster you must pass every key the script touches as a key argument so Redis can check they share a slot.
Pipelining. A pipeline just sends many commands without waiting for each reply, then reads all the replies. It saves network round trips and can speed things up dramatically. It is not atomic. Other clients' commands can slip in between yours.
Pub/Sub
Pub/Sub is a messaging pattern where publishers send messages to a named channel and every client subscribed to that channel at that moment receives them. Publishers and subscribers don't know each other exist.
The commands are short: SUBSCRIBE news listens on a channel, PSUBSCRIBE news:* listens on a pattern, and PUBLISH news "hello" sends a message and returns how many subscribers received it. A channel needs no setup. It exists as long as someone is subscribed.
The one property you must state clearly is that delivery is at-most-once and nothing is stored. If a subscriber is offline, or its connection blips, it never sees the message. There are no acknowledgements, no replay and no history. A subscriber that can't keep up gets disconnected once its output buffer passes a limit, so it doesn't eat the server's memory. And a client that's subscribed can't run normal commands on that connection under the older protocol, so apps use a separate connection for their other work.
There's a common outdated claim about scaling. In classic cluster Pub/Sub, every message was broadcast to every node, so adding nodes didn't add capacity. Since Redis 7 there's sharded Pub/Sub (SPUBLISH and SSUBSCRIBE), where a channel is hashed to a slot like a key, and only that shard handles it. Capacity grows with the cluster. Connection cost is per node, not per channel, so millions of channels doesn't mean millions of connections.
How does it compare with the alternatives? Streams keep messages, let consumers catch up after downtime, and support acknowledgements. They give at-least-once delivery. Kafka does the same at much larger scale with long retention and replay for many independent consumers. Pub/Sub is the right tool when you only care about whoever is listening right now and losing a message is acceptable.
In system design, that usually means something like chat across many app servers. Each server subscribes to the channels its connected users care about. When someone sends a message, it's published once, every server with an interested user receives it, and pushes it down the WebSocket. Live dashboards and presence indicators work the same way. If the requirement says offline users must receive it later, Pub/Sub alone isn't enough. Write the message to a database or a stream too.
A related trap: keyspace notifications (getting an event when a key expires, for example) are delivered over Pub/Sub, so they inherit the same lack of guarantees. Don't build correctness-critical logic on them.
System design problems where Redis shows up
This is where everything above gets used. For each one, say which data structure you'd pick and why, then say what could go wrong.
A distributed lock. Say several app servers must not do the same thing at once, like two people booking the same concert seat. Redis works as a lock manager because it's one shared place every server can reach, and each command is atomic. The lock is just a key:
SET lock:seat:343 <random-token> NX PX 30000
NX means "only set it if it doesn't exist," so exactly one caller gets OK. PX 30000 gives the lock a 30 second lease, so a crashed process can't hold it forever. The random token identifies who owns it.
Releasing is where people slip. If you just DEL the key, you might delete someone else's lock, because yours may have expired while you were still working and another client took it. So you delete only if the token still matches, and the check and delete have to be atomic, which means a Lua script:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Now the part interviewers actually care about, which is why this isn't a real guarantee. If Client A stalls (a long garbage collection pause, a slow network) past the lease, the lock expires, Client B takes it, and then Client A wakes up and carries on, believing it still holds the lock. Both write. A second problem is replication: if the primary grants the lock and dies before the replica hears about it, the promoted replica happily grants the same lock again.
The Redlock algorithm tries to solve the second problem by acquiring the lock on a majority of independent Redis nodes. It's controversial. Martin Kleppmann wrote a well-known critique showing that it still can't survive the paused-client problem. The standard defence for that is a fencing token: each lock grant comes with an increasing number, and the storage layer rejects any write carrying an older number than one it has already seen. Redis doesn't give you that out of the box.
So a Redis lock is good for efficiency (avoiding duplicate work most of the time) and shaky for correctness. If a stale lock holder would corrupt data, enforce the rule where the data lives. A SELECT ... FOR UPDATE row lock or a conditional update like UPDATE ... WHERE version = 7 in your database often removes the need for a distributed lock entirely. Saying this out loud in an interview scores well.
A cousin of the lock, for things like ticket booking, is a temporary hold: SET seat:343:A12 user42 NX EX 600 reserves a seat for ten minutes, and the TTL releases it if the user walks away.
A rate limiter. The simplest version is a fixed window. For each user and time window you keep a counter: run INCR on a key like rl:user42:1700000000, and if the result goes over the limit, reject with a 429 and a Retry-After header. One subtlety trips people up. You want to set the expiry only when INCR returns 1, meaning the first request in the window. If you call EXPIRE on every request, steady traffic keeps pushing the expiry forward and the window never resets. And if the process crashes between INCR and EXPIRE, you've got a counter that never expires. So do both steps in one Lua script:
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return current
The weakness of a fixed window is bursts at the edges. A user can send the full limit at the very end of one window and again at the start of the next, doubling the rate for a moment.
A sliding window fixes that with a sorted set per user, where each request is a member and its timestamp is the score. On each request you drop entries older than the window, count what's left, and add the new one if the count is under the limit. Again, do it in one script so it's atomic:
-- KEYS[1] = key, ARGV[1] = now (ms), ARGV[2] = window (ms)
-- ARGV[3] = limit, ARGV[4] = a unique id for this request
redis.call("ZREMRANGEBYSCORE", KEYS[1], 0, ARGV[1] - ARGV[2])
if redis.call("ZCARD", KEYS[1]) < tonumber(ARGV[3]) then
redis.call("ZADD", KEYS[1], ARGV[1], ARGV[4])
redis.call("PEXPIRE", KEYS[1], ARGV[2])
return 1
end
return 0
Use a unique member for each request. Two requests in the same millisecond would otherwise overwrite each other. The cost is memory: the set holds up to limit entries per user. If that's too much, mention the token bucket or a sliding window counter as cheaper approximations, and be ready to explain the trade-off between accuracy and memory.
A leaderboard. This is a sorted set, and it's about the easiest system design win there is. ZADD board 1500 alice sets a score (and re-adding the same member just updates it), ZINCRBY board 10 alice adds points, ZREVRANGE board 0 9 WITHSCORES returns the top ten, and ZREVRANK board alice gives a player's rank. Every operation is O(log N) plus the size of the slice you read. To keep only the top N, trim with ZREMRANGEBYRANK board 0 -101, which removes everything below the top 100. Ties are broken by member name, so if ties matter, say how you'd handle them. If someone pushes you to a huge scale, note that one sorted set is one key on one node, so you'd split it by region or by score range and merge for global queries.
A job queue. Lists with LPUSH and BRPOP work, but if a worker pops a job and then dies, the job is gone. Streams with consumer groups fix that:
XADD jobs * type email to a@b.com
XGROUP CREATE jobs workers 0 MKSTREAM
XREADGROUP GROUP workers worker-1 COUNT 10 BLOCK 5000 STREAMS jobs >
XACK jobs workers 1700000000000-0
XAUTOCLAIM jobs workers worker-2 60000 0-0
A producer appends with XADD. Workers in a group read with XREADGROUP, and Redis tracks each delivered but unacknowledged entry as pending for that worker. When the worker finishes, it calls XACK. If a worker dies, its pending entries sit idle, and another worker claims them with XAUTOCLAIM after they've been idle long enough (60 seconds in the example).
The catch: this gives you at-least-once processing. Redis can't tell a slow worker from a dead one, so a job can occasionally run twice. Make your jobs idempotent. Also, stream entries are only as durable as your persistence settings, so with defaults a crash can lose recent ones. Streams fit modest queues where you already run Redis, like background jobs, notification fan-out and work distribution. Kafka is the better answer when you need long retention, replay for many independent consumers, or throughput where losing a message is unacceptable.
A session store. Store each session as a hash (or a JSON string) under session:<id> with a TTL, and refresh the TTL on each request so active users stay logged in. Logging out is a DEL. It's fast and the TTL cleans up abandoned sessions. The question to raise yourself is what happens if Redis restarts and forgets everyone. If that's unacceptable, turn on AOF, or keep sessions in a database and use Redis as a cache in front of them.
Counting things. INCR for exact counters. HyperLogLog (PFADD, PFCOUNT) for approximate unique counts in tiny memory. Bitmaps for per-user yes/no flags across a huge user base, like daily active users, and BITOP AND across days to find users who came back. Geo commands for "nearby" queries.
When not to use Redis
Interviewers ask this to see whether you understand the limits, so have an answer ready.
Don't make it the only copy of data you can't lose. Between asynchronous replication and the persistence loss windows, acknowledged writes can vanish. Don't use it when your working set can't fit in memory at a cost that makes sense. Don't expect query flexibility: there are no joins, no secondary indexes out of the box, and in a cluster, multi-key operations only work within one slot. And don't use it as a replayable event log with long retention for many consumers, which is what Kafka is built for.
A few operational things worth knowing
You won't be asked to be a Redis administrator, but a handful of commands and habits make you sound like someone who has run it.
INFO prints server stats in sections, such as memory, persistence, replication and stats. The keyspace_hits and keyspace_misses counters let you work out your cache hit ratio. SLOWLOG GET shows the slowest recent commands. MONITOR streams every command the server receives, which is handy for debugging and expensive in production, so avoid leaving it running. Use SCAN to walk the keyspace in small steps instead of KEYS, and UNLINK to delete big keys without blocking (DEL frees memory on the main thread). redis-cli --bigkeys and MEMORY USAGE key help you find oversized keys.
When latency spikes, the usual suspects are a slow command on a big collection, a large key being deleted, a fork for a snapshot on a big dataset, or the machine swapping to disk. On security, don't expose Redis to the public internet, set authentication and ACLs, and use TLS if traffic crosses networks you don't control.
One more piece of context, in case it comes up. In 2024 Redis moved away from its permissive BSD license, and the Linux Foundation backed a fork called Valkey, based on Redis 7.2.4 and still BSD-licensed. Redis 8 later added AGPLv3 as a license option. The two projects speak the same protocol, so most clients work with either, but their features are starting to diverge. Managed services like AWS ElastiCache offer Valkey too.
How I'd practise this
Reading only gets you partway. Run Redis in Docker (docker run -p 6379:6379 redis) and type the commands into redis-cli until they feel obvious. Then build the three things interviews keep asking for: a lock with a safe release script, a sliding window rate limiter, and a leaderboard. Once those work, try to break them: kill the process mid-script, let a TTL expire at a bad moment, and see what happens.
Then try answering these out loud, without notes:
- Why is Redis fast, and what's the cost of that design?
- What's the difference between expiration and eviction, and what does the default policy do when memory is full?
- Walk me through cache-aside. What are the stale-data races?
- What's the difference between cache penetration, stampede and avalanche, and how do you fix each?
- How do RDB and AOF differ, and what would you use for a cache versus for sessions?
- A primary fails right after acknowledging a write. What can happen?
- When would you use Sentinel, and when Cluster? How does a key find its node?
- What's the difference between a pipeline, MULTI/EXEC and a Lua script?
- Why doesn't Pub/Sub work for messages that must not be lost, and what would you use instead?
- Design a rate limiter. Now design a distributed lock, and tell me why it isn't fully safe.
- Give me three reasons not to use Redis as your main database.
If you can answer those in your own words, with an example each, you know Redis well enough for most SDE interviews.















Top comments (0)