Redis is a fast in-memory data store used for caching, sessions, queues, pub/sub, leaderboards, and rate limiting. This reference covers every data structure and the patterns that matter in production.
Quick reference
The 25 commands that cover 90% of daily Redis work.
| Command | What it does |
|---|---|
SET key value |
Store a string value |
GET key |
Retrieve a string value |
SET key value EX 60 |
Store with 60-second TTL |
DEL key |
Delete a key |
EXISTS key |
Check if key exists (returns 0/1) |
EXPIRE key 300 |
Set TTL in seconds |
TTL key |
Remaining seconds (-1=no TTL, -2=gone) |
KEYS pattern |
Find keys matching pattern (avoid in prod) |
SCAN 0 MATCH user:* COUNT 100 |
Safe key iteration |
HSET hash field value |
Set hash field |
HGET hash field |
Get hash field |
HGETALL hash |
Get all fields and values |
LPUSH list value |
Prepend to list |
RPOP list |
Remove and return last element |
SADD set member |
Add to set |
SMEMBERS set |
Get all set members |
ZADD zset score member |
Add to sorted set |
ZRANGE zset 0 -1 WITHSCORES |
Get sorted set range |
INCR counter |
Atomic increment by 1 |
INCRBY counter 5 |
Atomic increment by N |
MSET k1 v1 k2 v2 |
Set multiple keys at once |
MGET k1 k2 |
Get multiple keys at once |
PUBLISH channel msg |
Publish to channel |
SUBSCRIBE channel |
Subscribe to channel |
FLUSHDB |
Delete all keys in current DB (⚠ irreversible) |
Strings
Strings are the foundation — any binary data up to 512 MB.
# Basic set / get
SET user:1:name "Alice"
GET user:1:name # "Alice"
# With TTL (seconds)
SET session:abc token123 EX 3600
TTL session:abc # 3599
# NX — only set if key does NOT exist (distributed lock pattern)
SET lock:resource unique_token NX EX 30
# returns OK if acquired, nil if already locked
# XX — only set if key DOES exist
SET user:1:name "Bob" XX
# Integers — atomic operations
SET page:views 0
INCR page:views # 1
INCRBY page:views 5 # 6
DECR page:views # 5
DECRBY page:views 2 # 3
# Floats
SET price 19.99
INCRBYFLOAT price 0.01 # 20.00
# Append
APPEND log "2026-07-13 boot\n"
# Get length
STRLEN user:1:name # 3
Hashes
Hashes store field-value maps — ideal for objects.
# Set fields
HSET user:1 name "Alice" email "alice@example.com" age 30
# Get a single field
HGET user:1 name # "Alice"
# Get all fields + values
HGETALL user:1
# 1) "name"
# 2) "Alice"
# 3) "email"
# 4) "alice@example.com"
# 5) "age"
# 6) "30"
# Get multiple fields
HMGET user:1 name email
# Check if field exists
HEXISTS user:1 email # 1
# Delete a field
HDEL user:1 age
# Count fields
HLEN user:1 # 2
# Increment a numeric field
HINCRBY user:1 score 10
HINCRBYFLOAT user:1 balance 5.50
# Iterate fields safely
HSCAN user:1 0 COUNT 10
Tip: Use hashes instead of storing JSON strings when you need to update individual fields without re-serialising the whole object.
Lists
Lists are doubly-linked — O(1) push/pop at both ends. Use for queues, recent activity, message log.
# Push
LPUSH tasks "task-1" "task-2" # prepend (task-2 is now head)
RPUSH tasks "task-3" # append
# Pop
LPOP tasks # remove + return head
RPOP tasks # remove + return tail
# Blocking pop (waits up to 5s)
BLPOP queue 5
# Peek without removing
LINDEX tasks 0 # first element
LINDEX tasks -1 # last element
# Range
LRANGE tasks 0 -1 # all elements
LRANGE tasks 0 4 # first 5
# Length
LLEN tasks
# Trim to last 1000 items
LTRIM log 0 999
# Remove elements
LREM tasks 0 "task-1" # remove all occurrences
Queue pattern (producer → consumer):
# Producer
RPUSH jobs:email '{"to":"alice@example.com","subject":"Hi"}'
# Consumer (blocking)
BRPOPLPUSH jobs:email jobs:processing 0
# atomically moves item to processing list
# replay-safe — item stays until you explicitly delete it
Sets
Sets are unordered collections of unique strings. Use for tags, unique visitors, friend lists.
# Add members
SADD tags:post:1 "redis" "caching" "backend"
# Check membership
SISMEMBER tags:post:1 "redis" # 1 (true)
SMISMEMBER tags:post:1 "redis" "nosql" # 1, 0
# Count
SCARD tags:post:1 # 3
# Get all
SMEMBERS tags:post:1
# Remove
SREM tags:post:1 "caching"
# Pop a random member
SPOP tags:post:1
# Set operations
SUNION set1 set2 # union
SINTER set1 set2 # intersection
SDIFF set1 set2 # difference (in set1, not set2)
# Store result
SUNIONSTORE dest set1 set2
SINTERSTORE dest set1 set2
Sorted Sets (ZSet)
Members with a float score — automatically ordered. Use for leaderboards, rate limiting by time, priority queues.
# Add with score
ZADD leaderboard 1500 "alice"
ZADD leaderboard 2200 "bob"
ZADD leaderboard 1800 "carol"
# Get by rank (0-indexed, lowest to highest)
ZRANGE leaderboard 0 -1 WITHSCORES
# Get by rank (highest to lowest)
ZREVRANGE leaderboard 0 2 WITHSCORES
# Get rank of a member
ZRANK leaderboard "alice" # 0 (lowest)
ZREVRANK leaderboard "alice" # 2 (highest)
# Get score
ZSCORE leaderboard "alice" # 1500.0
# Count members
ZCARD leaderboard
# Range by score
ZRANGEBYSCORE leaderboard 1000 2000 WITHSCORES
ZRANGEBYSCORE leaderboard -inf +inf LIMIT 0 10 # paginated
# Increment score
ZINCRBY leaderboard 100 "alice" # 1600
# Remove
ZREM leaderboard "bob"
# Remove by rank or score
ZREMRANGEBYRANK leaderboard 0 1
ZREMRANGEBYSCORE leaderboard -inf 1000
# Sliding window rate limit (member = request ID, score = timestamp)
ZADD rate:user:1 1721000000 "req-abc"
ZREMRANGEBYSCORE rate:user:1 -inf 1720996400 # remove older than 1h
ZCARD rate:user:1 # count in window
Key management
# Check type
TYPE user:1 # hash
TYPE tasks # list
# Rename
RENAME old_key new_key
RENAMENX old_key new_key # only if new_key doesn't exist
# Copy (Redis 6.2+)
COPY src dst
# Persist (remove TTL)
PERSIST session:abc
# Object encoding (internal representation)
OBJECT ENCODING mykey
# Dump / restore
DUMP user:1 # serialized
RESTORE user:99 0 <dump>
# Safe key scanning (use instead of KEYS in production)
SCAN 0 MATCH user:* COUNT 100
# Returns cursor + results; repeat until cursor == 0
Expiry patterns
# Set TTL on existing key
EXPIRE key 3600 # seconds
PEXPIRE key 3600000 # milliseconds
# Set absolute expiry
EXPIREAT key 1721000000 # Unix timestamp (seconds)
PEXPIREAT key 1721000000000 # milliseconds
# Check TTL
TTL key # seconds remaining (-1 = no TTL, -2 = key missing)
PTTL key # milliseconds remaining
# Remove TTL
PERSIST key
Pub / Sub
Redis pub/sub is fire-and-forget — no message persistence. Use for real-time notifications; use Streams for durable queues.
# Subscribe (blocks, waiting for messages)
SUBSCRIBE news sports
# Subscribe with pattern
PSUBSCRIBE news.*
# Publish (from another connection)
PUBLISH news "Breaking: Redis 8 released"
# List active channels
PUBSUB CHANNELS news.*
PUBSUB NUMSUB news # subscriber count
Node.js example:
import { createClient } from 'redis';
const sub = createClient();
const pub = createClient();
await sub.connect();
await pub.connect();
await sub.subscribe('alerts', (message) => {
console.log('Received:', message);
});
await pub.publish('alerts', JSON.stringify({ level: 'warn', msg: 'High CPU' }));
Streams (Redis 5+)
Streams are durable, append-only logs with consumer groups — like Kafka, built into Redis.
# Append entry (auto-ID = timestamp-sequence)
XADD events * type "click" user "alice"
# Returns ID like "1721000000000-0"
# Read latest 10 entries
XRANGE events - + COUNT 10
# Read from ID onward
XRANGE events 1721000000000-0 +
# Read in reverse
XREVRANGE events + - COUNT 5
# Consumer group
XGROUP CREATE events mygroup $ MKSTREAM
# Read as consumer (never-delivered messages)
XREADGROUP GROUP mygroup consumer1 COUNT 10 STREAMS events >
# Acknowledge processed
XACK events mygroup 1721000000000-0
# Pending messages (unacked)
XPENDING events mygroup - + 10
# Stream length
XLEN events
# Trim to last 1000
XTRIM events MAXLEN ~ 1000
Transactions
Redis transactions execute atomically but do not roll back on errors.
# MULTI / EXEC block
MULTI
SET balance 100
DECRBY balance 30
EXEC
# All commands run, or none (if DISCARD called)
# DISCARD cancels the queued transaction
MULTI
SET balance 100
DISCARD
# WATCH — optimistic locking
WATCH balance
MULTI
DECRBY balance 30
EXEC
# Returns nil if 'balance' changed between WATCH and EXEC
Node.js transaction:
const result = await client
.multi()
.set('balance', 100)
.decrBy('balance', 30)
.get('balance')
.exec();
// result = [null, 70, '70']
Lua scripting
Lua scripts run atomically — replace MULTI/EXEC when you need conditional logic.
# Run inline script (KEYS[1] = key, ARGV[1] = value)
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
# Atomic get-and-delete
EVAL "
local val = redis.call('GET', KEYS[1])
redis.call('DEL', KEYS[1])
return val
" 1 session:abc
# Load script, get SHA
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# Returns SHA: abc123...
# Run cached script
EVALSHA abc123 1 mykey
Common patterns
Cache-aside (most common)
async function getUser(id) {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await db.findUser(id);
await redis.setEx(`user:${id}`, 3600, JSON.stringify(user));
return user;
}
Distributed lock
const token = crypto.randomUUID();
const acquired = await redis.set('lock:resource', token, {
NX: true, // only if not exists
EX: 30, // 30 second TTL
});
if (!acquired) throw new Error('Could not acquire lock');
try {
// ... do work ...
} finally {
// Release only if we own the lock (Lua for atomicity)
await redis.eval(`
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
end
return 0
`, 1, 'lock:resource', token);
}
Rate limiter (sliding window)
async function isAllowed(userId, limitPerMinute) {
const now = Date.now();
const window = 60_000;
const key = `rate:${userId}`;
const pipe = redis.multi();
pipe.zRemRangeByScore(key, 0, now - window);
pipe.zCard(key);
pipe.zAdd(key, { score: now, value: `${now}-${Math.random()}` });
pipe.expire(key, 60);
const results = await pipe.exec();
const count = results[1];
return count < limitPerMinute;
}
Session store
// Save session
await redis.setEx(`session:${sessionId}`, 86400, JSON.stringify(sessionData));
// Get session
const data = await redis.get(`session:${sessionId}`);
if (data) await redis.expire(`session:${sessionId}`, 86400); // rolling TTL
// Destroy session
await redis.del(`session:${sessionId}`);
Persistence
| Mode | Config | Durability | Performance |
|---|---|---|---|
| No persistence | save "" |
None (pure cache) | Fastest |
| RDB snapshots | save 900 1 |
Last snapshot | Fast |
| AOF (append-only) | appendonly yes |
Near zero loss | Slightly slower |
| RDB + AOF | Both enabled | Best | Moderate |
# redis.conf — recommended production settings
appendonly yes
appendfsync everysec # fsync every 1 second (good balance)
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# Manual save
BGSAVE # background RDB snapshot
BGREWRITEAOF # background AOF rewrite
LASTSAVE # Unix timestamp of last successful save
Server & monitoring
# Server info (sections: server/clients/memory/stats/replication)
INFO
INFO memory
INFO replication
# Real-time command monitoring (dev only — high overhead)
MONITOR
# Slow log
SLOWLOG GET 10 # last 10 slow commands
SLOWLOG RESET
# Client list
CLIENT LIST
# Kill a client
CLIENT KILL ID 42
# Config at runtime
CONFIG GET maxmemory
CONFIG SET maxmemory 512mb
CONFIG SET maxmemory-policy allkeys-lru
# Database selection (0–15 by default)
SELECT 1
MOVE key 0 # move key to DB 0
# Flush
FLUSHDB # current DB
FLUSHALL # all DBs ⚠ DANGER in production
Memory policies
When Redis reaches maxmemory, the eviction policy controls what happens:
| Policy | Behaviour |
|---|---|
noeviction |
Return error on write (default) |
allkeys-lru |
Evict least-recently-used from all keys |
volatile-lru |
Evict LRU from keys with TTL |
allkeys-lfu |
Evict least-frequently-used from all keys |
volatile-lfu |
Evict LFU from keys with TTL |
allkeys-random |
Evict random key |
volatile-random |
Evict random key with TTL |
volatile-ttl |
Evict key closest to expiry |
Cache recommendation: Use
allkeys-lru. Every key is evictable; no wasted memory from forgotten non-expiring keys.
redis-cli tips
# Connect
redis-cli
redis-cli -h 127.0.0.1 -p 6379 -a password
redis-cli -u redis://user:pass@host:6379/0
# Pipe commands from file
redis-cli < commands.txt
# Inline execution
redis-cli GET user:1
# Latency test
redis-cli --latency
redis-cli --latency-history
# Memory usage of a key
redis-cli MEMORY USAGE user:1
# Find big keys
redis-cli --bigkeys
# Scan keys interactively
redis-cli --scan --pattern "user:*"
Common mistakes
| Mistake | Why it hurts | Fix |
|---|---|---|
KEYS * in production |
Blocks server for seconds on large datasets | Use SCAN with COUNT |
| No TTL on cached data | Memory fills up; stale data forever | Always set EX/EXPIREAT |
| Storing JSON blobs when fields differ | Re-read + re-write entire blob for one field update | Use Hash for mutable objects |
MULTI/EXEC for conditional logic |
No rollback; race conditions possible | Use Lua scripting |
SELECT to separate concerns |
Shares memory and connection pool; hard to monitor | Use separate Redis instances |
| Large lists as queues | No consumer groups; lost on crash | Use Streams for durable queues |
| Synchronous ops in hot path | Latency spikes | Use pipelines; batch operations |
FAQ
What's the difference between Redis and Memcached?
Both cache in memory. Redis adds persistence, Lua scripting, pub/sub, streams, sorted sets, and cluster support. Memcached is simpler and slightly faster for pure string caching.
Can Redis lose data?
Yes, if persistence is off and the server restarts. With AOF + appendfsync everysec you lose at most 1 second of writes. With always you lose nothing but writes are slower.
When should I use Streams vs Pub/Sub?
Pub/Sub: fire-and-forget real-time events (online users, live notifications). Streams: durable message queue where consumers can replay, acknowledge, and process at their own pace.
How do I handle a Redis cluster?
Use client-side cluster support (ioredis, redis-py, go-redis all support it). Keys are sharded across 16,384 hash slots. Multi-key operations require keys in the same slot — use hash tags {user:1}:name and {user:1}:email to colocate.
What is pipelining?
Sending multiple commands at once without waiting for each reply. Reduces round-trip overhead:
const pipe = redis.pipeline();
pipe.set('a', 1);
pipe.set('b', 2);
pipe.get('a');
const results = await pipe.exec();
How do I debug slow commands?
Set slowlog-log-slower-than 10000 (10ms in microseconds) in redis.conf. Then SLOWLOG GET 25 shows the worst offenders with their arguments and execution time.