The fastest request is the one never made; correct caching headers cut load times and server costs dramatically.
HTTP caches (the browser's, and shared ones like CDNs and proxies) store responses and reuse them. The server controls this with headers.
Freshness: Cache-Control: max-age=3600 means the response may be reused for an hour without contacting the server. While fresh, the browser doesn't even send a request.
Validation: once stale, the cache can ask "has it changed?" with a conditional request. If the response had an ETag: "v42" (a version identifier), the cache sends If-None-Match: "v42"; with Last-Modified, it sends If-Modified-Since. If nothing changed, the server replies 304 Not Modified with no body, and the cached copy is reused. That saves the transfer, though not the round trip.
The key Cache-Control directives:
max-age=N(all caches) ands-maxage=N(shared caches only).no-cache: store it, but revalidate before every use. Despite the name, it caches.no-store: never store it. For sensitive data.private: only the user's browser may store it, not a CDN.public: shared caches may store it even when they otherwise wouldn't, such as for authenticated requests.immutable: will never change during its freshness, so don't revalidate even on reload.
A standard strategy:
- HTML:
no-cache, so users always get the latest version, cheaply validated. - Static assets with a content hash in the filename (
app.3f9a1c.js):max-age=31536000, immutable. A new build produces a new filename, so the cache never needs invalidating.
Example
import http from "node:http";
import { createHash } from "node:crypto";
const body = JSON.stringify({ price: 12 });
const etag = `"${createHash("sha1").update(body).digest("hex").slice(0, 8)}"`;
const server = http.createServer((req, res) => {
res.setHeader("Cache-Control", "no-cache"); // reuse only after revalidating
res.setHeader("ETag", etag);
if (req.headers["if-none-match"] === etag) { res.statusCode = 304; return res.end(); }
res.setHeader("Content-Type", "application/json");
res.end(body);
});
server.listen(0, async () => {
const url = `http://localhost:${server.address().port}/price`;
const first = await fetch(url);
console.log(first.status, first.headers.get("etag")); // 200 "<hash>"
const again = await fetch(url, { headers: { "If-None-Match": etag } });
console.log(again.status, (await again.text()).length); // 304 0
server.close();
});Remember
- max-age = reuse without asking; ETag/Last-Modified = cheap revalidation via 304
- no-cache means revalidate every time; no-store means don't store
- private keeps responses out of shared caches
- Hashed filenames + immutable for assets, no-cache for HTML
Watch out for
- Using no-cache when you meant no-store for sensitive responses.