Redis EXPIRE is per key, and a plain SET clears it

Expiry in Redis is metadata attached to a key rather than an argument of the write, which is the opposite of the memcached-style API most people arrive from. The consequence is easy to miss: a plain SET over an existing key removes its TTL, and the cache entry becomes permanent.

SET page:home "<html>…"
EXPIRE page:home 300
TTL page:home            # (integer) 300

APPEND page:home "…"     # TTL survives — the value changed in place
TTL page:home            # (integer) 300

SET page:home "<html>…"  # TTL gone — the key was replaced
TTL page:home            # (integer) -1

SET page:home "<html>…" EX 300   # 2.6.12 and later: one command

The rule is that commands overwriting the whole value — SET, GETSET, RENAME onto an existing key — discard the expiry, while commands modifying it in place — INCR, APPEND, HSET, LPUSH — leave it alone. So a cache warmer that refreshes entries with a plain SET silently converts them into permanent keys. Nothing breaks; memory just climbs until eviction starts discarding entries somebody still wanted, and without a maxmemory-policy it does not evict at all. The EX option on SET, available since 2.6.12, makes it one atomic command and is worth making the default. TTL returning -1 means the key exists with no expiry; 2.8, out last month, finally returns -2 for a key that is simply not there.