Post

Redis: The Whiteboard

Redis is an in-memory data store used for caching, session management, queues, and more. Here's what it is, why it's fast, and how to think about it.

Redis: The Whiteboard

1. What Is Redis?

Redis is an in-memory data store. It stores data in RAM instead of on disk, which makes reads and writes orders of magnitude faster than a traditional database like Postgres or MySQL.

The name stands for Remote Dictionary Server, which is a decent hint at what it does at its core: it’s a giant key–value store. I give it a key, I get back a value. Simple.

But Redis isn’t just a fast dictionary. It supports rich data structures — strings, lists, sets, sorted sets, hashes — and comes with built-in features like TTL (time-to-live), pub/sub messaging, and atomic operations. That combination is what makes it genuinely useful across several different problems.


2. The Analogy: The Office Whiteboard

Imagine a busy office.

Every time someone needs a piece of information — say, “what’s the Q3 revenue number?” — they walk to the filing room, pull out a binder, flip through pages, find the number, walk back, and answer the question. This takes minutes. And it happens dozens of times a day, for the same question.

Redis is the whiteboard by the door.

Someone writes “Q3 Revenue: $4.2M” on the whiteboard. Now, every time anyone needs that number, they just glance at the whiteboard. Two seconds. The filing room still has the authoritative record, but the whiteboard handles the daily traffic.

When the numbers change, someone wipes the whiteboard and writes the new figure. The filing room is always the source of truth — the whiteboard is just fast.

That whiteboard is your cache. The filing room is your database.


3. The Mental Models

Redis earns its keep in three main patterns. Here’s how I think about each one.

1. Cache-Aside (the most common one)

Every time my app needs data, it checks Redis first:

1
2
Request → Check Redis → HIT? Return cached data ✅
                      → MISS? Query Postgres → Store in Redis → Return data

A HIT means Redis already has it — respond instantly. A MISS means Redis doesn’t — go to the database, return the result, and also write it to Redis so the next request gets a HIT.

The database doesn’t see repeat traffic for the same data. Redis absorbs it.

2. TTL — Data That Expires Itself

Some data is only valid for a window of time. OTPs, session tokens, rate-limit counters, flash messages. Redis handles this natively with the EX flag:

1
SET otp:user123 "4821" EX 300   ← expires in 5 minutes automatically

No cron job. No cleanup task. Redis deletes the key when the timer runs out. The code that checks for the OTP either finds it or doesn’t — that’s all it needs to know.

3. Queues — Async Work

When a request triggers work that doesn’t need to finish before I respond to the user — sending an email, processing an image, syncing to a third-party API — I push it to a Redis queue and return immediately:

1
API → push job to Redis queue → Worker picks it up → Processes async

The user gets a fast response. The worker processes at its own pace. Redis acts as the handoff point between them.

The Core Commands

Everything else builds on a handful of primitives:

1
2
3
4
5
SET  key  value       ← store something
GET  key              ← retrieve it
DEL  key              ← delete it
TTL  key              ← check how long until it expires
EXISTS key            ← check if it exists

That’s most of what I use day-to-day. The complexity isn’t in the commands — it’s in knowing when to reach for Redis and what to put in it.


4. In Code

The mental model only clicks once I see it written out. Here’s the cache-aside pattern in Python using redis-py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import redis
import json
import psycopg2

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

def get_user(user_id: int) -> dict:
    cache_key = f"user:{user_id}"

    # 1. Check Redis first
    cached = r.get(cache_key)
    if cached:
        print("HIT")
        return json.loads(cached)

    # 2. MISS — go to Postgres
    print("MISS")
    conn = psycopg2.connect("dbname=mydb user=postgres")
    cur = conn.cursor()
    cur.execute("SELECT id, name, email FROM users WHERE id = %s", (user_id,))
    row = cur.fetchone()
    user = {"id": row[0], "name": row[1], "email": row[2]}

    # 3. Store in Redis for 5 minutes, then return (simplified — use a connection pool in production)
    r.setex(cache_key, 300, json.dumps(user))
    return user

The first call hits Postgres and writes to Redis. Every call after that for the next 5 minutes is served from cache. When the TTL expires, the next request goes back to Postgres and refreshes the cache. The database never sees the flood.

The OTP example is even simpler — Redis TTL does all the work:

1
2
3
4
5
6
7
8
9
10
11
12
import random

def send_otp(user_id: int):
    otp = str(random.randint(1000, 9999))
    r.setex(f"otp:{user_id}", 300, otp)  # expires in 5 minutes
    # send otp via SMS / email...

def verify_otp(user_id: int, code: str) -> bool:
    stored = r.get(f"otp:{user_id}")
    if stored is None:
        return False   # expired or never sent
    return stored == code

No cleanup job. No expiry column in the database. When the 5 minutes are up, r.get() returns None and the OTP is gone. That’s it.


5. Redis vs. a Traditional Database

Redis and Postgres aren’t competing for the same job. They’re complementary.

 RedisPostgres / MySQL
StorageRAM (in-memory)Disk
SpeedSub-millisecond reads/writesMilliseconds to seconds
DurabilityOptional (volatile by default)Full ACID guarantees
Data modelKey–value, lists, sets, hashesRelational (tables, rows, joins)
Best forCaching, sessions, queues, rate limitingBusiness logic, persistent records
TTL supportBuilt-inRequires manual cleanup

The short version: Postgres is where data lives. Redis is where data works.

I wouldn’t store a user’s order history in Redis — I’d lose it on a restart. But I’d absolutely cache the result of an expensive join in Redis so my API doesn’t recompute it on every page load.


6. What to Watch Out For

Redis is fast and easy to reach for — which makes it easy to misuse.

  • Cache invalidation. The moment data changes in Postgres, the Redis copy is stale. I need to either set an appropriate TTL so it expires naturally, or explicitly delete the key when the underlying data changes. Stale caches are a real source of bugs.
  • Memory is finite. Redis lives in RAM. If I cache too aggressively, I’ll hit memory limits. Redis has eviction policies (LRU, LFU, etc.) but I shouldn’t rely on them blindly — I should cache deliberately.
  • It’s not a primary store. By default, Redis is volatile. A restart without persistence configured means the data is gone. That’s fine for cache, not fine for anything I can’t afford to lose.

7. The Takeaway

Redis solved a problem that every scaled backend eventually hits: databases are slow under load, and most of that load is repeated reads of the same data.

The mental model I keep coming back to is the whiteboard. My database is the filing room — complete, authoritative, durable. Redis is the whiteboard by the door — fast, convenient, and good enough for most questions most of the time.

Cache the things that are read often and change rarely. Set TTLs on anything time-sensitive. Use the queue pattern to decouple fast user-facing work from slow background work.

Redis won’t fix a bad data model. But in front of a healthy one, it’s one of the highest-leverage tools in the backend toolkit.

This post is licensed under CC BY 4.0 by the author.