Redis keys expire lazily, and the memory graph shows it late

Setting a TTL on a million session keys does not free a million keys when it elapses. Redis deletes an expired key when something touches it, plus a sample of random keys ten times a second, so memory comes down over minutes or hours rather than at the deadline — and a key nothing ever reads again can sit there for a very long time.

redis-cli INFO stats | grep expired_keys
# expired_keys:412088          cumulative, not a rate

redis-cli CONFIG GET hz
# 1) "hz"
# 2) "10"                      active expiry cycles per second

redis-cli CONFIG GET maxmemory-policy
# 1) "maxmemory-policy"
# 2) "noeviction"              the default, and rarely what a cache wants

redis-cli --stat
# keys       mem       clients  requests             connections
# 184220     412.55M   12       84102931 (+41)       9012

The active half of the cycle is governed by hz: ten times a second, take twenty keys from the set that has a TTL, delete the dead ones, and repeat immediately if more than a quarter of the sample was dead. That adapts well to a burst of expiries and badly to a long tail, which is why the memory graph descends in steps rather than smoothly. Raising hz makes the sweep more aggressive and costs CPU in a single-threaded server, so it is rarely the right lever. The one that matters is maxmemory with an eviction policy — without them, an instance whose expiry is running behind keeps allocating until the OOM killer settles the argument. allkeys-lru for a pure cache, volatile-lru when the same instance also holds data that must not vanish.