Redis Cheatsheet
Keys and Expiry
Use this Redis reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Naming and inspecting keys
A Redis key is an opaque byte string up to 512MB, but the whole ecosystem assumes
colon-separated namespaces. The convention is app:object:id:field, lowest cardinality
first, so a prefix scan is useful and a human reading MONITOR output can tell what
broke.
hu:user:1042:profile hash hu:user:1042:sessions set hu:session:9f3a...:data hash, TTL 86400 hu:leaderboard:2026-09 sorted set hu:ratelimit:ip:1.2.3.4 string counter, TTL 60
| Command | What it does | Complexity |
|---|---|---|
EXISTS key [key ...] | Counts how many of the named keys exist | O(n) keys |
TYPE key | string, list, set, zset, hash, stream, or none | O(1) |
DBSIZE | Number of keys in the current database | O(1) |
RANDOMKEY | One key at random, nil if empty | O(1) |
SELECT n | Switch database, 0 to 15 by default | O(1) |
SWAPDB a b | Atomically swap two databases | O(1) |
KEYS pattern | Every matching key, blocks the server | O(n) keyspace |
EXISTS counts, it does not deduplicate, so EXISTS k k returns 2 when k exists.
Numbered databases do not exist in Cluster mode, and most teams use a key prefix instead
of SELECT because clients pool connections and a stray SELECT leaks across requests.
Deleting, renaming, copying
| Command | What it does | Complexity |
|---|---|---|
DEL key [key ...] | Delete now, on the main thread | O(n) elements freed |
UNLINK key [key ...] | Unlink now, free in a background thread | O(1) per key |
RENAME old new | Rename, clobbering new, error if old is missing | O(1) |
RENAMENX old new | Rename only if new does not exist | O(1) |
COPY src dst [DB n] [REPLACE] | Copy a value to another key | O(n) |
MOVE key db | Move a key to another numbered database | O(1) |
Use UNLINK for anything big. DEL on a 10 million member set frees every element
inline and stalls the single-threaded event loop for hundreds of milliseconds. UNLINK
detaches the key immediately and reclaims memory on a background thread, so it is the
right default for collections. For a small string the two are identical.
RENAME on a key that carries a TTL keeps the TTL. RENAME onto an existing key
destroys the target's value and TTL.
Scanning instead of KEYS
Never run
KEYS *on a production server. It is O(n) over the entire keyspace and Redis is single threaded, so a million-key database freezes for every other client until it finishes.
SCAN is the safe replacement. It is a cursor-based iterator that returns a small batch
per call, so it never blocks. Start at cursor 0 and loop until the returned cursor is
0 again.
redis-cli SCAN 0 MATCH 'hu:session:*' COUNT 500 # 1) "17408" <- next cursor, NOT a count # 2) 1) "hu:session:9f3a" # 2) "hu:session:11bd"
# The cursor loop, in shell cursor=0 while :; do out=$(redis-cli SCAN "$cursor" MATCH 'hu:cache:*' COUNT 1000) cursor=$(echo "$out" | head -1) echo "$out" | tail -n +2 [ "$cursor" = "0" ] && break done # Or let redis-cli do it, which is what you actually want redis-cli --scan --pattern 'hu:cache:*'
| Option | Meaning |
|---|---|
MATCH pattern | Glob filter applied after the batch is fetched |
COUNT n | Work hint per call, default 10, not a hard limit |
TYPE string | Only keys of that type (6.0+) |
Guarantees worth knowing: every key present for the whole iteration is returned at least
once, keys added or removed mid-iteration may or may not appear, and a key can be
returned more than once, so make your handler idempotent. HSCAN, SSCAN, and
ZSCAN iterate inside one collection with the same contract, and HSCAN ... NOVALUES
(7.4+) returns field names only.
Setting expiry
| Command | What it does | Complexity |
|---|---|---|
EXPIRE key sec [NX XX GT LT] | TTL in seconds | O(1) |
PEXPIRE key ms [NX XX GT LT] | TTL in milliseconds | O(1) |
EXPIREAT key unix-sec [...] | Absolute deadline, seconds | O(1) |
PEXPIREAT key unix-ms [...] | Absolute deadline, milliseconds | O(1) |
EXPIRETIME key | The absolute unix second it dies (7.0+) | O(1) |
PERSIST key | Remove the TTL, make the key permanent | O(1) |
The condition flags are 7.0+ and they are what turn expiry into a safe primitive.
| Flag | Sets the TTL only if |
|---|---|
NX | The key currently has no TTL |
XX | The key already has a TTL |
GT | The new TTL is greater than the current one, or there is none |
LT | The new TTL is less than the current one, or there is none |
EXPIRE lock 100 NX # (integer) 1, no TTL before EXPIRE lock 50 GT # (integer) 0, refused, 50 < 100 EXPIRE lock 200 GT # (integer) 1, extended
GT is how a lock holder extends a lease without ever shortening someone else's, and
NX is how you set a default TTL without stomping a longer one already in place. Note
that GT and LT treat a missing TTL as infinity for GT purposes, so GT succeeds on
a persistent key while LT also succeeds on one.
Prefer setting the TTL in the write itself, which is one round trip and has no window where the key is immortal.
SET hu:session:abc '{"uid":42}' EX 3600 # value plus TTL, atomic SET hu:session:abc '{"uid":42}' KEEPTTL # new value, TTL untouched SETEX hu:session:abc 3600 '{"uid":42}' # older equivalent of SET ... EX GETEX hu:session:abc EX 3600 # read and refresh (6.2+) GETEX hu:session:abc PERSIST # read and clear the TTL
Any plain SET without KEEPTTL wipes the TTL. That single fact is behind most
"my sessions stopped expiring" bugs. Commands that mutate a value in place (INCR,
APPEND, HSET, LPUSH, SADD, ZADD) leave the TTL alone.
Reading a TTL
| Command | Returns |
|---|---|
TTL key | Remaining seconds |
PTTL key | Remaining milliseconds |
The two negative return values are the part people get wrong:
| Value | Means |
|---|---|
-1 | The key exists and has no expiry |
-2 | The key does not exist |
SET k v; TTL k # -1 EXPIRE k 60; TTL k # 60 DEL k; TTL k # -2
So TTL k returning a negative number is not an error, and checking > 0 is not the
same as checking existence. Use EXISTS for that.
Expiry is lazy plus sampled. A key is deleted when something touches it, or when the
background cycle happens to sample it, so DBSIZE and INFO keyspace can count keys
that are logically gone. Replicas do not expire keys on their own, they wait for the
primary's DEL, which is why a read from a replica can briefly return a logically
expired value.
Encoding and memory per key
| Command | What it tells you | Complexity |
|---|---|---|
OBJECT ENCODING key | The internal representation | O(1) |
OBJECT REFCOUNT key | Shared-object refcount, high for small ints | O(1) |
OBJECT IDLETIME key | Seconds since last access, needs an LRU policy | O(1) |
OBJECT FREQ key | Access frequency counter, needs an LFU policy | O(1) |
MEMORY USAGE key [SAMPLES n] | Bytes used, value plus overhead | O(1) sampled |
SET n 123 ; OBJECT ENCODING n # "int" SET t hello ; OBJECT ENCODING t # "embstr", strings up to 44 bytes RPUSH l a b c ; OBJECT ENCODING l # "listpack", small lists SADD s 1 2 3 ; OBJECT ENCODING s # "intset", all-integer sets SADD s2 a b ; OBJECT ENCODING s2 # "listpack", small non-integer sets HSET h f v ; OBJECT ENCODING h # "listpack", small hashes MEMORY USAGE h # (integer) 64
The compact encodings (listpack, intset) are dramatically smaller than the general
ones (hashtable, skiplist, quicklist), and Redis converts one way only, on the
first write that crosses a threshold. Those thresholds are configurable:
hash-max-listpack-entries (128) and -value (64), set-max-intset-entries (512),
set-max-listpack-entries (128), zset-max-listpack-entries (128) and -value (64),
list-max-listpack-size (128). Keeping collections under the limit is the single
biggest memory lever in Redis.
Eviction policies
When maxmemory is reached Redis applies maxmemory-policy. Set both, because the
default is to start failing writes.
| Policy | Candidate keys | Picks |
|---|---|---|
noeviction | none | Nothing, writes fail with an OOM error (default) |
allkeys-lru | every key | Approximated least recently used |
volatile-lru | keys with a TTL | Approximated least recently used |
allkeys-lfu | every key | Approximated least frequently used (4.0+) |
volatile-lfu | keys with a TTL | Approximated least frequently used |
allkeys-random | every key | At random |
volatile-random | keys with a TTL | At random |
volatile-ttl | keys with a TTL | Shortest remaining TTL first |
CONFIG SET maxmemory 2gb CONFIG SET maxmemory-policy allkeys-lfu CONFIG GET maxmemory-policy
Choosing between them:
- Pure cache, every key is disposable: allkeys-lfu. LFU beats LRU when a small hot
set is repeatedly scanned past by a large cold set. Tune with lfu-log-factor and
lfu-decay-time.
- Mixed store, some keys must survive: a volatile-* policy, and give every
disposable key a TTL. A volatile-* policy with no expiring keys behaves like
noeviction, which is a nasty surprise during an incident.
- LRU and LFU are approximate, sampled from maxmemory-samples keys (default 5).
Raising it to 10 gets close to exact LRU for more CPU.
Eviction is not expiry. OBJECT IDLETIME needs an LRU policy and OBJECT FREQ needs an
LFU policy, and each errors under the other.