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
UPPERCASEfor 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 argumentsreply.
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:
- 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.
- 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
EXwas supplied. - When
TTLis 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. - If a plain
SET(withoutKEEPTTL) 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 addEX/PXorKEEPTTLon 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/PXorEXPIRE) 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
SCANoverKEYSfor 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 andEXPIRE‘s — a0often means “nothing happened,” not an error. - Use
TYPE keywhen debugging an unexpectedWRONGTYPEerror 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:2001with fieldsname,price, andstock. Then write a single command that reads back only thepriceandstockfields (hint: look atHMGET) without fetchingname. - Exercise 2: Build a sorted set
leaderboard:weeklywith 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 withTTL.
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
WRONGTYPEerror rather than silently coercing. - Single-threaded execution makes every individual command atomic, which is why
INCRandSET ... NXare safe building blocks for counters and locks. KEYSis O(N) over the whole keyspace and blocks the server;SCANis cursor-based and safe for production.- A plain
SETclears any existing TTL unless you addKEEPTTL; useEXPIRE‘s andDEL‘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.
