Skip to content

Redis Hashes: Storing Objects

Aman Kumar Singh4 min read
Part 4 of 15From the Redis Masterclass series
Redis Hashes: Storing Objects — article by Aman Kumar Singh

When the thing you're storing has more than one field, a plain string forces an awkward choice: serialize the whole object into JSON and rewrite it on every change, or spread the fields across many separate keys. Hashes are the structure that fits objects properly. They store field-value pairs under one key, let you read and update individual fields, and keep related data grouped, which is exactly what you want for a user record, a session, or a cached row.

This is part 4 of the Redis Masterclass, following strings and counters.

The basic operations

A hash is a map of fields to values under a single key:

HSET user:1 name "Aman" plan "pro" logins "42"
HGET user:1 plan          # "pro"
HGETALL user:1            # all fields and values
HMGET user:1 name plan    # several fields at once
HDEL user:1 logins        # remove a field

The win over a serialized JSON string is direct field access. To read one field you HGET it, not fetch and parse the whole object. To update one field you HSET it, not read-modify-write the entire blob. On a large object that's both faster and safer under concurrency, because two clients updating different fields don't clobber each other.

Why not just store JSON in a string?

This is the choice people wrestle with, so it's worth being clear. Storing an object as a JSON string is simple and fine when you always read and write the whole object at once, like a cache entry you replace wholesale. A hash is better when:

  • You read or update individual fields often. HGET user:1 plan beats fetching and parsing a whole JSON blob to read one field.
  • Multiple clients update different fields concurrently. With JSON, two writers each read-modify-write the whole object and one loses the other's change. With a hash, HSET-ing different fields is independent and safe.
  • You increment a numeric field. HINCRBY bumps a field atomically, which JSON-in-a-string can't do without a read-modify-write race.

The rule: whole-object access, a JSON string is fine; field-level access or concurrent field updates, use a hash.

Atomic field operations

Like strings, hashes have atomic numeric operations, but scoped to a field:

HINCRBY user:1 logins 1          # atomically +1 on the logins field
HINCRBY cart:99 item:42 2        # +2 to the quantity of item 42
HINCRBYFLOAT account:1 balance 9.99

This is genuinely useful for objects with counters inside them. A shopping cart stored as a hash (field per product, value is quantity) lets you atomically adjust one item's quantity with HINCRBY while other fields stay untouched. A per-user stats object can bump logins or posts independently and safely. You get object grouping and atomic per-field counters together.

Memory efficiency

Hashes have a nice property at scale: for small hashes, Redis uses a compact memory encoding (a listpack) that's much more space-efficient than storing each field as a separate top-level key. So grouping an object's fields into one hash uses less memory than the equivalent set of individual keys, in addition to keeping them logically together.

This is why a common recommendation for storing many small objects is to use hashes rather than one key per field. A million user records as hashes use noticeably less memory than the same data spread across millions of individual string keys, and they're tidier to manage (one key per user, one DEL to remove a user entirely).

Expiry: the one caveat

There's a limitation to know. Historically, expiry (TTL) in Redis applies to a whole key, not to individual hash fields. You could expire the entire user:1 hash, but not just its logins field. This meant per-field expiry needed workarounds or a different structure.

Recent Redis versions (7.4 and later) added the ability to set TTLs on individual hash fields with HEXPIRE, which removes the historical limitation:

HEXPIRE user:1 3600 FIELDS 1 session_token   # expire one field in 1 hour

If you're on an older version, remember that TTL is key-level: to expire per-field data you'd store those fields under their own keys, or move to a version that supports HEXPIRE. For most object-storage uses you expire the whole hash anyway (a whole session, a whole cached record), so this rarely bites.

When to reach for a hash

Use a hash when you're storing an object whose fields you access or update independently:

  • User records and profiles: fields read and updated one at a time.
  • Sessions: a session object with several attributes, expired as a unit.
  • Cached database rows: a row's columns as fields, updatable individually.
  • Objects with embedded counters: carts, per-entity stats, anything with numeric fields to bump atomically.

Reach for a JSON string instead when you only ever read and write the whole object at once, and the simplicity of a single serialized blob outweighs field-level access.

Hashes are how Redis stores structured objects without giving up field-level access or atomic per-field updates. They group related data under one key, save memory for small objects, and let concurrent writers touch different fields safely. When your value is an object rather than a single thing, a hash is almost always the right structure.

Next, we cover lists: ordered sequences that back queues and recent-item feeds, and the blocking operations that turn them into simple work queues.

Key takeaways

  • A hash stores field-value pairs under one key, giving direct access to individual fields instead of a whole serialized blob.
  • Prefer a hash over a JSON string when you access fields individually or multiple clients update different fields concurrently.
  • `HINCRBY` atomically increments a single field, ideal for objects with embedded counters like carts and per-entity stats.
  • Small hashes use a compact memory encoding, so grouping fields is more space-efficient than many individual keys.
  • TTL is historically key-level; Redis 7.4+ adds per-field expiry with `HEXPIRE`, but you usually expire a whole hash anyway.

Frequently asked questions

When should I use a Redis hash instead of a JSON string?

Use a hash when you read or update individual fields often, or when multiple clients update different fields concurrently, since a hash allows independent field access and updates. Use a JSON string when you always read and write the whole object at once.

Can I increment a single field in a Redis hash?

Yes, with `HINCRBY` (or `HINCRBYFLOAT`), which atomically adds to one field's numeric value without touching the others. This is ideal for objects with embedded counters, like a cart's per-item quantities.

Are hashes more memory-efficient than separate keys?

For many small objects, yes. Redis encodes small hashes compactly, so grouping an object's fields into one hash uses less memory than storing each field as its own top-level key, and it keeps the data logically grouped.

Can I set an expiry on a single hash field?

Historically no, TTL applied only to the whole key. Redis 7.4 and later added `HEXPIRE` for per-field TTLs. On older versions you expire the entire hash or store per-field-expiring data under separate keys.

How do I store a user object in Redis?

Use a hash keyed like `user:1` with fields for each attribute (`name`, `plan`, `logins`). You can then read or update any field directly and increment numeric fields atomically, and delete the whole user with a single `DEL`.

Related articles

Redis Sets and Set Operations — Aman Kumar Singh
Redis Data Structures: An Overview — Aman Kumar Singh
Redis Caching Best Practices and Pitfalls — Aman Kumar Singh
Aman Kumar Singh

About the author

I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.