Redis Hash for Object Modeling: When and How to Use HSET
Redis Hashes are the natural choice for storing objects — users, products, sessions, configurations. But there are subtle decisions that affect memory usage, query patterns, and maintainability. This guide covers the right way to model data with Hashes.
The Problem
You need to store a user profile. Option A: serialize the whole object as a JSON string. Option B: use a Hash with individual fields.
SET user:1001 '{"name":"Alice","age":30,"email":"alice@example.com","role":"admin"}'
HSET user:1001 name "Alice" age "30" email "alice@example.com" role "admin"
Both work. But they have very different characteristics.
Hash vs String (JSON): When to Use Which
Use Hash When:
- You need to read/update individual fields without fetching the entire object.
- The object has a flat structure (no nesting).
- You want memory efficiency for many small objects (ziplist encoding).
Use String (JSON) When:
- The object is deeply nested.
- You always read/write the entire object.
- You need complex querying (consider RedisJSON module).
Basic HSET Patterns
Creating an Object
HSET user:1001 name "Alice" age "30" email "alice@example.com" role "admin" created_at "2026-01-15"
HGETALL user:1001
HSET supports setting multiple fields in a single command (Redis 4.0+).
Reading Fields
HGET user:1001 name
HGET user:1001 email
HMGET user:1001 name email role
HMGET fetches multiple fields in one round trip — much better than multiple HGET calls.
Partial Updates
This is where Hashes shine. Update one field without touching others:
HSET user:1001 role "superadmin"
HSET user:1001 last_login "2026-02-15"
HGETALL user:1001
With JSON strings, you'd need to GET, deserialize, modify, serialize, and SET — four operations instead of one.
Atomic Counters in Hashes
HSET product:2001 name "Widget" price "9.99" views "0" stock "100"
HINCRBY product:2001 views 1
HINCRBY product:2001 views 1
HINCRBY product:2001 stock -1
HGETALL product:2001
HINCRBY atomically increments a numeric field. Great for counters, stock levels, and view counts.
Float Increments
HSET account:1001 balance "100.00"
HINCRBYFLOAT account:1001 balance 25.50
HINCRBYFLOAT account:1001 balance -10.00
HGET account:1001 balance
Checking Field Existence
HEXISTS user:1001 email
HEXISTS user:1001 phone
Useful for conditional logic — e.g., only send verification email if the email field exists.
Deleting Fields
HDEL user:1001 created_at
HGETALL user:1001
HLEN user:1001
HDEL removes specific fields. HLEN returns the number of remaining fields.
Memory Optimization: Ziplist Encoding
Redis uses a compact "ziplist" encoding for small Hashes. This is significantly more memory-efficient than the hashtable encoding used for large Hashes.
Default thresholds:
hash-max-ziplist-entries: 128 (max number of fields)hash-max-ziplist-value: 64 (max bytes per value)
If your Hash stays within these limits, Redis stores it as a ziplist — using roughly 10x less memory.
HSET small:hash f1 "v1" f2 "v2" f3 "v3"
OBJECT ENCODING small:hash
You should see ziplist (or listpack in Redis 7+).
Pitfall
If any single field value exceeds 64 bytes, the entire Hash converts to hashtable encoding. Keep values short.
HSET test:encoding short "hello"
OBJECT ENCODING test:encoding
HSET test:encoding long "this is a very long string that exceeds the sixty four byte limit for ziplist encoding in redis"
OBJECT ENCODING test:encoding
Modeling Patterns
User Profile
HSET user:1001 username "alice" display_name "Alice Chen" email "alice@example.com" role "admin" status "active" login_count "0"
HINCRBY user:1001 login_count 1
HMGET user:1001 username display_name role
Shopping Cart
HSET cart:user:1001 "product:2001" "2" "product:2002" "1" "product:2003" "3"
HINCRBY cart:user:1001 "product:2001" 1
HDEL cart:user:1001 "product:2003"
HGETALL cart:user:1001
Field = product ID, value = quantity. Atomic increment for "add to cart."
Configuration Store
HSET config:app max_upload_size "10485760" maintenance_mode "false" api_rate_limit "100" feature_dark_mode "true"
HGET config:app maintenance_mode
HSET config:app maintenance_mode "true"
Session Data
HSET session:abc123 user_id "1001" ip "192.168.1.1" user_agent "Mozilla/5.0" created_at "1739600000"
EXPIRE session:abc123 3600
HGET session:abc123 user_id
TTL session:abc123
Handling Nested Data
Hashes are flat — no nesting. For nested objects, you have three options:
Option 1: Flatten with Dot Notation
HSET user:1001 "address.street" "123 Main St" "address.city" "Springfield" "address.zip" "62701"
HGET user:1001 "address.city"
Option 2: JSON in a Field
HSET user:1001 name "Alice" address '{"street":"123 Main St","city":"Springfield","zip":"62701"}'
HGET user:1001 address
Option 3: Separate Hash
HSET user:1001 name "Alice" address_key "user:1001:address"
HSET user:1001:address street "123 Main St" city "Springfield" zip "62701"
HGETALL user:1001:address
Pitfalls
- No per-field TTL: You can't expire individual fields. TTL applies to the entire key. If you need per-field expiration, use separate keys.
- No atomic multi-key Hash operations:
HMGETworks within one Hash. To read fields from multiple Hashes, useMULTI/EXECor pipeline. - Large Hashes block:
HGETALLon a Hash with 100K fields blocks Redis. UseHSCANfor large Hashes:
HSCAN user:1001 0 COUNT 10
- Type confusion: All Hash values are strings.
HGETreturns"30", not the integer30. Parse in your application. - Overusing Hashes: Don't use a single Hash as a "database table" with millions of fields. Use separate keys with a naming convention.
Try It in the Editor
Head to the Redis Online Editor and model some objects:
HSET user:1001 name "Alice" age "30" email "alice@example.com" role "admin"
HGETALL user:1001
HMGET user:1001 name role
HSET user:1001 role "superadmin" last_login "2026-02-15"
HINCRBY user:1001 age 1
HGETALL user:1001
HSET cart:user:1001 "product:2001" "2" "product:2002" "1"
HINCRBY cart:user:1001 "product:2001" 1
HGETALL cart:user:1001
Notice how partial updates and atomic increments work seamlessly. That's the power of Hash-based object modeling.