Engineering Playbook
Caching

CDN Caching

CDN strategies for static and dynamic content.

CDN Caching

Content Delivery Networks (CDNs) are the first line of defense for performance. They cache content at the edge, closer to your users.

CDN Caching Headers

Cache-Control

The most important header for controlling CDN behavior.

# Public assets (JS, CSS, images)
Cache-Control: public, max-age=31536000, immutable

# HTML pages
Cache-Control: public, max-age=0, must-revalidate

# API responses
Cache-Control: public, max-age=60, s-maxage=300

Breakdown:

  • public: Can be cached by CDNs and browsers
  • private: Only cache by browser, not CDN
  • max-age: Browser cache duration
  • s-maxage: CDN cache duration (overrides max-age for CDNs)
  • immutable: Content never changes (perfect for hashed assets)

ETag for Validation

// Generate ETag based on content
function generateETag(content) {
  return `"${crypto.createHash('md5').update(content).digest('hex')}"`;
}

// Check for conditional requests
app.get('/api/data', (req, res) => {
  const data = fetchData();
  const etag = generateETag(JSON.stringify(data));
  
  res.set('ETag', etag);
  
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end(); // Not Modified
  }
  
  res.json(data);
});

Static Asset Caching

Versioned Assets

// Build step: Hash filenames for cache busting
const assets = {
  'main.js': 'main.a1b2c3d4.js',
  'styles.css': 'styles.e5f6g7h8.css'
};

// HTML references versioned assets
<html>
  <link rel="stylesheet" href="/assets/styles.e5f6g7h8.css">
  <script src="/assets/main.a1b2c3d4.js"></script>
</html>

CDN Configuration

// Cloudflare Worker for edge caching
export default {
  async fetch(request) {
    const url = new URL(request.url);
    
    // Cache API responses
    if (url.pathname.startsWith('/api/')) {
      const cacheKey = new Request(url, request);
      const cache = caches.default;
      
      let response = await cache.match(cacheKey);
      
      if (!response) {
        response = await fetch(request);
        
        // Cache for 5 minutes
        response = new Response(response.body, response);
        response.headers.set('Cache-Control', 'public, max-age=300');
        
        cache.put(cacheKey, response.clone());
      }
      
      return response;
    }
    
    return fetch(request);
  }
};

Dynamic Content Caching

Edge-Side Includes (ESI)

<!-- Cache the layout but not user-specific content -->
<esi:include src="/api/user-header" />
<div class="content">
  <esi:include src="/api/content/123" />
</div>

Stale-While-Revalidate

// Serve stale content while refreshing
app.get('/api/products', async (req, res) => {
  const cacheKey = 'products:list';
  const cached = await redis.get(cacheKey);
  
  // Set cache control for CDN
  res.set({
    'Cache-Control': 'public, max-age=60, stale-while-revalidate=300',
    'Vary': 'Accept-Encoding'
  });
  
  if (cached) {
    // Return cached immediately, refresh async
    refreshProductsCache(cacheKey);
    return res.json(JSON.parse(cached));
  }
  
  const products = await fetchProducts();
  await redis.setex(cacheKey, 300, JSON.stringify(products));
  
  res.json(products);
});

Geographic Personalization

// Cache different versions by location
app.get('/api/content', (req, res) => {
  const country = req.headers['cf-ipcountry'] || 'US';
  const cacheKey = `content:${country}`;
  
  res.set({
    'Cache-Control': 'public, max-age=3600',
    'Vary': 'cf-ipcountry' // CDN creates separate cache per country
  });
  
  // Return country-specific content
  res.json(getContentForCountry(country));
});

Cache Invalidation Strategies

Time-Based Invalidation

// Assets expire naturally based on Cache-Control
// Best for static content with long TTLs

Purge API

// Purge specific URLs from CDN
async function purgeCDN(urls) {
  await fetch('https://api.cloudflare.com/client/v4/zones/zone_id/purge_cache', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer token',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ files: urls })
  });
}

// Purge when content changes
await purgeCDN([
  'https://cdn.example.com/api/products',
  'https://cdn.example.com/api/user/123'
]);

Cache Tags

// Tag content for bulk invalidation
app.get('/api/products', (req, res) => {
  res.set({
    'Cache-Control': 'public, max-age=3600',
    'Cache-Tag': 'products,category:electronics' // Custom header
  });
});

// Purge by tag
await fetch('/cdn/purge/tag', {
  method: 'POST',
  body: JSON.stringify({ tags: ['products'] })
});

Advanced CDN Patterns

API Response Caching

// Smart API caching middleware
function cdnCache(options = {}) {
  const {
    ttl = 300,           // 5 minutes default
    keyGenerator = req => req.url,
    vary = ['Accept-Encoding']
  } = options;
  
  return async (req, res, next) => {
    // Skip caching for non-GET requests
    if (req.method !== 'GET') return next();
    
    const cacheKey = keyGenerator(req);
    const cached = await redis.get(cacheKey);
    
    if (cached) {
      const data = JSON.parse(cached);
      res.set({
        'Cache-Control': `public, max-age=${ttl}`,
        'Vary': vary.join(', '),
        'X-Cache': 'HIT'
      });
      return res.json(data);
    }
    
    // Capture response
    const originalJson = res.json;
    res.json = function(data) {
      // Cache the response
      redis.setex(cacheKey, ttl, JSON.stringify(data));
      
      res.set({
        'Cache-Control': `public, max-age=${ttl}`,
        'Vary': vary.join(', '),
        'X-Cache': 'MISS'
      });
      
      return originalJson.call(this, data);
    };
    
    next();
  };
}

// Usage
app.get('/api/users', cdnCache({ ttl: 600 }), getUserList);

Image Optimization at Edge

// Cloudinary-style image transformations
app.get('/images/:path(*)', async (req, res) => {
  const { path } = req.params;
  const { width, height, format = 'auto' } = req.query;
  
  // Generate variation key
  const key = `image:${path}:${width}:${height}:${format}`;
  
  res.set({
    'Cache-Control': 'public, max-age=31536000, immutable',
    'Vary': 'Accept'
  });
  
  // Check edge cache
  const cached = await redis.get(key);
  if (cached) return res.send(cached);
  
  // Transform and cache
  const optimized = await transformImage(path, { width, height, format });
  await redis.set(key, optimized);
  
  res.send(optimized);
});

CDN Cache Busting

Never use ?v=123 query parameters for cache busting. Some CDNs/proxies ignore query parameters for caching.

Use filename hashing instead:

  • styles.cssstyles.a1b2c3d4.css
  • app.jsapp.e5f6g7h8.js

CDN Selection Guide

Cloudflare: Best for ease of use, security, and free tier AWS CloudFront: Best if you're already in AWS ecosystem Fastly: Best for real-time purging and edge computing Akamai: Best for enterprise needs and global scale