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
CommandWhat it doesComplexity
EXISTS key [key ...]Counts how many of the named keys existO(n) keys
TYPE keystring, list, set, zset, hash, stream, or noneO(1)
DBSIZENumber of keys in the current databaseO(1)
RANDOMKEYOne key at random, nil if emptyO(1)
SELECT nSwitch database, 0 to 15 by defaultO(1)
SWAPDB a bAtomically swap two databasesO(1)
KEYS patternEvery matching key, blocks the serverO(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

CommandWhat it doesComplexity
DEL key [key ...]Delete now, on the main threadO(n) elements freed
UNLINK key [key ...]Unlink now, free in a background threadO(1) per key
RENAME old newRename, clobbering new, error if old is missingO(1)
RENAMENX old newRename only if new does not existO(1)
COPY src dst [DB n] [REPLACE]Copy a value to another keyO(n)
MOVE key dbMove a key to another numbered databaseO(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:*'
OptionMeaning
MATCH patternGlob filter applied after the batch is fetched
COUNT nWork hint per call, default 10, not a hard limit
TYPE stringOnly 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

CommandWhat it doesComplexity
EXPIRE key sec [NX XX GT LT]TTL in secondsO(1)
PEXPIRE key ms [NX XX GT LT]TTL in millisecondsO(1)
EXPIREAT key unix-sec [...]Absolute deadline, secondsO(1)
PEXPIREAT key unix-ms [...]Absolute deadline, millisecondsO(1)
EXPIRETIME keyThe absolute unix second it dies (7.0+)O(1)
PERSIST keyRemove the TTL, make the key permanentO(1)

The condition flags are 7.0+ and they are what turn expiry into a safe primitive.

FlagSets the TTL only if
NXThe key currently has no TTL
XXThe key already has a TTL
GTThe new TTL is greater than the current one, or there is none
LTThe 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

CommandReturns
TTL keyRemaining seconds
PTTL keyRemaining milliseconds

The two negative return values are the part people get wrong:

ValueMeans
-1The key exists and has no expiry
-2The 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

CommandWhat it tells youComplexity
OBJECT ENCODING keyThe internal representationO(1)
OBJECT REFCOUNT keyShared-object refcount, high for small intsO(1)
OBJECT IDLETIME keySeconds since last access, needs an LRU policyO(1)
OBJECT FREQ keyAccess frequency counter, needs an LFU policyO(1)
MEMORY USAGE key [SAMPLES n]Bytes used, value plus overheadO(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.

PolicyCandidate keysPicks
noevictionnoneNothing, writes fail with an OOM error (default)
allkeys-lruevery keyApproximated least recently used
volatile-lrukeys with a TTLApproximated least recently used
allkeys-lfuevery keyApproximated least frequently used (4.0+)
volatile-lfukeys with a TTLApproximated least frequently used
allkeys-randomevery keyAt random
volatile-randomkeys with a TTLAt random
volatile-ttlkeys with a TTLShortest 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.