Databases 16 min read

How Redis Implements Lazy Deletion and When Expired Keys Stay Undeleted

This article explains Redis's two expiration mechanisms—lazy (access‑time) and periodic deletion—then dives into the source code of lookupKey, expireIfNeeded, deleteExpiredKeyAndPropagate, and the underlying delete functions, highlighting the differences between synchronous and asynchronous deletion, the conditions that prevent deletion of expired keys, and practical recommendations for configuring lazyfree.

dbaplus Community
dbaplus Community
dbaplus Community
How Redis Implements Lazy Deletion and When Expired Keys Stay Undeleted

Redis expiration mechanisms

Redis removes expired keys in two ways: lazy deletion (the key is deleted only when a client accesses it) and periodic deletion (a background timer runs every 100 ms, configurable via hz, to scan and delete a batch of expired keys). The article notes that low‑frequency access to expired keys can cause them to linger in memory.

lookupKey implementation

The core function robj *lookupKey(redisDb *db, robj *key, int flags) performs the following steps:

Find the dictionary entry for key in db->dict.

If found, retrieve the value and determine whether the instance is a read‑only replica.

Set expire_flags based on LOOKUP_WRITE, LOOKUP_NOEXPIRE, and replica status.

Call expireIfNeeded(db, key, expire_flags) to possibly delete the key.

If the value is still present, update its LFU/LRU counters and increment stat_keyspace_hits.

If the value is NULL, emit a key‑miss event and increment stat_keyspace_misses.

expireIfNeeded logic

The function int expireIfNeeded(redisDb *db, robj *key, int flags) checks whether the key is expired with keyIsExpired. If the server is a replica and EXPIRE_FORCE_DELETE_EXPIRED is not set, it returns early. It also respects EXPIRE_AVOID_DELETE_EXPIRED, client pause state, and finally calls deleteExpiredKeyAndPropagate to remove the key.

Deletion path

deleteExpiredKeyAndPropagate

decides between asynchronous and synchronous deletion based on the lazyfree_lazy_expire configuration, invoking either dbAsyncDelete or dbSyncDelete. Both functions delegate to dbGenericDelete, which:

Removes the key from db->expires and unlinks it from db->dict.

If asynchronous, calls freeObjAsync to free the value later; otherwise frees it immediately.

Updates cluster slot metadata if needed and finally frees the unlinked entry.

The helper dictFreeUnlinkedEntry releases the key and value memory via the macros dictFreeKey and dictFreeVal, which invoke the appropriate destructor functions ( dictSdsDestructor for keys and dictObjectDestructor for values).

Object destructors and reference counting

dictObjectDestructor

calls decrRefCount unless the value is already NULL. decrRefCount frees the object when its reference count reaches one, dispatching to type‑specific free functions (e.g., freeListObject for lists, which may call quicklistRelease for quicklist‑encoded lists).

Asynchronous free (freeObjAsync)

The function evaluates the free_effort of an object; only when this effort exceeds LAZYFREE_THRESHOLD (default 64) and the object's refcount is 1 does it schedule a background lazy‑free job via bioCreateLazyFreeJob. Otherwise it falls back to immediate decrRefCount.

When expired keys are not deleted

Read‑only replica reads of expired keys (no deletion, returns NULL).

Cluster slot migration/import checks that use LOOKUP_NOEXPIRE.

Client pause states triggered by CLIENT PAUSE, CLUSTER FAILOVER, FAILOVER, or a graceful SHUTDOWN without NOW.

Key takeaways

Both synchronous and asynchronous deletion ultimately call decrRefCount, but synchronous deletion runs in the main thread and can block the server for large keys, while asynchronous deletion offloads heavy frees to a background thread when the cost is high. The lazyfree-lazy-expire option is disabled by default; enabling it allows expired keys to be freed lazily on access. For Redis versions prior to 4.0 (no lazyfree), batch deletion is recommended for large keys.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Memory ManagementLazy Deletionasynchronous deletedbGenericDeletedecrRefCountexpireIfNeeded
dbaplus Community
Written by

dbaplus Community

Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.