Redis Explained: A Developer's Practical Guide to Caching and Beyond (2026)
Redis is the most loved database by developers, and it's not hard to see why. It's fast (microsecond latency), versatile (caching, queues, real-time analytics), and remarkably simple to use. If you're building web applications, Redis is almost certainly in your future.
This guide covers everything from Redis basics to advanced patterns used in production.
What Is Redis?
Redis (Remote Dictionary Server) is an in-memory data structure store. Think of it as a hashmap that lives in RAM — incredibly fast, with optional persistence to disk.
Key characteristics:
- In-memory: Data lives in RAM, not disk (microsecond latency)
- Single-threaded: No locks or race conditions for individual commands
- Versatile: Strings, hashes, lists, sets, sorted sets, streams, and more
- Optional persistence: RDB snapshots and/or AOF (Append-Only File)
- Atomic operations: Multi-command transactions with WATCH/MULTI/EXEC
Installing Redis
# Ubuntu/Debian
sudo apt install redis-server
redis-cli ping # Should return "PONG"
# Docker (recommended for development)
docker run --name redis -p 6379:6379 -d redis:7-alpine
# macOS
brew install redis
brew services start redis
# Redis Stack (includes modules like RedisSearch, RedisJSON)
docker run --name redis-stack -p 6379:6379 -p 8001:8001 -d redis/redis-stack
Redis Data Structures
1. Strings (Most Common)
# Set and get
SET user:1001:name "John Doe"
GET user:1001:name # "John Doe"
# Set with expiration (seconds)
SET session:abc123 "user_data" EX 3600 # Expires in 1 hour
TTL session:abc123 # 3599 (seconds remaining)
# Increment counters
INR page:home:views # 1
INR page:home:views # 2
INRBY page:home:views 10 # 12
# Append to string
APPEND log:2024-01-01 "New log entry\n"
2. Hashes (Objects)
# Store object-like data
HSET user:1001 username "johndoe" email "john@example.com" age 30 plan "pro"
# Get specific fields
HGET user:1001 email # "john@example.com"
# Get all fields
HGETALL user:1001
# username: "johndoe"
# email: "john@example.com"
# age: "30"
# plan: "pro"
# Update specific field
HSET user:1001 age 31
# Increment numeric field
HINCRBY user:1001 age 1
# Check if field exists
HEXISTS user:1001 email # 1 (true)
3. Lists (Queues)
# Push to list (queue)
LPUSH tasks "send_email" "process_payment" # Left push (stack)
RPUSH notifications "user_liked_post" # Right push (queue)
# Pop from list
LPOP tasks # "process_payment" (LIFO)
RPOP tasks # "send_email" (FIFO from other end)
# Range (view without removing)
LRANGE tasks 0 -1 # All items
LRANGE tasks 0 2 # First 3 items
# Length
LLEN tasks
# Blocking pop (wait for items — great for job queues)
BRPOP task_queue 30 # Wait up to 30 seconds for an item
4. Sets (Unique Collections)
# Add members
SADD user:1001:skills "python" "javascript" "docker"
# Check membership
SISMEMBER user:1001:skills "python" # 1 (true)
# Get all members
SMEMBERS user:1001:skills
# Set operations
SADD user:1002:skills "python" "rust" "kubernetes"
SINTER user:1001:skills user:1002:skills # Intersection: "python"
SUNION user:1001:skills user:1002:skills # Union: all unique skills
SDIFF user:1001:skills user:1002:skills # Difference: "javascript" "docker"
5. Sorted Sets (Leaderboards)
# Add with score
ZADD leaderboard 1500 "alice" 2800 "bob" 3200 "charlie"
# Get top 3 (descending)
ZREVRANGE leaderboard 0 2 WITHSCORES
# "charlie" "3200"
# "bob" "2800"
# "alice" "1500"
# Get rank
ZREVRANK leaderboard "bob" # 1 (second place)
# Increment score
ZINCRBY leaderboard 500 "alice" # alice now has 2000
# Range by score
ZRANGEBYSCORE leaderboard 2000 3000 # "alice" "bob"
Common Caching Patterns
Pattern 1: Cache-Aside (Lazy Loading)
The most common caching strategy:
import json
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
async def get_user(user_id: int):
# 1. Check cache first
cache_key = f"user:{user_id}"
cached = r.get(cache_key)
if cached:
return json.loads(cached) # Cache hit!
# 2. Cache miss — fetch from database
user = await db.fetch_user(user_id)
if not user:
return None
# 3. Store in cache with TTL
r.setex(cache_key, 3600, json.dumps(user)) # 1 hour TTL
return user
Pattern 2: Write-Through
Update cache when database changes:
async def update_user(user_id: int, data: dict):
# 1. Update database
user = await db.update_user(user_id, data)
# 2. Update cache
cache_key = f"user:{user_id}"
r.setex(cache_key, 3600, json.dumps(user))
return user
async def delete_user(user_id: int):
# 1. Delete from database
await db.delete_user(user_id)
# 2. Delete from cache
r.delete(f"user:{user_id}")
Pattern 3: Rate Limiting
def rate_limit(identifier: str, limit: int = 100, window: int = 60):
"""Limit to `limit` requests per `window` seconds."""
key = f"rate_limit:{identifier}"
current = r.incr(key)
if current == 1:
# First request in window — set expiration
r.expire(key, window)
if current > limit:
return False # Rate exceeded
return True
# Usage
if not rate_limit("user:1001", limit=100, window=60):
raise HTTPException(429, "Rate limit exceeded")
Pattern 4: Distributed Locking
import uuid
def acquire_lock(lock_name: str, timeout: int = 10) -> str | None:
"""Acquire a distributed lock. Returns lock token or None."""
token = str(uuid.uuid4())
acquired = r.set(lock_name, token, nx=True, ex=timeout)
if acquired:
return token
return None
def release_lock(lock_name: str, token: str) -> bool:
"""Release lock only if we own it."""
# Use Lua script for atomic check-and-delete
script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
return bool(r.eval(script, 1, lock_name, token))
# Usage
lock_token = acquire_lock("process:order:1001", timeout=30)
if not lock_token:
raise HTTPException(409, "Another process is handling this order")
try:
process_order(1001)
finally:
release_lock("process:order:1001", lock_token)
Pattern 5: Session Store
import secrets
def create_session(user_id: int) -> str:
"""Create a session and store in Redis."""
session_token = secrets.token_urlsafe(32)
session_data = json.dumps({
"user_id": user_id,
"created_at": int(time.time()),
})
r.setex(f"session:{session_token}", 86400, session_data) # 24 hour expiry
return session_token
def get_session(session_token: str) -> dict | None:
data = r.get(f"session:{session_token}")
if data:
return json.loads(data)
return None
def delete_session(session_token: str):
r.delete(f"session:{session_token}")
Pub/Sub (Real-Time Messaging)
Redis Pub/Sub is perfect for real-time notifications and chat:
# Publisher
def publish_message(channel: str, message: str):
r.publish(channel, message)
# publish_message("user:1001:notifications", "You have a new message!")
# Subscriber (in a separate process/thread)
def subscribe(channel: str):
pubsub = r.pubsub()
pubsub.subscribe(channel)
for message in pubsub.listen():
if message['type'] == 'message':
print(f"Received: {message['data']}")
# subscribe("user:1001:notifications")
Redis Streams (Event Sourcing)
Streams are Redis's most powerful data structure for event logging:
# Add events to stream
XADD orders * order_id 1001 user_id 200 status created
XADD orders * order_id 1002 user_id 201 status created
# Read events
XREAD COUNT 10 STREAMS orders 0 # Read from beginning
XRANGE orders - + # All events
XLEN orders # Total events
# Consumer groups (for distributed processing)
XGROUP CREATE orders order_processors $
XREADGROUP GROUP order_processors worker-1 COUNT 10 STREAMS orders >
# Python consumer group example
def process_orders():
# Create consumer group (run once)
try:
r.xgroup_create("orders", "processors", "$")
except redis.ResponseError:
pass # Group already exists
while True:
# Read new messages
messages = r.xreadgroup(
"processors", "worker-1",
{"orders": ">"},
count=10, block=5000
)
for stream, msg_list in messages:
for msg_id, data in msg_list:
process_order(data)
r.xack("orders", "processors", msg_id) # Acknowledge
Performance Optimization
Use Pipelines (Batch Commands)
# BAD: 3 round trips
r.set("key1", "value1")
r.set("key2", "value2")
r.set("key3", "value3")
# GOOD: 1 round trip
pipe = r.pipeline()
pipe.set("key1", "value1")
pipe.set("key2", "value2")
pipe.set("key3", "value3")
pipe.execute()
Use Connection Pooling
# Connection pool (reuse connections)
pool = redis.ConnectionPool(host='localhost', port=6379, max_connections=20)
r = redis.Redis(connection_pool=pool)
Set Memory Limits and Eviction Policies
# redis.conf
maxmemory 256mb
maxmemory-policy allkeys-lru # Evict least recently used keys
# Common policies:
# allkeys-lru: Evict least recently used (good for caching)
# volatile-lru: Evict LRU among keys with TTL set
# allkeys-lfu: Evict least frequently used
# noeviction: Return errors when memory is full
Monitoring
# Real-time stats
redis-cli INFO memory
redis-cli INFO stats
redis-cli INFO clients
# Monitor commands in real-time (useful for debugging)
redis-cli MONITOR
# Slow log (queries > 10ms)
redis-cli SLOWLOG GET 10
# Check memory usage of specific keys
redis-cli MEMORY USAGE user:1001
Production Deployment
Docker Compose with Redis
services:
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
app:
build: .
environment:
- REDIS_URL=redis://redis:6379
depends_on:
redis:
condition: service_healthy
volumes:
redis_data:
Redis Sentinel (High Availability)
For production systems that can't tolerate downtime:
# Sentinel monitors a master and automatically promotes a replica if master fails
sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
Conclusion
Redis is more than a cache — it's a versatile data platform. Start with caching (the most common use case), then explore queues, rate limiting, sessions, and real-time features as you need them.
The key Redis patterns every developer should know:
- Cache-aside for database query caching
- Rate limiting for API protection
- Session storage for web applications
- Pub/Sub for real-time notifications
- Sorted sets for leaderboards and rankings
Redis is fast, reliable, and has excellent client libraries in every language. If you're not using Redis yet, you're probably solving problems the hard way.