Topic 1.2
Hashes: Objects, Sessions, and Field-Level TTL
In one line
A hash maps field names to string values under one key: ideal for objects like users, sessions and carts. Small hashes are stored in a compact listpack, fields can be incremented atomically, and since Redis 7.4 individual fields can expire.
Think of it like this
A paper form in a folder. The folder has one label (the key), and inside are named boxes like Name, Plan and Country (fields). You can correct one box without rewriting the whole form, and a clerk can add 1 to the "visits" box without reading everything else.
Key ideas
- 01
Core commands:
HSET key f1 v1 f2 v2(multiple fields at once;HMSETis deprecated),HGET,HMGET,HGETALL,HDEL,HEXISTS,HINCRBY/HINCRBYFLOAT,HLEN,HKEYS,HVALS,HSETNX,HRANDFIELD, andHSCANfor iterating big hashes. Single-field operations are O(1);HGETALL,HKEYSandHVALSare O(N). - 02
Encoding: while a hash has at most
hash-max-listpack-entries(default 128) fields and every value is at mosthash-max-listpack-value(64 bytes), it's stored as a listpack, a compact contiguous byte array. That can be 5–10× smaller than a full hash table. Past either limit, it converts to a real hash table, permanently. - 03
Hash vs JSON string: a hash gives field-level reads (
HMGET), writes (HSET) and atomic increments (HINCRBY cart:1 sku:9 1), and uses less memory while it's a listpack. A JSON string is simpler when you always read and write the whole object, and supports nesting. Redis 8's JSON type gives both nesting and path-level updates (Topic 4.5). - 04
TTL:
EXPIREapplies to the whole key. Since Redis 7.4,HEXPIRE,HPEXPIRE,HEXPIREAT,HTTL,HPERSISTandHEXPIRETIMEset per-field expiry, useful for things like "remember this device's token for 24 hours" inside one user's hash. Redis 8 addsHGETEX,HSETEXandHGETDEL. On older servers or Valkey versions without this, model per-field expiry as separate keys or a sorted set of expiry times. - 05
Memory trick for millions of tiny objects: bucket them. Instead of 10M keys
user:123 → email, storeuser:bucket:1 → {123: email, ...}with ~100 fields per hash. Each hash stays a listpack and you save the per-key overhead (about 50–70 bytes per key). This is how the classic Instagram "300M photo IDs in 5 GB" story worked (Topic 2.4). - 06
Don't let hashes grow without bound:
HGETALLon a 2M-field hash blocks Redis and produces a huge reply. UseHSCANfor iteration, and split big hashes.
Code & diagrams
127.0.0.1:6379> HSET session:9f3c userId 123 role developer device "iPhone" ip 10.0.0.7 loginTime 1727520000
(integer) 5
127.0.0.1:6379> EXPIRE session:9f3c 1800
(integer) 1
127.0.0.1:6379> HMGET session:9f3c userId role
1) "123"
2) "developer"
127.0.0.1:6379> HINCRBY session:9f3c requests 1
(integer) 1
127.0.0.1:6379> OBJECT ENCODING session:9f3c
"listpack"
# Per-field TTL (Redis 7.4+): the CSRF token expires on its own after 10 minutes
127.0.0.1:6379> HSET session:9f3c csrf "a1b2c3"
(integer) 1
127.0.0.1:6379> HEXPIRE session:9f3c 600 FIELDS 1 csrf
1) (integer) 1
127.0.0.1:6379> HTTL session:9f3c FIELDS 2 csrf role
1) (integer) 600
2) (integer) -1 # role has no field-level TTLOne oversize value converts the whole hash to a hash table, and it never converts back.
127.0.0.1:6379> HSET small a 1 b 2
(integer) 2
127.0.0.1:6379> MEMORY USAGE small
(integer) 64
127.0.0.1:6379> HSET small big "<a 100-byte value ...............................................................>"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING small
"hashtable"
127.0.0.1:6379> MEMORY USAGE small
(integer) 352Interview problem
The problem
User session store for 100M sessions
Design session storage: session:{sessionId} with fields userId, loginTime, device, ip and role. Sessions expire after 30 minutes of inactivity. Support "log out all my devices" and 100M concurrent sessions.
You're given
- 100M sessions
- ~300 bytes of data per session
- 30-minute sliding expiry
- Log out everywhere
- Read on every request
The interviewer follows up
What happens when the session expires in the middle of a user's request?
How do you invalidate all sessions for all users, for example after a security incident?
When it breaks
A hash grows to millions of fields (for example, a per-tenant "all users" hash)
What you see
HGETALL calls take hundreds of ms and block Redis; the reply buffers use lots of memory; deleting the key blocks too.
Fix & prevent
Split by bucket (tenant:acme:users:{0..255}), iterate with HSCAN, delete with UNLINK. Find these with redis-cli --bigkeys.
Assuming EXPIRE works per field on an older server
What you see
Code calls HEXPIRE and gets ERR unknown command in production, or developers assume fields expire when only the whole key does.
Fix & prevent
Check the server version. On older servers, put short-lived data in separate keys.
Explain it without notes
When is a hash a better choice than a JSON string, and when is it worse?
Explain the listpack encoding and the two settings that control it.
Practice
Model a shopping cart with a hash: add an item, change a quantity atomically, remove an item, and read the whole cart.
Create a hash with 200 small fields and compare MEMORY USAGE to the same data in 200 separate string keys.
Trade-offs
- ↔
Bigger listpack thresholds save memory but make each field lookup a linear scan; keep them in the low hundreds.
- ↔
Field-level TTL (7.4+) is convenient but ties you to newer servers; separate keys are portable.
Done when you can
I can model an object, session or cart as a hash and explain why.
I can implement sliding and absolute session expiry.
I know when hashes use listpack encoding and how bucketing saves memory.
I know which Redis version added per-field expiry.