The 60 Redis questions interviewers actually ask, with direct answers, runnable commands, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
Redis (Remote Dictionary Server) is an open-source, in-memory data store created by Salvatore Sanfilippo and first released in 2009. It keeps the working set in RAM, which is why reads and writes land in microseconds instead of the milliseconds a disk-backed database needs. People call it a cache, but it's really a data structure server: you store and manipulate strings, hashes, lists, sets, sorted sets, bitmaps, HyperLogLogs, and streams with commands built for each type. It also does pub/sub messaging, atomic operations, Lua scripting, and optional durability. In interviews, Redis questions probe how you think about the trade-offs: RAM cost versus speed, persistence versus performance, single-threaded execution versus throughput, and how caching goes wrong under load. This page collects the 60 questions that come up most, each with a direct answer and runnable commands. The official Redis documentation is the canonical reference for data types and behavior. The first technical round increasingly runs as a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Top 5 Redis Use Cases
Video: Top 5 Redis Use Cases (ByteByteGo, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Redis certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Redis is an open-source, in-memory data store that keeps data in RAM for microsecond reads and writes. It works as a cache, a database, and a message broker, and stores more than plain strings: hashes, lists, sets, sorted sets, and streams.
Teams reach for it to cache database results, hold sessions, build leaderboards, rate-limit requests, queue jobs, and push real-time messages. Its speed comes from staying in memory and from a simple single-threaded core that runs one command at a time.
Key point: The it as an in-memory data structure server, not just a cache. Interviewers open with this to hear whether you know Redis does more than key-value caching.
Watch a deeper explanation
Video: Redis in 100 Seconds (Fireship, YouTube)
It keeps the working dataset in RAM instead of on disk, so a lookup never waits on a disk seek. That's why Redis answers in microseconds while a disk-backed database answers in milliseconds. Every read and write hits memory first, which is the whole reason Redis is so fast.
The trade-off is cost and size: RAM is pricier and smaller than disk, so your dataset must fit in memory. Redis can still persist to disk in the background for durability, but the live data path stays in RAM.
Key point: The expected follow-up is 'what happens if the server restarts?'. Have persistence (RDB, AOF) ready as your answer.
The core types are strings, hashes, lists, sets, sorted sets, streams, bitmaps, and HyperLogLogs. Each has commands tuned to it, which is why Redis is called a data structure server rather than a key-value store.
Picking the right type is most of the skill: a hash for an object's fields, a sorted set for a leaderboard, a list for a queue, a set for membership tests.
| Type | Good for | Example command |
|---|---|---|
| String | Caching values, counters | SET, GET, INCR |
| Hash | Objects with fields | HSET, HGETALL |
| List | Queues, timelines | LPUSH, RPOP |
| Set | Uniqueness, membership | SADD, SISMEMBER |
| Sorted set | Leaderboards, ranked data | ZADD, ZRANGE |
Key point: Interviewers often follow with 'which type would you use for X?'. Practice mapping problems to types out loud.
Watch a deeper explanation
Video: Redis Crash Course (Web Dev Simplified, YouTube)
SET stores a string value under a key, and GET reads it back. SET can take options like EX for a time-to-live in seconds and NX to only set if the key doesn't already exist.
These two commands are the backbone of caching: write a value under a key, read it later, and let it expire.
SET user:1042:name "Asha"
GET user:1042:name # "Asha"
SET session:abc "payload" EX 3600 # expires in 1 hour
SET lock:job NX "1" # only if not already setA cache stores the results of slow operations so repeat requests skip the slow path. Redis fits because it reads from memory in microseconds, holds far more than a single app's local cache, and is shared across every app server.
The usual pattern is cache-aside: check Redis first, and on a miss read the database, store the result in Redis with a TTL, then return it. Later reads hit Redis and never touch the database.
# cache-aside read for a product
GET product:88
# (miss) -> read DB, then:
SET product:88 "{...json...}" EX 300
GET product:88 # now a hit for 5 minutesKey point: Say cache-aside by name and The TTL matters. the key point is you know the read-through flow, not just that Redis is fast.
Watch a deeper explanation
Video: Redis Crash Course Tutorial (Traversy Media, YouTube)
You attach a time-to-live to a key, and Redis deletes it automatically once the TTL runs out. Set it with EXPIRE for seconds, PEXPIRE for milliseconds, or inline with SET key value EX 60. The TTL and PTTL commands read how much time a key has left before it disappears.
TTL returns the seconds remaining, or -1 if the key has no expiry, or -2 if the key doesn't exist. Expiry is how caches stay fresh and how sessions and locks clean themselves up.
SET otp:9931 "482013"
EXPIRE otp:9931 120 # expires in 2 minutes
TTL otp:9931 # 120, counting down
TTL missing:key # -2 (does not exist)
PERSIST otp:9931 # remove the expiryKey point: Knowing the -1 versus -2 return values is a small detail that signals you've actually used TTL.
Two ways together. Lazy expiration: when you touch a key, Redis checks its TTL and deletes it if it's expired. Active expiration: a background job samples random keys with TTLs a few times a second and deletes the expired ones it finds.
So an expired key can sit in memory briefly until it's accessed or sampled. That's fine for correctness because Redis never serves an expired key, but it's why memory doesn't drop the instant a TTL passes.
Key point: Most freshers only know lazy deletion. Mentioning the active sampling loop is an easy way to stand out.
Redis is in-memory and key-based; MySQL is disk-based and query-based. Redis gives you microsecond access to values you fetch by key, with no joins, no SQL, and no complex query planner. MySQL gives you rich queries, relationships, and durability by default.
They usually work together: MySQL as the system of record, Redis as the fast layer in front of it. Redis isn't a replacement for a relational database; it removes load from one.
| Redis | MySQL | |
|---|---|---|
| Storage | In-memory (RAM) | On disk |
| Access | By key | By SQL query |
| Joins / relations | No | Yes |
| Latency | Microseconds | Milliseconds |
| Typical role | Cache, sessions, queues | System of record |
Strings hold more than text. INCR and DECR treat the value as an integer and change it atomically, which makes counters trivial. APPEND adds to the end, GETRANGE and SETRANGE work on substrings, and STRLEN gives the length.
Because INCR is atomic, many clients can increment the same counter with no race condition and no locking.
SET views:page:home 0
INCR views:page:home # 1
INCRBY views:page:home 5 # 6
DECR views:page:home # 5
APPEND log:today "line"Use a hash to store an object with named fields under one key, like a user with a name, email, and role all grouped together. HSET writes one or more fields, HGET reads a single field, HGETALL reads them all, and HDEL removes a field without touching the rest.
It beats storing a JSON string when you want to read or update single fields without fetching and rewriting the whole object, and it uses memory efficiently for small objects.
HSET user:1042 name "Asha" role "engineer" team "platform"
HGET user:1042 role # "engineer"
HGETALL user:1042 # all fields and values
HINCRBY user:1042 logins 1Key point: The comparison to 'just store JSON' is the follow-up. Field-level reads and updates are the win.
A list is an ordered sequence of strings you push and pop from either end. LPUSH and RPUSH add, LPOP and RPOP remove. That makes it a natural queue (push one end, pop the other) or a recent-activity timeline.
BLPOP and BRPOP block until an item arrives, which turns a list into a simple work queue between producers and consumers.
LPUSH jobs "email:42"
LPUSH jobs "email:43"
RPOP jobs # "email:42" (FIFO order)
LRANGE jobs 0 -1 # view the whole list
BRPOP jobs 5 # block up to 5s for an itemA set is an unordered collection of unique strings, so it enforces uniqueness for you. SADD adds members and silently drops duplicates, SISMEMBER checks membership in constant time, SCARD counts the members, and SREM removes them. There's no ordering, just fast membership and set math.
Sets fit uniqueness and relationships: tags on an item, unique visitors today, or set operations like intersection (SINTER) to find common elements between two groups.
SADD online:users 1042 2087 3391
SISMEMBER online:users 2087 # 1 (yes)
SCARD online:users # 3
SADD skills:alice redis sql python
SINTER skills:alice skills:bob # shared skillsMembership check: set vs list
Elements scanned to answer 'is X a member?' in a collection of 10,000 items (worst case). SISMEMBER on a set is O(1); a list membership scan is O(n).
A sorted set stores unique members, each paired with a numeric score, and keeps them ordered by that score automatically. ZADD sets a member's score, ZRANGE reads members low to high, ZREVRANGE reads them high to low, and ZRANK tells you a member's position in the ordering.
That maintained ordering is what makes leaderboards, priority queues, and time-ranged data easy: the ranking updates itself, and rank lookups and range queries stay fast.
ZADD leaderboard 1500 "alice" 1800 "bob" 1200 "cara"
ZREVRANGE leaderboard 0 2 WITHSCORES # top 3
ZRANK leaderboard "alice" # alice's rank
ZINCRBY leaderboard 50 "alice" # bump her scoreKey point: Leaderboard is the classic answer. Mentioning that ranking stays sorted automatically is the detail the question needs.
KEYS pattern scans every key in the database in one blocking pass. Because Redis runs commands one at a time, a KEYS over millions of keys freezes the whole server until it finishes, stalling every other client.
Use SCAN instead. It walks the keyspace in small batches with a cursor, so it never blocks Redis for long. KEYS is fine only for debugging on a tiny local dataset.
# bad in production: blocks the server
KEYS user:*
# safe: iterate in batches
SCAN 0 MATCH user:* COUNT 100
# use the returned cursor to continue until it's 0Key point: This is a favorite gotcha. Knowing KEYS blocks and SCAN doesn't is a quick credibility signal.
EXISTS returns 1 if a key is present and 0 if not, and it accepts several keys to count how many exist. DEL removes one or more keys. UNLINK does the same but frees the memory in a background thread, which is safer for large values.
For a big key, prefer UNLINK so the deletion doesn't block the single command loop while Redis reclaims the memory.
EXISTS user:1042 # 1
EXISTS a b c # count of those that exist
DEL user:1042 # remove now
UNLINK huge:list # remove, reclaim memory asyncThe core command loop is single-threaded: Redis runs one command from start to finish before it picks up the next. That design is why every individual command is atomic and you rarely need locks for single operations, since nothing can interleave halfway through a command.
It also means one slow command, a big KEYS or a huge sort, blocks everyone. Modern Redis uses extra threads for network I/O and background tasks like UNLINK, but command execution stays serialized.
Key point: The insight the technical value is: single-threaded is why commands are atomic AND why one slow command hurts everyone.
Atomic means a command runs completely or not at all, with nothing interleaving in the middle. Because Redis executes one command at a time, operations like INCR, LPUSH, and SETNX finish fully before any other client's command starts.
That's why you can build a counter or a simple lock without a mutex: INCR from a thousand clients can't lose an update, and SET key val NX either sets the key or fails cleanly.
# atomic counter, no race even with many clients
INCR downloads:total
# atomic 'set if absent', the basis of a lock
SET lock:report "held" NX EX 30Not necessarily. Redis is in-memory, but it can persist to disk so it reloads data on restart. RDB writes point-in-time snapshots on a schedule; AOF logs every write and replays them on startup. You can run either, both, or neither.
With no persistence, a restart starts empty, which is fine for a pure cache. With AOF, you lose at most about a second of writes. The choice is a durability-versus-performance trade-off.
Key point: The nuance to land: Redis in-memory does not mean Redis is volatile. Persistence is configurable.
Redis listens on TCP port 6379 by default. You connect with the redis-cli command-line client, or with a client library in your language, pointing it at the host and port. A password or ACL user is added when the server requires authentication, which any production instance should.
Running redis-cli PING should return PONG on a healthy server, which is the quickest way to confirm the connection works before you send real commands.
redis-cli -h 127.0.0.1 -p 6379
> PING
PONG
> INFO server # version, uptime, config
> DBSIZE # number of keysA single Redis instance has numbered logical databases, 0 to 15 by default, and SELECT switches the current connection between them. Keys in one database are invisible to another, so it looks like a quick way to separate concerns on one server without spinning up more instances.
In practice most teams avoid them for separation because they share the same process, memory, and CPU, and Redis Cluster supports only database 0. Prefer key prefixes or separate instances for real isolation.
SELECT 1 # switch to database 1
SET x "only in db 1"
SELECT 0 # back to the default
GET x # (nil) hereKey point: Saying you'd use key prefixes or separate instances instead of numbered DBs indicates production experience.
MSET writes several key-value pairs in one atomic command, and MGET reads several keys in a single round trip, returning nil for any that are missing. Both cut network overhead compared with looping single GETs and SETs, and MSET applies all its writes together so no other client sees a half-finished batch.
Fewer round trips is a real speedup: one MGET of 100 keys beats 100 separate GETs because you pay the network latency once instead of a hundred times.
MSET color "green" size "L" qty "12"
MGET color size missing # "green" "L" (nil)Use a consistent, colon-separated convention that reads like a path: object:id:field, for example user:1042:profile or session:abc123. That structure groups related keys together, makes SCAN patterns predictable, and keeps a shared instance readable when several apps or teams write to the same Redis.
Keep keys reasonably short since every one lives in memory, and avoid spaces. There are no real folders in Redis; the colons are pure convention, but the whole ecosystem follows it.
Key point: A clean naming scheme signals you've maintained a real Redis instance, not just run tutorials.
The big ones are caching database results, storing user sessions, and rate limiting API requests. Beyond those, teams build leaderboards with sorted sets, queues and background jobs with lists or streams, real-time counters with INCR, and pub/sub notifications for live updates like chat or presence.
The thread through all of them is speed on data you'd otherwise recompute or fetch from a slower store. If a value is read far more than it changes, or needs to be counted or ranked in real time, it's a Redis candidate.
Key point: Give three concrete use cases mapped to data types, not a vague 'it's fast.' That mapping is what the question checks.
Watch a deeper explanation
Video: Redis In-memory Database Crash Course (Hussein Nasser, YouTube)
FLUSHDB deletes every key in the currently selected database, while FLUSHALL deletes every key in every database on the whole instance. Both are immediate and irreversible, and by default they block until the wipe finishes, though an ASYNC option lets Redis reclaim the memory in the background.
These are dangerous in production, since a stray FLUSHALL wipes the entire instance. Many teams rename or disable them in the config so nobody runs them by accident.
Key point: Volunteering that you'd disable or rename these commands in production shows respect for operational safety.
For candidates with working experience: persistence, eviction, caching patterns, and the design questions that separate users from understanders.
RDB saves a compact point-in-time snapshot of the whole dataset on a schedule, forked in the background. It restarts fast and is small, but a crash loses everything since the last snapshot. AOF logs every write command and replays the log on startup, so it loses far less, at the cost of a larger file and slightly slower writes.
Many production setups run both: AOF for durability, RDB for fast restores and backups. The choice comes down to how much data loss you can tolerate on a crash.
| RDB | AOF | |
|---|---|---|
| What it stores | Snapshot of dataset | Log of every write |
| Data loss on crash | Since last snapshot | About 1 second (default fsync) |
| File size | Small | Larger, grows over time |
| Restart speed | Fast | Slower (replays log) |
| Best for | Backups, fast restore | Durability |
Key point: Saying 'run both, AOF for durability and RDB for backups' is the answer that indicates production experience.
The AOF log grows forever because it records every write, including ones later overwritten. AOF rewrite compacts it: Redis forks and writes a new, minimal log that reproduces the current dataset with the fewest commands, then swaps it in.
Without rewrite, the file would balloon and startup replay would crawl. Redis triggers it automatically when the file grows past a configured percentage, or you can force it with BGREWRITEAOF.
CONFIG GET auto-aof-rewrite-percentage # e.g. 100
BGREWRITEAOF # compact the AOF now, in the background
INFO persistence # aof_last_rewrite status, sizesWhen memory hits maxmemory, Redis evicts keys according to a policy. The common ones: noeviction (reject writes), allkeys-lru (evict least recently used from all keys), volatile-lru (only keys with a TTL), allkeys-lfu and volatile-lfu (least frequently used), allkeys-random, volatile-random, and volatile-ttl (soonest to expire).
For a pure cache, allkeys-lru or allkeys-lfu is usual. If Redis holds data you can't lose, noeviction with alerting is safer so you notice before you run out of room.
| Policy | Evicts | Use when |
|---|---|---|
| noeviction | Nothing (writes fail) | Redis holds must-keep data |
| allkeys-lru | Least recently used, any key | General-purpose cache |
| allkeys-lfu | Least frequently used, any key | Skewed access, hot keys |
| volatile-lru | LRU among keys with a TTL | Mixed cache and persistent keys |
| volatile-ttl | Keys expiring soonest | TTL-driven freshness |
Key point: The trap is naming only LRU. LFU and the volatile-versus-allkeys split matters.
LRU (least recently used) evicts whatever hasn't been touched for the longest time, so it tracks recency of access. LFU (least frequently used) evicts whatever is accessed the least often overall, using a per-key frequency counter that decays over time so old popularity doesn't count forever.
LFU handles skewed traffic better: a key hit constantly stays cached even after a brief gap, whereas pure LRU might drop it after one quiet stretch. LFU became available in Redis 4.0.
Key point: The scenario that separates them: a very hot key that goes quiet for a moment. LFU keeps it; LRU may evict it.
In cache-aside, the application owns the cache: read Redis first, and on a miss read the database, write the result to Redis with a TTL, then return it. Writes update the database and either delete or update the cached key.
The main risk is stale data: if the database changes but the cached key isn't invalidated, readers see old values until the TTL expires. That's why invalidation on write, plus a sane TTL as a safety net, matters.
# read path
GET product:88
# miss -> load from DB, then
SET product:88 "{...}" EX 300
# write path: update DB, then invalidate
DEL product:88 # next read repopulatesKey point: Naming stale data as the risk, and invalidation-on-write as the fix, is what this question screens for.
Write-through: the app writes to the cache, and the cache synchronously writes to the database, so both stay consistent but every write pays the database latency. Write-behind (write-back): the app writes to the cache, which returns immediately and flushes to the database asynchronously, which is faster but risks losing buffered writes on a crash.
Cache-aside stays the most common because it's simple and keeps the database as the source of truth. Write-through and write-behind usually need a cache layer that integrates with the database, which Redis doesn't do on its own.
| Pattern | Write latency | Risk |
|---|---|---|
| Cache-aside | DB write, app manages cache | Stale cache if not invalidated |
| Write-through | Cache then DB, synchronous | Slower writes |
| Write-behind | Cache now, DB later async | Data loss on crash before flush |
A cache stampede (thundering herd) happens when a hot key expires and many concurrent requests all miss at once, so they all hit the database to rebuild the same value together, spiking load right when the cache should be protecting it.
Fixes: a short lock so only one request rebuilds while others wait or serve stale, recomputing slightly before expiry (early recompute), and adding random jitter to TTLs so keys don't all expire at the same instant.
Preventing a stampede with a rebuild lock
Add TTL jitter so popular keys don't all expire on the same second in the first place.
Key point: Naming all three defenses (lock, early recompute, jittered TTL) instead of just one signals you've fought this in production.
Cache penetration is when requests keep asking for keys that don't exist anywhere, so every single request misses the cache and falls through to the database. Attackers can exploit it deliberately by querying random non-existent IDs, and legitimate bugs can trigger it too, both pushing full load onto the backing store.
Two defenses: cache the negative result as well, storing a short-lived null marker so repeat misses stop at Redis, and put a Bloom filter in front to reject keys that definitely don't exist before touching the database.
Key point: Distinguish this from a stampede: penetration is missing keys that don't exist; a stampede is one existing key expiring under load.
MULTI starts a transaction, commands typed after it are queued instead of run immediately, and EXEC then runs the whole queue as one atomic unit with no other client's commands interleaved in the middle. DISCARD throws the queued commands away without running them if you change your mind.
Redis transactions aren't like SQL: there's no rollback. If a queued command fails at runtime, the others still run. They guarantee isolation and atomic execution, not all-or-nothing on logic errors.
MULTI
INCR orders:count
LPUSH orders:queue "order:551"
EXEC # both run together, atomicallyKey point: The 'no rollback' point is the follow-up interviewers love. Redis isolates but doesn't undo.
WATCH marks one or more keys for optimistic locking before a transaction begins. If any watched key changes between the WATCH and the EXEC, the EXEC aborts and returns nil instead of running, so your client retries. It's a check-and-set pattern that gives safety without holding a blocking lock on the key.
This is how you do a safe read-modify-write across keys: WATCH the key, read it, MULTI your change, EXEC, and loop if EXEC failed because someone else got there first.
WATCH balance:acct7
GET balance:acct7 # read current value
MULTI
SET balance:acct7 "new"
EXEC # nil if balance:acct7 changed since WATCH -> retryPublishers send messages to named channels with PUBLISH, and subscribers receive messages from any channel they SUBSCRIBE to. Redis routes each published message to every client currently subscribed to that channel, and PSUBSCRIBE lets a subscriber match many channels at once by pattern, like news.* for everything under news.
It's fire-and-forget: if no one is subscribed when a message is published, the message is gone. There's no history and no acknowledgment, so pub/sub fits live notifications, not durable messaging.
# terminal 1
SUBSCRIBE alerts
# terminal 2
PUBLISH alerts "deploy finished"
# subscribers connected right now receive it; others miss itKey point: The limitation is the point: no persistence, no delivery guarantee. Streams are the durable alternative.
A stream is an append-only log of entries with IDs, added by XADD and read by XREAD or XRANGE. Unlike pub/sub, a stream keeps its history, so consumers can read past messages, and consumer groups let multiple workers split the load with acknowledgment and retry.
Use pub/sub for ephemeral live signals where losing a message is fine. Use streams when you need durable, replayable messaging with at-least-once delivery, like a job queue or an event log.
| Pub/Sub | Streams | |
|---|---|---|
| History | None, fire-and-forget | Persistent append-only log |
| Delivery | Only to current subscribers | Replayable, consumer groups |
| Acknowledgment | No | Yes (XACK) |
| Best for | Live notifications | Job queues, event logs |
One primary handles writes and streams its changes to one or more replicas, which serve reads and stay ready to take over. Replication is asynchronous: the primary doesn't wait for replicas to confirm, so a replica can trail by a moment.
This scales reads and adds redundancy, but because it's async, a primary can acknowledge a write and crash before the replica gets it, which is a small window of possible loss to name when asked about consistency.
# on a replica
REPLICAOF 192.168.1.10 6379 # follow that primary
INFO replication # role, connected replicas, lag
REPLICAOF NO ONE # promote to standalone primarySentinel provides high availability for a primary-replica setup. A group of Sentinel processes monitors the primary and its replicas, agrees by quorum when the primary is actually down (not just slow), automatically promotes a healthy replica to primary, and then tells clients about the new address so traffic follows the failover.
It gives you automatic failover without sharding: the dataset still fits on one primary, but you're covered when that node dies. For horizontal scaling of data across nodes, you need Redis Cluster instead.
Key point: Draw the line clearly: Sentinel is failover for one dataset; Cluster is sharding across many. Mixing them up is a common miss.
Redis Cluster splits the keyspace into 16384 hash slots. Each key's slot is CRC16(key) modulo 16384, and every node owns a range of slots. That spreads data across nodes so the dataset can exceed one machine's memory.
A multi-key command works only when all its keys live in the same slot. Hash tags, a substring in braces like {user1042}, force related keys into the same slot so you can run those commands.
CLUSTER KEYSLOT user:1042 # which slot this key maps to
# hash tag forces same slot for related keys:
SET {user1042}:profile "..."
SET {user1042}:sessions "..." # same slot, multi-key ops workKey point: The 16384 number and the hash-tag trick for multi-key commands are the two details that mark real cluster experience.
Pipelining sends many commands at once without waiting for each reply, then reads all the replies together. It slashes latency by paying the network round trip once instead of per command; it's a speed optimization, not an atomicity guarantee.
A transaction (MULTI/EXEC) is about atomic execution: the queued commands run together with nothing interleaved. Pipelining is about throughput: the commands are still independent and other clients' commands can run among them.
Key point: The clean distinction: pipelining is for speed, transactions are for atomicity. Conflating them is a common intermediate slip.
The basic lock is SET key token NX EX ttl: NX sets it only if absent (so only one client wins) and EX gives it an expiry so a crashed holder doesn't block forever. To release, delete the key only if the token still matches, done with a small Lua script for atomicity.
For a single instance that's enough for most needs. Across multiple independent Redis nodes, the Redlock algorithm exists but is debated; for real correctness-critical locking, many engineers reach for a system built for consensus instead.
SET lock:invoice "token-93a" NX EX 30 # acquire, auto-expires
# release only if we still hold it (atomic via Lua):
# if redis.call('GET', KEYS[1]) == ARGV[1]
# then return redis.call('DEL', KEYS[1]) else return 0 endKey point: the EX expiry (so a dead holder frees the lock) and the token-check on release is what matters.
A simple fixed-window limiter: key on the user plus the current time bucket, INCR it, set an expiry the first time, and reject once the count passes the limit. INCR is atomic, so concurrent requests count correctly.
Fixed windows allow bursts at the boundary. A sliding-window log with a sorted set (store timestamps, trim old ones, count what's left) or a token bucket in Lua smooths that out when you need precision.
# fixed window: 100 requests per minute per user
INCR rl:user42:202607041530
EXPIRE rl:user42:202607041530 60 # only needed on first hit
# if the INCR result > 100, rejectSet TTLs so stale data leaves, pick memory-efficient types (small hashes and sorted sets use compact encodings), keep keys and values short, and use MEMORY USAGE and the INFO memory section to find the heavy keys.
Watch for big collections: a single list or hash with millions of elements is a big key that hurts latency and memory. Shard it, or use HyperLogLog for approximate counts and bitmaps for flags to save space.
INFO memory # used_memory, fragmentation ratio
MEMORY USAGE user:1042 # bytes for one key
CONFIG SET maxmemory 2gb
CONFIG SET maxmemory-policy allkeys-lruEVAL runs a Lua script on the server, and the whole script executes atomically, one shot, with nothing interleaving. That lets you do read-then-write logic (check a value, decide, update) with no race and one round trip instead of several.
It's how you build atomic operations Redis doesn't offer natively, like a safe lock release or a token-bucket rate limiter. Keep scripts short, since a long script blocks the single-threaded server the whole time it runs.
# atomic 'decrement stock only if > 0'
EVAL "if tonumber(redis.call('GET', KEYS[1])) > 0 then return redis.call('DECR', KEYS[1]) else return -1 end" 1 stock:sku88Key point: The keyword is atomic: one script, no interleaving. The caution is that a slow script blocks everything.
advanced rounds probe internals, design judgment, and production scars. Expect every answer here to draw a follow-up.
A single Redis instance is strongly consistent for its own commands because they run serially. Once you add replication it becomes eventually consistent: replication is asynchronous, so a replica can serve a slightly older value than the primary just wrote.
The sharp edge is failover. A primary can acknowledge a write and die before the replica receives it; the promoted replica then never had that write. WAIT can block until N replicas confirm, narrowing but not closing the window. Redis trades strict durability for speed by design.
Key point: Saying 'Redis is not a CP system under partition; acknowledged writes can be lost on failover' is the senior-level honesty this question rewards.
A big key (a huge list, set, or hash) makes any operation on it slow, and because Redis is single-threaded, that one slow operation stalls every other client. Deleting it with DEL can block; use UNLINK. A hot key (one key getting a huge share of traffic) can saturate the one node that owns it, and in a cluster it can't be spread because it's a single slot.
Fixes: shard a big collection across many keys, use UNLINK for large deletions, and for a hot key add a local per-app cache, replicate reads, or split the value across keys so the load spreads.
| Problem | Symptom | Fix |
|---|---|---|
| Big key | Slow ops block the server | Shard the collection, UNLINK to delete |
| Hot key | One node saturated | Local cache, read replicas, key splitting |
| Big-key delete | DEL blocks the loop | UNLINK (async free) |
Key point: Connecting big/hot keys back to the single-threaded model, why one slow op hurts everyone, is what makes this The production-ready answer.
Choose by the constraint. If your dataset fits in one node's memory and you only need automatic failover, Sentinel is simpler: it monitors and promotes replicas, no resharding, and full multi-key command support. If the dataset or write throughput exceeds one node, you need Cluster to shard across nodes.
Cluster's cost is real: multi-key commands work only within a slot (needing hash tags), some operations are restricted, and clients must be cluster-aware. So don't reach for Cluster until single-node plus replicas genuinely can't hold the load.
Key point: The judgment being evaluated: don't shard prematurely. Cluster adds constraints, so justify it by size or throughput, not by default.
Both snapshotting and AOF rewrite fork a child process. Fork uses copy-on-write, so it's cheap at first, but if the parent keeps writing heavily, the OS copies more memory pages and the fork can briefly spike memory toward double the dataset and stall the main process. On a memory-tight box this can cause a fork failure or an OOM kill.
Mitigations: keep enough headroom above the dataset size, schedule heavy snapshots off-peak, tune AOF rewrite thresholds, and watch latest_fork_usec in INFO. This is exactly the kind of production scar senior interviews probe.
Key point: Mentioning copy-on-write fork behavior and memory headroom is a strong signal you've run Redis under real load.
Anything O(n) over a large keyspace or collection is dangerous on the single thread: KEYS, a full SMEMBERS or LRANGE 0 -1 on a giant collection, a big SORT, FLUSHALL, and DEL on a huge key. Each holds the command loop until it finishes, stalling everyone.
Avoid them with the cursor-based iterators (SCAN, HSCAN, SSCAN, ZSCAN), UNLINK instead of DEL, ranged reads instead of full dumps, and pagination. Watch the slow log (SLOWLOG GET) to catch offenders in production.
SLOWLOG GET 10 # recent slow commands
SCAN 0 MATCH sess:* COUNT 200 # instead of KEYS
HSCAN big:hash 0 COUNT 100 # instead of HGETALL on a huge hashPick a strategy and accept its trade-off. TTL-only tolerates staleness up to the TTL, simplest but loosest. Invalidate-on-write deletes or updates the key when the database changes, tighter but needs every write path to remember. Change-data-capture streams database changes to a consumer that updates Redis, the most reliable at scale but the most moving parts.
The nuance: prefer deleting the key over updating it on write, because two concurrent writers updating the cache can race and leave a stale value, while delete-then-repopulate-on-read avoids that ordering problem.
Key point: The delete-versus-update-on-write insight (delete avoids a write-write race) is the detail that separates production-ready answers here.
Redis is in-memory and optimized for microsecond access to a dataset that fits in RAM, with rich data structures. Cassandra is a disk-based, distributed wide-column store built for enormous datasets, high write throughput, and tunable consistency across many nodes and data centers.
Reach for Redis when latency and data structures matter and the working set fits in memory: caching, sessions, leaderboards, queues. Reach for Cassandra when you need to store terabytes durably with linear write scaling. They often coexist: Cassandra as the durable store, Redis as the hot layer in front.
| Redis | Cassandra | |
|---|---|---|
| Storage | In-memory (RAM) | On disk, distributed |
| Dataset size | Fits in memory | Terabytes and beyond |
| Latency | Microseconds | Low milliseconds |
| Model | Data structure server | Wide-column store |
| Best at | Cache, sessions, real-time | Massive durable writes |
Replicas don't independently expire keys. The primary decides when a key expires and sends an explicit DEL (or UNLINK) to replicas, so they stay consistent with the primary rather than each deleting on its own clock.
This matters because a replica might still hold a logically expired key until the primary's delete arrives. Modern Redis guards against a replica returning such a stale key on reads, but the model is worth knowing: expiry is authoritative on the primary, propagated down.
Key point: Most candidates think each node expires independently. Knowing the primary drives expiry propagation is a genuine internals signal.
Memory (used_memory versus maxmemory and the fragmentation ratio), cache hit rate (keyspace_hits over hits plus misses), evicted and expired key counts, connected clients and blocked clients, replication lag, latency and slow-log entries, and fork timing (latest_fork_usec).
A falling hit rate, rising evictions, or climbing memory usually means the cache is undersized or a TTL policy is wrong. INFO exposes most of this, and Redis's built-in latency monitor and SLOWLOG pinpoint stalls.
INFO stats # keyspace_hits, keyspace_misses, evicted_keys
INFO memory # used_memory, mem_fragmentation_ratio
INFO replication # master_repl_offset, replica lag
LATENCY LATEST # recent latency spikesOpening a TCP connection per operation is wasteful, so clients pool connections and reuse them. Because the server is single-threaded, throughput comes from keeping the pipe busy: pipelining commands, reusing connections, and avoiding a flood of short-lived connections that spend the server's time on setup and teardown.
Watch connected_clients and the maxclients limit. A connection leak (clients opened and never returned to the pool) slowly exhausts the limit and starts refusing new connections, a classic production incident.
Modules extend Redis with new commands and data types written in C against its module API. RedisJSON adds native JSON with path queries, RediSearch adds secondary indexing and full-text search, RedisBloom adds probabilistic structures like Bloom filters and count-min sketch, and RedisTimeSeries adds time-series storage.
Reach for a module when you'd otherwise contort a core type to fake the feature: secondary search over values, JSON path updates, or approximate membership. The trade-off is an added dependency and version and licensing considerations to check before you commit.
Key point: Naming a couple of modules and the exact gap each fills (search, JSON paths, Bloom) shows breadth beyond the core commands.
Three tools, in order of power. MULTI/EXEC queues commands to run together with no interleaving but no rollback. WATCH adds optimistic locking so EXEC aborts if a watched key changed. A Lua script (EVAL) runs read-and-write logic atomically in one shot, which is the strongest for check-then-act.
In a cluster, all the keys must live in the same hash slot for any of these, which is what hash tags are for. If your keys can't share a slot, you can't do it atomically in Redis and need to rethink the data layout.
Key point: The cluster constraint (same slot required, hence hash tags) is the follow-up. it is useful because indicates real cluster experience.
Store each session under a key like session:<id> holding a hash or serialized blob, with a TTL matching the session timeout that you refresh on each request (a sliding expiry). Keep the payload small; store an ID and look up heavy data from the database. Use replication or Cluster for availability so a node loss doesn't log everyone out.
Decide the durability stance up front: pure cache means a total loss logs users out, which is usually acceptable; if not, enable AOF. Also plan for a stampede on the login path and invalidate sessions on logout by deleting the key.
HSET session:abc123 user_id 1042 role admin
EXPIRE session:abc123 1800 # 30-min sliding window
# on each request: EXPIRE session:abc123 1800 (refresh)
DEL session:abc123 # logoutWork from observation to cause. Check SLOWLOG for slow commands (a stray KEYS or big-key op), LATENCY LATEST and LATENCY DOCTOR for Redis's own diagnosis, and INFO for fork timing and memory pressure. Correlate spikes with RDB saves or AOF rewrites (fork stalls), swapping (Redis paged to disk destroys latency), and eviction storms when memory is tight.
Common culprits: an O(n) command on a big key, a fork during a snapshot, the box swapping, or network saturation. The method matters more than any single tool: observe, correlate with events, hypothesize, verify, fix.
SLOWLOG GET 20
LATENCY LATEST
LATENCY DOCTOR
INFO persistence # latest_fork_usec, rdb/aof statusKey point: The methodology is; the commands are supporting evidence.
Never expose it to the public internet: bind to private interfaces, use a firewall, and enable TLS for encryption in transit. Require authentication with ACLs (Redis 6+) so each client has a user with only the commands and key patterns it needs, instead of one shared password. Rename or disable dangerous commands (FLUSHALL, CONFIG, KEYS).
The most common breach is an open, unauthenticated Redis reachable from the internet, where an attacker can read everything or use CONFIG to write files. Defense in depth: network isolation, TLS, ACLs, and least privilege.
ACL SETUSER appreader on >secretpw ~cache:* +GET +MGET +EXISTS
CONFIG SET rename-command "FLUSHALL" "" # disable it
# bind 127.0.0.1 and enable TLS in redis.confKey point: Leading with 'don't expose it to the internet' plus ACLs and TLS covers the incidents that actually happen.
When the dataset can't fit in memory affordably, when you need rich queries, joins, or ad-hoc reporting (that's a relational or analytical database), when you need strong durability with zero tolerance for the async-replication loss window, or when you need complex transactions with rollback.
Redis is a fast layer and a data structure server, not a system of record for everything. Naming its limits, and picking it for the jobs it's built for, is the judgment senior interviewers are checking for.
Key point: Being willing to say 'not Redis here' shows you choose tools on fit, which is exactly the maturity this closing question probes.
Redis wins when you need more than a flat key-value cache: rich data types, atomic operations, persistence, and pub/sub in one process. Memcached is simpler and multi-threaded, so it can edge Redis on raw throughput for plain string caching, but it forgets everything on restart and offers no data structures. The other framing interviewers use is Redis as a cache versus Redis as a primary database. As a cache it's a natural fit; as a system of record it works only when you accept its durability model and can hold the dataset in RAM. Saying these trade-offs out loud is itself a signal: it shows you pick tools on merits, not habit.
| Dimension | Redis | Memcached |
|---|---|---|
| Data types | Strings, hashes, lists, sets, sorted sets, streams | Strings only |
| Persistence | RDB snapshots and AOF log, optional | None, cache only |
| Threading | Mostly single-threaded core | Multi-threaded |
| Extra features | Pub/sub, Lua, transactions, replication | Plain key-value get/set |
| Eviction | Multiple policies, TTL per key | LRU only, TTL per key |
Prepare in layers, and practice out loud. Most Redis rounds move from data-type questions to caching design to a scaling or failure discussion, so rehearse each stage rather than only reading answers.
The typical Redis interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Redis questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works