Complete Guide to Redis: Beyond Simple Caching
Redis is more than a cache. Master pub/sub, streams, sorted sets, rate limiting, sessions, job queues, and geospatial queries with practical examples.
Redis Is Not Just a Cache
Most developers learn Redis as a key-value cache: SET, GET, EXPIRE. That scratches about 20% of what Redis can do. Redis is an in-memory data structure server with pub/sub messaging, streams, sorted sets, HyperLogLog, geospatial indexes, and Lua scripting.
<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 180" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="180" rx="12" fill="#1a1a2e"/><ellipse cx="150" cy="55" rx="60" ry="18" fill="#6366f1" opacity="0.8"/><rect x="90" y="55" width="120" height="50" fill="#6366f1" opacity="0.8"/><ellipse cx="150" cy="105" rx="60" ry="18" fill="#6366f1" opacity="0.9"/><text x="150" y="85" text-anchor="middle" fill="#ffffff" font-size="12" font-family="system-ui" font-weight="bold">Primary</text><text x="150" y="140" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui">Read + Write</text><ellipse cx="400" cy="30" rx="50" ry="14" fill="#a855f7" opacity="0.7"/><rect x="350" y="30" width="100" height="35" fill="#a855f7" opacity="0.7"/><ellipse cx="400" cy="65" rx="50" ry="14" fill="#a855f7" opacity="0.8"/><text x="400" y="52" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Replica 1</text><ellipse cx="400" cy="110" rx="50" ry="14" fill="#a855f7" opacity="0.7"/><rect x="350" y="110" width="100" height="35" fill="#a855f7" opacity="0.7"/><ellipse cx="400" cy="145" rx="50" ry="14" fill="#a855f7" opacity="0.8"/><text x="400" y="132" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Replica 2</text><defs><marker id="arrow8" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#2dd4bf"/></marker></defs><path d="M212,65 Q280,30 348,48" stroke="#2dd4bf" stroke-width="1.5" fill="none" marker-end="url(#arrow8)"/><path d="M212,90 Q280,130 348,128" stroke="#2dd4bf" stroke-width="1.5" fill="none" marker-end="url(#arrow8)"/><text x="280" y="55" text-anchor="middle" fill="#2dd4bf" font-size="9" font-family="system-ui">WAL stream</text><text x="280" y="130" text-anchor="middle" fill="#2dd4bf" font-size="9" font-family="system-ui">WAL stream</text><text x="500" y="52" text-anchor="start" fill="#94a3b8" font-size="9" font-family="system-ui">Read-only</text><text x="500" y="132" text-anchor="start" fill="#94a3b8" font-size="9" font-family="system-ui">Read-only</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Database replication: the primary handles writes while replicas serve read queries via WAL streaming.</p></div>
At TechSaaS, our shared Redis instance (just 4MB of RAM for the container) handles caching, session management, rate limiting, and real-time messaging across a dozen services.
Data Structures You Should Know
Strings (The Basics)
SET user:1001:name "Alice"
GET user:1001:name # "Alice"
# Atomic counter
INCR page:home:views # 1
INCR page:home:views # 2
# Set with expiration
SETEX session:abc123 3600 '{"userId": 1001}'
# Set only if not exists (distributed lock)
SET lock:resource:42 "owner-1" NX EX 30Hashes (Object Storage)
HSET user:1001 name "Alice" email "[email protected]" plan "pro"
HGET user:1001 name # "Alice"
HGETALL user:1001 # All fields and values
HINCRBY user:1001 login_count 1 # Atomic field incrementPerfect for storing objects without serialization overhead.
Lists (Queues and Stacks)
# Queue (FIFO)
RPUSH queue:emails "msg1" "msg2" "msg3"
LPOP queue:emails # "msg1"
# Blocking pop (waits for new items)
BLPOP queue:emails 30 # Wait up to 30 seconds
# Recent items (keep last 100)
LPUSH recent:activity "user logged in"
LTRIM recent:activity 0 99Sorted Sets (Leaderboards and Rankings)
# Add scores
ZADD leaderboard 1500 "alice" 2200 "bob" 1800 "charlie"
# Top 3 players
ZREVRANGE leaderboard 0 2 WITHSCORES
# bob: 2200, charlie: 1800, alice: 1500
# Rank of a player
ZREVRANK leaderboard "alice" # 2 (0-indexed)
# Increment score
ZINCRBY leaderboard 500 "alice" # alice now 2000Sets (Unique Collections)
SADD online:users "user:1001" "user:1002" "user:1003"
SCARD online:users # 3
SISMEMBER online:users "user:1001" # 1 (true)
SREM online:users "user:1002" # Remove
# Intersection: users online AND premium
SADD premium:users "user:1001" "user:1004"
SINTER online:users premium:users # "user:1001"<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="200" rx="12" fill="#1a1a2e"/><path d="M100,30 L500,30 L460,65 L140,65 Z" fill="#3b82f6" opacity="0.8"/><text x="300" y="53" text-anchor="middle" fill="#ffffff" font-size="11" font-family="system-ui">Unoptimized Code — 2000ms</text><path d="M140,70 L460,70 L420,105 L180,105 Z" fill="#6366f1" opacity="0.8"/><text x="300" y="93" text-anchor="middle" fill="#ffffff" font-size="11" font-family="system-ui">+ Caching — 800ms</text><path d="M180,110 L420,110 L380,145 L220,145 Z" fill="#a855f7" opacity="0.8"/><text x="300" y="133" text-anchor="middle" fill="#ffffff" font-size="11" font-family="system-ui">+ CDN — 200ms</text><path d="M220,150 L380,150 L350,175 L250,175 Z" fill="#2dd4bf" opacity="0.9"/><text x="300" y="168" text-anchor="middle" fill="#1a1a2e" font-size="11" font-family="system-ui" font-weight="bold">Optimized — 50ms</text><text x="530" y="53" text-anchor="start" fill="#94a3b8" font-size="10" font-family="system-ui">Baseline</text><text x="445" y="93" text-anchor="start" fill="#2dd4bf" font-size="10" font-family="system-ui">-60%</text><text x="405" y="133" text-anchor="start" fill="#2dd4bf" font-size="10" font-family="system-ui">-90%</text><text x="365" y="168" text-anchor="start" fill="#2dd4bf" font-size="10" font-family="system-ui" font-weight="bold">-97.5%</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Performance optimization funnel: each layer of optimization compounds to dramatically reduce response times.</p></div>
Pattern 1: Rate Limiting
Sliding window rate limiter using sorted sets:
import redis
import time
r = redis.Redis()
def is_rate_limited(user_id: str, limit: int = 100, window: int = 60) -> bool:
"""Allow 'limit' requests per 'window' seconds."""
key = f"ratelimit:{user_id}"
now = time.time()
pipe = r.pipeline()
# Remove old entries outside the window
pipe.zremrangebyscore(key, 0, now - window)
# Add current request
pipe.zadd(key, {str(now): now})
# Count requests in window
pipe.zcard(key)
# Set key expiration
pipe.expire(key, window)
results = pipe.execute()
request_count = results[2]
return request_count > limitPattern 2: Session Management
import json
import secrets
def create_session(user_id: int, metadata: dict) -> str:
session_id = secrets.token_urlsafe(32)
session_data = {
"user_id": user_id,
"created_at": time.time(),
**metadata
}
r.setex(
f"session:{session_id}",
3600 * 24, # 24-hour expiry
json.dumps(session_data)
)
return session_id
def get_session(session_id: str) -> dict | None:
data = r.get(f"session:{session_id}")
if data:
# Refresh TTL on access (sliding expiration)
r.expire(f"session:{session_id}", 3600 * 24)
return json.loads(data)
return None
def destroy_session(session_id: str):
r.delete(f"session:{session_id}")Pattern 3: Pub/Sub for Real-Time Events
# Publisher
def publish_event(channel: str, event: dict):
r.publish(channel, json.dumps(event))
publish_event("notifications:user:1001", {
"type": "new_message",
"from": "bob",
"preview": "Hey, check out this PR..."
})
# Subscriber (separate process)
pubsub = r.pubsub()
pubsub.psubscribe("notifications:user:*") # Pattern subscribe
for message in pubsub.listen():
if message["type"] == "pmessage":
channel = message["channel"].decode()
data = json.loads(message["data"])
user_id = channel.split(":")[-1]
send_websocket_notification(user_id, data)Pattern 4: Redis Streams (Event Log)
Streams are like Kafka topics inside Redis — durable, consumer groups, acknowledgment:
# Produce events
r.xadd("events:orders", {
"order_id": "ORD-001",
"amount": "99.99",
"customer": "alice"
})
# Create consumer group
r.xgroup_create("events:orders", "order-processors", id="0", mkstream=True)
# Consume with acknowledgment
while True:
messages = r.xreadgroup(
groupname="order-processors",
consumername="worker-1",
streams={"events:orders": ">"},
count=10,
block=5000 # Wait 5s for new messages
)
for stream, entries in messages:
for msg_id, data in entries:
print(f"Processing order: {data}")
# Process...
r.xack("events:orders", "order-processors", msg_id)Pattern 5: Geospatial Queries
# Add locations
GEOADD restaurants 77.2090 28.6139 "delhi-hq"
GEOADD restaurants 72.8777 19.0760 "mumbai-branch"
GEOADD restaurants 80.2707 13.0827 "chennai-branch"
# Find restaurants within 500km of a point
GEOSEARCH restaurants FROMLONLAT 77.5946 12.9716 BYRADIUS 500 KM ASC
# Returns: chennai-branch
# Distance between two locations
GEODIST restaurants "delhi-hq" "mumbai-branch" km
# "1153.3"Pattern 6: Distributed Locks
import uuid
class RedisLock:
def __init__(self, redis_client, resource, ttl=30):
self.redis = redis_client
self.resource = f"lock:{resource}"
self.token = str(uuid.uuid4())
self.ttl = ttl
def acquire(self) -> bool:
return self.redis.set(
self.resource, self.token, nx=True, ex=self.ttl
)
def release(self):
# Lua script ensures atomicity: only release if we own the lock
script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
self.redis.eval(script, 1, self.resource, self.token)
# Usage
lock = RedisLock(r, "payment:process:order-123")
if lock.acquire():
try:
process_payment("order-123")
finally:
lock.release()<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="200" rx="12" fill="#1a1a2e"/><rect x="0" y="0" width="600" height="28" rx="12" fill="#2d2d44"/><rect x="0" y="12" width="600" height="16" fill="#2d2d44"/><circle cx="18" cy="14" r="5" fill="#ef4444"/><circle cx="34" cy="14" r="5" fill="#f59e0b"/><circle cx="50" cy="14" r="5" fill="#2dd4bf"/><text x="300" y="18" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui">docker-compose.yml</text><rect x="0" y="28" width="35" height="172" fill="#1e1e32"/><text x="25" y="48" text-anchor="end" fill="#94a3b8" font-size="10" font-family="monospace" opacity="0.5">1</text><text x="25" y="66" text-anchor="end" fill="#94a3b8" font-size="10" font-family="monospace" opacity="0.5">2</text><text x="25" y="84" text-anchor="end" fill="#94a3b8" font-size="10" font-family="monospace" opacity="0.5">3</text><text x="25" y="102" text-anchor="end" fill="#94a3b8" font-size="10" font-family="monospace" opacity="0.5">4</text><text x="25" y="120" text-anchor="end" fill="#94a3b8" font-size="10" font-family="monospace" opacity="0.5">5</text><text x="25" y="138" text-anchor="end" fill="#94a3b8" font-size="10" font-family="monospace" opacity="0.5">6</text><text x="25" y="156" text-anchor="end" fill="#94a3b8" font-size="10" font-family="monospace" opacity="0.5">7</text><text x="25" y="174" text-anchor="end" fill="#94a3b8" font-size="10" font-family="monospace" opacity="0.5">8</text><text x="25" y="192" text-anchor="end" fill="#94a3b8" font-size="10" font-family="monospace" opacity="0.5">9</text><text x="45" y="48" fill="#a855f7" font-size="11" font-family="monospace">version</text><text x="100" y="48" fill="#e2e8f0" font-size="11" font-family="monospace">: "3.8"</text><text x="45" y="66" fill="#a855f7" font-size="11" font-family="monospace">services</text><text x="105" y="66" fill="#e2e8f0" font-size="11" font-family="monospace">:</text><text x="55" y="84" fill="#3b82f6" font-size="11" font-family="monospace"> web</text><text x="80" y="84" fill="#e2e8f0" font-size="11" font-family="monospace">:</text><text x="55" y="102" fill="#2dd4bf" font-size="11" font-family="monospace"> image</text><text x="110" y="102" fill="#e2e8f0" font-size="11" font-family="monospace">: nginx:alpine</text><text x="55" y="120" fill="#2dd4bf" font-size="11" font-family="monospace"> ports</text><text x="102" y="120" fill="#e2e8f0" font-size="11" font-family="monospace">:</text><text x="55" y="138" fill="#e2e8f0" font-size="11" font-family="monospace"> - "80:80"</text><text x="55" y="156" fill="#2dd4bf" font-size="11" font-family="monospace"> volumes</text><text x="118" y="156" fill="#e2e8f0" font-size="11" font-family="monospace">:</text><text x="55" y="174" fill="#e2e8f0" font-size="11" font-family="monospace"> - ./html:/usr/share/nginx</text><rect x="365" y="164" width="2" height="14" fill="#6366f1" opacity="0.8"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">A well-structured configuration file is the foundation of reproducible infrastructure.</p></div>
Memory Optimization
Redis stores everything in RAM. Optimize memory usage:
# Check memory usage per key
redis-cli MEMORY USAGE "user:1001"
# Set max memory and eviction policy
CONFIG SET maxmemory 256mb
CONFIG SET maxmemory-policy allkeys-lru
# Use compressed data types
CONFIG SET hash-max-ziplist-entries 128
CONFIG SET hash-max-ziplist-value 64Tips:
SCAN instead of KEYS in production (non-blocking)INFO memory and redis-cli --bigkeysRedis is one of the most versatile tools in any developer's toolkit. Master its data structures and you will find yourself reaching for it far beyond simple caching.
Need help with tutorials?
TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.