Your First Commands
Verified on Redis 8.10.2 (
redis:8.10.2-alpine), ioredis 6.0.0, Node 24, Apple-silicon Mac. The transcripts below form one session in document order (every# cli/...fence is replayed by the course harness), and the timings are from this Mac.
The key-value mental model
Section titled “The key-value mental model”Redis is, at its core, a key-value store. Every piece of data lives under a key. Keys are always strings (binary-safe, up to 512 MB, though you should keep them short). Values have a type — string, list, hash, set, sorted set, or stream — and the type determines which commands you can run on that value.
Think of it like a giant dictionary: you look up a key, you get its value. You can also ask whether a key exists, what type its value is, or delete it entirely.
SET and GET
Section titled “SET and GET”SET key value stores a value. GET key retrieves it. If the key already exists, SET overwrites it.
127.0.0.1:6379> SET name "Redis"OK127.0.0.1:6379> GET name"Redis"127.0.0.1:6379> SET score 100OKEXISTS — check if a key exists
Section titled “EXISTS — check if a key exists”EXISTS key returns 1 if the key exists, 0 if it does not. You can pass multiple keys — the return value is the count of keys that exist (a key named twice counts twice).
127.0.0.1:6379> EXISTS name(integer) 1127.0.0.1:6379> EXISTS missing(integer) 0127.0.0.1:6379> EXISTS name score missing(integer) 2TYPE — inspect the value type
Section titled “TYPE — inspect the value type”TYPE key returns the type of the value stored at a key: string, list, hash, set, zset, or stream. It returns none if the key does not exist.
127.0.0.1:6379> TYPE namestring127.0.0.1:6379> TYPE missingnoneKEYS — list keys by pattern
Section titled “KEYS — list keys by pattern”KEYS pattern returns all keys matching a glob pattern. KEYS * matches everything. The order is whatever the internal hash table happens to give you — never rely on it.
127.0.0.1:6379> KEYS *1) "score"2) "name"WARNING: Never use
KEYS *in production. Command execution is single-threaded —KEYSblocks the entire server until it has scanned every key. UseSCANinstead, which iterates in small batches. The next section measures how much this costs.
DEL — delete one or more keys
Section titled “DEL — delete one or more keys”DEL key [key ...] deletes one or more keys and returns the number of keys actually deleted. If a key does not exist, it is simply skipped and does not count toward the return value.
127.0.0.1:6379> DEL name(integer) 1127.0.0.1:6379> GET name(nil)127.0.0.1:6379> DEL name score(integer) 1The second DEL names a key that is already gone and one that is not, and returns 1, not 2.
SET name "Redis"
GET name
SET score 100
EXISTS name
EXISTS missing
TYPE name
KEYS *
DEL name
GET nameAll six types, and how Redis stores them
Section titled “All six types, and how Redis stores them”One key of each type, then TYPE and OBJECT ENCODING for each (the encoding is how Redis lays the value out in memory — the word for small values is listpack, a compact array; you will see the switch to bigger structures in later lessons):
127.0.0.1:6379> SET user:42:name "Ada"OK127.0.0.1:6379> RPUSH user:42:events login view(integer) 2127.0.0.1:6379> HSET user:42 name Ada plan pro(integer) 2127.0.0.1:6379> SADD user:42:tags admin beta(integer) 2127.0.0.1:6379> ZADD user:42:score 100 Ada(integer) 1127.0.0.1:6379> XADD user:42:log * action login"1790747304943-0"127.0.0.1:6379> TYPE user:42:eventslist127.0.0.1:6379> TYPE user:42hash127.0.0.1:6379> TYPE user:42:tagsset127.0.0.1:6379> TYPE user:42:scorezset127.0.0.1:6379> TYPE user:42:logstream127.0.0.1:6379> OBJECT ENCODING user:42:name"embstr"127.0.0.1:6379> OBJECT ENCODING user:42:events"listpack"127.0.0.1:6379> GET user:42:events(error) WRONGTYPE Operation against a key holding the wrong kind of valueTwo things to see. The stream ID starts with the server’s millisecond clock (1790747304943 here; yours will differ). And the last line is the type system biting: GET on a list is refused with WRONGTYPE, so a key’s type is a contract your code has to keep.
The cost of KEYS, measured
Section titled “The cost of KEYS, measured”“KEYS blocks the server” is a claim you can measure. I loaded 200,000 then 1,000,000 keys (b1:key:<n>) into a fresh 8.10.2 container and ran KEYS 'b1:key:*' with slowlog-log-slower-than 0 so the server itself reports how long the command held the single command thread:
| Keys in the database | KEYS server time (SLOWLOG) | Longest single SCAN ... COUNT 1000 call |
|---|---|---|
| 200,000 | 15.8 ms | — |
| 1,000,000 | 155.9 ms | 1.8 ms |
During those 156 ms nothing else runs: a second connection that PINGs every millisecond saw its worst round trip jump from 2.4 ms (idle) to 17.6 ms in the 200k case; the client-side wall clock for the whole KEYS call (network transfer and parsing 200k strings included) was 121 ms. SCAN walked all 1M keys in 1,000 calls of at most 1.8 ms each; its total wall time was longer (1.3 s vs 0.6 s for KEYS) — you trade total time for never stalling anyone. It scales linearly, so ten million keys is over a second of frozen Redis. The Scan Keys lesson digs into SCAN itself.
Your first client script
Section titled “Your first client script”The CLI is for exploring; an application uses a client library. The same commands through ioredis 6.0.0 — note what the return values look like in JavaScript:
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);redis.on('error', (err) => console.error('redis error:', err.message));
await redis.set('b1:name', 'Redis');console.log(await redis.get('b1:name')); // "Redis"console.log(await redis.get('b1:missing')); // null (the CLI's (nil))console.log(await redis.exists('b1:name', 'b1:missing')); // 1 (a number, not a string)console.log(await redis.type('b1:name')); // "string"console.log(await redis.del('b1:name')); // 1
await redis.quit(); // waits for pending replies, then closesOutput when run: Redis, null, 1, string, 1. A missing key is null, integers are numbers, and quit() (not disconnect()) lets in-flight commands finish. The error listener matters: without it an unreachable server raises an unhandled error event.
Key naming best practices
Section titled “Key naming best practices”Keys are just strings, so you can name them anything — but meaningful names make debugging and monitoring far easier. The standard convention is colon-separated namespaces:
| Pattern | Example |
|---|---|
object:id | user:42 |
object:id:field | user:42:email |
type:identifier | session:abc123 |
This makes it easy to group related keys, use glob patterns safely, and understand what a key holds at a glance in tools like RedisInsight.
Reading AI-generated code
Section titled “Reading AI-generated code”An assistant is asked for an “online users” endpoint and writes:
🐛 Spot the bug
app.get('/online', async (_req, res) => {// count active sessionsconst keys = await redis.keys('session:*');res.json({ online: keys.length });});Show the answer
KEYS in a request handler is the bug. Every call freezes Redis for the whole scan: we measured 16 ms at 200k keys and 156 ms at 1M — and each concurrent request adds another full scan, so a busy endpoint queues everyone else’s commands behind it. It also returns every key name over the network just to count them. The fix is to walk the keyspace in small SCAN batches — or, better for a counter you read often, keep the count as its own number (INCR/DECR, covered in the Counters lesson) or a Set of session ids:
// @runimport { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
const seed = redis.pipeline();for (let i = 0; i < 1000; i++) seed.set(`b1:session:${i}`, '1');await seed.exec();
// SCAN in small batches: each call is cheap, the server stays responsive between callslet online = 0;for await (const batch of redis.scanStream({ match: 'b1:session:*', count: 200 })) { online += (batch as string[]).length;}console.log('online sessions:', online); // 1000
const cleanup = redis.pipeline();for (let i = 0; i < 1000; i++) cleanup.del(`b1:session:${i}`);await cleanup.exec();await redis.quit();This printed online sessions: 1000. (scanStream may yield a key twice on a real, changing keyspace; that is SCAN’s contract, and why the Scan Keys lesson dedupes.)
Mental Model
Section titled “Mental Model”Redis is one huge dictionary with typed values. SET/GET/DEL/EXISTS/TYPE are the dictionary’s own verbs; each value type then brings its own verbs (RPUSH, HSET, …). Because the dictionary is one hash table and commands run one at a time, anything that walks the whole table (KEYS) blocks everyone, while anything that goes straight to one key (GET) is O(1) no matter how big the database is.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
| Schema-less key-value model | Simple to start — store anything immediately without defining tables up front | No server-side schema or type enforcement; type mismatches surface at runtime (WRONGTYPE) |
Using KEYS * to see everything | Convenient for experimenting on a small dev dataset | Blocks the command thread for the whole scan: 156 ms at 1M keys, linear beyond that; never use it on a large production dataset |
| Storing keys with no TTL | Data does not disappear on its own — good for data that must persist | If you forget to set an expiry on data that should be a cache, memory grows without bound |
Common Misconceptions
Section titled “Common Misconceptions”- “
KEYSis fine, my database is small.” It is fine until it is not, and the failure is silent latency spikes. MakeSCANthe habit. - “
DEL a breturns 2.” It returns how many keys were actually deleted. - “Redis keys are namespaced by type.” One flat keyspace:
user:42cannot be both a hash and a string. - “
OBJECT ENCODINGis a fixed property of the type.” It changes with size and content (embstrtoraw,listpacktohashtable).
Common mistakes
Section titled “Common mistakes”- Using
KEYS *in production to see all keys — this command blocks the server until it finishes scanning. On a large dataset it can stall every other client. UseSCANinstead when working with real data. - Not setting a TTL on a key that should be temporary — if you
SETa cache value without an expiry, memory accumulates until it hits yourmaxmemorylimit. - Testing commands through
redis-cliand assuming the behavior is identical in real application code — a client library returnsnullfor(nil), numbers for integers, and throws onWRONGTYPE. Test through the real library before shipping to production.
Real-world example
Stack Overflow uses this same simple
SET/GETas part of an L1/L2 cache in front of SQL Server, cutting repeated queries for pages with heavy concurrent reads, at a reported scale of roughly 160 billion operations a month (Stack Overflow: The Architecture – 2016 Edition, Nick Craver, 2016 — ten years old; treat it as history).