HSET, HGET, and HGETALL
A Redis hash is a data type that maps field names to values, all stored under a single top-level key — like a small dictionary or object nested inside your keyspace. Instead of splitting a user’s name, email, and age across three separate string keys, you store them as three fields inside one hash key. HSET, HGET, and HGETALL are the core commands for writing, reading one field, and reading every field of a hash, and mastering them is essential for modeling structured records in Redis.
Overview / How Hashes Work
A hash key holds an unordered collection of field-value pairs. Both the field name and the value are binary-safe strings, just like a regular Redis string value. This makes a hash a natural fit for representing an entity’s attributes — a user profile, a product, a session, a configuration object — under one key, rather than as a serialized blob or as many scattered top-level keys sharing a naming prefix.
Internally, Redis chooses between two encodings for a hash, automatically and transparently. Small hashes — by default, up to 128 fields where every field and value is under 64 bytes — use a compact, densely packed listpack encoding. This is memory-efficient and fast for small collections because there’s no per-field pointer or hash-table overhead. Once a hash exceeds either threshold (configurable via hash-max-listpack-entries and hash-max-listpack-value), Redis converts it to a full hash table encoding, trading some memory for guaranteed O(1) average-time field lookups regardless of size. This conversion happens once and is not reversed even if you later delete fields back below the threshold.
Because Redis is single-threaded — every command runs to completion on the main thread before the next one starts — a single HSET call, even one that writes several field-value pairs at once, executes atomically. No other client’s command can interleave mid-write and see a partially updated hash. This is one of the reasons hashes are safe to update concurrently from multiple clients without extra application-level locking.
HGET reads exactly one field and is O(1). HGETALL reads every field and value in the hash and is O(N), where N is the number of fields — Redis has to walk the whole hash to build the reply. For a hash with a handful of fields this is trivial, but for a hash with thousands of fields, repeatedly calling HGETALL in a hot path can add real latency, since it blocks the single-threaded server for the duration of the walk (much smaller in scope than KEYS, but the same underlying principle: work proportional to size costs time on a single thread).
Syntax
HSET key field value [field value ...]
HGET key field
HGETALL key
| Command | Description | Time Complexity |
|---|---|---|
HSET key field value ... |
Sets one or more field-value pairs in the hash at key, creating the hash if it doesn’t exist. Existing fields are overwritten with the new value. |
O(1) per field-value pair (O(N) for N pairs in one call) |
HGET key field |
Returns the value of a single field, or (nil) if the field or the key doesn’t exist. |
O(1) |
HGETALL key |
Returns all fields and values in the hash as a flat array (field, value, field, value, …), or an empty array if the key doesn’t exist. | O(N), N = number of fields in the hash |
- key — the top-level Redis key under which the hash is stored, e.g.
user:1001. - field — the name of an attribute inside the hash, e.g.
nameorprice. - value — the string value for that field. Quote it if it contains spaces.
Examples
Example 1: Setting and reading a single field
HSET user:1001 name "Ada"
HGET user:1001 name
(integer) 1
"Ada"
HSET returns (integer) 1 because it created one new field (name) on a hash that didn’t exist yet, implicitly creating the hash. HGET then returns "Ada" for that field.
Example 2: Setting multiple fields at once
HSET user:1002 name "Grace" email "grace@example.com" age "34"
HGETALL user:1002
(integer) 3
1) "name"
2) "Grace"
3) "email"
4) "grace@example.com"
5) "age"
6) "34"
A single HSET call can set as many field-value pairs as you need in one atomic write. HGETALL returns them as a flat array alternating field name and value — your client library will typically turn this into a native map or dictionary.
Example 3: Updating an existing field
HSET product:2001 name "Widget" price "9.99" stock "100"
HGET product:2001 price
HSET product:2001 price "12.99"
HGETALL product:2001
(integer) 3
"9.99"
(integer) 0
1) "name"
2) "Widget"
3) "price"
4) "12.99"
5) "stock"
6) "100"
The first HSET creates three new fields, so it returns 3. The second HSET only updates the existing price field — no new field is created — so it returns 0, even though the value did change. HGETALL confirms only price changed; name and stock are untouched.
How It Works Step by Step
When HSET key field value ... runs, Redis first looks up key in the main keyspace dictionary. If the key doesn’t exist, it creates a new, empty hash object using listpack encoding. Then, for each field-value pair in the command, Redis checks whether that field already exists in the hash: if it does, the value is overwritten in place; if not, a new field is inserted and the operation’s new-field counter increments. That counter is what HSET returns as its integer reply — the number of fields that were newly added, not the number of fields you passed in or the hash’s total size.
HGET looks up the hash object for key, then looks up the requested field directly within it — a single O(1) operation whether the hash uses listpack or hashtable encoding (listpacks are so small that a linear scan is effectively instant too). HGETALL instead walks every stored field-value pair in order and appends each one to the reply array, which is why its cost scales with the hash’s size.
TTLs interact with the whole key, not individual fields. Only EXPIRE (or similar) applied to the hash key itself sets an expiration — there’s no way to expire a single field independently:
HSET session:abc999 user_id "42"
EXPIRE session:abc999 60
TTL session:abc999
(integer) 1
(integer) 1
(integer) 60
EXPIRE returns 1 because it successfully attached a 60-second TTL to session:abc999, and TTL confirms 60 seconds remain. When that TTL elapses, the entire hash — every field in it — is deleted at once.
Common Mistakes
Mistake 1: Storing a serialized blob instead of a hash
SET user:5001 '{"name":"Leo","age":"40"}'
GET user:5001
OK
"{\"name\":\"Leo\",\"age\":\"40\"}"
This works, but it’s the wrong tool: to change just age, you’d have to fetch the whole string, parse it in your application, modify it, and write the entire blob back — not atomic, and wasteful for a one-field change. Model the record as a hash instead, so each field is independently readable and writable:
HSET user:5001 name "Leo" age "40"
HSET user:5001 age "41"
HGETALL user:5001
(integer) 2
(integer) 0
1) "name"
2) "Leo"
3) "age"
4) "41"
Now updating age is a single atomic command that touches only that field.
Mistake 2: Calling hash commands on the wrong type
SET session:abc123 "token-value"
HGETALL session:abc123
OK
(error) WRONGTYPE Operation against a key holding the wrong kind of value
Every Redis key has exactly one type. session:abc123 was created with SET, so it holds a string — running any hash command against it fails with a WRONGTYPE error instead of silently doing something unexpected. Delete the key and recreate it with HSET, or use a differently named key for the hash, to fix this.
Mistake 3: Misreading HSET’s return value
As shown in Example 3, HSET‘s integer reply counts only newly created fields — it is easy to assume it reports the total number of fields in the hash, or the number of fields updated. It does neither. If you need the total field count, use HLEN key instead of inferring it from HSET‘s reply.
Best Practices
- Use a hash to group an entity’s related attributes under one key instead of scattering many top-level string keys with a shared prefix — fewer keys, less per-key overhead, and one
EXPIREcovers the whole record. - Avoid serializing an object into a single string value when you need to read or update individual fields often; a hash makes each field independently addressable without a read-modify-write round trip.
- When you only need a few known fields, prefer
HMGET(or targetedHGETcalls) overHGETALLto avoid transferring fields you don’t need, especially on wide hashes. - Keep hash values reasonably small; very large per-field values force the listpack-to-hashtable conversion sooner and hurt memory locality.
- Remember TTLs apply to the entire hash key, never to a single field — if you need per-field expiry, model it with separate keys or a sorted set tracking expiration timestamps.
- Don’t infer field counts from
HSET‘s return value — useHLENwhen you need the actual number of fields in a hash. - For counters stored inside a hash (like a view count or stock level), use
HINCRBYrather than a GET-modify-HSETround trip, which is not atomic across separate calls.
Practice Exercises
- Model a shopping cart as a hash at
cart:7001with fieldsitem_countandtotal_price. Set both in a singleHSETcall, then useHGETto read back justtotal_price. - Create a hash
config:appwith three feature-flag fields of your choosing. Update only one flag’s value, then useHGETALLto confirm the other two fields are unchanged. - Create a key with
SET, then tryHGETALLagainst it. Note the exact error Redis returns and explain, in your own words, why hashes and strings can’t be mixed on the same key.
Summary
HSET key field value [field value ...]writes one or more fields to a hash atomically, in O(1) per pair, creating the hash if needed.HGET key fieldreads a single field in O(1), returning(nil)if the field or key is missing.HGETALL keyreturns every field and value as a flat array in O(N) — use it sparingly on very large hashes.- Small hashes use the compact listpack encoding; hashes that grow large enough auto-convert to a hashtable encoding, permanently.
HSET‘s integer reply counts newly added fields only, not updated fields or the hash’s total size — useHLENfor that.- TTLs set with
EXPIREapply to the whole hash key; individual fields cannot expire independently. - Running a hash command on a key created with
SET(or vice versa) raises aWRONGTYPEerror, since every key has exactly one type.
