TTL (Time to Live)
A TTL answers one question: how stale is acceptable? Choose TTLs per data type, use sliding expiry for sessions, and avoid the synchronized-expiry stampede with jitter and stale-while-revalidate.
🎯 What you'll learn
- What a TTL is and how to choose one
- Absolute vs sliding expiration
- The synchronized-expiry problem and jitter
- Soft/hard TTLs (stale-while-revalidate), HTTP TTLs, and how Redis expires keys
📚 Before you start
1What a TTL Is
A TTL is a deadline attached to a cache entry: after it passes, the entry is treated as absent. It's the simplest invalidation strategy (05-cache_invalidation/) and the safety net under every other one.
await redis.set("product:42", json, "EX", 300); // expire in 300 s
await redis.expire("session:abc", 1800); // set/refresh a TTL on an existing key
await redis.ttl("product:42"); // seconds remaining, -1 = no TTL, -2 = missingA TTL answers one question: how stale am I willing to let this data be? That's a product decision, not just a technical one.
2Choosing a TTL
| Data | Typical TTL | Reasoning |
|---|---|---|
| exchange rates, stock tickers | seconds | value changes constantly; stale = wrong |
| product details, prices | minutes | edits are rare; explicit invalidation + TTL as backstop |
| user profile, avatars | 5–60 min | small staleness is harmless |
| configuration, feature flags | 30–60 s | changes must propagate reasonably fast |
| static assets with hashed names | 1 year (immutable) | the URL changes when content changes |
| session data | sliding 30 min | expires with inactivity |
| negative results ("not found") | short (10–60 s) | prevent a new item looking missing for long |
Rules of thumb: the TTL should be long enough to get a good hit ratio and short enough that staleness is acceptable if an invalidation is missed. If you can't tolerate any staleness, a TTL alone is the wrong tool.
3Absolute vs Sliding Expiration
- Absolute: expires N seconds after it was written. Good for data that goes stale with time.
- Sliding: every access pushes the deadline out again (
GETEX key EX 1800orEXPIREon each read). Good for sessions: active users stay logged in, idle ones are dropped. Combine with an absolute maximum (e.g. 12 h) so a stolen session can't be kept alive forever.
4The Synchronized-Expiry Problem
If 10,000 keys are written at deploy time with EX 3600, they all expire at the same second an hour later. Every one becomes a miss simultaneously, and the database takes the whole load at once. Same thing for one hot key: when it expires, every concurrent request misses together — a cache stampede (a.k.a. thundering herd, dog-piling).
Fixes:
- TTL jitter — randomize:
ttl = base * (1 + random(-0.1, 0.1)). Expirations spread out. - Request coalescing (single-flight) — only one caller recomputes a missing key; others wait for its result (in-process with a promise map, or across instances with a short Redis lock
SET lock:key 1 NX PX 5000). - Stale-while-revalidate — keep serving the old value for a grace period while one background refresh runs.
- Probabilistic early expiration (XFetch) — each reader, with a probability that grows as expiry approaches, refreshes early; the hottest keys almost never actually expire.
5Soft TTL + Hard TTL (Stale-While-Revalidate)
Store the value with a soft expiry (when it should be refreshed) and a hard expiry (when it must not be served at all):
type Entry<T> = { value: T; softExpiresAt: number; hardExpiresAt: number };
// read path:
// now < soft → fresh: serve
// soft ≤ now < hard → stale: serve immediately AND trigger one background refresh
// now ≥ hard → expired: must recompute before answeringHTTP has the same idea built in: Cache-Control: max-age=60, stale-while-revalidate=300.
6TTLs in HTTP Caching
Cache-Control: public, max-age=300 # shared caches (CDN) may store it for 5 minutes
Cache-Control: private, max-age=60 # only the browser, not the CDN
Cache-Control: no-store # never store (sensitive data)
Cache-Control: public, max-age=31536000, immutable # hashed static assets
CDN-specific: s-maxage=600 # shared-cache TTL, overrides max-age for CDNsAfter max-age, a cache may revalidate with If-None-Match: <etag> and get a cheap 304 Not Modified instead of the full body.
Under the HoodHow Redis Actually Expires Keys▾
Redis stores expiry deadlines in a separate dictionary (key → absolute Unix ms timestamp). It deletes expired keys in two complementary ways:
- Lazy (passive) expiration — every command that touches a key first checks its deadline; if passed, the key is deleted right there and the command behaves as if it never existed. Costs nothing for keys nobody reads… which is the problem: unread expired keys would sit in memory forever.
- Active expiration cycle — about 10 times per second, Redis samples 20 random keys that have a TTL, deletes the expired ones, and repeats immediately if more than 25% were expired, within a CPU-time budget. Statistically this keeps the fraction of expired-but-not-yet-deleted keys below ~25%.
Consequences: memory isn't freed exactly at expiry; DBSIZE/INFO keyspace can count already-expired keys; and replicas don't expire keys on their own clock — the primary sends explicit DELs to replicas, so a replica read may briefly return a logically-expired key (newer versions hide it at read time). Note also that SET without KEEPTTL removes an existing TTL — a common way keys accidentally become immortal.
⚠️ Common Pitfalls
- No TTL at all — one missed invalidation becomes permanent staleness, and memory fills with dead keys.
- Identical TTLs for bulk-written keys — synchronized expiry → database spike. Add jitter.
SEToverwriting a key and dropping its TTL (useSET ... EXagain orKEEPTTL).- Very long TTLs on data that must change (permissions, prices during a sale).
- Sliding sessions without an absolute cap — a hijacked session lives forever.
- Caching "not found" with a long TTL — newly created items appear missing.
📋 Quick Reference
| Need | Redis | HTTP |
|---|---|---|
| expire after N s | SET k v EX N | max-age=N |
| keep existing TTL on update | SET k v KEEPTTL | — |
| sliding expiry | GETEX k EX N | — |
| inspect | TTL k / PTTL k | Age header |
| serve stale while refreshing | soft/hard TTL in app | stale-while-revalidate=N |
| avoid stampede | jitter + single-flight | stale-while-revalidate |
🧠 Key takeaways
- A TTL is a product decision: how stale may this data be? Short for volatile data, long for immutable assets.
- Absolute expiry for time-sensitive data, sliding for sessions.
- Avoid synchronized expiry: add jitter, single-flight, stale-while-revalidate or probabilistic early refresh.
- Redis expires lazily on access plus active sampling; HTTP has max-age, s-maxage, stale-while-revalidate.
🎤 Interview Q&A
Q1How do you choose a TTL?core▾
Based on how stale the data may be for the business (seconds for prices, hours for product descriptions), how often it changes, and the cost of a miss. Short TTLs mean fresher data but more load; long TTLs need explicit invalidation.
- Can TTL replace invalidation? Only when bounded staleness is acceptable.
Q2Why add jitter to TTLs?core▾
Keys cached at the same time (e.g. after a deploy or warm-up) would expire together and cause a load spike on the database. Random jitter (±10–20%) spreads expirations out.
- Example? TTL = 3600 s ± random(0, 600).
Q3How does Redis expire keys?senior▾
Lazily when a key is accessed, and actively by a background process sampling keys with TTLs and deleting expired ones. Expired keys may consume memory briefly; eviction policies handle memory pressure separately.
- Does
SETwithoutKEEPTTLkeep the TTL? No, overwriting a key clears its TTL unless KEEPTTL is used.
Q4What's the difference between TTL and eviction?core▾
TTL removes entries after a time regardless of memory; eviction removes entries when memory is full, based on a policy (LRU/LFU). A cache needs both: TTL for freshness, eviction for capacity.
- What if noeviction is set? Writes fail when memory is full.
Q5What is stale-while-revalidate?core▾
Serving an expired entry for a grace period while refreshing it in the background, so users never wait for the slow path and the DB isn't stampeded. Supported in HTTP caching headers and implementable in app caches.
- HTTP header?
Cache-Control: max-age=60, stale-while-revalidate=300.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.