A Redis hash is a map of field → value stored under one key. Think of it like a tiny dict-inside-a-dict, or a database row where the columns are the fields. It’s the most natural way to store objects in Redis.
Why not just stringify a JSON blob and stuff it in a string key? Because with a hash we can read or update individual fields without touching the rest — no parse, mutate, re-serialize dance.
The basics
HSET user:42 name "Alice" age 30 city "Mumbai"
# (integer) 3 — three new fields
HGET user:42 name
# "Alice"
HGETALL user:42
# 1) "name"
# 2) "Alice"
# 3) "age"
# 4) "30"
# 5) "city"
# 6) "Mumbai"
HMGET user:42 name city
# 1) "Alice"
# 2) "Mumbai"
Update one field cheaply
This is the killer move. Compared to “fetch JSON → parse → mutate → write back”:
HSET user:42 city "Bangalore"
HGET user:42 city
# "Bangalore"
One round trip, one operation, no parsing.
Atomic field counters
HINCRBY is to hashes what INCR is to strings — atomic and useful.
HSET stats:user:42 logins 0 page_views 0
HINCRBY stats:user:42 logins 1
# (integer) 1
HINCRBY stats:user:42 page_views 5
# (integer) 5
Real-world: per-user counters, per-product stock levels, per-feature toggles.
Layout
| field | value |
|---|---|
| name | Alice |
| age | 30 |
| city | Mumbai |
| alice@example.com |
Memory: hashes are tiny when small
When a hash has fewer than ~128 fields and each value is under 64 bytes (defaults configurable via hash-max-listpack-entries), Redis encodes it as a listpack — a contiguous, cache-friendly byte array. This is dramatically smaller than the same data spread across individual string keys.
Real-world example: storing 1M user profiles as user:{id}:name, user:{id}:age etc. uses ~5x more memory than HSET user:{id} name ... age .... This is why hashes are often called the “object encoding” of Redis.
Inspecting
HKEYS user:42 # all field names
HVALS user:42 # all values
HEXISTS user:42 city # 1 or 0
HDEL user:42 city
HLEN user:42 # number of fields
Real-world: cached user profile
async function cacheUser(user) {
await redis.hset(`user:${user.id}`, {
name: user.name,
email: user.email,
plan: user.plan,
lastSeen: user.lastSeen,
});
await redis.expire(`user:${user.id}`, 3600);
}
async function getUserField(id, field) {
return redis.hget(`user:${id}`, field); // one round trip, no parsing
}
Gotchas
HGETALLis O(N) — fine for small hashes, painful when the hash holds 100k fields. UseHSCANfor large ones.- Hash fields don’t have individual TTLs (until Redis 7.4 added per-field TTL). Older versions: the whole key expires together.
- Field names use memory too — keep them short.
nbeatsnameif you have a billion of them; usually not worth it.