Step 1 of 3 · Reading · ~3 min
Lazy Deletion
Key Expiry
Passive Expiry — Lazy Deletion
You can now set TTLs and query them, but so far nothing actually enforces expiry — a key with a lapsed TTL just sits in store forever unless something checks it. This lesson closes that gap by making every read-path command consult the expiry table before answering.
Two strategies Redis actually uses
Real Redis combines two expiry mechanisms:
- Active expiry — a background cycle periodically samples random keys with TTLs and deletes the ones that have lapsed, so memory isn't held by dead keys indefinitely.
- Passive (lazy) expiry — whenever a key is accessed by any command, the server first checks whether its TTL has lapsed and, if so, deletes it right then, before doing anything else, as if the key were never there.
This lesson implements only the second, simpler mechanism — good enough for a single-machine clone, and the same trick every cache library uses when it doesn't want a dedicated background sweep thread.
A single chokepoint: is_expired / get_live
The key design move is to not scatter now() >= expires[key] checks inside every command handler. Instead, write one helper that every command that touches store calls first:
Then, at the top of GET, EXISTS, and anywhere else that reads a key:
Because expire_if_needed actually deletes the entry from both store and expires, the key is genuinely gone after this call — not just hidden. This matters for DBSIZE.
DBSIZE must reflect only live keys
DBSIZE naively returning len(store) breaks the moment a key expires but hasn't been touched since — it'll still be counted even though GET would say it doesn't exist. Two ways to fix this:
- Eager sweep: before counting, iterate all keys with TTLs and call
expire_if_neededon each. Simple, but O(n) perDBSIZEcall. - Filtered count: count keys minus those currently expired, without mutating state:
sum(1 for k in store if not is_expired(k)).
Either is acceptable for this project's scale; the important invariant is that DBSIZE never counts a key whose TTL has already lapsed, even if lazy cleanup hasn't physically removed it yet.
TTL and EXISTS on expired keys
EXISTSon an expired key must trigger the same lazy check and return:0\r\n, same as if the key were absent.TTLon an expired key returns:-2\r\n— the same code used for "never existed" — because from the client's perspective, an expired key and a nonexistent key are indistinguishable.
Why this ordering discipline matters
The pattern — check expiry, clean up if needed, then answer — needs to run as the very first step of every command that reads a key, before any other logic (including NX/XX checks in SET, which need to see an expired key as absent, not present). Get this chokepoint wrong in even one command and you'll have subtle test failures where SET key val NX incorrectly refuses to write because it saw a technically-still-present-but-logically-expired key.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…