hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Caching with @hectoday/http

Why Caching

  • The Same Query, A Thousand Times
  • Project Setup

HTTP Caching

  • Cache-Control Headers
  • ETags and Conditional Requests
  • Stale-While-Revalidate

Server-Side Caching

  • In-Memory Caching with Map
  • TTL and Expiration
  • Cache-Aside Pattern
  • LRU Eviction

What to Cache

  • Caching Database Queries
  • Caching Computed Results
  • Caching External API Responses

Invalidation

  • The Hardest Problem
  • Time-Based Invalidation
  • Event-Based Invalidation
  • Tag-Based Invalidation

Putting It All Together

  • Caching Checklist
  • Capstone: Caching the Book Catalog

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:

DataChangesStaleness toleranceTTL
Top booksEvery few hoursMinutes5 minutes
Book detailRarelyMinutes10 minutes
Book listWhen books are addedMinutes5 minutes
Search resultsWhen books are addedSeconds30 seconds
User profileWhen user editsSeconds30 seconds
Auth sessionOn login/logoutNoneDo 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-age serve the same purpose at different levels. max-age=60 tells 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?

← In-Memory Caching with Map Cache-Aside Pattern →

© 2026 hectoday. All rights reserved.