The most surprising thing about TTL and expiration is that they aren’t about when data becomes invalid, but when the system is allowed to forget it.
Let’s see this in action with Redis. Imagine we have a cache for user profiles.
# Set a user profile with a TTL of 5 minutes (300 seconds)
redis-cli SET user:101 '{"name": "Alice", "email": "alice@example.com"}' EX 300
# Get the user profile
redis-cli GET user:101
# Output: '{"name": "Alice", "email": "alice@example.com"}'
# Wait for 6 minutes
# ... time passes ...
# Try to get the user profile again
redis-cli GET user:101
# Output: (nil)
Here, EX 300 tells Redis to automatically remove user:101 after 300 seconds. It’s not that the data itself changes after 5 minutes; it’s just that Redis is instructed to delete the key and its associated value.
The core problem TTL and expiration solve is managing the finite resources of your cache. Without them, your cache would grow indefinitely, consuming memory and potentially slowing down lookups as more data needs to be scanned. They act as a garbage collector for your cache, ensuring that stale or less frequently accessed data is automatically purged to make way for newer, more relevant information.
Internally, when you set a TTL, Redis (or similar systems) associates a timestamp with the key, representing the point in time when the key should expire. Redis has background processes that periodically scan for expired keys. More actively, when you try to access a key, Redis checks its expiration timestamp. If the current time is past the expiration time, Redis will treat the key as non-existent, return (nil), and then delete it. This "lazy expiration" is efficient because it only incurs the cost of checking and deleting when a key is actually requested.
The primary levers you control are the Time To Live (TTL) for individual keys and, in some systems, global expiration policies that might apply to all keys or specific patterns.
For individual keys, you’re setting a duration. This duration needs to be carefully chosen based on how fresh the data needs to be for your application. A session token might have a TTL of 30 minutes, while a product catalog listing might have a TTL of 1 hour.
# Example: Setting a TTL of 1 hour (3600 seconds) for a product listing
redis-cli SET product:55 '{"name": "Widget", "price": 19.99}' EX 3600
The EX command sets the expiration in seconds. You can also use PX for milliseconds if your cache system supports it.
# Example: Setting a TTL of 1 hour using PX (milliseconds)
redis-cli SET product:55 '{"name": "Widget", "price": 19.99}' PX 3600000
Some systems also support setting expiration when a key is created using SETEX (set with expiration):
# Example: SETEX combines SET and EX
redis-cli SETEX user:102 600 '{"name": "Bob"}' # Sets user:102 with a 600-second TTL
You can also update the TTL of an existing key using the EXPIRE command (in seconds) or PEXPIRE (in milliseconds):
# Example: Extend the TTL of user:101 to 1 hour
redis-cli EXPIRE user:101 3600
If you want to remove the expiration from a key and make it permanent (until manually deleted), you use PERSIST:
# Example: Make user:101 permanent
redis-cli PERSIST user:101
The "churn" aspect comes into play when your TTLs are too short or your data is constantly being updated. If data is written, then read shortly after, then updated again, and its TTL is very short, it might expire and be re-fetched from the origin store many times. This can negate the performance benefits of caching and put undue load on your primary data source. Conversely, if TTLs are too long, your cache can become stale, serving outdated information.
A common pattern is "sliding expiration," where a TTL is reset every time the key is accessed. This is useful for things like user sessions, where you want the session to last for a period after the last activity. While not a direct TTL command, it’s implemented in application logic: when a session is accessed, you re-set its TTL.
What most people don’t realize is that the expiration time itself is not a hard guarantee. If your cache system is under heavy load, or if the background expiration processes are falling behind, it’s possible for a key to exist for a brief period after its logical expiration time. This is why you should never rely on a key not existing to indicate a state change; always check your primary data source if absolute accuracy is critical, or design your application to tolerate slightly stale data.
The next problem you’ll likely encounter is handling cache misses efficiently when your TTLs are set appropriately.