redis/redis
Expiration
Active contributors: antirez, Oran Agra, debing.sun, Binbin.
Every key in Redis can carry a TTL. When the TTL elapses the key is deleted. Hash fields can also carry their own TTLs (Redis 7.4+).
Source layout
| File | Role |
|---|---|
src/expire.c |
The TTL command implementations and the active expiration cron. |
src/estore.c, src/estore.h |
Per-DB expiration store. |
src/ebuckets.c, src/ebuckets.h |
Bucketed time-ordered structure. |
src/keymeta.c, src/keymeta.h |
Per-key metadata, including hash-field minimum TTL. |
src/entry.c, src/entry.h |
Field-level entry types with per-field TTLs. |
src/t_hash.c |
Hash-field expiration commands. |
Commands
| Command | Purpose |
|---|---|
EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT |
Set TTL (relative or absolute, seconds or milliseconds). With NX/XX/GT/LT modifiers since Redis 7.0. |
PERSIST |
Remove TTL. |
TTL, PTTL |
Get TTL in seconds / milliseconds. |
EXPIRETIME, PEXPIRETIME |
Get the absolute expiration time. |
OBJECT IDLETIME / FREQ |
Read the LRU/LFU bits (used by eviction). |
HEXPIRE, HPEXPIRE, HEXPIREAT, HPEXPIREAT |
Per-field TTL on hashes. |
HTTL, HPTTL, HEXPIRETIME, HPEXPIRETIME |
Read per-field TTLs. |
HPERSIST |
Remove a field's TTL. |
HSETEX, HGETEX, HGETDEL |
Atomic combinations of set/get + TTL. |
DELEX, XDELEX, XACKDEL |
Conditional deletes that consider expiration state. |
Lazy vs active expiration
Lazy. When a command accesses a key, expireIfNeeded(db, key) is called first. If the key has expired it is deleted, a keyspace notification is fired (expired), and the access is treated as a miss.
Active. serverCron calls activeExpireCycle which walks the expiration store sampling buckets. If too many sampled keys have expired (the threshold is ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE), it sweeps more aggressively. The cycle has a CPU budget of active-expire-effort % of the cron's per-tick budget.
The cycle is split into "fast" and "slow" modes. The fast mode runs more often but with a smaller budget; it is invoked from beforeSleep to keep latency low. The slow mode runs from the cron and does the bulk of the work.
Expiration store
Per database, the estore (src/estore.c) holds a mapping from (slot, key) → expire-time. It is backed by an ebuckets — a bucketed radix structure where buckets correspond to time ranges. The bucketing trades absolute precision for cheap "give me everything that has already expired" sampling.
graph LR
DB[redisDb] --> Estore[estore]
Estore --> Bucket1[bucket: 0..1024 ms from epoch]
Estore --> Bucket2[bucket: 1024..2048 ms]
Estore --> Bucket3[bucket: ...]
Bucket1 --> E1[entry key=foo expire=1234]
Bucket1 --> E2[entry key=bar expire=1245]When a TTL is set the entry is inserted into the right bucket. When it is removed (key deleted, TTL extended past the bucket boundary) the entry is moved or unlinked. Buckets older than "now" are eligible for sweeping.
This design beats a global priority queue because:
- Insertions are O(1) given a precomputed bucket.
- Sweeping a single bucket is O(items in bucket).
- Memory overhead per entry is small (no skiplist/heap pointers).
Hash-field TTLs
Hash-field TTLs (Redis 7.4+) attach a per-field expiration to entries inside a hash. The implementation pulls in:
- A new entry type (
src/entry.c/h) that carries a TTL alongside the field value. - Field-aware listpack encodings (
OBJ_ENCODING_LISTPACK_EX,OBJ_ENCODING_LISTPACK_HFE). - A per-key minimum-TTL index in
keymetasoexpireIfNeededknows whether to look at field-level expirations at all. - Sweeping integrated into the same
activeExpireCycle— buckets contain hash entries too.
HEXPIRE, HPEXPIRE etc. take a list of fields and a TTL plus optional NX/XX/GT/LT.
OBJECT ENCODING of a hash with field TTLs returns listpack_ex (the listpack-with-extras format) until it converts to hashtable for size; keymeta continues to track the minimum TTL across fields.
Replication of expirations
Expiration is propagated as a DEL command at the moment of expiration on the master. Replicas do not run their own active-expiration cron; they trust the master. Lazy expiration on a replica is also disabled (the read side of expireIfNeeded is skipped) — instead the replica returns the value as if it hadn't expired, until the master deletes it.
This is critical: if the replica decided to delete locally, two replicas could disagree about a key's existence, and a failover would propagate that disagreement. The "single writer for expiration" rule keeps the dataset consistent.
Tuning
| Option | Effect |
|---|---|
active-expire-effort (1..10) |
Higher means more CPU spent on expiration each cron tick. |
lazyfree-lazy-expire (yes/no) |
Asynchronously free large objects when they expire. |
notify-keyspace-events Ex |
Get a Pub/Sub __keyevent@<db>__:expired for each expiration. |
hz |
Higher means more frequent cron ticks; expiration work is per-tick budgeted. |
Where to start modifying
- Tweak the cycle —
activeExpireCycleinsrc/expire.c. - Change bucket sizes —
EB_BUCKET_KEY_PRECISIONinsrc/ebuckets.h. - Add a TTL command variant — pattern after
expireGenericCommandinsrc/expire.c.
Related pages
- ebuckets — the underlying structure.
- Memory management — expiration interacts with eviction and lazyfree.
- Replication — only the master runs active expiration.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.