The Error Scenario
While building a session cache for a Node.js service, I ran into a bug that only appeared when users reached the very end of their login period. My code calculated the TTL (Time To Live) dynamically by subtracting the current time from the session's expiry timestamp. During high-traffic testing, everything seemed fine, but as soon as a session had only milliseconds left, the logs started filling up with this:
(error) ERR invalid expire time in 'set' command
You can easily trigger this error in the Redis CLI. If you try to use SET with an EX (seconds) or PX (milliseconds) argument of zero or less, Redis refuses the operation entirely.
# This fails because 0 is not a positive integer
redis> SET session:user:123 "data" EX 0
(error) ERR invalid expire time in 'set' command
# Negative values also trigger the error
redis> SET session:user:123 "data" EX -10
(error) ERR invalid expire time in 'set' command
Analysis: Why the SET Command is Strict
Redis requires the EX and PX options to be positive integers. This is a common trap for developers because the standalone EXPIRE command is much more lenient. Running EXPIRE key 0 simply deletes the key. However, SET ... EX is an atomic operation. Redis enforces stricter validation here to prevent ambiguous commands from being partially executed.
Most crashes occur when the TTL math looks like this:
# Python example
ttl = target_expiry - current_time
# If current_time >= target_expiry, ttl is 0 or negative
redis_client.set("session_key", "value", ex=ttl)
Even a 5ms delay in your application logic can turn a positive TTL into a zero. This causes your entire database write to fail instead of just expiring the data gracefully.
The Difference Between SET EX and EXPIRE
It helps to think of SET ... EX as a creation command and EXPIRE as a maintenance command. Since SET is trying to write new data, Redis wants to ensure the expiration logic makes sense before it commits the value to memory. If the time is invalid, it rejects the whole command. The data isn't saved, and your application likely throws an unhandled exception.
Solution 1: Sanitize Your TTL Logic
The most immediate fix is to ensure your TTL is always at least one second (or one millisecond for PX). If your calculation results in zero or less, the data is technically already expired. In that case, you should either skip the SET entirely or delete any existing key to maintain consistency.
Python (redis-py) Implementation
def safe_set(key, value, ttl_seconds):
# Ensure TTL is at least 1, or skip the write
if ttl_seconds > 0:
redis_client.set(key, value, ex=int(ttl_seconds))
else:
# The data is already stale; just ensure it's gone
redis_client.delete(key)
Node.js (ioredis) Implementation
async function setWithExpiry(key, value, ttlMs) {
if (ttlMs > 0) {
await redis.set(key, value, "PX", ttlMs);
} else {
// Handle the edge case where the session expired during processing
await redis.del(key);
}
}
Solution 2: Use Absolute Expiration (EXAT/PXAT)
If you are running Redis 6.2 or newer, you can avoid TTL math altogether by using EXAT or PXAT. These options let you provide a specific Unix timestamp (e.g., 1735689600) rather than a relative offset. This is much more reliable for session management.
# Set key to expire at a specific timestamp
redis> SET session:user:123 "data" EXAT 1735689600
Pro Tip: Unlike EX, if you provide a timestamp in the past to EXAT, Redis 6.2+ will return OK but will either delete the existing key or never create the new one. This behavior is much safer for dynamic applications because it prevents the "invalid expire time" crash entirely.
Verifying the Fix
Test your implementation against these three scenarios to ensure stability:
- Normal Case: Provide a TTL of 60 seconds. Verify the key exists and
TTL keyreturns approximately 60. - The Edge Case: Force a TTL of 0 or -1 in your code. Ensure the application doesn't crash and the key is not present in Redis.
- Clock Sync: If your app and Redis run on different servers, use NTP to keep clocks in sync. A 1-second drift can cause havoc when sessions are near their limit.
The Bottom Line
When you see ERR invalid expire time, itβs almost always a sign of a race condition in your TTL math. Redis SET expects a positive number. By adding a simple if ttl > 0 check or switching to EXAT, you can make your caching layer much more resilient to the "last second" session bug.

