Redis Interview Questions — 35 with Code and Real Answers
Redis shows up in almost every backend and system design interview. Not as a trick question — as a signal. Interviewers use Redis to filter candidates who understand distributed systems, caching tradeoffs, and operational reality from those who just know how to call GET and SET.
This guide covers 35 questions you'll actually face, with code in Python and Node.js, real use cases, and the nuance that separates a "good" answer from a hire.
Data Structures
1. What is Redis and how does it differ from a traditional database?
Redis is an in-memory data structure store. It can act as a cache, message broker, or primary datastore. The key difference from a relational database: data lives in RAM first, making reads and writes dramatically faster (sub-millisecond), but the data model is fundamentally different — keys map to specific data structures, not rows in tables.
What interviewers look for: Don't just say "it's fast." Explain the tradeoff: RAM is expensive and volatile. Redis is not a replacement for Postgres — it's a complement.
2. Walk me through Redis's core data types and give a use case for each.
| Type | Use case |
|------|----------|
| String | Session tokens, counters, cached HTML fragments |
| Hash | User profile objects |
| List | Activity feeds, queues |
| Set | Unique visitors, tagging |
| Sorted Set | Leaderboards, rate limiting windows |
| Stream | Event logs, real-time messaging |
| Bitmap | Feature flags per user at scale |
| HyperLogLog | Approximate unique counts |
3. How do Strings work in Redis, and when would you use `INCR`?
Redis Strings are binary-safe and can hold up to 512 MB. INCR atomically increments an integer value stored at a key — this is crucial because it avoids race conditions you'd get from a GET → increment → SET sequence.
import redis
r = redis.Redis()
# Atomic counter — safe under concurrent load
r.set("page_views:homepage", 0)
r.incr("page_views:homepage") # → 1
r.incrby("page_views:homepage", 5) # → 6
# Get current value
count = r.get("page_views:homepage")
print(count) # b'6'const redis = require('redis');
const client = redis.createClient();
await client.connect();
await client.set('page_views:homepage', 0);
await client.incr('page_views:homepage'); // 1
const count = await client.incrBy('page_views:homepage', 5); // 6Real use case: Page view counters, request counters for rate limiting, inventory counts.
4. When would you use a Hash over a String for storing an object?
Use a Hash when you need to read or update individual fields of an object without serializing/deserializing the whole thing.
# String approach — must fetch and parse the whole JSON to update one field
r.set("user:123", '{"name": "Ana", "email": "ana@example.com", "plan": "pro"}')
# Hash approach — update single field atomically
r.hset("user:123", mapping={
"name": "Ana",
"email": "ana@example.com",
"plan": "pro"
})
# Update just the plan — no read needed
r.hset("user:123", "plan", "enterprise")
# Read a single field
plan = r.hget("user:123", "plan") # b'enterprise'Tradeoff: Hashes are more memory-efficient for small objects (< 128 fields, values < 64 bytes) because Redis uses a ziplist encoding internally. For large objects, the difference narrows.
5. Explain Redis Lists — what are `LPUSH`/`RPUSH`/`LPOP`/`RPOP` and how would you build a queue?
Lists are doubly-linked lists of strings. LPUSH prepends, RPUSH appends. Combine them to build queues (RPUSH + LPOP) or stacks (LPUSH + LPOP).
# Producer
r.rpush("job_queue", "job:101", "job:102", "job:103")
# Consumer — blocking pop waits up to 5 seconds
job = r.blpop("job_queue", timeout=5)
# Returns: (b'job_queue', b'job:101')
# Check queue length
length = r.llen("job_queue") # 2// Non-blocking pop
const job = await client.lPop('job_queue');
// Blocking pop — ideal for workers
const result = await client.blPop('job_queue', 5);Real use case: Background job queues (email sending, image processing). BLPOP lets workers sleep instead of polling, reducing CPU load.
Common mistake: Using LRANGE 0 -1 to inspect the queue in production on a list with millions of items. It'll block Redis.
6. How do Sets differ from Lists, and when would you use `SINTERSTORE`?
Sets are unordered collections of unique strings. Lists allow duplicates and maintain insertion order. Sets give you O(1) membership checks and set operations (union, intersection, difference).
# Track which users have seen a notification
r.sadd("notified:campaign_42", "user:1", "user:2", "user:3")
r.sadd("premium_users", "user:2", "user:3", "user:4")
# Who is premium AND was notified?
r.sinterstore("premium_notified", "notified:campaign_42", "premium_users")
members = r.smembers("premium_notified") # {b'user:2', b'user:3'}
# Is user:1 a member?
r.sismember("notified:campaign_42", "user:1") # TrueReal use case: Social graphs (mutual friends = intersection of follower sets), A/B test group membership, deduplication.
7. What are Sorted Sets and how does the score work?
Sorted Sets are like Sets but every member has a floating-point score. Redis keeps members sorted by score at all times. This makes range queries by rank or score very fast.
# Leaderboard
r.zadd("leaderboard:weekly", {
"player:alice": 9500,
"player:bob": 8200,
"player:carlos": 11000
})
# Top 3
top3 = r.zrevrange("leaderboard:weekly", 0, 2, withscores=True)
# [(b'player:carlos', 11000.0), (b'player:alice', 9500.0), (b'player:bob', 8200.0)]
# Add score
r.zincrby("leaderboard:weekly", 500, "player:alice")
# Rank of a player (0-indexed, ascending)
rank = r.zrevrank("leaderboard:weekly", "player:alice")await client.zAdd('leaderboard:weekly', [
{ score: 9500, value: 'player:alice' },
{ score: 11000, value: 'player:carlos' }
]);
const top3 = await client.zRangeWithScores('leaderboard:weekly', 0, 2, { REV: true });Real use case: Leaderboards, rate limiting windows (score = timestamp), job priority queues, expiring sets using timestamp as score.
8. How would you implement a sliding window rate limiter using Sorted Sets?
import time
def is_rate_limited(user_id: str, limit: int, window_seconds: int) -> bool:
key = f"rate_limit:{user_id}"
now = time.time()
window_start = now - window_seconds
pipe = r.pipeline()
# Remove entries outside the window
pipe.zremrangebyscore(key, 0, window_start)
# Count remaining entries
pipe.zcard(key)
# Add current request
pipe.zadd(key, {str(now): now})
# Set expiry to clean up old keys
pipe.expire(key, window_seconds)
results = pipe.execute()
request_count = results[1]
return request_count >= limit
# Usage
if is_rate_limited("user:42", limit=10, window_seconds=60):
raise Exception("Rate limit exceeded")Why Sorted Sets? The score (timestamp) lets you efficiently remove stale entries with ZREMRANGEBYSCORE. The cardinality gives you the exact count within the window.
Persistence
9. What is RDB persistence and what are its tradeoffs?
RDB (Redis Database) takes point-in-time snapshots of your dataset and writes them to disk as a .rdb file. Configured with SAVE directives like save 900 1 (save if at least 1 key changed in 900 seconds).
Pros:
- Compact binary format, great for backups
- Faster restarts — loading a single file is faster than replaying a log
- Minimal performance impact during normal operation (fork-based)
Cons:
- You can lose data between snapshots — if Redis crashes 5 minutes after the last snapshot, you lose 5 minutes of writes
BGSAVEforks the process; on large datasets with copy-on-write, this can spike memory usage
10. What is AOF persistence and how does it differ from RDB?
AOF (Append Only File) logs every write operation. On restart, Redis replays the log to reconstruct the dataset.
appendfsync options:
always— fsync after every write. Maximum durability, slowest.everysec— fsync every second. At most 1 second of data loss. Default recommendation.no— let the OS decide. Fastest, least durable.
Tradeoffs vs RDB:
- AOF files are larger and restarts are slower
- You get much better durability (at most 1 second of loss with
everysec) - AOF can be rewritten/compacted with
BGREWRITEAOF
What interviewers want to hear: "RDB for backups and disaster recovery, AOF for durability. In production, run both."
11. What happens during an AOF rewrite?
Over time, the AOF grows as it records every mutation. A rewrite compacts it by generating the minimal set of commands to recreate the current state. It's safe and non-blocking:
- 1Redis forks a child process
- 2The child writes a new AOF from the current in-memory dataset
- 3New writes go to both the old AOF and an in-memory buffer
- 4When the child finishes, the buffer is appended to the new AOF
- 5The new file atomically replaces the old one
Configure with auto-aof-rewrite-percentage 100 and auto-aof-rewrite-min-size 64mb.
Pub/Sub and Streams
12. How does Redis Pub/Sub work and what are its limitations?
Pub/Sub is a fire-and-forget messaging pattern. Publishers send messages to channels; subscribers receive them in real time.
# Subscriber (runs in a thread/separate process)
import threading
def listen():
pubsub = r.pubsub()
pubsub.subscribe("notifications:user:42")
for message in pubsub.listen():
if message['type'] == 'message':
print(f"Received: {message['data']}")
thread = threading.Thread(target=listen, daemon=True)
thread.start()
# Publisher
r.publish("notifications:user:42", "Your order shipped!")const subscriber = client.duplicate();
await subscriber.connect();
await subscriber.subscribe('notifications:user:42', (message) => {
console.log('Received:', message);
});
// Publisher
await client.publish('notifications:user:42', 'Your order shipped!');Limitations:
- No message persistence — if a subscriber is offline, it misses messages
- No message acknowledgment
- No replay capability
- Subscribers must be connected at publish time
Use Streams instead if you need persistence, consumer groups, or at-least-once delivery.
13. When would you use Redis Streams over Pub/Sub?
Streams are a persistent, append-only log. Think Kafka, but simpler and embedded in Redis.
# Producer
message_id = r.xadd("events:orders", {
"order_id": "ord_123",
"user_id": "user:42",
"amount": "99.99",
"status": "created"
})
# Consumer group — multiple workers share the load
r.xgroup_create("events:orders", "order_processors", id="0", mkstream=True)
# Worker reads from the group
messages = r.xreadgroup(
groupname="order_processors",
consumername="worker-1",
streams={"events:orders": ">"}, # ">" means new messages
count=10,
block=5000
)
# Acknowledge processed message
r.xack("events:orders", "order_processors", message_id)Use Streams when:
- Messages must survive subscriber downtime
- Multiple workers should share load (consumer groups)
- You need to replay history
- Order matters and you need durable delivery
14. What is `XPENDING` and why does it matter in production?
XPENDING shows messages that were delivered to a consumer but not yet acknowledged. This is your safety net for detecting stuck or crashed workers.
# Check pending messages
pending = r.xpending("events:orders", "order_processors")
print(pending)
# {'pending': 3, 'min': '1701234567890-0', 'max': '...', 'consumers': [{'name': 'worker-1', 'pending': 3}]}
# Claim messages pending for more than 30 seconds (worker may have crashed)
claimed = r.xautoclaim(
"events:orders",
"order_processors",
"worker-2",
min_idle_time=30000, # ms
start_id="0-0"
)Production pattern: Run a separate monitoring process that periodically calls XPENDING and reclaims stale messages to a recovery worker.
Redis Cluster and Sentinel
15. What is Redis Sentinel and what problem does it solve?
Sentinel provides high availability for a single Redis master. It monitors master and replica health, automatically promotes a replica if the master fails (failover), and notifies clients of the new master address.
Key Sentinel concepts:
- Quorum: number of Sentinels that must agree before a failover
down-after-milliseconds: how long to wait before marking a node as downfailover-timeout: maximum time for a failover
from redis.sentinel import Sentinel
sentinel = Sentinel([
('sentinel1.example.com', 26379),
('sentinel2.example.com', 26379),
('sentinel3.example.com', 26379),
], socket_timeout=0.1)
# Get master connection
master = sentinel.master_for('mymaster', socket_timeout=0.1)
# Get replica connection for reads
replica = sentinel.slave_for('mymaster', socket_timeout=0.1)
master.set("key", "value")
value = replica.get("key")Limitation: Sentinel doesn't shard data. It's HA for a single dataset. For horizontal scaling, use Cluster.
16. How does Redis Cluster work?
Redis Cluster automatically shards data across multiple nodes using hash slots. There are 16,384 slots total; each master owns a subset.
Key lookup: HASH_SLOT = CRC16(key) % 16384
from redis.cluster import RedisCluster
rc = RedisCluster(
startup_nodes=[{"host": "redis-node-1", "port": 7001}],
decode_responses=True
)
# Keys go to different nodes automatically
rc.set("user:1", "alice") # might go to node 1
rc.set("user:2", "bob") # might go to node 2
# Hash tags force keys to the same slot
rc.set("{user:1}.profile", "...") # same slot as user:1
rc.set("{user:1}.preferences", "...") # same slot as user:1Hash tags {}: Force multiple keys to the same slot so you can use multi-key operations (MSET, MGET) and transactions on them.
What interviewers check: Do you understand that multi-key operations across slots are not supported? And the hash tag workaround?
17. What is the difference between a Redis Cluster split-brain and how do you prevent it?
Split-brain occurs when a network partition makes nodes unable to communicate, and multiple nodes believe they're the master. Redis Cluster prevents this with the majority rule: a master is only considered available if it can communicate with the majority of masters.
If a node can't reach majority, it stops accepting writes (CLUSTERDOWN error). This trades availability for consistency (CP in the CAP theorem sense for the affected partition).
Prevention:
- Odd number of masters (3, 5, 7) for clear majority
- Deploy across availability zones, not just racks
- Configure
cluster-require-full-coverage yescarefully — if set tono, the cluster serves reads/writes even when some slots are unavailable
18. When would you choose Sentinel over Cluster?
| Scenario | Use |
|----------|-----|
| Dataset fits on one machine, need HA | Sentinel |
| Dataset too large for one machine | Cluster |
| Need read scaling via replicas | Sentinel (or Cluster) |
| Need automatic horizontal sharding | Cluster |
| Simpler client setup required | Sentinel |
Most applications start with Sentinel. Switch to Cluster when your dataset or write throughput exceeds a single node's capacity.
Cache Invalidation Patterns
19. What is cache-aside (lazy loading) and what are its tradeoffs?
Cache-aside is the most common caching pattern. The application manages the cache directly: check cache first, on miss fetch from DB and populate cache.
def get_user(user_id: str) -> dict:
key = f"user:{user_id}"
# 1. Check cache
cached = r.get(key)
if cached:
return json.loads(cached)
# 2. Cache miss — fetch from DB
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
# 3. Populate cache with TTL
r.setex(key, 3600, json.dumps(user))
return userTradeoffs:
- Pro: Cache only contains data that's actually read; resilient to cache failures (app falls back to DB)
- Con: Cache miss causes a round trip; risk of thundering herd on popular keys; cache can contain stale data until TTL expires
20. What is write-through caching and when should you use it?
Write-through updates the cache synchronously on every write. The cache never contains stale data.
def update_user(user_id: str, data: dict):
key = f"user:{user_id}"
# Write to DB first
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
# Then update cache
r.setex(key, 3600, json.dumps(data))When to use: When stale reads are unacceptable and write latency is acceptable. Pairs well with read-through caching.
Downside: Every write pays the cache-update cost, even for data that will never be read again. Cache fills with cold data.
21. What is the cache stampede (thundering herd) problem and how do you solve it?
When a popular key expires, thousands of requests simultaneously miss the cache and hammer the database.
import time
def get_popular_item(item_id: str) -> dict:
key = f"item:{item_id}"
lock_key = f"lock:{key}"
cached = r.get(key)
if cached:
return json.loads(cached)
# Try to acquire lock — only one process rebuilds the cache
acquired = r.set(lock_key, "1", nx=True, ex=10) # nx = only if not exists
if acquired:
try:
# This process rebuilds
data = db.fetch_item(item_id)
r.setex(key, 3600, json.dumps(data))
return data
finally:
r.delete(lock_key)
else:
# Other processes wait briefly then retry
time.sleep(0.1)
return get_popular_item(item_id)Alternative: Probabilistic early expiration — before the key actually expires, start refreshing it with some probability proportional to how close to expiry it is.
22. What is the difference between `DEL` and `UNLINK`?
DEL is synchronous — it blocks Redis while freeing memory. For large keys (a hash with a million fields), this can pause Redis for hundreds of milliseconds.
UNLINK (Redis 4.0+) is asynchronous — it unlinks the key immediately (making it invisible) and frees memory in a background thread. Always prefer UNLINK for potentially large keys.
# Dangerous for large keys
r.delete("huge_hash_with_1M_fields")
# Safe — non-blocking
r.unlink("huge_hash_with_1M_fields")Rate Limiting with Redis
23. Implement a fixed window rate limiter.
def fixed_window_rate_limit(user_id: str, limit: int, window_seconds: int) -> bool:
import math
# Window key based on current time window
window = math.floor(time.time() / window_seconds)
key = f"ratelimit:{user_id}:{window}"
current = r.incr(key)
if current == 1:
# First request in this window — set expiry
r.expire(key, window_seconds)
return current > limit
# Usage
if fixed_window_rate_limit("user:42", limit=100, window_seconds=60):
return Response(status=429, body="Too Many Requests")Weakness: A user can make 100 requests at second 59 and 100 more at second 61 — 200 requests in 2 seconds. This is why sliding window (question 8) is often preferred.
24. How would you rate limit at the IP level across a Redis Cluster?
Use hash tags to ensure all rate limit keys for an IP land on the same slot, enabling atomic operations:
def cluster_rate_limit(ip: str, limit: int, window: int) -> bool:
# Hash tag {ip} forces to same slot
key = f"{{ratelimit:{ip}}}:count"
pipe = rc.pipeline()
pipe.incr(key)
pipe.expire(key, window)
results = pipe.execute()
return results[0] > limitDistributed Locks
25. How do you implement a distributed lock in Redis?
The fundamental pattern uses SET key value NX EX seconds — atomic set-if-not-exists with expiry.
import uuid
def acquire_lock(resource: str, ttl: int = 10) -> str | None:
lock_key = f"lock:{resource}"
token = str(uuid.uuid4()) # Unique value to identify this lock holder
acquired = r.set(lock_key, token, nx=True, ex=ttl)
return token if acquired else None
def release_lock(resource: str, token: str) -> bool:
lock_key = f"lock:{resource}"
# Lua script — atomic check-and-delete
lua_script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
result = r.eval(lua_script, 1, lock_key, token)
return result == 1
# Usage
token = acquire_lock("payment:order:123", ttl=30)
if token:
try:
process_payment()
finally:
release_lock("payment:order:123", token)
else:
raise Exception("Could not acquire lock")Why the unique token? Without it, Process A could release Process B's lock if A took longer than the TTL and B acquired the lock in the meantime.
Why Lua for release? The GET → compare → DEL sequence must be atomic. A plain GET + DEL has a race condition.
26. What is Redlock and when is it necessary?
Redlock is an algorithm for distributed locks across multiple independent Redis nodes (not replicas). It solves the problem where a master fails after acquiring a lock but before replicating to a replica — the replica gets promoted and the same lock can be acquired by another client.
Algorithm:
- 1Get current timestamp
- 2Try to acquire lock on N/2+1 nodes (majority) within a small timeout
- 3Lock is valid only if acquired on majority AND total elapsed time < lock TTL
- 4Release on all nodes if failed
# Using the redlock-py library
from redlock import Redlock
dlm = Redlock([
{"host": "redis-1", "port": 6379},
{"host": "redis-2", "port": 6379},
{"host": "redis-3", "port": 6379},
])
my_lock = dlm.lock("resource_name", 10000) # 10 second TTL
if my_lock:
try:
do_critical_section()
finally:
dlm.unlock(my_lock)When to use: Only when your Redis setup uses multiple independent masters and you need strong lock guarantees. For most applications with a single Redis master + Sentinel, the simple SET NX EX pattern is sufficient.
27. What happens if a process holds a lock and then crashes?
The lock will expire based on the TTL. This is why TTL is mandatory. Without it, a crashed process leaves a permanent lock and no other process can proceed.
Design considerations:
- TTL should be longer than the expected operation duration
- If the operation might take variable time, consider lock renewal (heartbeat pattern)
- Log every lock acquisition and release for debugging
Pipelines and Transactions
28. What is a Redis pipeline and when should you use it?
A pipeline batches multiple commands and sends them in a single network round trip, reducing latency significantly for bulk operations.
# Without pipeline — N round trips
for i in range(1000):
r.set(f"key:{i}", f"value:{i}")
# With pipeline — 1 round trip
pipe = r.pipeline()
for i in range(1000):
pipe.set(f"key:{i}", f"value:{i}")
pipe.execute()
# Pipeline with reads — commands still execute sequentially on server
pipe = r.pipeline()
pipe.get("user:1")
pipe.get("user:2")
pipe.hgetall("session:abc")
results = pipe.execute()
user1, user2, session = resultsconst pipeline = client.multi();
for (let i = 0; i < 1000; i++) {
pipeline.set(`key:${i}`, `value:${i}`);
}
await pipeline.exec();When to use: Any time you're executing multiple independent commands. Especially useful for bulk imports, initializing data, and batch reads.
Common mistake: Using r.pipeline() expecting transactions. Pipelines are NOT atomic — use MULTI/EXEC for transactions.
29. What are Redis transactions and how do `MULTI`/`EXEC`/`DISCARD` work?
MULTI starts a transaction block. Commands are queued, not executed. EXEC runs all queued commands atomically. DISCARD aborts.
# Transfer credits between users — must be atomic
pipe = r.pipeline()
pipe.multi() # Start transaction
pipe.decrby("user:1:credits", 100)
pipe.incrby("user:2:credits", 100)
results = pipe.execute() # Both commands execute atomicallyImportant caveat: Redis transactions are atomic in the sense that no other client can interleave commands between MULTI and EXEC. BUT if a command fails during EXEC (wrong type, etc.), Redis still executes the others — there's no rollback.
pipe.multi()
pipe.set("key", "string")
pipe.incr("key") # Will fail — "key" is a string, not int
pipe.set("other", "fine")
results = pipe.execute(raise_on_error=False)
# results: [True, ResponseError, True]
# "other" was set despite the error — no rollback30. What is `WATCH` and how do you implement optimistic locking?
WATCH monitors keys for modification. If a watched key changes before EXEC, the transaction is aborted (returns None).
def transfer_credits(from_user: str, to_user: str, amount: int) -> bool:
from_key = f"user:{from_user}:credits"
to_key = f"user:{to_user}:credits"
with r.pipeline() as pipe:
while True:
try:
pipe.watch(from_key)
balance = int(pipe.get(from_key) or 0)
if balance < amount:
pipe.unwatch()
return False
pipe.multi()
pipe.decrby(from_key, amount)
pipe.incrby(to_key, amount)
pipe.execute()
return True
except redis.WatchError:
# Another client modified the key — retry
continueUse WATCH when: You need to read a value, make a decision based on it, and write back atomically. It's optimistic locking — assumes conflict is rare, retries if not.
31. How do Lua scripts in Redis differ from transactions?
Lua scripts run atomically on the Redis server — they're a single operation from Redis's perspective. Unlike MULTI/EXEC, you can include conditional logic and use results of one command in subsequent commands.
# Atomic compare-and-swap — impossible with just MULTI/EXEC
cas_script = """
local current = redis.call("GET", KEYS[1])
if current == ARGV[1] then
redis.call("SET", KEYS[1], ARGV[2])
return 1
else
return 0
end
"""
# Atomically set key to "new_value" only if it's currently "old_value"
result = r.eval(cas_script, 1, "mykey", "old_value", "new_value")Best practice: Use EVALSHA in production to avoid sending the script on every call:
sha = r.script_load(cas_script)
result = r.evalsha(sha, 1, "mykey", "old_value", "new_value")Memory Optimization
32. How does Redis manage memory and what happens when `maxmemory` is reached?
Set maxmemory to cap Redis memory usage. When reached, Redis applies an eviction policy:
| Policy | Behavior |
|--------|----------|
| noeviction | Return error on write. Safe, but breaks the app. |
| allkeys-lru | Evict least-recently-used keys. Good for general cache. |
| volatile-lru | LRU among keys with TTL set. |
| allkeys-lfu | Evict least-frequently-used (Redis 4.0+). Often better than LRU. |
| volatile-ttl | Evict keys with shortest TTL first. |
| allkeys-random | Random eviction. Rarely useful. |
# redis.conf
maxmemory 4gb
maxmemory-policy allkeys-lfuInterview answer: For a pure cache, use allkeys-lfu or allkeys-lru. For a mixed cache/store (some keys must never expire), use volatile-lru and only set TTLs on cacheable keys.
33. What are Redis encoding optimizations and how do they save memory?
Redis uses compact internal encodings for small data structures:
- Hash: ziplist (now listpack in Redis 7.0) when ≤
hash-max-listpack-entriesentries (default 128) AND all values ≤hash-max-listpack-valuebytes (default 64) - List: listpack for small lists, quicklist for larger
- Sorted Set: listpack for small sets, skiplist + hash for larger
- Set: intset when all members are integers, listpack otherwise
# Check encoding
r.object("encoding", "my_small_hash") # b'listpack'
r.object("encoding", "my_large_hash") # b'hashtable'
# Memory usage
r.memory_usage("my_key") # bytesPractical tip: Storing user data as a Hash with many small fields can be 10x more memory-efficient than one JSON string per user, because ziplist/listpack packs fields contiguously.
34. How do you find and eliminate large or unexpected memory consumers?
# Redis memory report
redis-cli --memkeys # scans and reports biggest keys (slow, use off-peak)
# Or via DEBUG OBJECT
redis-cli DEBUG OBJECT mykey
# Memory doctor — high-level diagnosis
redis-cli MEMORY DOCTOR
# Per-key memory
redis-cli MEMORY USAGE mykey [SAMPLES count]# Find top 10 largest keys by pattern (slow scan — use on replica)
def find_large_keys(pattern: str = "*", top_n: int = 10):
results = []
for key in r.scan_iter(pattern, count=100):
size = r.memory_usage(key) or 0
results.append((key, size))
results.sort(key=lambda x: x[1], reverse=True)
return results[:top_n]Never use KEYS * in production — it blocks Redis for the entire scan. Always use SCAN with count hints.
35. What is key expiration in Redis and how does it work internally?
Redis uses two expiration mechanisms:
- 1Lazy expiration: When you access a key, Redis checks if it's expired and deletes it on access. Zero background cost.
- 2Active expiration: Every 100ms (by default), Redis samples random keys with TTLs and deletes expired ones. Continues until fewer than 25% of sampled keys are expired.
# Set TTL on creation
r.setex("session:abc", 3600, "data") # expires in 1 hour
r.set("token:xyz", "data", ex=86400) # expires in 24 hours
r.set("flag:123", "1", px=5000) # expires in 5000ms
# Add TTL to existing key
r.expire("existing_key", 600) # 10 minutes
r.expireat("existing_key", unix_timestamp) # expire at specific time
# Check remaining TTL
r.ttl("session:abc") # seconds remaining (-1 = no TTL, -2 = key doesn't exist)
r.pttl("session:abc") # millisecondsGotcha: In Redis Cluster, EXPIREAT with a time in the past deletes the key immediately. But if clocks are out of sync across your app servers, you can accidentally expire keys prematurely.
Common Patterns and Anti-Patterns
Tips interviewers rarely hear but always remember
1. Don't use KEYS * in production. Use SCAN. KEYS * is O(N) and blocks Redis for the entire operation.
2. Design your key namespace deliberately. Use consistent patterns like entity:id:field (e.g., user:123:profile). Makes debugging and bulk operations (SCAN MATCH user:*) much easier.
3. Always set TTLs on cache entries. A cache without TTLs becomes a memory leak. Even if you're doing manual invalidation, a TTL is insurance.
4. Connection pooling is mandatory. Creating a new Redis connection per request kills performance. Use a connection pool with a sensible max size (typically 10-50 connections per app instance).
# Connection pool
pool = redis.ConnectionPool(host='redis', port=6379, max_connections=20)
r = redis.Redis(connection_pool=pool)5. Redis is single-threaded for commands. A slow Lua script or a SMEMBERS on a set with 10 million members blocks every other client. Profile your commands with SLOWLOG.
redis-cli SLOWLOG GET 10 # Last 10 slow commands
redis-cli CONFIG SET slowlog-log-slower-than 10000 # Log commands > 10ms6. Use OBJECT FREQ for LFU tuning. When using allkeys-lfu, you can inspect how frequently a key is being accessed to validate your eviction behavior.
7. Test failover. Kill your Redis master in staging at least once before going to production with Sentinel. Clients handle failover differently — some stall, some need reconnect logic.
What Interviewers Are Really Testing
In a Redis interview, the question is rarely the point. Interviewers use Redis to probe:
- Do you understand tradeoffs? Every answer should acknowledge what you're giving up. AOF gives you durability but larger files. Cluster gives you scale but breaks multi-key operations across slots.
- Do you know when NOT to use a pattern? Pub/Sub is a common wrong answer for "reliable messaging." If durability matters, you need Streams or a proper queue.
- Have you operated Redis in production? Knowing about
SLOWLOG,MEMORY DOCTOR,INFOcommand output, and connection pool configuration signals real experience.
- Can you reason about atomicity? Redis is single-threaded, but that doesn't mean every sequence of commands is atomic. Knowing when to use Lua scripts vs. transactions vs. simple pipelines is a genuine skill.
- Do you treat Redis as a black box or a tool you understand? Candidates who can explain ziplist encoding, the two-pass expiration algorithm, or copy-on-write behavior during
BGSAVEstand out sharply.
Quick Reference: Commands You Should Know Cold
# Strings
SET key value [EX seconds] [NX]
GET key
INCR / INCRBY / INCRBYFLOAT
MSET / MGET
# Hashes
HSET / HGET / HMGET / HGETALL
HDEL / HEXISTS / HLEN
# Lists
LPUSH / RPUSH / LPOP / RPOP
BLPOP / BRPOP
LRANGE / LLEN / LINDEX
# Sets
SADD / SREM / SMEMBERS / SCARD
SISMEMBER / SMISMEMBER
SUNION / SINTER / SDIFF
# Sorted Sets
ZADD / ZREM / ZSCORE / ZRANK
ZRANGE / ZREVRANGE / ZRANGEBYSCORE
ZCARD / ZINCRBY
# Streams
XADD / XREAD / XLEN
XGROUP CREATE / XREADGROUP / XACK
XPENDING / XAUTOCLAIM
# Server
INFO [section]
SLOWLOG GET
MEMORY USAGE key
SCAN cursor [MATCH pattern] [COUNT count]
DEBUG SLEEP / DEBUG JMAP
CONFIG GET / CONFIG SET