Redis interviews cover its core data structures and the common patterns — caching, rate limiting, leaderboards — built on top of them.
Redis is an in-memory data structure store that doubles as a database, cache, and message broker. The core reason it's fast is that it keeps the entire working dataset in RAM, so reads and writes skip the disk seek time and I/O overhead that slow down traditional databases. It also uses efficient, purpose-built encodings internally, like skip lists for sorted sets and compact `ziplist`/`listpack` representations for small collections, so most operations run close to O(1) or O(log n). On top of that, Redis executes commands from a single thread using an event loop built on `epoll`/`kqueue`, which avoids the locking and context-switching costs you'd pay with a multi-threaded server. Put together, in-memory storage, lean data structures, and a simple event loop is what lets a single Redis instance handle hundreds of thousands of operations per second.
Redis is more than a plain key-value store; it exposes several native data types that map cleanly onto common problems. Strings are the simplest type, holding text, numbers, or binary blobs, and are commonly used for caching serialized objects or as counters via `INCR`. Hashes store field-value pairs under one key, so you can represent something like a user profile as a single object instead of juggling multiple string keys. Lists are ordered collections backed by a linked-list-like structure, often used to build queues or stacks with `LPUSH`/`RPOP`. Sets hold unique, unordered members and are handy for things like tracking unique visitors or computing intersections for tagging. Sorted sets attach a score to each member and keep everything ordered by that score, which is exactly what you want for leaderboards, priority queues, or range queries like "give me the top 10."
RDB takes point-in-time snapshots of the whole dataset, forking a child process that writes a compact binary file to disk at configured intervals or via `BGSAVE`, which makes it great for backups and fast restarts but means you can lose whatever was written since the last snapshot. AOF instead logs every write operation to an append-only file, which it replays on restart to rebuild state, so it's far more durable, especially with `fsync` set to `everysec` or `always`. The tradeoff is that AOF files grow larger and replaying them on startup is slower, though Redis periodically rewrites the AOF in the background to compact it. In practice, many production setups enable both: AOF for durability and RDB (or the RDB preamble inside AOF) for faster recovery and portable backups. Neither is strictly better; it's a durability-versus-performance-and-simplicity tradeoff you tune per use case.
You can attach a time-to-live to any key with `EXPIRE` or `PEXPIRE`, check remaining time with `TTL`, or remove the expiration with `PERSIST`. Redis doesn't expire keys purely on a timer running in the background; it uses a combination of lazy expiration, where a key is checked and deleted the moment it's accessed and found stale, and active expiration, where a background cycle randomly samples keys with a TTL set and removes the expired ones. This hybrid approach keeps expiration cheap without needing to scan the whole keyspace constantly. When a key expires, Redis propagates a `DEL` (or `UNLINK`) to replicas and to the AOF so all copies stay in sync. TTLs are commonly used for session data, cache entries, and rate-limiting counters where stale data should disappear automatically.
When Redis hits the `maxmemory` limit you've configured, it needs a policy for what to evict, and you choose that policy with `maxmemory-policy`. The `noeviction` setting simply rejects further writes once memory is full, which is safe but can break your application if you're not prepared for write errors. The LRU-based policies, `allkeys-lru` and `volatile-lru`, evict the least recently used keys, either across the whole keyspace or only among keys that have a TTL set; Redis approximates true LRU using random sampling rather than tracking exact recency for every key, which is a deliberate performance tradeoff. The LFU-based policies, `allkeys-lfu` and `volatile-lfu`, instead track access frequency with a decaying counter, which tends to work better than LRU for workloads with a stable set of "hot" keys accessed repeatedly. Choosing between them depends on your access pattern: LRU suits recency-driven workloads like caches, while LFU suits workloads where popularity matters more than recency.
As a cache, Redis typically sits in front of a canonical datastore like Postgres or MySQL, following a cache-aside pattern where you read from Redis first and fall back to the database on a miss, populating the cache with a TTL so it self-invalidates over time. This is the safer default because if Redis loses data, whether from a restart or eviction, the source of truth is still intact. Using Redis as a primary datastore is viable too, especially with AOF persistence and replication enabled, but you're accepting real constraints: your dataset has to fit comfortably in RAM, there's no rich query language or joins, and you need a solid backup and failover strategy since durability isn't as battle-tested as a traditional RDBMS. In practice, most teams use Redis as a cache or for data that's inherently ephemeral or derived, like sessions, rate limits, or leaderboards, and reserve a relational or document database for the durable system of record. The decision really comes down to how much you value query flexibility and long-term durability guarantees versus raw speed.
Redis pub/sub lets clients `SUBSCRIBE` to named channels, or `PSUBSCRIBE` to pattern-matched channels, and receive messages that other clients send with `PUBLISH`. It's a fire-and-forget broadcast mechanism: Redis doesn't store the message anywhere, it just forwards it to whichever clients happen to be subscribed at that moment. That means if no one is listening when a message is published, it's gone for good, and a subscriber that disconnects and reconnects will have missed anything sent in between. This makes it a poor fit for anything requiring durability or guaranteed delivery, like a task queue; for that you'd reach for Redis Streams, which support persistence, consumer groups, and replay. Pub/sub is best suited for real-time, best-effort notifications, like invalidating local caches across app instances or broadcasting live updates.
`MULTI` starts a transaction by queuing up subsequent commands on that connection instead of running them immediately, and `EXEC` runs the whole queue as a single atomic block, meaning no other client's commands can interleave in the middle of it. If you need optimistic locking, you can `WATCH` one or more keys beforehand; if a watched key gets modified by another client before `EXEC` runs, the whole transaction is aborted and returns a null reply, so you can retry. The big limitation people get tripped up by is that there's no rollback on runtime errors: if one queued command fails at execution time, say you run a string operation on a key that's actually a list, the other commands in the transaction still execute normally. Only errors caught at queue time, like a genuinely malformed command, abort the entire transaction before it starts. Compared to relational database transactions, Redis transactions guarantee isolation and atomicity of execution, but not the kind of error-driven rollback semantics you'd get from ACID compliance.
Redis replication is asynchronous by default: one master accepts writes, and one or more replicas connect to it and apply the same stream of write commands to stay in sync. Replicas use `PSYNC` with a replication ID and offset to do partial resynchronization after a brief disconnect, only pulling the missed portion of the backlog rather than a full resync, which keeps reconnects cheap. Replicas are useful for scaling reads and for having warm standbys, but because replication is async, a replica can lag behind the master and a hard master failure can lose the last few writes. Redis Sentinel is a separate set of processes that monitors masters and replicas, using a quorum of Sentinel instances to agree that a master is actually down before acting, which avoids false positives from a single Sentinel's network hiccup. When it confirms a failure, Sentinel automatically promotes a replica to master and reconfigures the other replicas and clients, giving you automatic failover without a human paging in at 3am.
Redis Cluster shards data across multiple nodes by splitting the keyspace into 16384 hash slots, and each key is assigned to a slot using `CRC16(key) mod 16384`. Every node owns a subset of those slots, and nodes gossip with each other over a binary protocol to keep the cluster topology and slot ownership map consistent. Each shard can also have its own replicas for high availability, so if a master node holding certain slots goes down, one of its replicas is promoted automatically, similar in spirit to what Sentinel does for non-clustered setups. Clients need to be cluster-aware, because a request for a key not owned by the node you queried gets a `MOVED` or `ASK` redirection pointing you to the right node. One real constraint is that multi-key operations only work if all the keys hash to the same slot, which you can force using hash tags like `{user123}:profile` so related keys land together.
A simple fixed-window rate limiter uses `INCR` on a key like `ratelimit:user123:2026-07-07T10:00` and sets an `EXPIRE` on it the first time it's created, then rejects requests once the counter passes your threshold within that window. The downside is the classic edge-of-window burst problem, where a client can send double the allowed requests right around the window boundary. A sliding-window approach fixes that using a sorted set: you `ZADD` the current timestamp as both the score and a unique member on each request, use `ZREMRANGEBYSCORE` to drop entries older than your window, then `ZCARD` to count how many requests remain in the window before deciding to allow or reject. Because these are multiple round trips, you typically wrap the check-and-increment logic in a Lua script run via `EVAL` so it executes atomically on the server and avoids race conditions between concurrent requests. Both patterns lean on Redis's speed and TTL support, which is why it's such a common choice for this problem.
A sorted set is purpose-built for this: you `ZADD leaderboard <score> <player>` to add a player or update their score, or `ZINCRBY` if you just want to bump an existing score up or down. Redis keeps the set ordered by score internally using a skip list, so retrieving the top N players with `ZREVRANGE leaderboard 0 9 WITHSCORES` or finding a specific player's standing with `ZREVRANK` are both O(log n) operations even with millions of entries. You can also pull a player's neighbors, like the five players ranked just above and below them, using rank-based range queries, which is a common feature in game leaderboards. Because ties are broken by member name lexicographically when scores match, some implementations encode a secondary tiebreaker, like timestamp, into the score itself to get more meaningful ordering. This is a good example of picking the right data structure making a feature almost trivial to implement correctly and efficiently.
The basic building block is `SET lockkey <unique-token> NX PX 30000`, which atomically sets the key only if it doesn't already exist and attaches an expiration, so a crashed client that never releases the lock doesn't cause a permanent deadlock. When releasing, you should never blindly `DEL` the key; instead run a small Lua script that checks the stored value matches the token you set before deleting it, otherwise you risk releasing a lock that another client has since acquired after your expiration fired. For coordinating a lock across multiple independent Redis instances rather than trusting a single node, there's the Redlock algorithm, which requires acquiring the lock on a majority of instances within a time budget. It's worth knowing that Redlock's safety guarantees have been genuinely debated in the community, notably Martin Kleppmann's critique against antirez's defense, mostly around clock drift and process pauses invalidating the lock's assumptions. For most applications a single well-configured Redis instance with the `SET NX PX` pattern is good enough; reach for Redlock only when you truly need it and understand its edge cases.
Redis executes commands using a single thread that reads a command, runs it to completion, and moves to the next one, avoiding the locking and context-switching overhead a multi-threaded design would need to protect shared data structures. Since Redis 6, network I/O can be handled by additional threads, but command execution itself is still single-threaded, so this is a performance optimization for parsing and socket I/O, not a change to the execution model. The practical benefit is that every command is inherently atomic: operations like `INCR`, `LPUSH`, or `ZADD` can never be interleaved with another client's command, so you get strong consistency for individual operations without needing explicit locks. The tradeoff is that a single slow command, like a `KEYS *` scan over a huge keyspace or a large `SORT`, blocks every other client until it finishes, since there's no concurrent execution happening underneath it. Because a single instance can't use multiple CPU cores for command processing, horizontal scaling for throughput comes from running multiple Redis instances or shards, which is exactly what Redis Cluster is designed to manage.
A Redis Stream is an append-only log data structure, added in Redis 5, where each entry gets a unique, time-ordered ID and the data persists in the stream until explicitly trimmed, unlike pub/sub messages which vanish the instant they're delivered. Because entries are durable, a consumer that was offline or crashed can reconnect and read everything it missed starting from a specific ID, which pub/sub simply can't do. Streams also support consumer groups, where multiple consumers coordinate to each process a distinct subset of entries and explicitly acknowledge processing with `XACK`, giving you at-least-once delivery semantics closer to a real message queue like Kafka than to a fire-and-forget broadcast. Commands like `XADD` to append, `XREAD` to consume, and `XRANGE` to query by ID range round out the API. In short, pub/sub is for ephemeral, best-effort broadcast, while Streams are for durable, replayable event logs with proper consumer tracking.