PureTools

Caching Strategies: Browser, CDN, and Server-Side

PureTools Team· 8 min read
Caching Strategies: Browser, CDN, and Server-Side

Caching: The Fastest Code Is Code That Doesn't Run

Caching is the most impactful performance optimization. A response served from cache is 10-1000x faster than one generated from scratch. But caching introduces complexity: stale data, cache invalidation, and consistency issues.

The Caching Layers

User → Browser Cache → CDN Edge → Server Cache (Redis) → Application → Database
         ~0ms           ~20ms          ~1ms             ~50ms         ~100ms

Browser Cache (Cache-Control)

# Cache for 1 hour, then revalidate
Cache-Control: public, max-age=3600

# Cache for 1 year (immutable assets with hash in filename)
Cache-Control: public, max-age=31536000, immutable

# Don't cache at all
Cache-Control: no-store

# Cache but always revalidate with server
Cache-Control: no-cache
# (confusing name — it DOES cache, but always checks freshness)

# Private (only browser, not CDN)
Cache-Control: private, max-age=3600
DirectiveMeaning
publicAny cache can store this (browser, CDN, proxy)
privateOnly the browser can cache (not CDN)
max-age=NCache for N seconds
s-maxage=NCDN cache duration (overrides max-age for shared caches)
no-cacheMust revalidate before using cached version
no-storeNever cache, ever
stale-while-revalidate=NServe stale for N seconds while fetching fresh copy
immutableContent will never change (skip revalidation)

What to Cache Where

ContentStrategyCache-Control
Static assets (JS, CSS, images)Long cache + content hash in filenamepublic, max-age=31536000, immutable
HTML pages (static)CDN with revalidationpublic, s-maxage=3600, stale-while-revalidate=600
API responses (public)Short CDN cachepublic, s-maxage=60
API responses (user-specific)Private or no-storeprivate, no-cache
Auth pagesNever cacheno-store

CDN Edge Caching

# Vercel/Next.js — ISR (Incremental Static Regeneration)
export const revalidate = 3600; // regenerate every hour

export default async function Page() {
  const data = await fetch('https://api.example.com/data');
  return 
{/* render data */}
; } # Cloudflare Workers return new Response(body, { headers: { 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=600', }, });

Server-Side Cache (Redis)

// Cache database queries
async function getProduct(id) {
  const cacheKey = `product:${id}`;
  
  // Check cache
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);
  
  // Cache miss — query database
  const product = await db.products.findById(id);
  
  // Store in cache with TTL
  await redis.set(cacheKey, JSON.stringify(product), 'EX', 300); // 5 min
  
  return product;
}

// Invalidate on update
async function updateProduct(id, data) {
  await db.products.update(id, data);
  await redis.del(`product:${id}`); // bust the cache
}

Cache Invalidation (The Hard Part)

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

  • TTL-based: Cache expires after N seconds. Simple but may serve stale data.
  • Event-based: Invalidate when data changes (database triggers, pub/sub).
  • Versioned keys: product:42:v3 — bump version on update, old versions expire naturally.
  • Stale-while-revalidate: Serve stale data immediately, refresh in background.

Test your headers: HTTP Reference — verify cache headers and status codes.