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.
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.
| Redis | Postgres / MySQL | |
|---|---|---|
| Storage | RAM (in-memory) | Disk |
| Speed | Sub-millisecond reads/writes | Milliseconds to seconds |
| Durability | Optional (volatile by default) | Full ACID guarantees |
| Data model | Key–value, lists, sets, hashes | Relational (tables, rows, joins) |
| Best for | Caching, sessions, queues, rate limiting | Business logic, persistent records |
| TTL support | Built-in | Requires 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.
