Open-Source Wikis

/

Redis

/

Features

/

Expiration

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 keymeta so expireIfNeeded knows 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 cycleactiveExpireCycle in src/expire.c.
  • Change bucket sizesEB_BUCKET_KEY_PRECISION in src/ebuckets.h.
  • Add a TTL command variant — pattern after expireGenericCommand in src/expire.c.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Expiration – Redis wiki | Factory