TTL and cleanup — lazy, active, and the freer thread

TTL expiration runs on two independent paths — passive on access plus a 10Hz active sampler that pulls 20 random TTL'd keys per tick and resamples while >25% are expired — and the cleanup itself stalls the single loop on a big DEL unless lazyfree-* (or UNLINK) hands the free to the background thread.

Previously

Eviction is the memory-pressure path. TTL is the clock path — keys leave because their time is up, not because Redis ran out of room. Same lazyfree lane, different trigger.

Scene 05a

TTL and cleanup — lazy, active, and the freer thread

  1. Watch
  2. Try it
  3. Predict
  4. Capture
active-expire · 10 Hzsample 20 random TTL'd keys per tick · resample if >25% expiredTHIS TICK0 resamplesSAMPLED0 keysKEYSPACE · 24 keys · 12 with TTLsolid border = TTL'd (sampler-eligible) · dashed = no TTLsmallsession:42smallrate:97smalllock:txsmallfeed:88smallcart:9smalluser:42smallcache:homesmallcache:profilesmallws:7smallperm:77smalltag:betasmallotp:51smalluser:73no TTLsmallsettingsno TTLsmallcountryno TTLsmalllangno TTLsmallcityno TTLsmallpolicyno TTLsmallsku:103no TTLsmalltopologyno TTLsmallcfg:flagno TTLsmallshard:1no TTLsmallflag:expno TTLsmallrule:99no TTLRECLAIMED · this tick0keys freedEXPIRED-BUT-RESIDENTlogically gone, still occupies bytes0drains as sampler ticksPASSIVE EXPIREGET on an expired key:delete inline · return nilEVENT LOOPserving 3 clientsone command at a time · single threadBACKGROUND FREER THREADUNLINK / lazyfree-* offloadqueue depth · 0 jobsidleidleidleidleidleCLIENTS WAITING · 3Client A · GET user:42servedwait 0 msClient B · GET cart:9servedwait 0 msClient C · SET pingservedwait 0 ms
Active-expire ticker fires at 10 Hz. Each pulse samples a random subset of the TTL'd cells (4 in this demo; 20 in production).
What to watch for

The 10Hz active-expire ticker pulses across the top. Each pulse samples a small random subset of the TTL'd cells (production Redis: 20 against millions of keys; this demo: 4 against 12 — see the pseudocode for the production constant). Expired cells in the sample drain to the right-side counter; expired cells the sampler missed pile up on the EXPIRED-BUT-RESIDENT lag gauge. A passive GET on an expired key deletes it inline. Replicas don't actively expire — they wait for the master's DEL.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

activeExpireCycle
10Hz tick: sample 20 TTL'd keys, resample if >25% expired
def activeExpireCycle(): # runs every 100ms
for round in range(max_rounds):
sample = random_sample(db.expires, n=20)
expired = [k for k in sample
if k.expire_at <= now()]
for k in expired:
deleteOrLazyfree(k) # lazyfree-lazy-expire
ratio = len(expired) / len(sample)
if ratio <= 0.25: return # under threshold, rest
if cpu_used() > active_expire_budget: return
# else: resample inside the same 100ms tick
lookupKey (passive expire)
every GET checks the TTL; expired ⇒ delete inline, return nil
def lookupKey(db, key):
entry = db.get(key)
if entry is None: return None
if entry.has_ttl and entry.expire_at <= now():
deleteOrLazyfree(entry) # passive path
return None # GET returns nil
return entry.value
deleteOrLazyfree
DEL stalls the loop; UNLINK / lazyfree-* hands off to bg thread
def deleteOrLazyfree(entry):
cost = estimate_free_cost(entry) # ~elements to walk
offload = (cost > LAZYFREE_THRESHOLD
or lazyfree_for(entry.callsite))
if offload:
unlink_from_keyspace(entry) # O(1) on the loop
background_freer_queue.push(entry)
else:
walk_and_free(entry) # WHOLE struct, on the loop

Where this sits in Build Redis

Scene 05a of 10, in the Memory act — Persistence, eviction, TTL — what makes RAM disappear.. Passive + 10Hz active sampler expire keys; DEL of a big value freezes the loop unless `lazyfree-*`/UNLINK offloads to the background freer thread. Cache stampede + jitter / SET NX rebuilder lock.

Up next. Replication — one node now has every cleanup mechanic in place. But one node is still a single point of failure. Keeping a copy on another node is the first answer.

All 10 scenes in Build Redis · Every curriculum

Built with Arqly
Every scene in Build Redis builds on the one before it.All 10 Build Redis scenes