skip to content

How does Redis keep track of which keys carry an expiry, what does that bookkeeping cost, and can an individual field inside a hash expire on its own?

level: seniorimportance: nice to knowfreq 28%

answer

  1. two dicts: keyspace + expires (absolute ms)
  2. volatile key = one extra hash entry
  3. INFO keyspace: keys= vs expires=
  4. TTL is per top-level key, not per element
  5. HEXPIRE/HTTL/HPERSIST for hash fields — Redis 7.4+

basics

~20 s

Each database holds a second dictionary, expires, mapping keys that have a TTL to an absolute millisecond deadline — so a volatile key costs an extra hash-table entry. Expiry is per top-level key; Redis 7.4 added HEXPIRE for per-field hash TTLs, and before that you emulated it.

solid answer

~50 s

A Redis database is two dictionaries: the **keyspace** (key → value) and **`expires`** (key → absolute ms deadline), holding entries only for keys that have a TTL. `TTL` subtracts the clock from that stored deadline; `PERSIST` removes the entry; the volatile-only eviction policies use this table as their candidate pool. The cost is one extra hash-table entry per volatile key — the key object is shared, so it is roughly tens of bytes, not a copy of the key. Meaningful only at very large key counts, but it is why `INFO keyspace` reports `expires=` separately from `keys=`. **Expiry has always been per top-level key.** You cannot expire a list element, a set member, or (historically) a hash field: the `expires` table is keyed by top-level key. The classic workarounds are one key per item with its own TTL, or a sorted set scored by expiry timestamp that a sweeper trims with `ZRANGEBYSCORE` + `ZREM`. Redis 7.4 changed this for hashes specifically, adding `HEXPIRE`/`HPEXPIRE`/`HTTL`/`HPERSIST` for per-field TTLs.

code

text · 6 lines
text
> INFO keyspace
# Keyspace
db0:keys=1048576,expires=1041233,avg_ttl=0
# 'expires' is the size of the expires dictionary:
# the eviction pool for any volatile-* policy, and
# the table the background expiry cycle samples.

go deeper

for a junior

Know that TTLs are tracked separately from values and that an expiry applies to a whole key, not to one field or element.

for a middle

Describe the second expires dictionary of absolute deadlines, connect it to INFO keyspace, and name the separate-keys workaround for per-item expiry.

for a senior

Quantify the per-key cost, tie the table to the volatile eviction pool, and choose between separate keys, a sorted-set index, and Redis 7.4 hash-field TTLs with version awareness.

for a principal

Treat expiry granularity as a data-modelling constraint: how key design determines what can be reclaimed, what becomes eviction-eligible, and what your managed Redis version actually supports.

## The two dictionaries Internally a Redis database (`redisDb`) contains, among other things: - **`dict`** — the keyspace: key object → value object, for every key that exists. - **`expires`** — key object → `long long` absolute expiry in milliseconds, containing an entry **only** for keys that have a TTL. The key object is shared between the two tables rather than duplicated, so the marginal cost of making a key volatile is one hash-table entry (a bucket slot plus a small entry structure) — on the order of tens of bytes. For a million volatile keys that is a handful of megabytes: irrelevant at normal scale, worth knowing when you hold hundreds of millions of keys and are weighing whether *everything* needs a TTL. This structure explains several behaviors at once: - `TTL`/`PTTL` are O(1) lookups that subtract the current clock from the stored deadline. - `PERSIST` simply deletes the `expires` entry; the value is untouched. - Deleting a key removes entries from both tables. - `INFO keyspace` reports `db0:keys=…,expires=…,avg_ttl=…` — the two counts come straight from the two tables, which is why the ratio is such a useful health signal. - The `volatile-*` eviction policies draw candidates from `expires`, so a small table means a small eviction pool. - The background expiry cycle samples this table rather than scanning the whole keyspace — the detail of that cycle is its own topic; the relevant point here is that the table exists precisely to make "which keys can expire" cheap to enumerate. ## Expiry granularity: top-level keys only Because `expires` is keyed by the top-level key object, a TTL applies to the entire key. There is no way for `EXPIRE` to reach inside a value: a single list element, set member, sorted-set member or (before 7.4) hash field cannot carry its own deadline. `EXPIRE mysessionhash 60` kills the whole hash — every field with it. This surprises people modeling "a hash of user attributes where each attribute has a different lifetime" or "a set of tokens that expire individually". Two long-standing workarounds: 1. **One key per item.** `session:42:token` with its own TTL instead of a field inside `session:42`. Costs more keys and more `expires` entries, but you get real, server-enforced expiry and the items become independent eviction candidates. This is the default answer. 2. **A sorted set as an expiry index.** Store members in a `ZSET` scored by their expiry timestamp, then have a periodic job (or the read path) run `ZRANGEBYSCORE key -inf <now>` and `ZREM` the results. Nothing expires automatically — you are implementing expiry yourself — so the sweeper must actually run, and readers should filter by score to avoid serving logically-dead members between sweeps. ## Redis 7.4: hash-field expiration Redis 7.4 introduced per-field TTLs for hashes: - `HEXPIRE key seconds [NX|XX|GT|LT] FIELDS numfields field …` - `HPEXPIRE`, `HEXPIREAT`, `HPEXPIREAT` — the millisecond and absolute variants - `HTTL` / `HPTTL` — remaining time per field - `HPERSIST` — remove a field's TTL - `HGETEX` / `HGETDEL` in later 7.4+/8.x releases for read-with-TTL-change and read-and-delete Fields expire independently, and when the last field of a hash expires the key itself is removed. This finally makes the "hash of independently-expiring attributes" model native, and it is the modern answer to the workarounds above — but only if you are on 7.4 or newer, and only for hashes: lists, sets and sorted sets still have no per-element TTL. ## Practical guidance - **Do not TTL everything reflexively.** Every volatile key is an extra `expires` entry and, more importantly, changes eviction semantics under `volatile-*` policies. Give TTLs to data that is genuinely time-bounded. - **Watch the `keys` vs `expires` ratio** in `INFO keyspace` as a hygiene metric: a cache namespace drifting toward fewer expires means some write path is dropping TTLs. - **Prefer separate keys over emulated field expiry** on pre-7.4 servers unless the field count is huge and the read pattern really wants a single hash. - **Check your deployment's version before promising per-field TTLs** — managed Redis offerings lag upstream, and a design that assumes `HEXPIRE` fails hard on 7.2.

  • On a Redis older than 7.4, how would you give individual hash fields different lifetimes?
    Either split them into separate top-level keys, each with its own `EXPIRE` — simplest, server-enforced, and it makes each item an independent eviction candidate — or keep a sorted set scored by expiry timestamp alongside the hash and have a sweeper run `ZRANGEBYSCORE`/`ZREMRANGEBYSCORE` for due members. The sorted-set approach is not real expiry: nothing happens unless your job runs, so readers should also filter by score to avoid serving logically-dead data between sweeps.
  • What is the memory cost of giving a key a TTL, and when does it matter?
    One extra entry in the database's `expires` dictionary — a bucket slot plus a small entry referencing the existing key object, on the order of tens of bytes; the key itself is not duplicated. At a million keys that is single-digit megabytes and irrelevant, but at hundreds of millions of keys it becomes a real line item. The more consequential effect is semantic: under a `volatile-*` eviction policy, only keys in that table are eligible for eviction.

saying these in an interview costs you the question

  • Claiming `EXPIRE` can target a list element or set member.
  • Assuming per-field hash TTLs have always existed, rather than arriving in Redis 7.4.
  • Believing the expires table duplicates the key, so TTLs double key memory.
  • Treating a sorted-set expiry index as real expiry even when no sweeper runs.
  • Thinking `PERSIST` rewrites the value rather than just deleting an entry in the expires table.

context