Redis: The Multi-Tool You're Probably Not Using Enough

Hey there! So you're using Redis as a cache, maybe even as a task queue. That's cool... but we need to talk. You might be missing out on one of the most versatile tools in your development toolkit.

What Redis Really Is (Beyond Just a Cache)

Let's be honest—most of us started using Redis because someone told us "it's fast" or "use it for caching." And they weren't wrong! But Redis is like that Swiss Army knife you bought thinking you'd only use the blade, only to discover there's a whole world of tools folded inside.

Redis is an in-memory data structure store that can act as:

  • A lightning-fast cache (obviously)

  • A message broker for job queues

  • A real-time leaderboard engine

  • A session store that won't crash your app

  • A rate-limiter for your APIs

The magic of Redis lies in how it combines simplicity with versatility. It's essentially a server that manipulates data structures over a network—but does it so blazingly fast that it feels like you're just working with local data structures.

Why Redis Feels So Natural for Python Devs

One thing I love about Redis? It feels like using Python's native data types, but over the network.

Think about it:

  • Redis strings = Python variables

  • Redis hashes = Python dictionaries

  • Redis lists = Python lists (with push/pop operations)

Once you get used to it, Redis feels like working with Python dicts or lists — most commands just make sense.

Redis Core Operations Explained

Before diving into code, let's break down some essential Redis commands you'll use daily:

String Operations

SET key value         # Store a string value
GET key               # Retrieve a string value
DEL key               # Delete a key
EXPIRE key seconds    # Set a key to expire after X seconds

Hash Operations (Think Dictionaries)

HSET user:1 name "John"   # Set a field in a hash
HGET user:1 name          # Get a field value
HGETALL user:1            # Get all field-value pairs
HDEL user:1 name          # Delete a field

List Operations (Think Queues and Stacks)

LPUSH mylist "item"       # Push to left (front) of list
RPUSH mylist "item"       # Push to right (end) of list
LPOP mylist               # Pop from left (front) of list
RPOP mylist               # Pop from right (end) of list
LRANGE mylist 0 -1        # Get all elements (0 to last)

Here's how to think about list operations:

  • LPUSH + RPOP = queue (first in, first out)

  • LPUSH + LPOP = stack (last in, first out)

  • RPUSH + LPOP = queue in reverse order

Set and Sorted Set Operations

SADD myset "value"        # Add to a set (no duplicates)
SMEMBERS myset            # Get all set members
SISMEMBER myset "value"   # Check if value exists in set

ZADD scores 100 "player1" # Add to sorted set with score
ZRANGE scores 0 -1        # Get all members (by score, ascending)
ZREVRANGE scores 0 -1     # Get all members (by score, descending)

The power of these operations comes from their simplicity and atomic nature. Redis handles all the locking and concurrency issues for you, so these operations are thread-safe without any extra work on your part.

Setting Up Async Redis in Python (Without the Complexity)

Before we dive into code, let's understand why async Redis matters. In modern web apps, you don't want Redis operations blocking your event loop, especially for high-traffic APIs or real-time systems.

Luckily, the official Redis Python library now has excellent async support. Here's how to get started:

# Install the library with async support
pip install redis[async]

Now, let's create a simple helper class that wraps common Redis operations:

# redis_client.py
import redis.asyncio as redis

class RedisClient:
    def __init__(self, host="localhost", port=6379, db=0):
        self.client = redis.Redis(host=host, port=port, db=db, decode_responses=True)

    async def set_value(self, key: str, value: str):
        await self.client.set(key, value)

    async def get_value(self, key: str) -> str:
        return await self.client.get(key)

    async def hset_value(self, hash_key: str, field: str, value: str):
        await self.client.hset(hash_key, field, value)

    async def hget_value(self, hash_key: str, field: str) -> str:
        return await self.client.hget(hash_key, field)

    async def lpush_value(self, list_key: str, value: str):
        await self.client.lpush(list_key, value)

    async def lpop_value(self, list_key: str) -> str:
        return await self.client.lpop(list_key)

Using this helper is straightforward:

# main.py
import asyncio
from redis_client import RedisClient

async def main():
    redis_client = RedisClient()

    # Simple key-value store
    await redis_client.set_value("name", "John")
    print(await redis_client.get_value("name"))  # John

    # Working with a hash (think nested dictionary)
    await redis_client.hset_value("user:1", "email", "john@example.com")
    print(await redis_client.hget_value("user:1", "email"))

    # Using lists as queues
    await redis_client.lpush_value("task_queue", "task_1")
    print(await redis_client.lpop_value("task_queue"))  # task_1

asyncio.run(main())

Reference Guide: 3 Redis Patterns for Everyday Use for common use

Let's move beyond theory and look at practical examples where Redis shines.

1. Building a Priority Notification Queue

Imagine you're sending notifications to users, and some are more urgent than others:

async def queue_notification(user_id, message, priority="normal"):
    notification = json.dumps({
        "user_id": user_id,
        "message": message,
        "timestamp": time.time()
    })

    # High priority notifications go to the front of their queue
    if priority == "high":
        await redis_client.lpush_value("notifications:high", notification)
    else:
        await redis_client.lpush_value("notifications:normal", notification)

Your worker process can then prioritize processing:

async def process_notifications():
    while True:
        # Always check high priority first
        notification = await redis_client.rpop_value("notifications:high") 

        if not notification:
            # Fall back to normal queue
            notification = await redis_client.rpop_value("notifications:normal")

        if notification:
            data = json.loads(notification)
            await send_push_notification(data["user_id"], data["message"])
        else:
            # No work, brief pause before checking again
            await asyncio.sleep(0.1)

2. Real-time Leaderboards in 10 Lines of Code

Redis Sorted Sets make leaderboards almost trivially easy:

async def update_score(user_id, score):
    # This automatically sorts users by score
    await redis_client.client.zadd("leaderboard", {user_id: score})

async def get_top_players(count=10):
    # Get top scores in descending order (highest first)
    return await redis_client.client.zrevrange("leaderboard", 0, count-1, withscores=True)

async def get_player_rank(user_id):
    # Find a specific player's position
    rank = await redis_client.client.zrevrank("leaderboard", user_id)
    return rank + 1 if rank is not None else None

3. Simple API Rate Limiting

Need to limit how many requests a user can make? Redis handles this beautifully:

async def rate_limit(user_id, limit=100, period=3600):
    """Limit users to {limit} requests per {period} seconds"""
    current_time = int(time.time())
    key = f"ratelimit:{user_id}"

    # Remove expired requests from our window
    window_start = current_time - period
    await redis_client.client.zremrangebyscore(key, 0, window_start)

    # Count current requests
    request_count = await redis_client.client.zcard(key)

    if request_count < limit:
        # Track this request with timestamp
        await redis_client.client.zadd(key, {str(current_time): current_time})
        await redis_client.client.expire(key, period)  # Auto-cleanup
        return True  # Request allowed
    else:
        return False  # Rate limit exceeded

When to Use Redis (and When Not To)

Redis is fantastic, but it's not the answer to every problem. Here's my rule of thumb:

Use Redis when:

  • Speed is crucial (sub-millisecond responses)

  • Your access patterns are simple and predictable

  • You're building real-time features

  • You need atomic operations on data structures

  • Your data fits comfortably in memory

Consider alternatives when:

  • Your data is larger than available memory

  • You need complex SQL-like queries

  • You require ACID transactions

  • Data durability is your top priority


Final Byte

Redis isn’t just fast — it’s versatile. Whether you’re caching data, queuing tasks, or building real-time features, Redis often has your back in more ways than you think.

It’s one of those rare tools that helps you focus on solving problems, not fighting infrastructure.

Until next time, happy building! 🚀

4
Subscribe to my newsletter

Read articles from Akhilesh Thykkat directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Akhilesh Thykkat
Akhilesh Thykkat

Hey, I’m Akhilesh M T, a backend engineer who loves learning and building scalable systems while diving into system design. When I’m not coding, I’m usually traveling, exploring new places, or reading up on the latest tech. Always learning, always optimizing, and looking for new challenges—whether in tech or in life.