In practice, caching requires deciding where to cache, how long to cache, and what happens when the underlying data changes. Get these decisions wrong and you serve stale data, waste memory, or create bugs that only surface under load.
The request path
When a user requests a page, the request travels through multiple layers. Each layer is a potential caching opportunity.
User's Browser
→ Browser Cache (local disk)
→ CDN Edge Node (geographically close)
→ Application Server
→ Application Cache (Redis / in-memory)
→ Database Query Cache
→ Database (disk)A cache hit at any layer short-circuits the restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources. of the path. If the browser has the response, the request never leaves the user's machine. If the CDNWhat is cdn?Content Delivery Network - a network of servers around the world that caches your files and serves them from the location closest to the user, making pages load faster. has it, the request never reaches your server.
Browser cache
The browser cache lives on the user's device. When your server sends a response with a Cache-Control header, the browser stores it locally and reuses it for subsequent requests to the same URL.
// Express: set cache headers for static assets
app.use('/static', express.static('public', {
maxAge: '1y', // Cache for 1 year
immutable: true, // Don't even revalidate
}));
// Dynamic API responses: short cache or no cache
app.get('/api/user/profile', (req, res) => {
res.set('Cache-Control', 'private, max-age=60'); // 60 seconds, only this user
res.json(userData);
});
// Data that must never be cached
app.get('/api/checkout', (req, res) => {
res.set('Cache-Control', 'no-store'); // Never cache this
res.json(checkoutData);
});Key directives: public (any cache can store it), private (only the user's browser), max-age (seconds until expiration), no-store (never cache). For hashed assets like app.a3f9c2.js, you can safely set a 1-year max-age because changing the file produces a new hash and URL.
CDNWhat is cdn?Content Delivery Network - a network of servers around the world that caches your files and serves them from the location closest to the user, making pages load faster. cache
A CDN (Content Delivery Network) caches responses at edge nodes worldwide. A user in Tokyo hits a CDN node in Tokyo instead of your server in Virginia. CDN caching respects your Cache-Control headers, and most providers let you configure rules separately.
// Setting CDN-specific cache rules via headers
app.get('/api/products', (req, res) => {
// s-maxage is for shared caches (CDN), max-age is for the browser
res.set('Cache-Control', 'public, s-maxage=300, max-age=60');
// CDN caches for 5 minutes, browser caches for 1 minute
res.json(products);
});CDNs are effective for static assets, public APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. responses, and anything that does not vary per user.
Application-level cache
This is the cache you control directly in your application code. It comes in two flavors.
In-memory cache lives in your process's RAM. Fastest option (nanoseconds) but disappears on restart, and each app instance has its own separate cache.
// Simple in-memory cache with a Map
const cache = new Map();
async function getUser(userId) {
const cacheKey = `user:${userId}`;
if (cache.has(cacheKey)) {
const { data, expiresAt } = cache.get(cacheKey);
if (Date.now() < expiresAt) return data; // Cache hit
cache.delete(cacheKey); // Expired, remove it
}
// Cache miss - fetch from database
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
cache.set(cacheKey, { data: user, expiresAt: Date.now() + 60_000 }); // TTL: 60s
return user;
}External cache (Redis/Memcached) runs as a separate service shared across all app instances. Slightly slower (1-2ms network hop) but survives restarts and keeps instances in sync.
import Redis from 'ioredis';
const redis = new Redis();
async function getUser(userId) {
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached); // Cache hit
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
await redis.setex(cacheKey, 60, JSON.stringify(user)); // TTL: 60 seconds
return user;
}Database query cache
Some databases have a built-in query cache. MySQL had one (removed in 8.0). PostgreSQL uses a buffer pool for frequently accessed pages. The database query cache is opaque (you cannot control what gets cached or for how long) and does not reduce the network round-trip to the database server. Do not rely on it as your primary caching strategy.
Hit and miss mechanics
Every cache lookup has two outcomes: hit (data found, return immediately) or miss (not found or expired, fetch from originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443., store in cache, return).
The hit rate is the percentage of requests served from cache. Monitoring it is critical, a drop means you are caching the wrong things, TTLs are too short, or the cache is too small.
Hit rate = cache hits / (cache hits + cache misses) * 100
95% hit rate → origin handles 5% of traffic
80% hit rate → origin handles 20% of traffic (4x more than 95%)
50% hit rate → cache is barely helpingTTLWhat is ttl?Time-to-Live - a countdown attached to cached data that automatically expires it after a set number of seconds. basics
TTL (Time to Live) is how long a cached entry remains valid. Choosing a TTL is a tradeoff: short TTLs give fresher data but higher originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443. load, long TTLs reduce load but risk staleness.
| Data type | Suggested TTL | Reasoning |
|---|---|---|
| Static assets (JS, CSS, images) | 1 year | Filename hash changes on update |
| Product catalog | 5-15 minutes | Changes infrequently, brief staleness is acceptable |
| User profile | 1-5 minutes | May change, but not real-time critical |
| Shopping cart | No cache or very short | Staleness causes real problems |
| Stock price / live data | No cache | Must always be current |
| Search results | 1-10 minutes | Depends on how often the index updates |
Cache type comparison
| Cache type | Location | Latency | Typical size | Best for |
|---|---|---|---|---|
| Browser cache | User's device | 0ms (disk I/O) | 50-300 MB | Static assets, API responses for current user |
| CDN cache | Edge servers worldwide | 5-50ms | Terabytes across network | Public content, static files, media |
| In-memory cache | App server RAM | < 1ms | 256 MB, 4 GB | Hot data, session data, computed results |
| Redis / Memcached | Dedicated cache server | 1-5ms | 8-256 GB | Shared state across app instances, rate limiting |
| Database query cache | Database server | 1-10ms | Managed by DB | Repeated identical queries |
How layers work together
In production, you stack these layers. A request for a product page:
- Browser has CSS and JS cached (hit, 0ms)
- Browser requests the product data APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.
- CDNWhat is cdn?Content Delivery Network - a network of servers around the world that caches your files and serves them from the location closest to the user, making pages load faster. has the data cached (hit, 20ms), done, no originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443. hit
- If CDN misses: app checks Redis (hit, 3ms), done, no DB hit
- If Redis misses: app queries the database (50ms), stores in Redis, returns it
- CDN caches the response for subsequent users in that region
Most users get a response in under 50ms, and your database handles a fraction of total traffic.
Cache-Control headers on static assets means returning visitors load your site almost instantly, no server involved.Cache-Control: private on responses that vary by user to prevent the CDN from serving one user's data to another.