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

One-field diagnostic start

Send one work email. Yash replies with the matching service path, first evidence step, and owner handoff for this issue.

No calendar step. The full contact form stays available if you want to add system context.

One owner, one affected system, and the next buyer or recovery deadline mapped.

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 the next owner and evidence step mapped?

Send the current system and deadline. Yash replies with the service path, first proof artifact, and handoff owner.