Redis Cheatsheet
Hashes
Use this Redis reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Command table
A hash is a map from field to string value inside one key. It is the natural fit for an
object, and small hashes are stored as a flat listpack that costs a fraction of the
equivalent set of top-level keys.
| Command | What it does | Complexity |
|---|---|---|
HSET key f v [f v ...] | Set fields, return how many were new | O(n) fields |
HSETNX key f v | Set one field only if it does not exist | O(1) |
HGET key f | One field's value, nil if absent | O(1) |
HMGET key f [f ...] | Several fields, nil per missing field | O(n) fields |
HGETALL key | Every field and value, flattened | O(n) hash size |
HDEL key f [f ...] | Delete fields, return how many were removed | O(n) fields |
HEXISTS key f | 1 or 0 | O(1) |
HLEN key | Number of fields | O(1) |
HSTRLEN key f | Byte length of one field's value | O(1) |
HKEYS key | Every field name | O(n) |
HVALS key | Every value | O(n) |
HINCRBY key f n | Add a signed integer to a field | O(1) |
HINCRBYFLOAT key f f | Add a float to a field | O(1) |
HRANDFIELD key [n [WITHVALUES]] | Random fields, negative n allows repeats (6.2+) | O(n) returned |
HSCAN key cursor [MATCH p] [COUNT n] [NOVALUES] | Cursor iterate, NOVALUES is 7.4+ | O(1) per call |
HGETEX / HGETDEL | Get with TTL options, get and delete (8.0+) | O(n) fields |
HMSET is a deprecated alias for HSET and has been variadic-equivalent since 4.0. Use
HSET.
Reading and writing
HSET hu:user:1042 name "Ada" email "ada@example.com" plan free # (integer) 3, the count of NEW fields, so a re-run returns 0 HGET hu:user:1042 email # "ada@example.com" HMGET hu:user:1042 name plan missing # "Ada", "free", (nil) HGETALL hu:user:1042 # field, value, field, value, ... HDEL hu:user:1042 plan # (integer) 1 HLEN hu:user:1042 # (integer) 2 HEXISTS hu:user:1042 plan # (integer) 0
HGETALL returns a flat array in RESP2 and a real map in RESP3, so a client on
HELLO 3 hands you a dict while a RESP2 client hands you alternating entries. HDEL of
the last field deletes the key itself, and HLEN on a missing key is 0, so there is
no way to distinguish an empty hash from an absent one. Empty hashes do not exist.
HGETALLon a hash with 500,000 fields is O(n) on a single-threaded server and ships the whole thing over the wire. UseHMGETfor the fields you need, orHSCANto walk it in batches.
Counters inside a hash
HINCRBY gives you a whole panel of counters under one key, with one expiry and one
memory allocation.
HINCRBY hu:stats:post:42 views 1 HINCRBY hu:stats:post:42 likes 1 HINCRBY hu:stats:post:42 views 5 HINCRBYFLOAT hu:cart:9 total 19.99 HGETALL hu:stats:post:42 # views 6, likes 1 EXPIRE hu:stats:post:42 86400 # one TTL covers every counter
Same rules as INCR: the field must parse as an int64, a missing field starts at 0, the
result can go negative, and HINCRBYFLOAT returns a string. There is no HDECRBY, pass a
negative number.
Iterating and sampling
# Walk a large hash without blocking HSCAN hu:user:1042 0 MATCH 'pref:*' COUNT 100 HSCAN hu:user:1042 0 NOVALUES # field names only, 7.4+ # Random sampling HRANDFIELD hu:user:1042 2 WITHVALUES # 2 distinct fields plus values HRANDFIELD hu:user:1042 -5 # 5 draws WITH repetition HRANDFIELD hu:user:1042 # exactly one field name
The sign of the count is the whole API: positive means distinct and capped at the hash
size, negative means repeats allowed and the exact count you asked for. SRANDMEMBER,
ZRANDMEMBER, and HRANDFIELD all share that convention.
Per-field TTL (7.4+)
Until 7.4 a TTL applied to the whole key, which is why people fell back to N string keys whenever individual fields had to expire. That is fixed.
| Command | What it does |
|---|---|
HEXPIRE key sec [NX XX GT LT] FIELDS n f [f ...] | TTL per field, in seconds |
HPEXPIRE key ms ... FIELDS n f ... | Same, milliseconds |
HEXPIREAT / HPEXPIREAT | Absolute deadline per field |
HTTL / HPTTL key FIELDS n f ... | Remaining TTL per field |
HEXPIRETIME / HPEXPIRETIME | Absolute expiry time per field |
HPERSIST key FIELDS n f ... | Remove a field's TTL |
HSET hu:session:abc csrf tok123 uid 1042 HEXPIRE hu:session:abc 300 FIELDS 1 csrf # 1) (integer) 1 HTTL hu:session:abc FIELDS 1 csrf # 1) (integer) 300 HPERSIST hu:session:abc FIELDS 1 csrf # 1) (integer) 1
The FIELDS numfields block is mandatory and the count must match, which trips everyone
the first time. Return codes per field: 1 set, 0 refused by an NX/XX/GT/LT
condition, -2 no such field, and 2 when the TTL was in the past so the field was
deleted immediately. When the last field with a TTL expires the key disappears.
Note that per-field TTL forces the hash out of listpack encoding, so it costs memory.
Hash or N string keys
| One hash | N string keys | |
|---|---|---|
| Memory for a small object | Far less, one listpack allocation | ~50-100 bytes overhead each |
| Read all fields | One HGETALL | One MGET, needs every key name |
| Expire one field | HEXPIRE (7.4+) | Native, any version |
| Atomic multi-field write | HSET f v f v, one command | MSET, also atomic |
| Cluster locality | All fields on one slot, so scripts work | Keys scatter, CROSSSLOT errors |
| Secondary index on a value | No, hashes are not indexed | No, same |
OBJECT ENCODING hu:user:1042 # "listpack" while small, "hashtable" after
Encoding flips to hashtable on the first write that exceeds
hash-max-listpack-entries (128) or hash-max-listpack-value (64 bytes), and it
never flips back even if you delete fields down again. That one-way conversion is why
the classic advice is to shard a huge logical hash into buckets:
# 10M ids in 100k hashes of ~100 fields each stays in listpack encoding HSET hu:emails:bucket:0042 1042 "ada@example.com"
Rules of thumb:
- Use a hash for an object whose fields you read or write individually.
- Use one string holding JSON when you always read and write the whole thing, since it
is one round trip and the client does the parsing.
- Never use a hash as a general index. There is no "find the field whose value is X"
short of HSCAN over everything.