fivenines

Theory / 10 min

Expiration System

Expiration is one of those Redis features that looks simple from the outside and reveals a careful design inside.

From the user's perspective, the idea is direct:

SET session:7 abc
EXPIRE session:7 60

After sixty seconds, the key should behave as though it no longer exists. The interesting question is how Redis makes that happen without creating a timer for every expiring key.

Expiration Is Metadata

Redis stores values and expiration deadlines separately:

main keyspace: key -> object
expires index: key -> absolute expire timestamp

A key can exist with no expiration. If it does have an expiration, the deadline is an absolute point in time, typically represented in milliseconds.

Absolute time matters. If a server persists a key that expires at 10:05 and restarts at 10:10, the key should not come back to life. Storing "expires in 60 seconds" would be ambiguous after downtime. Storing "expires at this timestamp" makes recovery sane.

Passive Expiration

The cleanest place to discover an expired key is during lookup.

lookup key
  -> check whether it has an expire timestamp
  -> if timestamp <= now, delete it
  -> behave as if the key is missing

This is passive expiration. It is precise for keys that clients touch. A GET for an expired key returns a missing value and removes the key as a side effect.

Passive expiration also keeps command semantics natural. An expired key is not a special third state from the user's point of view. It is gone.

Active Expiration

Passive expiration alone is not enough. A cold key might expire and never be read again. If Redis only deleted expired keys during access, memory could fill with dead data that nobody happens to touch.

Active expiration solves this by periodically sampling keys that have deadlines:

sample expiring keys
delete the ones already expired
continue within a time budget when many sampled keys are dead

The time budget is the important part. Redis could scan every expiring key constantly and keep memory perfectly clean, but that would compete with real client work. Instead, it samples. The system accepts that some expired keys may remain in memory briefly in exchange for predictable server responsiveness.

Expiration Is Not Eviction

Expiration and eviction both delete keys, but they answer different questions.

Expiration asks:

has this key's deadline passed?

Eviction asks:

which key should be removed because memory is too full?

An expired key is logically dead. An evicted key may have been perfectly valid, but memory policy selected it as a victim. Mixing these concepts leads to confused designs.

Command Semantics Flow Through Lookup

Because expiration affects whether a key exists, it should live in shared lookup helpers rather than inside every command. GET, HGET, LPUSH, EXISTS, TTL, and persistence logic all need a consistent view of expired keys.

When the lookup path owns expiration checks, commands naturally agree:

expired key -> missing key

Some commands need specialized behavior, especially those that inspect or modify TTL metadata. But the general key-access path should make stale data disappear before command logic builds on it.

A Small Feature With System-Wide Reach

Expiration touches memory usage, persistence, replication, command semantics, and startup recovery. It has to be lazy enough to stay cheap, active enough to reclaim memory, and precise enough that users can reason about TTLs.

The elegance is in the compromise: metadata for deadlines, passive deletion for touched keys, active sampling for cold keys, and absolute timestamps so persisted data does not cheat time.