Redis Interview Questions (Free Preview)
Free sample of 15 from 66 questions available
Commands and Operations
What is the difference between EXPIRE, EXPIREAT, and PEXPIRE?
The 30-Second Answer: EXPIRE sets a timeout in seconds from now, EXPIREAT sets an absolute Unix timestamp (in seconds) when the key should expire, and PEXPIRE sets a timeout in milliseconds from now. All three commands set a time-to-live (TTL) on keys, but they differ in whether they use relative or absolute time and the precision of the timeout.
The 2-Minute Answer (If They Want More):
These three commands are part of Redis's key expiration system, which automatically removes keys after a specified time. EXPIRE is the most commonly used command and accepts a relative timeout in seconds. For example, EXPIRE session:abc 3600 will delete the key after 3600 seconds (1 hour) from the current moment. This is ideal for implementing session timeouts, caching with TTL, and temporary data storage.
EXPIREAT takes an absolute Unix timestamp (seconds since epoch) instead of a relative timeout. This is useful when you need to expire keys at a specific point in time, such as "delete this promotional code on January 1st, 2026 at midnight UTC." Using EXPIREAT promotion:code 1735689600 ensures the key expires at exactly that timestamp, regardless of when the command was executed.
PEXPIRE works identically to EXPIRE but uses milliseconds instead of seconds, providing finer-grained control over expiration times. This precision is valuable for rate limiting, short-lived tokens, or time-sensitive operations where second-level precision isn't sufficient. There's also PEXPIREAT for absolute timestamps in milliseconds.
All expiration commands return 1 if the timeout was set successfully and 0 if the key doesn't exist. You can check the remaining TTL using the TTL command (returns seconds) or PTTL (returns milliseconds). Setting a new expiration overwrites any existing one, and using PERSIST removes the expiration entirely, making the key permanent again.
Code Example:
# EXPIRE - relative timeout in seconds
SET session:user123 "active"
EXPIRE session:user123 1800 # Expires in 30 minutes (1800 seconds)
TTL session:user123 # Check remaining time → 1799, 1798...
# EXPIREAT - absolute timestamp in seconds
SET promotion:summer2025 "50% off"
EXPIREAT promotion:summer2025 1735689600 # Expires on Jan 1, 2026 00:00:00 UTC
TTL promotion:summer2025 # Shows seconds until that timestamp
# PEXPIRE - relative timeout in milliseconds
SET rate:limit:192.168.1.1 "100"
PEXPIRE rate:limit:192.168.1.1 500 # Expires in 500 milliseconds
PTTL rate:limit:192.168.1.1 # Returns milliseconds remaining
# PEXPIREAT - absolute timestamp in milliseconds
PEXPIREAT token:verify 1735689600000 # Precise expiration time
# Comparison and management
SET mykey "value"
EXPIRE mykey 300 # Set 5 minute expiration
TTL mykey # → 299, 298, 297...
PERSIST mykey # Remove expiration
TTL mykey # → -1 (no expiration)
# Expiration behavior
EXPIRE nonexistent 100 # → 0 (key doesn't exist)
SET newkey "data"
EXPIRE newkey 60 # → 1 (success)
EXPIRE newkey 120 # → 1 (overwrites previous expiration)
Comparison Table:
| Command | Time Format | Precision | Use Case |
|---|---|---|---|
| EXPIRE | Relative (from now) | Seconds | Session timeouts, general caching |
| EXPIREAT | Absolute (Unix timestamp) | Seconds | Scheduled expiration at specific time |
| PEXPIRE | Relative (from now) | Milliseconds | Rate limiting, precise short-lived data |
| PEXPIREAT | Absolute (Unix timestamp) | Milliseconds | Precise scheduled expiration |
References:
↑ Back to topWhat is the difference between DEL and UNLINK?
The 30-Second Answer: DEL synchronously deletes keys immediately and blocks Redis until deletion completes, while UNLINK asynchronously removes the key from the keyspace immediately but performs the actual memory reclamation in a background thread. UNLINK is non-blocking and preferred for deleting large objects (big lists, sets, hashes) in production to avoid freezing Redis.
The 2-Minute Answer (If They Want More): DEL is Redis's traditional deletion command that operates synchronously. When you execute DEL, Redis immediately removes the key from the keyspace and frees all associated memory before returning to the client. For small values like strings or small collections, this happens in microseconds. However, for large data structures—like a hash with millions of fields, a sorted set with hundreds of thousands of members, or a list with gigabytes of data—DEL can block Redis for hundreds of milliseconds or even seconds while it deallocates memory for every element.
UNLINK (introduced in Redis 4.0) solves this blocking problem by performing deletion in two stages. First, it immediately removes the key from the keyspace and returns to the client (O(1) operation). Second, it hands off the actual memory deallocation work to a background thread, which processes the deletion asynchronously without blocking the main Redis thread. This makes UNLINK effectively O(1) from the perspective of the main thread, regardless of object size.
The practical difference is significant in production environments. If your application needs to delete a 10GB sorted set, DEL would freeze your entire Redis instance until that memory is freed, causing all client requests to timeout. UNLINK removes the key instantly (making it inaccessible) and frees the memory gradually in the background, keeping Redis responsive.
For small objects, DEL and UNLINK perform similarly, and DEL might even be slightly faster due to less overhead. The Redis team recommends using UNLINK as the default deletion method in modern applications, especially when you're uncertain about object sizes. Commands like FLUSHDB and FLUSHALL also have ASYNC variants that work similarly to UNLINK.
Code Example:
# DEL - synchronous deletion (blocks until complete)
SET small_key "small value"
DEL small_key # Fast - blocks for microseconds
LPUSH large_list "item1" "item2" ... # (millions of items)
DEL large_list # SLOW - can block for seconds!
# UNLINK - asynchronous deletion (non-blocking)
SET small_key "small value"
UNLINK small_key # Fast, similar to DEL
LPUSH large_list "item1" "item2" ... # (millions of items)
UNLINK large_list # Fast - immediately returns, frees memory in background
# Deleting multiple keys
DEL key1 key2 key3 # Deletes all, blocks until all freed
UNLINK key1 key2 key3 # Removes all immediately, frees async
# Practical example: Deleting a large hash
HSET user:analytics:1000 pageview1 "data" pageview2 "data" ... # (millions of fields)
TIME # Note current time
UNLINK user:analytics:1000 # Returns immediately → 1
TIME # Only microseconds elapsed
EXISTS user:analytics:1000 # → 0 (key already gone from keyspace)
# Memory is freed in background thread
# Compare with DEL on large object
HSET user:analytics:2000 pageview1 "data" ... # (millions of fields)
TIME # Note current time
DEL user:analytics:2000 # Blocks for seconds
TIME # Significant time elapsed
# Async flush operations (similar concept)
FLUSHDB # Synchronous - blocks until DB cleared
FLUSHDB ASYNC # Asynchronous - returns immediately
FLUSHALL # Synchronous - blocks until all DBs cleared
FLUSHALL ASYNC # Asynchronous - returns immediately
# Return values (both commands)
DEL existing_key # → 1 (number of keys deleted)
DEL nonexistent_key # → 0
UNLINK existing_key # → 1 (number of keys unlinked)
UNLINK nonexistent_key # → 0
Comparison Table:
| Feature | DEL | UNLINK |
|---|---|---|
| Operation Mode | Synchronous | Asynchronous |
| Blocking | Yes, until memory freed | No, O(1) keyspace removal |
| Time Complexity | O(N) where N = elements | O(1) main thread, O(N) background |
| Small Objects | Very fast | Very fast (similar to DEL) |
| Large Objects | ❌ Can block for seconds | ✅ Always returns immediately |
| Production Recommendation | Use for small known objects | âś… Preferred default choice |
| Memory Reclamation | Immediate | Background thread |
| Redis Version | All versions | Redis 4.0+ |
| Use Case | Small keys, debugging | Large objects, production systems |
References:
↑ Back to topReplication and High Availability
What is Redis replication and how does it work?
The 30-Second Answer: Redis replication uses a master-replica architecture where data from one Redis instance (master) is asynchronously copied to one or more replica instances. Replicas can serve read queries to distribute load, and the replication happens automatically with the master sending commands to replicas to keep them synchronized.
The 2-Minute Answer (If They Want More): Redis replication follows a simple yet powerful master-replica model. When a replica connects to a master, it initiates a full synchronization process (SYNC or PSYNC for partial resync) where the master creates a snapshot of its dataset and sends it to the replica. After the initial sync, the master streams all write commands to replicas in real-time to keep them updated.
The replication is asynchronous by default, meaning the master doesn't wait for replicas to acknowledge writes before confirming to clients. This provides excellent performance but means replicas might lag slightly behind the master. Replicas are read-only by default and can serve read queries to scale read performance horizontally.
Redis 2.8+ introduced partial resynchronization (PSYNC), which allows replicas that disconnect briefly to resume replication from where they left off rather than requiring a full resync. This significantly improves reliability in networks with occasional connectivity issues.
Replicas can also have their own replicas, creating a cascading replication topology. This is useful for geographically distributed deployments or when you need many replicas and want to reduce the load on the master.
Code Example:
# On the replica instance - configure it to replicate from master
REPLICAOF 192.168.1.100 6379
# Check replication status
INFO replication
# On master, you'll see:
# role:master
# connected_slaves:1
# slave0:ip=192.168.1.101,port=6379,state=online
# On replica, you'll see:
# role:slave
# master_host:192.168.1.100
# master_port:6379
# master_link_status:up
# To promote a replica to master (manual failover)
REPLICAOF NO ONE
# Configuration in redis.conf
replicaof 192.168.1.100 6379
masterauth <password>
replica-read-only yes
repl-diskless-sync no
repl-backlog-size 1mb
Mermaid Diagram:
flowchart TD
Client1[Client Writes] --> Master[Master Redis<br/>Port 6379]
Client2[Client Reads] --> Master
Client3[Client Reads] --> Replica1[Replica 1<br/>Port 6380]
Client4[Client Reads] --> Replica2[Replica 2<br/>Port 6381]
Master -->|Async Replication<br/>Command Stream| Replica1
Master -->|Async Replication<br/>Command Stream| Replica2
Replica1 -->|Cascading Replication| Replica3[Replica 3<br/>Port 6382]
style Master fill:#ff6b6b
style Replica1 fill:#4ecdc4
style Replica2 fill:#4ecdc4
style Replica3 fill:#95e1d3
References:
↑ Back to topRedis Cluster
What is the difference between Redis Cluster and Redis Sentinel?
The 30-Second Answer: Redis Sentinel provides high availability for single-master setups through automatic failover, while Redis Cluster provides both horizontal scaling (sharding) and high availability. Sentinel monitors master-replica pairs and promotes replicas when masters fail; Cluster automatically distributes data across multiple masters, each with their own replicas for failover.
The 2-Minute Answer (If They Want More): Redis Sentinel and Redis Cluster solve different problems, though they both provide high availability. Sentinel is a monitoring and failover system for traditional master-replica Redis deployments. It runs as a separate process that watches your Redis instances and automatically promotes a replica to master if the master becomes unavailable. Sentinel is ideal when your data fits on a single Redis instance but you need automatic failover—it's simpler to set up and doesn't require application changes since clients still connect to a single master.
Redis Cluster, on the other hand, is a distributed system that shards your data across multiple master nodes. Each master has its own replicas for failover, and the cluster handles both data distribution and automatic failover without external tools. You'd choose Cluster when you need to scale beyond what a single Redis instance can handle—either because your dataset is too large or your throughput requirements exceed one server's capacity.
Key architectural differences: Sentinel requires at least 3 sentinel processes monitoring your Redis instances (typically 1 master + N replicas), and it uses quorum voting to decide when failover is needed. Cluster requires at least 3 master nodes (and ideally 3 replicas), and uses a gossip protocol where nodes communicate directly with each other to detect failures. Sentinel can monitor multiple independent Redis master-replica groups, while Cluster is a single unified system where all nodes know about each other.
From a client perspective, Sentinel clients need to query sentinels to discover the current master's address, while Cluster clients need to be cluster-aware, handling MOVED and ASK redirections as keys may be on different nodes. For smaller deployments where high availability is needed but data fits on one server, Sentinel is simpler. For large-scale applications requiring horizontal scaling, Cluster is the right choice despite its added complexity.
Architecture Comparison:
flowchart TD
subgraph Sentinel["Redis Sentinel Setup"]
S1[Sentinel 1]
S2[Sentinel 2]
S3[Sentinel 3]
SM[Redis Master<br/>All Data]
SR1[Redis Replica 1<br/>All Data]
SR2[Redis Replica 2<br/>All Data]
S1 -.->|monitors| SM
S1 -.->|monitors| SR1
S1 -.->|monitors| SR2
S2 -.->|monitors| SM
S3 -.->|monitors| SM
SM -->|replicates| SR1
SM -->|replicates| SR2
Client1[Client] -->|asks for master| S1
Client1 -->|writes/reads| SM
end
subgraph Cluster["Redis Cluster Setup"]
CM1["Master 1<br/>Slots 0-5460"]
CM2["Master 2<br/>Slots 5461-10922"]
CM3["Master 3<br/>Slots 10923-16383"]
CR1["Replica 1<br/>(M1 backup)"]
CR2["Replica 2<br/>(M2 backup)"]
CR3["Replica 3<br/>(M3 backup)"]
CM1 -->|replicates| CR1
CM2 -->|replicates| CR2
CM3 -->|replicates| CR3
CM1 -.->|gossip| CM2
CM2 -.->|gossip| CM3
Client2[Client] -->|connects to any node| CM1
Client2 -->|redirected as needed| CM2
Client2 -->|redirected as needed| CM3
end
Code Example:
# ========== SENTINEL CONFIGURATION ==========
# sentinel.conf
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
# Start sentinel
redis-sentinel /path/to/sentinel.conf
# Client discovers master via sentinel
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# -> 1) "127.0.0.1"
# 2) "6379"
# All operations go to the single master
SET user:1000 "data" # Goes to master
GET user:1000 # Can go to master or replica (if configured)
# ========== CLUSTER CONFIGURATION ==========
# Create cluster with 3 masters, 3 replicas
redis-cli --cluster create \
127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1
# Client connects to cluster (any node)
redis-cli -c -p 7000
# Operations distributed across masters
SET user:1000 "data" # -> Redirected to node with slot 15316
SET user:2000 "data" # -> Redirected to node with slot 8855
SET user:3000 "data" # -> Redirected to node with slot 11455
# No sentinel needed - cluster handles failover internally
CLUSTER INFO
# Shows cluster health, including automatic failover status
When to Use Each:
| Use Case | Sentinel | Cluster |
|---|---|---|
| Dataset < 25GB, fits in RAM | ✅ Best choice | ❌ Overkill |
| Dataset > 25GB, needs sharding | ❌ Can't shard | ✅ Best choice |
| Need automatic failover only | ✅ Simpler setup | ⚠️ Works but complex |
| Need horizontal scaling | ❌ No sharding | ✅ Designed for this |
| Multi-key operations | ✅ All keys on master | ⚠️ Only with hash tags |
| Setup complexity | ⚠️ Moderate | ⚠️⚠️ Higher |
References:
- Redis Sentinel vs Cluster - Redis Official Documentation
- High Availability with Redis Sentinel - Redis.io
Transactions and Atomicity
What is the difference between transactions and Lua scripts for atomicity?
The 30-Second Answer: Transactions (MULTI/EXEC) queue commands for atomic execution but can't contain conditional logic or loops. Lua scripts execute atomically with full programming capabilities (conditionals, loops, computations), are faster for complex operations, and reduce network round trips by executing server-side.
The 2-Minute Answer (If They Want More): While both Redis transactions and Lua scripts provide atomicity, they serve different purposes and have distinct capabilities. Transactions with MULTI/EXEC are simpler and work by queuing commands on the client side, then executing them sequentially on the server without interruption. However, transactions are "dumb" - they can't make decisions based on intermediate results or contain conditional logic. All commands must be determined before EXEC is called.
Lua scripts, on the other hand, execute entirely on the Redis server with full programming capabilities. They can read values, make decisions with if/else statements, use loops, perform calculations, and conditionally execute different commands based on data state. This makes them far more powerful for complex operations. Scripts also execute faster for multi-step operations because everything happens server-side in one atomic block, eliminating network latency between commands.
Another key difference is error handling. In transactions, if a command fails during EXEC due to a runtime error (like wrong type), Redis continues executing subsequent commands - there's no rollback. With Lua scripts, you have more control: you can use redis.call() which stops execution on error, or redis.pcall() which allows you to catch and handle errors within the script logic.
Performance-wise, Lua scripts are generally superior for complex operations. A transaction requiring WATCH for optimistic locking might need multiple retry loops, each involving network round trips. A Lua script accomplishes the same atomically in a single server-side execution. However, transactions are simpler for straightforward multi-command operations where no conditional logic is needed.
Comparison Table:
| Feature | Transactions (MULTI/EXEC) | Lua Scripts (EVAL) |
|---|---|---|
| Atomicity | Yes - sequential execution | Yes - blocks Redis completely |
| Conditional Logic | No | Yes - full if/else, loops |
| Server-side Execution | No - commands queued client-side | Yes - entire script runs on server |
| Network Round Trips | Multiple (for WATCH retries) | Single |
| Error Handling | No rollback, continues on error | Configurable with call/pcall |
| Performance | Good for simple operations | Better for complex operations |
| Complexity | Simple, easy to use | More complex, requires Lua knowledge |
| Use Case | Multiple related commands | Complex logic, validation, computations |
| Optimistic Locking | Requires WATCH + retry loop | Built into script logic |
Code Example:
# TRANSACTION approach: Increment if below threshold
# Requires retry loop and multiple round trips
WATCH counter
GET counter
# Client checks value < 100
MULTI
INCR counter
EXEC
# If EXEC returns null, retry entire process
# LUA SCRIPT approach: Same operation, one atomic call
EVAL "
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
if current < tonumber(ARGV[1]) then
redis.call('INCR', KEYS[1])
return 1
else
return 0
end
" 1 counter 100
# TRANSACTION: Calculate average (not possible atomically)
WATCH scores
LRANGE scores 0 -1
# Calculate average in application
MULTI
SET average_score [calculated_value]
EXEC
# LUA SCRIPT: Calculate average atomically
EVAL "
local scores = redis.call('LRANGE', KEYS[1], 0, -1)
local sum = 0
for i, score in ipairs(scores) do
sum = sum + tonumber(score)
end
local avg = #scores > 0 and sum / #scores or 0
redis.call('SET', KEYS[2], avg)
return avg
" 2 scores average_score
# TRANSACTION: Limited error handling
MULTI
INCR mystring # Wrong type error
SET otherkey "value"
EXEC
# Returns: 1) (error) 2) OK - second command still executes
# LUA SCRIPT: Controlled error handling
EVAL "
local result = redis.pcall('INCR', KEYS[1])
if type(result) == 'table' and result.err then
return {err = 'Operation failed: ' .. result.err}
end
redis.call('SET', KEYS[2], 'value')
return {ok = 'Both operations completed'}
" 2 mystring otherkey
References:
- Redis Transactions vs Lua Scripts - Stack Overflow Discussion
- Redis Documentation: Transactions
- Redis Documentation: Lua Scripting
Pub/Sub and Messaging
What is Redis Pub/Sub and how does it work?
The 30-Second Answer: Redis Pub/Sub is a messaging pattern where publishers send messages to channels without knowing who will receive them, and subscribers listen to channels without knowing who sent the messages. It's a fire-and-forget system with no message persistence - if no subscribers are listening when a message is published, the message is lost forever.
The 2-Minute Answer (If They Want More):
Redis Pub/Sub implements the publish-subscribe messaging paradigm, enabling real-time message broadcasting between components in a distributed system. Publishers use the PUBLISH command to send messages to named channels, while subscribers use SUBSCRIBE or PSUBSCRIBE (for pattern matching) to listen to one or more channels.
The system works through a simple broker model where Redis acts as the message router. When a publisher sends a message to a channel, Redis immediately forwards it to all currently connected subscribers of that channel. The operation is synchronous from the publisher's perspective - the PUBLISH command returns the number of subscribers that received the message.
Pub/Sub is ideal for scenarios like real-time notifications, chat applications, live updates, and event broadcasting where message loss is acceptable. However, it's important to understand that Redis Pub/Sub provides at-most-once delivery semantics with no guarantees - messages are not stored, there's no acknowledgment mechanism, and subscribers that disconnect miss any messages sent during their absence.
The pattern-matching capability with PSUBSCRIBE allows subscribers to listen to multiple channels using glob-style patterns (e.g., news.* matches news.sports, news.weather), making it flexible for hierarchical topic structures.
Code Example:
# Publisher (in one client)
PUBLISH news:sports "Lakers win championship"
# Returns: (integer) 2 # number of subscribers that received the message
# Subscriber 1 (in another client)
SUBSCRIBE news:sports news:weather
# Reading messages... (waiting for messages)
# 1) "subscribe"
# 2) "news:sports"
# 3) (integer) 1
# 1) "message"
# 2) "news:sports"
# 3) "Lakers win championship"
# Subscriber 2 (using pattern matching)
PSUBSCRIBE news:*
# Will receive messages from all channels starting with "news:"
# Unsubscribe when done
UNSUBSCRIBE news:sports
PUNSUBSCRIBE news:*
Mermaid Diagram:
flowchart LR
P1[Publisher 1] -->|PUBLISH channel1| R[Redis<br/>Pub/Sub Broker]
P2[Publisher 2] -->|PUBLISH channel2| R
R -->|message| S1[Subscriber 1<br/>channel1]
R -->|message| S2[Subscriber 2<br/>channel1, channel2]
R -->|message| S3[Subscriber 3<br/>channel2]
R -.->|no subscribers| N[Message Lost]
style R fill:#DC382D
style N fill:#ff9999,stroke:#ff0000,stroke-dasharray: 5 5
References:
↑ Back to topPerformance and Optimization
What is pipelining in Redis and how does it improve performance?
The 30-Second Answer: Pipelining sends multiple commands to Redis without waiting for individual replies, then reads all responses at once. This dramatically reduces network round-trip time (RTT) overhead, potentially improving throughput by 5-10x when executing many commands, as you pay the network latency cost once instead of per command.
The 2-Minute Answer (If They Want More): Redis pipelining is a technique for improving performance by sending multiple commands to the server without waiting for each reply. Normally, each Redis command requires a full network round trip: send command, wait for response, send next command. If your network latency is 1ms, executing 100 commands sequentially takes at least 100ms just in network overhead, regardless of how fast Redis processes them.
With pipelining, you send all 100 commands in one batch, and Redis queues them for execution and returns all responses together. Now you only pay the network latency cost twice (once to send, once to receive), reducing that 100ms to approximately 2ms plus actual execution time. This is particularly effective for bulk operations like importing data, batch updates, or processing queues.
It's important to understand that pipelining doesn't make Redis execute commands faster or in parallel—it still processes them sequentially. The performance gain comes purely from reducing network round trips. Pipelining also doesn't provide atomicity like transactions (MULTI/EXEC), so use transactions when you need atomic operations or when later commands depend on earlier results.
Keep pipeline batches reasonable in size (typically 100-1000 commands) to avoid excessive memory usage on both client and server. Most Redis clients provide built-in pipelining support. For scenarios requiring atomic execution or conditional logic based on results, use Lua scripting or transactions instead.
Code Example:
# Without pipelining (pseudo-code showing concept)
SET user:1:name "Alice" # RTT 1
SET user:2:name "Bob" # RTT 2
SET user:3:name "Charlie" # RTT 3
GET user:1:name # RTT 4
GET user:2:name # RTT 5
# Total: 5 RTTs
# With pipelining (shown in redis-cli)
# Commands sent together:
PING
SET counter 1
INCR counter
GET counter
# All responses received together
# Total: 1 RTT (approximately)
// Node.js with ioredis
const Redis = require('ioredis');
const redis = new Redis();
// Without pipelining - slow
async function withoutPipelining() {
const start = Date.now();
for (let i = 0; i < 1000; i++) {
await redis.set(`key:${i}`, `value${i}`);
}
console.log(`Without pipelining: ${Date.now() - start}ms`);
}
// With pipelining - fast
async function withPipelining() {
const start = Date.now();
const pipeline = redis.pipeline();
for (let i = 0; i < 1000; i++) {
pipeline.set(`key:${i}`, `value${i}`);
}
await pipeline.exec();
console.log(`With pipelining: ${Date.now() - start}ms`);
}
// Pipelining with mixed commands
async function mixedPipeline() {
const pipeline = redis.pipeline();
pipeline.set('user:1000:name', 'Alice');
pipeline.set('user:1000:email', 'alice@example.com');
pipeline.hset('user:1000:profile', 'age', 30);
pipeline.sadd('users:active', '1000');
pipeline.expire('user:1000:session', 3600);
pipeline.get('user:1000:name');
const results = await pipeline.exec();
// results is an array of [error, result] pairs
console.log(results);
}
# Python with redis-py
import redis
import time
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# Without pipelining
start = time.time()
for i in range(1000):
r.set(f'key:{i}', f'value{i}')
print(f"Without pipelining: {time.time() - start:.3f}s")
# With pipelining
start = time.time()
pipe = r.pipeline()
for i in range(1000):
pipe.set(f'key:{i}', f'value{i}')
pipe.execute()
print(f"With pipelining: {time.time() - start:.3f}s")
Mermaid Diagram:
sequenceDiagram
participant Client
participant Network
participant Redis
rect rgb(255, 200, 200)
Note over Client,Redis: Without Pipelining (Slow)
Client->>Network: SET key1 value1
Network->>Redis: SET key1 value1
Redis->>Network: OK
Network->>Client: OK
Note over Client,Redis: RTT #1
Client->>Network: SET key2 value2
Network->>Redis: SET key2 value2
Redis->>Network: OK
Network->>Client: OK
Note over Client,Redis: RTT #2
Client->>Network: GET key1
Network->>Redis: GET key1
Redis->>Network: value1
Network->>Client: value1
Note over Client,Redis: RTT #3
end
rect rgb(200, 255, 200)
Note over Client,Redis: With Pipelining (Fast)
Client->>Network: SET key1 value1<br/>SET key2 value2<br/>GET key1
Network->>Redis: All 3 commands
Redis->>Redis: Process sequentially
Redis->>Network: OK<br/>OK<br/>value1
Network->>Client: All 3 responses
Note over Client,Redis: Only 1 RTT!
end
References:
↑ Back to topRedis Fundamentals
What is Redis and what are its primary use cases?
The 30-Second Answer: Redis (Remote Dictionary Server) is an open-source, in-memory data structure store used as a database, cache, message broker, and streaming engine. It supports various data structures like strings, hashes, lists, sets, and sorted sets, providing sub-millisecond latency for real-time applications.
The 2-Minute Answer (If They Want More): Redis is fundamentally different from traditional databases because it keeps all data in RAM, allowing it to achieve exceptional performance with millions of operations per second. This makes it ideal for scenarios where speed is critical and data can fit in memory or be managed through eviction policies.
Primary use cases include caching (storing frequently accessed data to reduce database load), session management (storing user session data in web applications), real-time analytics (counting page views, tracking user activity), message queuing (implementing pub/sub patterns for event-driven architectures), leaderboards and ranking systems (using sorted sets), and rate limiting (tracking API request counts). Redis is also used for geospatial data indexing, full-text search with RediSearch, and as a vector database for AI/ML applications.
The versatility comes from Redis's rich data structure support. Unlike simple key-value stores, Redis provides native operations for complex data types, allowing developers to solve problems directly in Redis rather than fetching data and processing it in application code. This reduces network overhead and improves performance.
Code Example:
# Caching example
SET user:1000:profile '{"name":"John","email":"john@example.com"}' EX 3600
# Session management
HSET session:abc123 user_id 1000 last_active 1640000000
EXPIRE session:abc123 1800
# Leaderboard
ZADD game:leaderboard 9500 "player1" 8200 "player2" 7800 "player3"
ZREVRANGE game:leaderboard 0 9 WITHSCORES
# Rate limiting
INCR api:user:1000:requests
EXPIRE api:user:1000:requests 60
Mermaid Diagram:
flowchart TD
A[Application] -->|Fast Read/Write| B[Redis In-Memory]
A -->|Slow Queries| C[Primary Database]
B -->|Cache Miss| C
C -->|Populate Cache| B
B -->|Persistence| D[RDB Snapshots]
B -->|Persistence| E[AOF Log]
F[Pub/Sub Clients] -->|Subscribe| B
B -->|Publish Events| F
References:
↑ Back to topCaching Strategies
What is cache invalidation and what strategies exist?
The 30-Second Answer: Cache invalidation is the process of removing or updating stale data from the cache to ensure consistency with the source of truth. Main strategies include: TTL-based expiration (time-based), explicit invalidation on writes, event-driven invalidation, and tag-based invalidation for related data.
The 2-Minute Answer (If They Want More): Phil Karlton famously said "There are only two hard things in Computer Science: cache invalidation and naming things." Cache invalidation is challenging because you must balance consistency with performance while avoiding race conditions.
TTL-based invalidation is the simplest approach - set an expiration time on cached data and accept that it might be stale until expiry. This works well for data that changes infrequently or where eventual consistency is acceptable. Choose shorter TTLs for frequently changing data and longer TTLs for static content.
Explicit invalidation happens on writes - when data changes, you immediately delete or update the cache. The "delete" approach is safer than "update" because it avoids race conditions where concurrent updates might write stale data to the cache. However, it causes a cache miss on the next read, which might trigger a thundering herd problem if many requests arrive simultaneously.
Event-driven invalidation uses pub/sub or message queues to notify cache instances when data changes. This enables distributed cache invalidation across multiple servers. Tag-based invalidation groups related cache keys (e.g., all user data) so you can invalidate entire categories at once using Redis Sets or with key patterns.
Advanced strategies include versioning (keep multiple versions of cached data), soft deletion (mark as stale but serve until updated), and probabilistic early expiration (refresh cache before expiry based on probability) to reduce thundering herd risk.
Code Example:
class CacheInvalidationStrategies {
constructor(redis) {
this.redis = redis;
}
// 1. TTL-based expiration
async setWithTTL(key, value, seconds) {
await this.redis.setEx(key, seconds, JSON.stringify(value));
}
// 2. Explicit invalidation on write
async updateAndInvalidate(key, updateFn) {
// Update database
const newValue = await updateFn();
// Delete from cache (safer than update)
await this.redis.del(key);
return newValue;
}
// 3. Tag-based invalidation
async setWithTags(key, value, tags, ttl = 3600) {
const pipeline = this.redis.pipeline();
// Store the value
pipeline.setEx(key, ttl, JSON.stringify(value));
// Add key to each tag set
tags.forEach(tag => {
const tagKey = `tag:${tag}`;
pipeline.sAdd(tagKey, key);
pipeline.expire(tagKey, ttl);
});
await pipeline.exec();
}
async invalidateByTag(tag) {
const tagKey = `tag:${tag}`;
// Get all keys with this tag
const keys = await this.redis.sMembers(tagKey);
if (keys.length > 0) {
// Delete all keys and the tag set
await this.redis.del([...keys, tagKey]);
}
}
// 4. Event-driven invalidation (pub/sub)
async publishInvalidation(pattern) {
await this.redis.publish('cache:invalidate', pattern);
}
setupInvalidationListener(callback) {
const subscriber = this.redis.duplicate();
subscriber.subscribe('cache:invalidate');
subscriber.on('message', async (channel, pattern) => {
// Invalidate keys matching pattern
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(keys);
}
callback(pattern, keys);
});
}
// 5. Versioned cache entries
async setVersioned(key, value, ttl = 3600) {
const version = await this.redis.incr(`${key}:version`);
const versionedKey = `${key}:v${version}`;
await this.redis.setEx(versionedKey, ttl, JSON.stringify(value));
await this.redis.setEx(`${key}:current`, ttl, version.toString());
return version;
}
async getVersioned(key) {
const version = await this.redis.get(`${key}:current`);
if (!version) return null;
const versionedKey = `${key}:v${version}`;
const value = await this.redis.get(versionedKey);
return value ? JSON.parse(value) : null;
}
// 6. Probabilistic early expiration
async getWithEarlyExpiration(key, ttl, beta = 1) {
const value = await this.redis.get(key);
if (!value) return null;
const ttlRemaining = await this.redis.ttl(key);
// XFetch algorithm: refresh probabilistically before expiry
const delta = Date.now() / 1000;
const expiry = delta + ttlRemaining;
const shouldRefresh = delta - beta * Math.log(Math.random()) >= expiry;
return {
value: JSON.parse(value),
shouldRefresh
};
}
// 7. Two-tier invalidation (soft delete)
async softDelete(key) {
const value = await this.redis.get(key);
if (value) {
// Mark as stale but keep available
await this.redis.setEx(`${key}:stale`, 60, value);
await this.redis.del(key);
}
}
async getOrStale(key) {
let value = await this.redis.get(key);
if (!value) {
// Try to get stale version
value = await this.redis.get(`${key}:stale`);
if (value) {
return {
value: JSON.parse(value),
isStale: true
};
}
}
return {
value: value ? JSON.parse(value) : null,
isStale: false
};
}
}
// Usage examples
const cache = new CacheInvalidationStrategies(redis);
// TTL-based
await cache.setWithTTL('user:123', userData, 3600);
// Tag-based
await cache.setWithTags(
'user:123:profile',
profileData,
['user:123', 'profiles'],
3600
);
await cache.invalidateByTag('user:123'); // Invalidate all user data
// Event-driven
cache.setupInvalidationListener((pattern, keys) => {
console.log(`Invalidated ${keys.length} keys matching ${pattern}`);
});
await cache.publishInvalidation('user:*');
// Probabilistic early expiration
const result = await cache.getWithEarlyExpiration('user:123', 3600);
if (result && result.shouldRefresh) {
// Refresh cache in background
refreshCacheAsync('user:123');
}
Comparison Table:
| Strategy | Consistency | Complexity | Use Case |
|---|---|---|---|
| TTL-based | Eventual | Low | Frequently accessed, infrequently changed data |
| Explicit Delete | Strong | Medium | Write-heavy workloads, critical consistency |
| Explicit Update | Strong | Medium | Read-heavy, avoid cache miss penalty |
| Tag-based | Strong | High | Related data groups, bulk invalidation |
| Event-driven | Strong | High | Distributed systems, microservices |
| Versioned | Strong | High | Concurrent updates, multiple versions |
| Probabilistic | Eventual | Medium | High traffic, prevent thundering herd |
References:
↑ Back to topSecurity
What is the ACL (Access Control List) system in Redis 6.0+?
The 30-Second Answer: Redis ACL (Access Control List) is a fine-grained permission system introduced in Redis 6.0 that allows you to create multiple users with specific permissions for commands, keys, and channels. It replaces the single-password authentication model with role-based access control.
The 2-Minute Answer (If They Want More): The ACL system in Redis 6.0+ provides sophisticated access control by allowing administrators to define multiple users, each with specific permissions regarding which commands they can execute, which keys they can access, and which Pub/Sub channels they can subscribe to. This is a major improvement over the legacy single-password system, enabling proper multi-tenant deployments and principle of least privilege.
ACL rules can specify command permissions (allow/deny specific commands or categories), key patterns (using glob-style patterns to restrict access to specific keys), and Pub/Sub channel patterns. You can also enable or disable users, set passwords, and define whether a user is active or not. The system uses a simple DSL (Domain Specific Language) for defining rules.
Redis stores ACL configurations in an ACL file (specified by aclfile in redis.conf) that persists user definitions across restarts. You can manage ACLs dynamically using commands like ACL SETUSER, ACL DELUSER, ACL LIST, and ACL WHOAMI. The default user (named "default") is automatically created and initially has full permissions for backward compatibility.
ACLs are particularly valuable in shared hosting environments, microservices architectures where different services should have limited access, and compliance scenarios requiring audit trails and access controls. They also improve security by allowing you to create read-only users, application-specific users with minimal permissions, and administrative users with full access.
Code Example:
# Create a read-only user for analytics
ACL SETUSER analytics on >analyticsPass ~analytics:* +get +scan +keys
# Create an application user with specific command access
ACL SETUSER myapp on >myappPass ~app:* +get +set +del +expire +ttl
# Create an admin user with full access
ACL SETUSER admin on >adminPass ~* +@all
# Create a user that can only publish to specific channels
ACL SETUSER publisher on >pubPass &news:* +publish -@all
# List all ACL rules
ACL LIST
# Show current user
ACL WHOAMI
# Check what a user can do
ACL GETUSER myapp
# Delete a user
ACL DELUSER olduser
# ACL categories examples
# +@read - all read commands
# +@write - all write commands
# +@admin - all admin commands
# +@dangerous - dangerous commands
# +@all - all commands
# Example ACL file entry (aclfile directive)
# user readonly on >readpass ~* +@read -@write -@admin
# user worker on >workpass ~jobs:* +@all -@dangerous
References:
↑ Back to topPersistence
What is RDB (Redis Database) persistence?
The 30-Second Answer: RDB persistence creates point-in-time snapshots of your Redis dataset at specified intervals by dumping the entire dataset to disk as a binary dump.rdb file. It's compact, fast for backups and disaster recovery, but can lose data between snapshots if Redis crashes.
The 2-Minute Answer (If They Want More): RDB (Redis Database) is Redis's snapshot-based persistence mechanism that creates compressed binary dumps of the entire in-memory dataset at configurable intervals. When RDB is triggered (either by meeting save conditions, manually via SAVE/BGSAVE commands, or during shutdown), Redis forks a child process that writes the entire dataset to a temporary file, then atomically replaces the old dump.rdb file once complete.
The key advantage of RDB is performance and compactness. Since it's a single file containing the entire dataset, it's perfect for backups, disaster recovery, and replication. RDB files are also more compact than AOF files and allow Redis to restart faster since loading an RDB file is typically quicker than replaying an AOF log.
However, RDB has a critical trade-off: potential data loss. If Redis crashes between snapshots, all data written since the last snapshot is lost. For example, with "save 900 1" configuration (snapshot every 15 minutes if at least 1 key changed), you could lose up to 15 minutes of writes. Additionally, the fork operation required for RDB can cause brief latency spikes on large datasets, and the process is CPU and I/O intensive during the snapshot creation.
RDB is ideal for scenarios where some data loss is acceptable, where you need efficient backups, or where you're running Redis as a cache rather than a primary data store.
Code Example:
# Redis configuration for RDB persistence (redis.conf)
# Save the DB on disk - format: save <seconds> <changes>
save 900 1 # Save after 900 seconds (15 min) if at least 1 key changed
save 300 10 # Save after 300 seconds (5 min) if at least 10 keys changed
save 60 10000 # Save after 60 seconds if at least 10000 keys changed
# The filename where to dump the DB
dbfilename dump.rdb
# The working directory where the dump file will be saved
dir /var/lib/redis
# Compress string objects using LZF when dumping
rdbcompression yes
# Add a CRC64 checksum at the end of the file
rdbchecksum yes
# Manual snapshot commands
SAVE # Synchronous save (blocks all clients)
BGSAVE # Background save (forks a child process)
LASTSAVE # Get Unix timestamp of last successful save
Mermaid Diagram:
flowchart TD
A[Redis Server] -->|save threshold met| B{Fork Process}
B -->|Parent| C[Continue serving clients]
B -->|Child| D[Write to temp file]
D --> E[dump-temp.rdb]
E -->|Write complete| F[Atomic rename]
F --> G[dump.rdb]
G --> H[Child process exits]
C -->|Normal operations| C
style A fill:#e1f5ff
style G fill:#d4edda
style D fill:#fff3cd
References:
↑ Back to topUse Cases and Patterns
What is the SET NX pattern for distributed locking?
The 30-Second Answer:
SET key value NX PX milliseconds is Redis's atomic command for distributed locking: NX means "only set if not exists" (acquire lock), PX sets expiration in milliseconds (auto-release), and a unique value ensures only the lock holder can release it safely using a Lua script to check ownership before deletion.
The 2-Minute Answer (If They Want More): The SET NX pattern is the foundation for distributed locking in Redis. The command combines three operations atomically: checking if key exists, setting it if it doesn't (NX = "Not eXists"), and setting an expiration time (PX for milliseconds, EX for seconds). This atomicity is crucial—without it, there would be a race condition between check and set operations.
The expiration (TTL) is a critical safety mechanism. If a client acquires a lock and then crashes or experiences network issues without releasing it, the lock would block other clients forever. The automatic expiration ensures locks are eventually released even if the client fails. However, this creates a new challenge: you must choose a TTL long enough to complete your work but short enough to avoid long delays if a client crashes.
The unique value (typically a random string or UUID) ensures lock safety during release. Without it, a client might accidentally release another client's lock in this scenario: Client A acquires lock with 10s TTL, Client A's operation takes 12s (lock expires at 10s), Client B acquires the same lock at 11s, Client A finishes at 12s and deletes the key (releasing Client B's lock). By checking the value matches before deletion using a Lua script, we ensure only the lock owner can release it.
For production use, always use the full pattern: atomic SET NX PX with unique value for acquisition, and atomic check-and-delete (Lua script) for release. Never use separate commands like SETNX + EXPIRE (not atomic) or simple DEL (unsafe). Libraries like Redlock, node-redlock, or ioredis's built-in lock utilities implement this correctly.
Code Example:
const redis = require('redis');
const crypto = require('crypto');
class RedisLock {
constructor(client) {
this.client = client;
}
// Generate unique lock identifier
generateLockId() {
return crypto.randomBytes(16).toString('hex');
}
// Acquire lock using SET NX pattern
async acquire(lockKey, ttlMs = 10000) {
const lockId = this.generateLockId();
// SET key value NX PX milliseconds
// Returns 'OK' if lock acquired, null if key already exists
const result = await this.client.set(lockKey, lockId, {
NX: true, // Only set if Not eXists
PX: ttlMs // Set expiration in milliseconds
});
if (result === 'OK') {
return {
key: lockKey,
id: lockId,
expiresAt: Date.now() + ttlMs
};
}
return null; // Lock acquisition failed
}
// Release lock safely using Lua script
// Only deletes if the value matches (we own the lock)
async release(lock) {
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
const result = await this.client.eval(script, {
keys: [lock.key],
arguments: [lock.id]
});
return result === 1; // true if lock was deleted, false if we didn't own it
}
// Extend lock TTL (useful for long-running operations)
async extend(lock, additionalMs) {
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("pexpire", KEYS[1], ARGV[2])
else
return 0
end
`;
const result = await this.client.eval(script, {
keys: [lock.key],
arguments: [lock.id, additionalMs.toString()]
});
if (result === 1) {
lock.expiresAt = Date.now() + additionalMs;
return true;
}
return false;
}
// Acquire with retry logic
async acquireWithRetry(lockKey, ttlMs = 10000, maxRetries = 5, retryDelayMs = 100) {
for (let i = 0; i < maxRetries; i++) {
const lock = await this.acquire(lockKey, ttlMs);
if (lock) {
return lock;
}
// Wait before retry with exponential backoff
const delay = retryDelayMs * Math.pow(2, i) + Math.random() * 100;
await new Promise(resolve => setTimeout(resolve, delay));
}
throw new Error(`Failed to acquire lock after ${maxRetries} retries: ${lockKey}`);
}
// Execute function with automatic lock management
async withLock(lockKey, fn, options = {}) {
const {
ttl = 10000,
maxRetries = 5,
retryDelay = 100,
autoExtend = false,
extendInterval = 5000
} = options;
const lock = await this.acquireWithRetry(lockKey, ttl, maxRetries, retryDelay);
let extendTimer = null;
if (autoExtend) {
// Automatically extend lock while function is running
extendTimer = setInterval(async () => {
await this.extend(lock, ttl);
}, extendInterval);
}
try {
return await fn();
} finally {
if (extendTimer) {
clearInterval(extendTimer);
}
await this.release(lock);
}
}
}
// Common anti-patterns and their problems
class AntiPatterns {
// ❌ WRONG: Not atomic, race condition between SETNX and EXPIRE
async wrongPattern1(client, key, value, ttl) {
const acquired = await client.setNX(key, value);
if (acquired) {
await client.expire(key, ttl); // If crash happens here, lock never expires!
}
return acquired;
}
// ❌ WRONG: Releasing without checking ownership
async wrongPattern2(client, key) {
await client.del(key); // Might delete someone else's lock!
}
// ❌ WRONG: Using GET + SET (not atomic)
async wrongPattern3(client, key, value, ttl) {
const exists = await client.get(key);
if (!exists) {
await client.set(key, value, { EX: ttl }); // Race condition here!
return true;
}
return false;
}
}
// Example usage scenarios
async function examples() {
const client = redis.createClient();
await client.connect();
const lock = new RedisLock(client);
// Example 1: Basic lock acquisition and release
async function basicExample() {
const myLock = await lock.acquire('resource:inventory:item-123', 5000);
if (myLock) {
try {
// Critical section - only one process can be here
console.log('Processing inventory update...');
await updateInventory('item-123', -1);
} finally {
await lock.release(myLock);
}
} else {
console.log('Could not acquire lock, someone else is processing');
}
}
// Example 2: Using withLock helper
async function helperExample() {
try {
await lock.withLock('resource:payment:user-456', async () => {
console.log('Processing payment...');
await processPayment('user-456');
}, {
ttl: 30000, // 30 second timeout
maxRetries: 3, // Retry 3 times
retryDelay: 200 // Start with 200ms delay
});
} catch (error) {
console.error('Failed to acquire lock:', error);
}
}
// Example 3: Long-running operation with auto-extend
async function autoExtendExample() {
await lock.withLock('resource:report:generate', async () => {
console.log('Generating large report...');
// This might take 60+ seconds
await generateLargeReport();
}, {
ttl: 15000, // Initial 15 seconds
autoExtend: true, // Automatically extend
extendInterval: 10000 // Extend every 10 seconds
});
}
// Example 4: Manual lock extension
async function manualExtendExample() {
const myLock = await lock.acquire('resource:batch:processing', 10000);
if (myLock) {
try {
for (let i = 0; i < 100; i++) {
await processBatchItem(i);
// Extend lock every 10 items
if (i % 10 === 0) {
await lock.extend(myLock, 10000);
}
}
} finally {
await lock.release(myLock);
}
}
}
}
// Helper functions for examples
async function updateInventory(itemId, quantity) {
// Simulate inventory update
await new Promise(resolve => setTimeout(resolve, 100));
}
async function processPayment(userId) {
// Simulate payment processing
await new Promise(resolve => setTimeout(resolve, 500));
}
async function generateLargeReport() {
// Simulate long-running report generation
await new Promise(resolve => setTimeout(resolve, 30000));
}
async function processBatchItem(index) {
// Simulate batch processing
await new Promise(resolve => setTimeout(resolve, 200));
}
Mermaid Diagram:
flowchart TD
A[Client wants Lock] --> B[SET key uuid NX PX ttl]
B --> C{Key exists?}
C -->|No| D[âś“ Set key with TTL<br/>Return OK]
C -->|Yes| E[âś— Key exists<br/>Return null]
D --> F[Client owns lock<br/>uuid stored in key]
F --> G[Perform Critical Work]
G --> H{Check if lock<br/>still valid?}
H -->|Yes| I[Continue work]
H -->|Expired| J[⚠️ Lock expired<br/>Another client may have it]
I --> K[Release Lock]
K --> L[Lua Script:<br/>if GET key == uuid<br/>then DEL key]
L --> M{Value matches?}
M -->|Yes| N[âś“ Delete key<br/>Lock released]
M -->|No| O[âś— Don't delete<br/>Not our lock]
E --> P[Wait/Retry or Fail]
style D fill:#90EE90
style E fill:#FFB6C1
style J fill:#FFA500
style N fill:#90EE90
style O fill:#FFB6C1
References:
↑ Back to topData Types and Structures
What are the basic data types in Redis?
The 30-Second Answer: Redis supports five basic data types: Strings (binary-safe sequences up to 512MB), Lists (linked lists of strings), Sets (unordered collections of unique strings), Sorted Sets (ordered sets with scores), and Hashes (field-value pairs). Additionally, Redis provides specialized types including Bitmaps, HyperLogLogs, Streams, and Geospatial indexes built on top of these fundamental structures.
The 2-Minute Answer (If They Want More): Redis is often called a "data structure server" rather than just a key-value store because it provides rich, native support for complex data types. The five fundamental types each serve specific use cases: Strings are the simplest and most versatile, used for caching, counters, and binary data. Lists maintain insertion order and enable queue/stack implementations. Sets provide O(1) membership testing and set operations like unions and intersections. Sorted Sets combine the uniqueness of sets with ordering capabilities via scores, perfect for leaderboards and priority queues. Hashes efficiently store objects as field-value mappings, reducing memory overhead compared to separate string keys.
Beyond these core types, Redis offers specialized structures built on the primitives: Bitmaps (strings with bit operations) for space-efficient flags and analytics, HyperLogLogs for cardinality estimation with minimal memory, Streams for append-only logs with consumer groups (like Kafka-lite), and Geospatial indexes (sorted sets with geographic operations) for location-based queries. This variety allows Redis to solve diverse problems natively without requiring external data structure implementations.
Understanding these data types is crucial for Redis optimization. Choosing the wrong type can lead to poor performance or excessive memory usage. For example, storing a list of values as a JSON-encoded string requires parsing on every access, while using a native List or Set provides atomic operations and better performance.
Code Example:
# String
SET user:1000:name "John Doe"
GET user:1000:name
# List
LPUSH notifications "New message" "Friend request"
LRANGE notifications 0 -1
# Set
SADD user:1000:tags "developer" "redis" "python"
SMEMBERS user:1000:tags
# Sorted Set
ZADD leaderboard 100 "player1" 200 "player2" 150 "player3"
ZRANGE leaderboard 0 -1 WITHSCORES
# Hash
HSET user:1000 name "John" age "30" city "NYC"
HGETALL user:1000
# Bitmap
SETBIT user:1000:login:2024 0 1
GETBIT user:1000:login:2024 0
# HyperLogLog
PFADD unique_visitors "user1" "user2" "user3"
PFCOUNT unique_visitors
# Stream
XADD events * action "login" user "john"
XRANGE events - +
# Geospatial
GEOADD locations 13.361389 38.115556 "Palermo"
GEORADIUS locations 15 37 200 km
References:
↑ Back to topMonitoring and Administration
What are the important Redis metrics to monitor?
The 30-Second Answer: Monitor memory usage (used_memory, maxmemory), performance metrics (ops/sec, latency), persistence status (last_save_time, aof_rewrite_in_progress), replication lag, connected clients, keyspace statistics, and eviction counters. These metrics help identify bottlenecks, memory issues, and replication delays before they impact production.
The 2-Minute Answer (If They Want More):
Memory Metrics are critical: used_memory shows actual memory consumption, used_memory_rss shows OS-level memory, and mem_fragmentation_ratio (RSS/used) indicates fragmentation - values below 1.0 suggest swapping, above 1.5 suggests fragmentation. Monitor evicted_keys to detect if maxmemory is being hit.
Performance Metrics include instantaneous_ops_per_sec for throughput, latency measurements, and keyspace_hits/keyspace_misses ratio for cache effectiveness. A low hit rate indicates poor cache utilization. Track total_commands_processed and rejected_connections for load patterns.
Persistence and Replication metrics ensure data durability: rdb_last_save_time and rdb_changes_since_last_save for RDB snapshots, aof_current_size and aof_rewrite_in_progress for AOF. For replicas, monitor master_link_status, master_last_io_seconds_ago, and master_repl_offset vs slave_repl_offset to detect replication lag.
Resource Metrics like connected_clients, blocked_clients, total_connections_received, and rejected_connections help identify connection pool issues. CPU usage (used_cpu_sys, used_cpu_user) and network I/O also matter for capacity planning.
Key Metrics Table:
| Metric | Category | Normal Range | Alert If |
|---|---|---|---|
| mem_fragmentation_ratio | Memory | 1.0-1.5 | <1.0 or >1.5 |
| keyspace_hit_rate | Performance | >90% | <80% |
| connected_clients | Connections | Varies | Near maxclients |
| evicted_keys | Memory | 0 | >0 (sustained) |
| master_last_io_seconds_ago | Replication | 0-1 | >3 |
| used_memory_rss | Memory | < maxmemory | >90% of RAM |
Code Example:
# Get key metrics
INFO memory
INFO stats
INFO replication
INFO clients
# Monitor real-time operations
MONITOR
# Get specific stat
INFO stats | grep instantaneous_ops_per_sec
# Check slow queries
SLOWLOG GET 10
# Get memory usage by key pattern
MEMORY USAGE mykey
References:
↑ Back to topTroubleshooting
How do you diagnose Redis memory issues?
The 30-Second Answer:
Use INFO memory to check memory usage, MEMORY DOCTOR for automated diagnosis, and MEMORY STATS for detailed allocation breakdown. Monitor the used_memory_rss vs used_memory ratio to detect fragmentation, and use MEMORY MALLOC-STATS to examine allocator-level details.
The 2-Minute Answer (If They Want More):
Redis memory issues typically manifest as high memory usage, fragmentation, or OOM errors. The diagnostic process starts with INFO memory which provides overall metrics like used_memory (logical memory), used_memory_rss (physical RSS), and mem_fragmentation_ratio. A fragmentation ratio above 1.5 indicates significant fragmentation requiring attention.
The MEMORY DOCTOR command (Redis 4.0+) provides automated analysis and recommendations based on current memory state. It examines fragmentation, eviction policies, and usage patterns to suggest specific remediation steps. For deeper analysis, MEMORY STATS breaks down allocation by type, showing overhead from different data structures.
Use MEMORY MALLOC-STATS to access jemalloc/libc statistics and understand allocator behavior. The MEMORY PURGE command can help reclaim fragmented memory by forcing the allocator to release unused pages back to the OS. For key-level analysis, MEMORY USAGE <key> shows the exact memory footprint of individual keys, helping identify memory-hogging entries.
Set up monitoring for key metrics: used_memory_peak, total_system_memory, maxmemory, and eviction counters (evicted_keys). Track these over time to identify trends before they become critical issues. Consider enabling activedefrag (Redis 4.0+) to automatically combat fragmentation in real-time.
Code Example:
# Basic memory diagnostics
INFO memory
# Automated diagnosis with recommendations
MEMORY DOCTOR
# Detailed memory allocation breakdown
MEMORY STATS
# Check specific key memory usage
MEMORY USAGE mykey SAMPLES 5
# Examine memory fragmentation details
MEMORY MALLOC-STATS
# Force memory purge to reclaim fragmented memory
MEMORY PURGE
# Check top memory consumers
MEMORY USAGE mylist
MEMORY USAGE myhash
MEMORY USAGE myzset
# Monitor eviction activity
INFO stats | grep evicted_keys
Common Memory Issues and Solutions:
| Issue | Symptom | Diagnostic | Solution |
|---|---|---|---|
| High fragmentation | mem_fragmentation_ratio > 1.5 |
INFO memory, MEMORY STATS |
Enable activedefrag, restart Redis, adjust allocator |
| Memory leak | Continuously growing used_memory |
MEMORY STATS, key sampling |
Identify growing keys, check TTLs, audit application |
| Eviction pressure | High evicted_keys counter |
INFO stats, maxmemory-policy |
Increase maxmemory, optimize data, review eviction policy |
| Large keys | OOM on specific operations | MEMORY USAGE <key>, --bigkeys |
Refactor data model, split large structures |
References:
↑ Back to top