CDN & Edge Caching
Deliver content faster with CDNs - Cloudflare, AWS CloudFront, and edge caching patterns.
5 min read
What is a CDN?#
Content Delivery Network - servers distributed globally that cache your content closer to users:
Without CDN:
User (Tokyo) ──────────────────────► Server (New York)
200ms latency
With CDN:
User (Tokyo) ───► CDN Edge (Tokyo) ─► Server (New York)
20ms (cached)
CDN Options#
| CDN | Best For | Features |
|---|---|---|
| Cloudflare | Everything | Free tier, WAF, Workers |
| AWS CloudFront | AWS users | Lambda@Edge, S3 integration |
| Vercel Edge | Next.js | Zero config, global |
| Fastly | Enterprise | Instant purge, VCL |
| Bunny.net | Budget | Cheap, simple |
Cloudflare Setup#
1. Basic Configuration#
javascript
// Set cache headers - Cloudflare respects these
app.get('/api/products', (req, res) => {
res.set('Cache-Control', 'public, max-age=3600, s-maxage=86400');
// max-age: Browser cache (1 hour)
// s-maxage: CDN cache (24 hours)
res.json({ data: products });
});
2. Cache Rules (Cloudflare Dashboard)#
Rule: Cache API responses
Match: /api/products/*
Cache Level: Cache Everything
Edge TTL: 1 day
Browser TTL: 1 hour
3. Purge Cache#
javascript
// Purge specific URLs via API
async function purgeCloudflareCache(urls) {
await fetch(`https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${CF_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ files: urls }),
});
}
// After updating product
await Product.findByIdAndUpdate(id, data);
await purgeCloudflareCache([
`https://api.example.com/api/products/${id}`,
'https://api.example.com/api/products',
]);
AWS CloudFront#
1. Origin Configuration#
yaml
# CloudFront distribution config
Origins:
- DomainName: api.example.com
CustomOriginConfig:
HTTPPort: 443
OriginProtocolPolicy: https-only
CacheBehaviors:
- PathPattern: /api/products/*
CachePolicyId: !Ref ProductsCachePolicy
OriginRequestPolicyId: !Ref ForwardHeadersPolicy
- PathPattern: /api/users/*
CachePolicyId: !Ref NoCachePolicy # Don't cache user data
2. Cache Policy#
javascript
// Lambda@Edge for custom caching logic
export const handler = async (event) => {
const request = event.Records[0].cf.request;
const response = event.Records[0].cf.response;
// Add cache headers based on path
if (request.uri.startsWith('/api/products')) {
response.headers['cache-control'] = [{
key: 'Cache-Control',
value: 'public, max-age=3600',
}];
}
return response;
};
3. Invalidate Cache#
javascript
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
const client = new CloudFrontClient({ region: 'us-east-1' });
async function invalidateCloudFront(paths) {
await client.send(new CreateInvalidationCommand({
DistributionId: process.env.CF_DISTRIBUTION_ID,
InvalidationBatch: {
CallerReference: Date.now().toString(),
Paths: {
Quantity: paths.length,
Items: paths, // ['/api/products/*', '/api/products/123']
},
},
}));
}
Cache-Control Patterns#
javascript
// src/middleware/cacheControl.js
export function cacheControl(options = {}) {
return (req, res, next) => {
const {
public: isPublic = false,
maxAge = 0,
sMaxAge = null,
staleWhileRevalidate = null,
staleIfError = null,
noStore = false,
} = options;
if (noStore) {
res.set('Cache-Control', 'no-store');
return next();
}
const directives = [];
directives.push(isPublic ? 'public' : 'private');
directives.push(`max-age=${maxAge}`);
if (sMaxAge !== null) {
directives.push(`s-maxage=${sMaxAge}`);
}
if (staleWhileRevalidate !== null) {
directives.push(`stale-while-revalidate=${staleWhileRevalidate}`);
}
if (staleIfError !== null) {
directives.push(`stale-if-error=${staleIfError}`);
}
res.set('Cache-Control', directives.join(', '));
next();
};
}
// Usage
app.get('/api/products',
cacheControl({
public: true,
maxAge: 60, // Browser: 1 minute
sMaxAge: 3600, // CDN: 1 hour
staleWhileRevalidate: 86400, // Serve stale for 24h while refreshing
}),
getProducts
);
app.get('/api/me',
cacheControl({ noStore: true }), // Never cache
getProfile
);
Vary Header#
Tell CDN to cache different versions:
javascript
// Cache different versions based on headers
app.get('/api/products', (req, res) => {
res.set('Vary', 'Accept-Language, Accept-Encoding');
res.set('Cache-Control', 'public, max-age=3600');
const lang = req.headers['accept-language']?.split(',')[0] || 'en';
// CDN caches separate versions for en, es, fr, etc.
res.json({ data: products, lang });
});
// Cache based on auth state (not the token itself!)
app.get('/api/content', (req, res) => {
const isAuthenticated = !!req.user;
res.set('Vary', 'X-Auth-State');
res.set('X-Auth-State', isAuthenticated ? 'authenticated' : 'anonymous');
res.set('Cache-Control', 'public, max-age=3600');
res.json({
data: isAuthenticated ? premiumContent : freeContent,
});
});
Edge Functions#
Run code at the edge (closest to users):
Cloudflare Workers#
javascript
// workers/api-cache.js
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Check cache first
const cacheKey = new Request(url.toString(), request);
const cache = caches.default;
let response = await cache.match(cacheKey);
if (!response) {
// Fetch from origin
response = await fetch(request);
// Clone and cache
response = new Response(response.body, response);
response.headers.set('Cache-Control', 'public, max-age=3600');
// Store in edge cache
await cache.put(cacheKey, response.clone());
}
return response;
},
};
Vercel Edge Config#
javascript
// middleware.ts (Next.js)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Add cache headers at the edge
if (request.nextUrl.pathname.startsWith('/api/products')) {
response.headers.set(
'Cache-Control',
'public, s-maxage=3600, stale-while-revalidate=86400'
);
}
return response;
}
export const config = {
matcher: '/api/:path*',
};
Monitoring Cache Performance#
javascript
// Track cache hits via headers
app.use((req, res, next) => {
res.on('finish', () => {
const cacheStatus = res.get('CF-Cache-Status') || // Cloudflare
res.get('X-Cache') || // CloudFront
'NONE';
metrics.increment('api.requests', {
path: req.path,
cacheStatus,
});
});
next();
});
Key Takeaways#
- Use CDN for public data - Products, posts, static content
- Set s-maxage for CDN - Different TTL than browser
- Vary for personalization - Cache different versions
- Purge on updates - Clear CDN when data changes
- Monitor hit rates - Track cache effectiveness
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.