TTL and Expiration
The staleness problem
The previous lesson’s Map cache stores data forever. A review is posted. The cache still shows the old top 10 list. The data is stale — it no longer matches the database.
Time-To-Live (TTL) is the maximum age of a cached entry. After the TTL expires, the entry is considered stale and must be refreshed from the source.
Adding TTL to the cache
// src/cache.ts
interface CacheEntry<T> {
value: T;
expiresAt: number;
}
const cache = new Map<string, CacheEntry<any>>();
export function cacheGet<T>(key: string): T | undefined {
const entry = cache.get(key);
if (!entry) return undefined;
// Check if expired
if (Date.now() > entry.expiresAt) {
cache.delete(key); // Lazy cleanup
return undefined;
}
return entry.value;
}
export function cacheSet<T>(key: string, value: T, ttlMs: number): void {
cache.set(key, {
value,
expiresAt: Date.now() + ttlMs,
});
}
export function cacheDelete(key: string): void {
cache.delete(key);
}
export function cacheClear(): void {
cache.clear();
} Each cache entry stores the value and an expiration timestamp. cacheGet checks the timestamp before returning — if expired, it deletes the entry and returns undefined (a cache miss).
// Cache for 60 seconds
cacheSet("top-books", books, 60_000);
// 30 seconds later: hit (entry is fresh)
cacheGet("top-books"); // returns books
// 90 seconds later: miss (entry expired)
cacheGet("top-books"); // returns undefined, triggers fresh query Lazy vs active expiration
The cache above uses lazy expiration: entries are deleted when accessed after their TTL. This means expired entries sit in memory until someone requests them.
Active expiration runs a periodic cleanup:
// Clean up expired entries every 60 seconds
setInterval(() => {
const now = Date.now();
for (const [key, entry] of cache) {
if (now > entry.expiresAt) {
cache.delete(key);
}
}
}, 60_000); Lazy expiration is simpler and works well when the cache is accessed frequently (expired entries are cleaned up quickly). Active expiration is useful when the cache has many entries that are rarely re-accessed — without it, they would leak memory.
This course uses lazy expiration for simplicity. Active expiration is added if needed.
Choosing TTL values
The TTL should match how quickly the data changes and how stale is acceptable:
| Data | Changes | Staleness tolerance | TTL |
|---|---|---|---|
| Top books | Every few hours | Minutes | 5 minutes |
| Book detail | Rarely | Minutes | 10 minutes |
| Book list | When books are added | Minutes | 5 minutes |
| Search results | When books are added | Seconds | 30 seconds |
| User profile | When user edits | Seconds | 30 seconds |
| Auth session | On login/logout | None | Do not cache |
Short TTL (10-30 seconds): Data changes frequently or staleness is noticeable. Search results, prices.
Medium TTL (1-5 minutes): Data changes occasionally. Listings, aggregations, rankings.
Long TTL (1-24 hours): Data rarely changes. Static content, configuration, reference data.
[!NOTE] Server-side TTL and HTTP
max-ageserve the same purpose at different levels.max-age=60tells the browser to cache for 60 seconds (no request to the server). Server-side TTL of 60 seconds tells the server to cache for 60 seconds (no query to the database). Use both together for maximum efficiency.
Exercises
Exercise 1: Add TTL to the cache. Set a 10-second TTL. Verify data expires after 10 seconds.
Exercise 2: Cache with a 60-second TTL. Add a review. Call the endpoint immediately (stale data). Wait 60 seconds. Call again (fresh data).
Exercise 3: Add active expiration. Log when entries are cleaned up. Verify expired entries are removed even without being accessed.
What happens when a cache entry's TTL expires?