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.

Y
Yash Pritwani
15 min read

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 30

Hashes (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 increment

Perfect 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 99

Sorted 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 2000

Sets (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 > limit

Pattern 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">: &quot;3.8&quot;</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"> - &quot;80:80&quot;</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 64

Tips:

Use hashes instead of multiple string keys (saves ~50% memory)
Set TTLs on everything — stale data is wasted RAM
Use SCAN instead of KEYS in production (non-blocking)
Monitor with INFO memory and redis-cli --bigkeys

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

#redis#caching#pub-sub#streams#data-structures#tutorial

Need help with tutorials?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.