A Content Delivery Network puts your content closer to users worldwide. Proper CDN configuration dramatically reduces latency and improves user experience.
How CDNs Work#
Without CDN:
User (Tokyo) → Origin Server (New York)
Latency: ~200ms
With CDN:
User (Tokyo) → Edge Server (Tokyo) → Origin Server (New York)
↑
(Only on cache miss)
Latency: ~20ms (cache hit)
What to Cache#
Static Assets (Long TTL):
✓ JavaScript bundles
✓ CSS files
✓ Images
✓ Fonts
✓ Videos
Dynamic Content (Short/No TTL):
✓ HTML pages (with care)
✓ API responses (selective)
✓ User-specific content
Never Cache:
✗ Authentication tokens
✗ Payment information
✗ Personal data
✗ Real-time data
Cache Headers#
1// Express middleware for cache headers
2
3function cacheControl(options: {
4 maxAge: number;
5 sMaxAge?: number;
6 staleWhileRevalidate?: number;
7 private?: boolean;
8}) {
9 return (req: Request, res: Response, next: NextFunction) => {
10 const directives: string[] = [];
11
12 if (options.private) {
13 directives.push('private');
14 } else {
15 directives.push('public');
16 }
17
18 directives.push(`max-age=${options.maxAge}`);
19
20 if (options.sMaxAge !== undefined) {
21 directives.push(`s-maxage=${options.sMaxAge}`);
22 }
23
24 if (options.staleWhileRevalidate !== undefined) {
25 directives.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
26 }
27
28 res.setHeader('Cache-Control', directives.join(', '));
29 next();
30 };
31}
32
33// Static assets - cache for 1 year
34app.use(
35 '/static',
36 cacheControl({ maxAge: 31536000, sMaxAge: 31536000 }),
37 express.static('public')
38);
39
40// API responses - cache for 5 minutes at edge
41app.get(
42 '/api/products',
43 cacheControl({ maxAge: 0, sMaxAge: 300, staleWhileRevalidate: 60 }),
44 productsHandler
45);
46
47// User-specific - no CDN caching
48app.get(
49 '/api/me',
50 cacheControl({ maxAge: 0, private: true }),
51 meHandler
52);Versioned Assets#
1// Add content hash to filenames
2// styles.abc123.css → cache forever
3
4// webpack.config.js
5module.exports = {
6 output: {
7 filename: '[name].[contenthash].js',
8 chunkFilename: '[name].[contenthash].chunk.js',
9 },
10};
11
12// vite.config.ts
13export default {
14 build: {
15 rollupOptions: {
16 output: {
17 entryFileNames: '[name].[hash].js',
18 chunkFileNames: '[name].[hash].js',
19 assetFileNames: '[name].[hash].[ext]',
20 },
21 },
22 },
23};CDN Configuration (Cloudflare)#
1// Page rules via API
2const pageRules = [
3 {
4 // Static assets
5 pattern: '*.example.com/static/*',
6 cacheLevel: 'aggressive',
7 browserTTL: 31536000,
8 edgeCacheTTL: 31536000,
9 },
10 {
11 // API with short cache
12 pattern: '*.example.com/api/public/*',
13 cacheLevel: 'standard',
14 browserTTL: 0,
15 edgeCacheTTL: 300,
16 },
17 {
18 // Bypass for authenticated routes
19 pattern: '*.example.com/api/me*',
20 cacheLevel: 'bypass',
21 },
22];Cache Invalidation#
1// Cloudflare cache purge
2async function purgeCache(urls: string[]) {
3 const response = await fetch(
4 `https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
5 {
6 method: 'POST',
7 headers: {
8 'Authorization': `Bearer ${CF_API_TOKEN}`,
9 'Content-Type': 'application/json',
10 },
11 body: JSON.stringify({ files: urls }),
12 }
13 );
14
15 return response.json();
16}
17
18// Purge by cache tag
19async function purgeCacheByTag(tags: string[]) {
20 const response = await fetch(
21 `https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
22 {
23 method: 'POST',
24 headers: {
25 'Authorization': `Bearer ${CF_API_TOKEN}`,
26 'Content-Type': 'application/json',
27 },
28 body: JSON.stringify({ tags }),
29 }
30 );
31
32 return response.json();
33}
34
35// Set cache tags in response
36app.get('/api/products/:id', async (req, res) => {
37 const product = await getProduct(req.params.id);
38
39 res.setHeader('Cache-Tag', `product-${product.id}, category-${product.categoryId}`);
40 res.setHeader('Cache-Control', 'public, s-maxage=3600');
41 res.json(product);
42});
43
44// Invalidate on update
45async function updateProduct(id: string, data: ProductData) {
46 const product = await db.product.update({ where: { id }, data });
47
48 // Purge CDN cache
49 await purgeCacheByTag([`product-${id}`]);
50
51 return product;
52}Edge Computing#
1// Cloudflare Worker
2export default {
3 async fetch(request: Request): Promise<Response> {
4 const url = new URL(request.url);
5
6 // A/B testing at edge
7 const variant = Math.random() < 0.5 ? 'A' : 'B';
8
9 // Modify request to origin
10 const originUrl = new URL(url);
11 originUrl.searchParams.set('variant', variant);
12
13 const response = await fetch(originUrl.toString(), request);
14
15 // Clone and modify response
16 const newResponse = new Response(response.body, response);
17 newResponse.headers.set('X-Variant', variant);
18
19 return newResponse;
20 },
21};Image Optimization#
1<!-- Use CDN image optimization -->
2<img
3 src="https://cdn.example.com/images/hero.jpg?width=800&format=webp&quality=80"
4 srcset="
5 https://cdn.example.com/images/hero.jpg?width=400&format=webp 400w,
6 https://cdn.example.com/images/hero.jpg?width=800&format=webp 800w,
7 https://cdn.example.com/images/hero.jpg?width=1200&format=webp 1200w
8 "
9 sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
10 alt="Hero image"
11 loading="lazy"
12>1// Cloudflare image transform
2function getOptimizedImageUrl(
3 originalUrl: string,
4 options: { width?: number; height?: number; format?: string; quality?: number }
5) {
6 const params = new URLSearchParams();
7
8 if (options.width) params.set('width', options.width.toString());
9 if (options.height) params.set('height', options.height.toString());
10 if (options.format) params.set('format', options.format);
11 if (options.quality) params.set('quality', options.quality.toString());
12
13 return `/cdn-cgi/image/${params.toString()}/${originalUrl}`;
14}Monitoring#
1// Track cache performance
2interface CDNMetrics {
3 cacheHit: number;
4 cacheMiss: number;
5 cacheBypass: number;
6}
7
8// Parse Cloudflare headers
9function parseCDNHeaders(headers: Headers): CDNMetrics {
10 const cfCacheStatus = headers.get('cf-cache-status');
11
12 return {
13 cacheHit: cfCacheStatus === 'HIT' ? 1 : 0,
14 cacheMiss: cfCacheStatus === 'MISS' ? 1 : 0,
15 cacheBypass: cfCacheStatus === 'BYPASS' ? 1 : 0,
16 };
17}
18
19// Analytics
20async function trackCDNPerformance(request: Request, response: Response) {
21 const metrics = parseCDNHeaders(response.headers);
22
23 await analytics.track('cdn_request', {
24 url: request.url,
25 ...metrics,
26 responseTime: performance.now(),
27 });
28}Multi-CDN Strategy#
1// Load balancing across CDNs
2const cdnProviders = [
3 { name: 'cloudflare', baseUrl: 'https://cf.example.com', weight: 0.6 },
4 { name: 'fastly', baseUrl: 'https://fastly.example.com', weight: 0.3 },
5 { name: 'cloudfront', baseUrl: 'https://d123.cloudfront.net', weight: 0.1 },
6];
7
8function selectCDN(): typeof cdnProviders[0] {
9 const random = Math.random();
10 let cumulative = 0;
11
12 for (const provider of cdnProviders) {
13 cumulative += provider.weight;
14 if (random <= cumulative) {
15 return provider;
16 }
17 }
18
19 return cdnProviders[0];
20}
21
22// DNS-based multi-CDN (recommended)
23// Use services like NS1, Cloudflare Load Balancing, or Route 53Best Practices#
DO:
✓ Version static assets with content hashes
✓ Set appropriate Cache-Control headers
✓ Use cache tags for targeted invalidation
✓ Monitor cache hit rates
✓ Compress responses (gzip/brotli)
✓ Use HTTP/2 or HTTP/3
DON'T:
✗ Cache user-specific content publicly
✗ Set overly long TTLs for dynamic content
✗ Forget to invalidate after deployments
✗ Cache error responses
✗ Ignore Vary headers
Conclusion#
CDNs are essential for global performance. Start with static asset caching, carefully implement dynamic content caching, and monitor your cache hit rates.
The goal is to serve as much as possible from the edge—closer to your users.