Skip to content

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.

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 key value stores a value. GET key retrieves it. If the key already exists, SET overwrites it.

cli/intro-first-commands-1.redis
127.0.0.1:6379> SET name "Redis"
OK
127.0.0.1:6379> GET name
"Redis"
127.0.0.1:6379> SET score 100
OK

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).

cli/intro-first-commands-2.redis
127.0.0.1:6379> EXISTS name
(integer) 1
127.0.0.1:6379> EXISTS missing
(integer) 0
127.0.0.1:6379> EXISTS name score missing
(integer) 2

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.

cli/intro-first-commands-3.redis
127.0.0.1:6379> TYPE name
string
127.0.0.1:6379> TYPE missing
none

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.

cli/intro-first-commands-4.redis
127.0.0.1:6379> KEYS *
1) "score"
2) "name"

WARNING: Never use KEYS * in production. Command execution is single-threaded — KEYS blocks the entire server until it has scanned every key. Use SCAN instead, which iterates in small batches. The next section measures how much this costs.

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.

cli/intro-first-commands-5.redis
127.0.0.1:6379> DEL name
(integer) 1
127.0.0.1:6379> GET name
(nil)
127.0.0.1:6379> DEL name score
(integer) 1

The 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 name

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):

cli/intro-first-commands-6.redis
127.0.0.1:6379> SET user:42:name "Ada"
OK
127.0.0.1:6379> RPUSH user:42:events login view
(integer) 2
127.0.0.1:6379> HSET user:42 name Ada plan pro
(integer) 2
127.0.0.1:6379> SADD user:42:tags admin beta
(integer) 2
127.0.0.1:6379> ZADD user:42:score 100 Ada
(integer) 1
127.0.0.1:6379> XADD user:42:log * action login
"1790747304943-0"
127.0.0.1:6379> TYPE user:42:events
list
127.0.0.1:6379> TYPE user:42
hash
127.0.0.1:6379> TYPE user:42:tags
set
127.0.0.1:6379> TYPE user:42:score
zset
127.0.0.1:6379> TYPE user:42:log
stream
127.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 value

Two 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.

“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 databaseKEYS server time (SLOWLOG)Longest single SCAN ... COUNT 1000 call
200,00015.8 ms—
1,000,000155.9 ms1.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.

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:

src/intro-first-commands-basic.ts
// @run
import { 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 closes

Output 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.

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:

PatternExample
object:iduser:42
object:id:fielduser:42:email
type:identifiersession: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.

An assistant is asked for an “online users” endpoint and writes:

🐛 Spot the bug

app.get('/online', async (_req, res) => {
// count active sessions
const 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:

src/intro-first-commands-count.ts
// @run
import { 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 calls
let 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.)

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.

ChoiceBenefitCost
Schema-less key-value modelSimple to start — store anything immediately without defining tables up frontNo server-side schema or type enforcement; type mismatches surface at runtime (WRONGTYPE)
Using KEYS * to see everythingConvenient for experimenting on a small dev datasetBlocks 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 TTLData does not disappear on its own — good for data that must persistIf you forget to set an expiry on data that should be a cache, memory grows without bound
  • “KEYS is fine, my database is small.” It is fine until it is not, and the failure is silent latency spikes. Make SCAN the habit.
  • “DEL a b returns 2.” It returns how many keys were actually deleted.
  • “Redis keys are namespaced by type.” One flat keyspace: user:42 cannot be both a hash and a string.
  • “OBJECT ENCODING is a fixed property of the type.” It changes with size and content (embstr to raw, listpack to hashtable).
  • 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. Use SCAN instead when working with real data.
  • Not setting a TTL on a key that should be temporary — if you SET a cache value without an expiry, memory accumulates until it hits your maxmemory limit.
  • Testing commands through redis-cli and assuming the behavior is identical in real application code — a client library returns null for (nil), numbers for integers, and throws on WRONGTYPE. Test through the real library before shipping to production.

Real-world example

Stack Overflow uses this same simple SET/GET as 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).

What does DEL return?
What does EXISTS return for a key that does not exist?
What does TYPE return for a key holding a plain string value, and for a missing key?
Why should KEYS * be avoided in production?
In ioredis, what does await redis.get('missing') return?
You run GET user:42:events on a key that holds a list. What happens?