Redis β Part 1: Foundations & Core Concepts
maang.io System Design Series
What is this?
Imagine the fastest clerk you've ever seen, working a single counter. Behind them isn't a warehouse with aisles to walk β it's a wall of labeled bins, every one within arm's reach: a bin of sticky notes (simple values), a bin of pigeonhole slots (a lookup table), a tube where slips slide in one end and out the other (a queue), a bag that refuses duplicates (a set), a always-sorted ranking rack (a leaderboard), a receipt roll that only ever grows (a log). The clerk takes one request at a time, but each request is so fast β grab from a bin, done β that the line never actually backs up. No walking, no digging through drawers, no waiting on a filing cabinet. Everything is right there.
That clerk is Redis. The "wall of bins within arm's reach" is RAM β Redis keeps your data in memory, not on disk. The bins aren't dumb byte-blobs; they're real data structures (lists, hashes, sets, sorted sets, and more) with operations built in. And the "one request at a time" is Redis's famous single-threaded command execution, which sounds like a weakness and is secretly a superpower (Part 2 explains why). We'll come back to this counter clerk in every chapter β it's the through-line.
The one-line idea: Redis is an in-memory data-structure server. Instead of storing opaque rows on disk and making you compute over them, it holds rich data structures in RAM and gives you atomic operations on them β so reads and writes land in microseconds, and problems like "leaderboard," "rate limit," or "session store" become a single command.
Let's unpack why that combination β in-memory and data structures β is such a big deal.
1. Why Redis exists β the problem it solves
Before Redis, if your web app needed something fast and shared across servers, your options were awkward. A relational database (the SQL chapter) is durable and powerful, but every query pays for disk I/O, a query planner, locks, and ACID machinery β beautiful for a bank ledger, painful when all you want is "have I seen this session token in the last 10 minutes?" hundreds of thousands of times a second. You could cache in each app server's local memory, but then every server has a different copy β no good for a counter or a session that must be consistent across the fleet (this is exactly the problem the caching chapter in the 101 tier frames).
Two pains, specifically:
- Disk is slow. A spinning disk seek is milliseconds; even an SSD read is tens of microseconds plus syscall overhead. RAM is ~100 nanoseconds. For hot data touched constantly, that gap is the whole ballgame.
- In-process caches don't scale out. Ten app servers each with their own
HashMapcan't agree on a global rate-limit counter or share a login session. You need one fast, shared place.
Redis answers both at once: a single shared server (or cluster) that all your app servers talk to, holding data in RAM so every operation is sub-millisecond, exposing data structures so the common problems have a built-in answer instead of a hand-rolled one.
π‘ The mental shift: a SQL database is where your data lives (the source of truth, durable, queryable a hundred ways). Redis is where your data is fast (hot, shared, shaped for one specific access pattern). Most real systems use both β Redis in front of Postgres is one of the most common architectures on the internet.
2. Core concepts & vocabulary
Redis's whole model is small enough to hold in your head. Here it is.
Keys and values. Redis is a key-value store: every value is stored under a unique string key (user:1042:session, leaderboard:global). But unlike a plain key-value store, the value isn't just bytes β it's one of several types, each a real data structure with its own commands.
The core value types β this is the wall of bins:
| Type | The bin | What it's for | Signature commands |
|---|---|---|---|
| String | a sticky note | any single value: text, a number, a serialized blob, even a bitmap | SET, GET, INCR |
| Hash | a set of pigeonhole slots | an object with fields (a user record) | HSET, HGET, HGETALL |
| List | a slip tube | ordered sequence; queues & stacks | LPUSH, RPOP, LRANGE |
| Set | a bag, no dupes | membership, tags, unique visitors | SADD, SISMEMBER, SINTER |
| Sorted Set (ZSet) | a ranking rack | anything ordered by a score: leaderboards, priority queues | ZADD, ZRANGE, ZRANK |
| Stream | a receipt roll | append-only log with consumer groups (messaging, events) | XADD, XREADGROUP |
| Bitmap | a strip of on/off pins | dense boolean arrays (daily active flags) | SETBIT, BITCOUNT |
| HyperLogLog | a magic tally counter | approximate unique counts in tiny fixed space | PFADD, PFCOUNT |
| Geospatial | a map pinboard | "who's near me" queries | GEOADD, GEOSEARCH |
Keys are flat and global. There are no tables and no nested paths β just one giant namespace of keys. Structure comes from naming conventions (user:1042:profile) and from choosing the right value type. This flatness is deliberate: it's what keeps lookups O(1) and the engine simple.
Everything is (optionally) ephemeral. Any key can carry a TTL (time-to-live): EXPIRE session:abc 600 makes it vanish after 10 minutes. This one feature is why Redis is the default for caches, sessions, and rate limiters β expiry is built in, not bolted on.
Atomic operations. Because a single clerk runs one command at a time to completion, every command is atomic. INCR reads-modifies-writes a counter with zero risk of two clients racing. This is subtle but huge: a whole class of concurrency bugs simply cannot happen (more in Part 2).
Persistence is opt-in, not the default identity. Redis can save to disk (Part 3), but it thinks of itself as an in-memory system first. You configure durability to taste, rather than paying for it on every write like a traditional database.
3. How you actually use it
Here's the feel of it β a few real commands, not an app dump. Notice how each problem collapses into one or two lines.
# Cache a value with a 5-minute expiry (the cache-aside pattern)
SET user:1042:profile "{...json...}" EX 300
GET user:1042:profile
# A page-view counter β atomic, no race, no read-modify-write in your app
INCR page:home:views
# A global leaderboard: add a score, then read the top 3
ZADD leaderboard:global 4820 "alice"
ZADD leaderboard:global 5310 "bob"
ZREVRANGE leaderboard:global 0 2 WITHSCORES # -> bob 5310, alice 4820, ...
# A session store: a hash for the object, plus a TTL for auto-logout
HSET session:abc123 user_id 1042 role admin
EXPIRE session:abc123 1800
# A simple job queue: producers push, a worker blocks until work arrives
LPUSH jobs:email "{to: 'a@b.com'}"
BRPOP jobs:email 0 # worker blocks here until a job appears
That's the ergonomics that made Redis ubiquitous: the leaderboard you'd otherwise hand-build with a sorted index is ZADD + ZREVRANGE; the rate limiter is INCR + EXPIRE; the session store is a HSET with a TTL. The data structure is the feature.
π‘ Rule of thumb for the cache-aside pattern (the one you'll use most): on read, check Redis first; on a miss, read the database, then
SETit into Redis with a TTL; on write, update the database and invalidate (delete) the Redis key. The 101 caching chapter walks through the invalidation traps β they're where cache bugs actually live.
4. When to reach for it β and when not
Redis is the right tool when data is hot, shared, and shaped like a structure, and where losing the most recent second of it is survivable (or you've configured persistence to make it not).
Reach for Redis when you need:
- A cache in front of a slower database or service (the classic use).
- A shared counter / rate limiter β atomic
INCRacross your whole fleet. - A leaderboard or ranking β sorted sets are purpose-built for it.
- A session store β shared across servers, with TTL-based expiry.
- A lightweight queue or stream β
LPUSH/BRPOP, or Streams with consumer groups. - Pub/Sub fan-out β covered in the sibling Redis Pub/Sub topic.
- "Near me" geo queries β built on sorted sets and geohashing (its own topic in this tier).
Reach for something else when:
| You need⦠| Use instead |
|---|---|
| A durable system of record with rich queries, joins, transactions | A SQL database (SQL vs NoSQL topic) |
| Datasets far larger than RAM at rest | A disk-based store β Cassandra, DynamoDB, RocksDB |
| Full-text search | Elasticsearch / Apache Lucene |
| A durable, replayable event log at massive scale | Apache Kafka |
| Graph traversals | Neo4j |
β οΈ The number-one Redis footgun: treating it as your only database. Redis lives in RAM, and RAM is expensive and finite. If your working set outgrows memory, you either pay a fortune or start evicting data you needed. Redis is brilliant as the fast layer; it's a risky choice as the only layer for large, must-never-be-lost data. Know where your source of truth is.
5. Key takeaways
- Redis is an in-memory data-structure server β a shared, single-counter clerk with a wall of labeled bins (strings, hashes, lists, sets, sorted sets, streams, and more) all within arm's reach in RAM. That's why it's fast, and why common problems become one command.
- It exists to solve two pains at once: disk is slow (so keep hot data in RAM) and per-server caches don't scale out (so share one fast place). Redis-in-front-of-a-database is one of the internet's most common patterns.
- The mental model is small: a flat keyspace, values that are real data structures, atomic operations (one clerk, one command), and built-in TTLs for expiry.
- Reach for it for caches, counters/rate-limiters, leaderboards, sessions, queues, pub/sub, and geo β anywhere data is hot, shared, and structure-shaped.
- Don't make it your only database: it's bounded by RAM and is a fast layer, not a durable system of record for large data.
Next, Part 2 opens the counter and looks inside: how one thread can be this fast, how each bin (data type) is actually encoded in memory for speed and compactness, and exactly what happens when a command lands.