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-cachedoes not mean “do not cache.” It means “cache, but revalidate before using.”no-storemeans “do not cache.” This naming is confusing but it is the HTTP standard.
Choosing the right directive
| Endpoint | Data changes | Who sees it | Directive |
|---|---|---|---|
| Book listing | Rarely | Everyone | public, max-age=300 (5 min) |
| Book detail | Rarely | Everyone | public, max-age=60 (1 min) |
| Top books | When reviews are posted | Everyone | public, max-age=120 (2 min) |
| Search results | When books are added | Everyone | public, max-age=60 |
| User profile | When user edits it | Only that user | private, max-age=60 |
| Auth token | Every request | Only that user | no-store |
| Order history | When orders change | Only that user | private, 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?