System Design/
Lesson

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.

02

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.

03

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.

04

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;
}
05

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.

06

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 helping
07

TTLWhat 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 typeSuggested TTLReasoning
Static assets (JS, CSS, images)1 yearFilename hash changes on update
Product catalog5-15 minutesChanges infrequently, brief staleness is acceptable
User profile1-5 minutesMay change, but not real-time critical
Shopping cartNo cache or very shortStaleness causes real problems
Stock price / live dataNo cacheMust always be current
Search results1-10 minutesDepends on how often the index updates
08

Cache type comparison

Cache typeLocationLatencyTypical sizeBest for
Browser cacheUser's device0ms (disk I/O)50-300 MBStatic assets, API responses for current user
CDN cacheEdge servers worldwide5-50msTerabytes across networkPublic content, static files, media
In-memory cacheApp server RAM< 1ms256 MB, 4 GBHot data, session data, computed results
Redis / MemcachedDedicated cache server1-5ms8-256 GBShared state across app instances, rate limiting
Database query cacheDatabase server1-10msManaged by DBRepeated identical queries
09

How layers work together

In production, you stack these layers. A request for a product page:

  1. Browser has CSS and JS cached (hit, 0ms)
  2. 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.
  3. 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
  4. If CDN misses: app checks Redis (hit, 3ms), done, no DB hit
  5. If Redis misses: app queries the database (50ms), stores in Redis, returns it
  6. 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.

AI pitfall
AI loves to recommend Redis for every project. For an app with 100 users, your database query cache and a CDN are enough. Redis adds operational complexity that is not worth it until you have measurable performance problems.
Good to know
Browser caching is the most impactful and cheapest cache layer. Setting proper Cache-Control headers on static assets means returning visitors load your site almost instantly, no server involved.
Edge case
CDN caching can serve stale content to logged-in users if you cache personalized pages. Always set Cache-Control: private on responses that vary by user to prevent the CDN from serving one user's data to another.