DevOps

Redis Explained: A Developer's Practical Guide to Caching and Beyond (2026)

2026-07-05·12 min read
#Redis#caching#database#backend

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:

  1. Cache-aside for database query caching
  2. Rate limiting for API protection
  3. Session storage for web applications
  4. Pub/Sub for real-time notifications
  5. 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.