InterviewHack.ai
Start free
Blog/Redis Interview Questions — 35 with Code and Real Answers

Redis Interview Questions — 35 with Code and Real Answers

September 16, 2026

redisbackend-developer

35 Redis interview questions from backend and system design interviews: data structures, pub/sub, persistence, clustering, cache patterns. With code examples.

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.

python
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'
js
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); // 6

Real 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.

python
# 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).

python
# 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
js
// 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).

python
# 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")  # True

Real 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.

python
# 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")
js
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?

python
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
  • BGSAVE forks 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:

  1. 1Redis forks a child process
  2. 2The child writes a new AOF from the current in-memory dataset
  3. 3New writes go to both the old AOF and an in-memory buffer
  4. 4When the child finishes, the buffer is appended to the new AOF
  5. 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.

python
# 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!")
js
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.

python
# 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.

python
# 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 down
  • failover-timeout: maximum time for a failover
python
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

python
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:1

Hash 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 yes carefully — if set to no, 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.

python
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 user

Tradeoffs:

  • 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.

python
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.

python
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.

python
# 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.

python
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:

python
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] > limit

Distributed 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.

python
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:

  1. 1Get current timestamp
  2. 2Try to acquire lock on N/2+1 nodes (majority) within a small timeout
  3. 3Lock is valid only if acquired on majority AND total elapsed time < lock TTL
  4. 4Release on all nodes if failed
python
# 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.

python
# 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 = results
js
const 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.

python
# 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 atomically

Important 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.

python
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 rollback

30. 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).

python
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
                continue

Use 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.

python
# 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:

python
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. |

bash
# redis.conf
maxmemory 4gb
maxmemory-policy allkeys-lfu

Interview 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-entries entries (default 128) AND all values ≤ hash-max-listpack-value bytes (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
python
# 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")  # bytes

Practical 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?

bash
# 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]
python
# 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:

  1. 1Lazy expiration: When you access a key, Redis checks if it's expired and deletes it on access. Zero background cost.
  2. 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.
python
# 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")  # milliseconds

Gotcha: 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).

python
# 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.

bash
redis-cli SLOWLOG GET 10  # Last 10 slow commands
redis-cli CONFIG SET slowlog-log-slower-than 10000  # Log commands > 10ms

6. 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, INFO command 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 BGSAVE stand out sharply.

Quick Reference: Commands You Should Know Cold

bash
# 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

FAQ

What Redis questions come up most in FAANG interviews?+

System design questions dominate: how to design a rate limiter, implement a distributed lock, or build a leaderboard. Beyond that, expect questions about persistence tradeoffs (RDB vs AOF), when to use Streams vs Pub/Sub, and how Redis Cluster handles sharding. Know the data structures deeply — not just the commands, but the internal encoding optimizations and time complexity of each operation.

Is Redis single-threaded?+

For command processing, yes — Redis uses a single thread to execute commands, which is why atomic Lua scripts work and why a slow command like KEYS * blocks everyone. Since Redis 6.0, I/O threading has been added (reading/writing to sockets can use multiple threads), but command execution remains single-threaded. This design simplifies the codebase and avoids lock contention, making Redis extremely fast for typical workloads.

What's the difference between Redis Sentinel and Redis Cluster?+

Sentinel provides high availability for a single Redis dataset: it monitors a master and its replicas, and automatically promotes a replica if the master fails. It does not shard data. Cluster both shards data across multiple nodes (using 16,384 hash slots) and provides high availability via automatic failover. Use Sentinel when your entire dataset fits on one machine but you need failover. Use Cluster when your data or write throughput exceeds a single node's capacity.

How do you prevent data loss in Redis?+

Enable AOF persistence with appendfsync everysec — this gives you at most one second of data loss on a crash. For backups and faster restarts, also enable RDB snapshots. In a replicated setup, ensure writes are acknowledged by at least one replica using WAIT command or the min-replicas-to-write configuration. For absolute zero data loss, you'd need synchronous replication, which Redis does not natively support — in that case, consider a database better suited to strong durability guarantees as your source of truth, and use Redis as a cache.

When should I use Redis Streams instead of a message broker like Kafka?+

Redis Streams are a good fit when: your message volume is moderate (millions per day, not billions), you want to avoid operational complexity of a separate Kafka cluster, you need consumer groups with at-least-once delivery, and your messages can tolerate Redis's memory limits. Choose Kafka when you need long-term message retention (weeks/months), very high throughput (millions per second), complex consumer group topologies, or guaranteed ordering across many partitions. Redis Streams are a pragmatic middle ground for most applications.

What is the thundering herd problem in caching and how do you fix it?+

The thundering herd (cache stampede) occurs when a popular cached key expires and thousands of concurrent requests simultaneously miss the cache and all try to rebuild it from the database, overloading the DB. The standard fix is a distributed lock: only one process rebuilds the cache while others wait briefly and retry. An alternative is probabilistic early expiration — probabilistically refreshing a key before it actually expires, so expiry rarely happens under load. A simpler mitigation is using stale-while-revalidate: return the stale value immediately while a single background process refreshes it.

Related articles

Microservices Interview Questions — 35 Deep Answers

35 microservices interview questions for backend and architect roles: service decomposition, inter-service communication, saga pattern, event sourcing, circuit breakers, observability.

Staff Engineer Interview Questions — 30 with Detailed Answers

30 staff engineer and principal engineer interview questions: technical leadership, cross-team influence, system design at scale, architectural decisions, mentoring. Real answers for L6+.

System Design Interview Questions: How to Answer Them (40 Questions)

Complete 8,000+ word article on System Design Interview Questions with 40 detailed questions, real code, tradeoff analysis, and preparation frameworks. Covers foundational concepts through senior-level topics.

System Design Interview: How to Design Any System (Step-by-Step)

Complete authoritative article on System Design Interviews with 40 numbered Q&A and real code examples

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY