Redis Command Reference

Redis has hundreds of commands, but the vast majority of real-world usage revolves around a much smaller “working set”: commands for strings, hashes, lists, sorted sets, and key management (expiration, type inspection, and scanning). This lesson is a practical reference to that working set — what each command does, its exact syntax, its time complexity, and how it behaves in edge cases like missing keys or wrong types. Think of it as the page you come back to whenever you forget an argument order or a return-value quirk.

Overview / How it works

Every Redis command you type at the redis-cli prompt is sent to the server as a request in the RESP (REdis Serialization Protocol) format, and Redis replies with one of a handful of reply types: simple strings (OK), bulk strings (a quoted value like "Ada"), integers ((integer) 1), arrays (numbered lists), errors ((error) ...), or nil ((nil)) for a missing value. Learning to read these reply shapes is as important as memorizing command names, because they tell you exactly what happened without needing extra documentation.

Redis executes commands one at a time on a single main thread. This means each individual command — even one that touches a large hash or list — runs to completion before the next command starts; there is no interleaving. That single-threaded model is why commands like INCR or HSET are safe to use as counters and atomic updates without any external locking: nothing else can run in the middle of them.

Every key in Redis holds exactly one value of exactly one type — string, hash, list, set, sorted set, stream, or a few others. The type is fixed once the key is created and enforced strictly: calling a list command on a key that holds a string returns a WRONGTYPE error rather than silently coercing the value. You can always check a key’s type with TYPE key before working with it if you’re not sure.

Keys can optionally carry a time-to-live (TTL). Expiration in Redis is enforced two ways: lazily, when a key is accessed and found to be past its expiry time (it’s deleted on the spot and treated as missing), and actively, via a background cycle that periodically samples keys with a TTL set and removes any that have expired, even if nothing ever reads them. This dual approach keeps expired keys from silently piling up in memory while avoiding the cost of scanning the entire keyspace on every tick.

Syntax

Most Redis commands follow the same general shape: a command name, a key, and zero or more arguments, all space-separated on one line.

COMMAND_NAME key [argument ...]
SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]

Key points that apply across almost every command:

  • Command names are case-insensitive to the server, but this site (and most style guides) writes them in UPPERCASE for readability.
  • Keys and values are binary-safe strings; quote them if they contain spaces ("Ada Lovelace").
  • Square brackets in documentation mean an argument is optional; a pipe (NX|XX) means “choose one.”
  • Arguments after the key are almost always positional and order-sensitive — getting the order wrong is one of the most common sources of a (error) ERR wrong number of arguments reply.

Command quick-reference table

Command Category Time complexity What it does
SET / GET String O(1) Write / read a string value
INCR / DECR String O(1) Atomically increment / decrement a numeric string
EXISTS / TYPE Generic O(1) Check existence / inspect the value’s type
DEL Generic O(N) over keys removed Delete one or more keys
EXPIRE / TTL Generic O(1) Set / read a key’s remaining lifetime
KEYS Generic O(N) over the whole keyspace Return all keys matching a pattern (blocking)
SCAN Generic O(1) per call Cursor-based, non-blocking iteration over keys
HSET / HGETALL Hash O(1) per field / O(N) fields Write a field / read all fields of a hash
RPUSH / LRANGE List O(1) per element / O(S+N) Push to a list / read a range of elements
ZADD / ZRANGE Sorted set O(log N) per member / O(log N + M) Add scored members / read a score-ordered range

Examples

The examples below move from a plain string, to a hash-and-list combination, to a sorted set used as a leaderboard.

Example 1: strings

SET user:1001:name "Ada"
GET user:1001:name
TYPE user:1001:name
DEL user:1001:name
GET user:1001:name

Output:

OK
"Ada"
string
(integer) 1
(nil)

SET always replies OK on success. GET returns the bulk string value. TYPE confirms it’s a plain string. DEL returns the integer count of keys actually removed (here, 1). After deletion, GET on the now-missing key returns (nil) — not an error, just an empty reply.

Example 2: hashes and lists together

HSET user:1001 name "Ada" age "30"
HGETALL user:1001
RPUSH queue:emails "a@example.com" "b@example.com"
LRANGE queue:emails 0 -1
LLEN queue:emails

Output:

(integer) 2
1) "name"
2) "Ada"
3) "age"
4) "30"
(integer) 2
1) "a@example.com"
2) "b@example.com"
(integer) 2

HSET returns how many new fields were created (2, since both were new). HGETALL returns a flat array alternating field names and values. RPUSH returns the list’s new length after pushing. LRANGE key 0 -1 is the standard idiom for “give me the whole list” — index -1 means the last element. LLEN confirms the length in O(1) without transferring any elements.

Example 3: sorted sets as a leaderboard

ZADD leaderboard:global 100 "alice" 200 "bob" 150 "carol"
ZREVRANGE leaderboard:global 0 -1 WITHSCORES
ZSCORE leaderboard:global "bob"
ZRANK leaderboard:global "alice"

Output:

(integer) 3
1) "bob"
2) "200"
3) "carol"
4) "150"
5) "alice"
6) "100"
"200"
(integer) 0

A sorted set (ZADD/ZRANGE family) keeps every member ordered by a floating-point score — this is what makes it the right structure for leaderboards and range queries, unlike a plain SET, which has no ordering at all. ZREVRANGE ... WITHSCORES lists members highest score first, interleaved with their scores. ZSCORE looks up one member’s score directly. ZRANK returns a member’s position counting from the lowest score, so alice (score 100, the lowest) is rank 0.

How it works step by step

Walking through what happens when you run SET session:abc123 "active" EX 100 followed later by TTL session:abc123:

  1. The client sends the command over its connection; Redis’s single event loop picks it up and executes it immediately and atomically — no other command runs in between.
  2. Redis stores the string value under the key in its main hash table (the keyspace), and separately records an absolute expiration timestamp (now + 100 seconds) in an internal expiration dictionary, since EX was supplied.
  3. When TTL is called, Redis looks up that expiration timestamp, subtracts the current time, and returns the remaining whole seconds as an integer reply. If no TTL had been set, it would return -1; if the key didn’t exist at all, it would return -2.
  4. If a plain SET (without KEEPTTL) is later run against the same key, Redis treats it as a brand-new write and clears any existing expiration — the key becomes persistent again unless you add EX/PX or KEEPTTL on that same call.
SET session:abc123 "active" EX 100
TTL session:abc123
SET session:abc123 "updated" KEEPTTL
TTL session:abc123
SET session:abc123 "reset"
TTL session:abc123

Output:

OK
(integer) 100
OK
(integer) 100
OK
(integer) -1

(The exact TTL integer may read a second lower than 100 depending on how quickly the commands execute — that’s expected, not a bug.) Notice the pattern: KEEPTTL preserves the countdown across an update, but a bare SET wipes it back to no-expiration.

Common Mistakes

Mistake 1: scanning a large keyspace with KEYS

SET product:1:name "Widget"
SET product:2:name "Gadget"
KEYS product:*

Output:

OK
OK
1) "product:1:name"
2) "product:2:name"

KEYS pattern works fine here because the dataset is tiny, but it is O(N) over the entire keyspace and, because Redis is single-threaded, it blocks every other client until it finishes. On a production database with millions of keys this can freeze the server for seconds. (Also note: KEYS does not guarantee any particular ordering of results.) Use SCAN instead — it walks the keyspace incrementally via a cursor and never blocks for more than a fraction of a millisecond per call:

SET product:1:name "Widget"
SET product:2:name "Gadget"
SCAN 0 MATCH product:* COUNT 100

Output:

OK
OK
1) "0"
2) 1) "product:1:name"
   2) "product:2:name"

The first element of the reply is the next cursor to pass back in; a cursor of "0" means the iteration is complete. In a real application you’d keep calling SCAN with the returned cursor until it comes back as 0, rather than assuming one call returns everything.

Mistake 2: ignoring a key’s type

SET session:token "xyz"
LPUSH session:token "a"

Output:

OK
(error) WRONGTYPE Operation against a key holding the wrong kind of value

session:token already holds a string, so pushing to it as if it were a list fails immediately with a WRONGTYPE error rather than converting or overwriting the value. Always check TYPE key if you’re unsure what a key currently holds, or namespace your keys clearly enough (as in queue:emails vs user:1001:name) that this can’t happen by accident.

Mistake 3: assuming EXPIRE on a missing key does something

EXPIRE nosuchkey 100

Output: (integer) 0

This doesn’t error, which is exactly what makes it easy to miss: EXPIRE returns 1 if the timeout was set and 0 if the key doesn’t exist, so a silently-ignored 0 can mean your TTL was never actually applied. Always check the return value in scripts that call EXPIRE after a conditional write.

Mistake 4: GET-then-SET instead of an atomic command

Reading a counter, adding to it in your application code, and writing it back with a separate SET is a race condition — two clients can read the same starting value and both write back the same incorrect result. Use an atomic command instead:

SET counter:visits 10
INCR counter:visits
GET counter:visits

Output:

OK
(integer) 11
"11"

INCR reads, increments, and writes the value as one atomic, single-threaded operation — no other command can interleave, so concurrent callers can never lose an update.

Best Practices

  • Namespace keys with colons (user:1001:name, session:abc123) so related keys are easy to scan and reason about.
  • Set a TTL (EX/PX or EXPIRE) on any key that represents cache data or a session — a key with no TTL lives forever and is a common cause of unbounded memory growth.
  • Prefer SCAN over KEYS for anything touching production data, no matter how small the dataset looks today.
  • Use atomic single commands (INCR, SET ... NX) instead of read-modify-write sequences from application code whenever the operation allows it.
  • Check command return values, especially integer replies like DEL‘s and EXPIRE‘s — a 0 often means “nothing happened,” not an error.
  • Use TYPE key when debugging an unexpected WRONGTYPE error rather than guessing.

A quick pattern worth knowing: SET key value NX only sets the key if it doesn’t already exist, which is the building block for simple distributed locks.

SET lock:resource1 "worker-1" NX EX 30
SET lock:resource1 "worker-2" NX EX 30

Output:

OK
(nil)

The first caller acquires the lock; the second caller’s NX condition fails (the key already exists) and Redis replies (nil) instead of an error — worker-2 knows to back off without needing to catch an exception.

Practice Exercises

  • Exercise 1: Create a hash at product:2001 with fields name, price, and stock. Then write a single command that reads back only the price and stock fields (hint: look at HMGET) without fetching name.
  • Exercise 2: Build a sorted set leaderboard:weekly with at least four members and scores. Write commands to find the rank of the second-highest scorer and to remove the lowest scorer entirely.
  • Exercise 3: Set a key with a 30-second TTL, confirm the TTL with TTL, then update the key’s value without losing the countdown. Afterward, update it again in a way that resets the TTL back to “no expiration,” and confirm with TTL.

Summary

  • Redis replies come in a few consistent shapes — simple string, bulk string, integer, array, nil, and error — learn to read them and you can predict most command behavior.
  • Every key has exactly one fixed type; mismatched commands fail fast with a WRONGTYPE error rather than silently coercing.
  • Single-threaded execution makes every individual command atomic, which is why INCR and SET ... NX are safe building blocks for counters and locks.
  • KEYS is O(N) over the whole keyspace and blocks the server; SCAN is cursor-based and safe for production.
  • A plain SET clears any existing TTL unless you add KEEPTTL; use EXPIRE‘s and DEL‘s integer return values to confirm they actually did something.
  • Sorted sets (ZADD/ZRANGE) add ordering by score on top of set semantics, making them the right tool for leaderboards and ranked data — a plain set has no such ordering.