Build Redis (10 scenes)
Scene 5.5 · TTL and cleanup — lazy, active, and the freer thread
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.
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 5.5
TTL and cleanup — lazy, active, and the freer thread
Diagram
Top: a 10Hz active-expire ticker that flashes every 100ms — it samples 20 random TTL'd cells from the keyspace grid below and drains expired ones to a 'reclaimed' lane on the right. Bottom: the event-loop bar (blue = serving, red = stalled on a big DEL) and a background freer-thread queue that UNLINK / lazyfree-* feeds into. A separate stampede panel lights up when many clients miss a TTL'd key at the same instant.
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.
Implementation
activeExpireCycle
10Hz tick: sample 20 TTL'd keys, resample if >25% expired
1def activeExpireCycle(): # runs every 100ms2 for round in range(max_rounds):3 sample = random_sample(db.expires, n=20)4 expired = [k for k in sample5 if k.expire_at <= now()]6 for k in expired:7 deleteOrLazyfree(k) # lazyfree-lazy-expire8 ratio = len(expired) / len(sample)9 if ratio <= 0.25: return # under threshold, rest10 if cpu_used() > active_expire_budget: return11 # else: resample inside the same 100ms tick
lookupKey (passive expire)
every GET checks the TTL; expired ⇒ delete inline, return nil
1def lookupKey(db, key):2 entry = db.get(key)3 if entry is None: return None4 if entry.has_ttl and entry.expire_at <= now():5 deleteOrLazyfree(entry) # passive path6 return None # GET returns nil7 return entry.value
deleteOrLazyfree
DEL stalls the loop; UNLINK / lazyfree-* hands off to bg thread
1def deleteOrLazyfree(entry):2 cost = estimate_free_cost(entry) # ~elements to walk3 offload = (cost > LAZYFREE_THRESHOLD4 or lazyfree_for(entry.callsite))5 if offload:6 unlink_from_keyspace(entry) # O(1) on the loop7 background_freer_queue.push(entry)8 else:9 walk_and_free(entry) # WHOLE struct, on the loop