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

Cache-Control Headers

HTTP caching is free

Before writing any caching code on the server, you can make browsers and CDNs cache your responses automatically. One HTTP header tells the browser: “You can reuse this response for the next 60 seconds without asking the server again.”

route.get("/books/top", {
  resolve: () => {
    const books = db.prepare("SELECT ...").all();
    return new Response(JSON.stringify(books), {
      headers: {
        "Content-Type": "application/json",
        "Cache-Control": "public, max-age=60",
      },
    });
  },
});

The Cache-Control: public, max-age=60 header tells the browser: this response is cacheable by anyone (public), and it is valid for 60 seconds (max-age=60). For the next 60 seconds, the browser does not contact the server at all — it serves the response from its local cache.

Cache-Control directives

max-age=N — The response is fresh for N seconds. After N seconds, it is stale and the browser must revalidate or fetch a new copy.

Cache-Control: max-age=3600    → fresh for 1 hour
Cache-Control: max-age=86400   → fresh for 1 day
Cache-Control: max-age=0       → immediately stale (must revalidate every time)

public — Any cache can store this response: the browser, a CDN, a proxy server. Use for data that is the same for all users (book listings, product pages).

private — Only the user’s browser can cache this. CDNs and proxies must not store it. Use for user-specific data (profile pages, order history).

Cache-Control: private, max-age=60    → only the user's browser caches
Cache-Control: public, max-age=3600   → CDNs and browsers cache

no-cache — The browser can store the response, but must revalidate with the server before using it. “You have a copy, but check with me before serving it.” Used with ETags (next lesson).

no-store — Do not store the response at all. Not in the browser, not in a CDN, not anywhere. Use for sensitive data (authentication tokens, personal information).

Cache-Control: no-store    → never cache (bank balances, auth tokens)
Cache-Control: no-cache    → cache but always revalidate (user profiles)

[!WARNING] no-cache does not mean “do not cache.” It means “cache, but revalidate before using.” no-store means “do not cache.” This naming is confusing but it is the HTTP standard.

Choosing the right directive

EndpointData changesWho sees itDirective
Book listingRarelyEveryonepublic, max-age=300 (5 min)
Book detailRarelyEveryonepublic, max-age=60 (1 min)
Top booksWhen reviews are postedEveryonepublic, max-age=120 (2 min)
Search resultsWhen books are addedEveryonepublic, max-age=60
User profileWhen user edits itOnly that userprivate, max-age=60
Auth tokenEvery requestOnly that userno-store
Order historyWhen orders changeOnly that userprivate, no-cache

A helper function

// src/cache-headers.ts
type CacheProfile = "public-long" | "public-short" | "private" | "no-cache" | "no-store";

const PROFILES: Record<CacheProfile, string> = {
  "public-long": "public, max-age=3600", // 1 hour
  "public-short": "public, max-age=60", // 1 minute
  private: "private, max-age=60", // 1 minute, browser only
  "no-cache": "no-cache", // always revalidate
  "no-store": "no-store", // never cache
};

export function withCacheHeaders(body: unknown, profile: CacheProfile): Response {
  return new Response(JSON.stringify(body), {
    headers: {
      "Content-Type": "application/json",
      "Cache-Control": PROFILES[profile],
    },
  });
}
route.get("/books/top", {
  resolve: () => {
    const books = db.prepare("SELECT ...").all();
    return withCacheHeaders(books, "public-short");
  },
});

What Cache-Control does NOT do

Cache-Control tells the browser and CDN how long to cache. It does not cache on the server. If no browser cache exists (first visit, cache expired), the request still hits your server and runs the database query. Server-side caching (Section 3) handles that.

Exercises

Exercise 1: Add Cache-Control: public, max-age=60 to the /books endpoint. Open the browser dev tools (Network tab). Call the endpoint twice. The second request should show “(from disk cache)” or a 304.

Exercise 2: Change to no-store. Call twice. Both should hit the server.

Exercise 3: Use the helper function. Apply different cache profiles to different endpoints.

What is the difference between no-cache and no-store?

← Project Setup ETags and Conditional Requests →

© 2026 hectoday. All rights reserved.