Redis hash fields don’t actually expire individually; you can only set an expiration on the entire hash key itself.

Let’s see how this works with a quick example. Imagine we have a user profile stored in a Redis hash:

> HMSET user:100 name "Alice" email "alice@example.com" last_login "2023-10-27T10:00:00Z"
OK
> HGETALL user:100
1) "name"
2) "Alice"
3) "email"
4) "alice@example.com"
5) "last_login"
6) "2023-10-27T10:00:00Z"

We can set a TTL on the user:100 key, say 60 seconds. After 60 seconds, the entire hash key user:100 will disappear, along with all its fields.

> EXPIRE user:100 60
(integer) 1
> TTL user:100
(integer) 59

After 60 seconds:

> GET user:100
(nil)

This is the fundamental behavior: the expiration is tied to the key, not the individual fields within the hash.

So, how do you achieve the effect of individual field expiration? The most common pattern is to use separate Redis keys for each field, often combined with a timestamp to track when that "field" should be considered expired.

For instance, instead of storing last_login directly in the user:100 hash, you might have a separate key like user:100:last_login.

> SET user:100:last_login "2023-10-27T10:00:00Z" EX 3600
OK

Here, we’ve set the last_login value in its own key and applied an expiration of 1 hour (3600 seconds) directly to that key. If you need to check the user’s last login, you’d fetch this specific key. If it returns (nil), it’s expired.

This approach allows for independent expiration of data points associated with a user. You could have user:100:email expire after 24 hours and user:100:session_token expire after 15 minutes, all while the main user:100 hash (if you still kept one for core, non-expiring data) remains.

The downside is that fetching all user data now requires multiple GET or HGETALL calls, potentially increasing latency and network round trips. To mitigate this, you can use Redis pipelining. Pipelining allows you to send multiple commands to the Redis server at once and receive all the replies together, reducing the overhead of individual network requests.

Let’s say you want to retrieve a user’s name and their last login, with the last login expiring after an hour.

Without Pipelining:

# Assuming redis-py client
pipe = redis_client.pipeline()
pipe.hget("user:100", "name")
pipe.get("user:100:last_login")
results = pipe.execute()

name = results[0]
last_login = results[1]

This executes two separate round trips to Redis.

With Pipelining:

# Assuming redis-py client
pipe = redis_client.pipeline()
pipe.hget("user:100", "name")
pipe.get("user:100:last_login")
results = pipe.execute() # This is a single round trip

name = results[0]
last_login = results[1]

The execute() call bundles all the commands into one request.

This pattern of using separate keys for expirable data is crucial when you need fine-grained control over data lifetimes within a logical entity. It’s a common workaround for the lack of native per-field expiration in Redis hashes. You’re essentially decomposing your hash into a set of individual key-value pairs, each with its own TTL.

When designing your Redis schema, consider what data truly needs to expire and at what granularity. If a piece of data is transient or session-specific, it’s a prime candidate for its own key with an EXPIRE or SET ... EX command. Core, persistent data can remain in a hash, or even a separate key if it’s a single value. The key is to map your application’s data expiration requirements to Redis’s key-based expiration mechanism.

The next challenge you’ll likely encounter is managing the cleanup of these individual expirable keys when the parent entity (like the user) is deleted, to avoid orphaned data.

Want structured learning?

Take the full Redis course →