CDN and Origin Architecture — Serving Globally Without a Global Database
Advertisement
Introduction
Why This Matters
Every millisecond of latency costs conversion rate. A user in Tokyo hitting your origin server in us-east-1 adds 150–200ms of round-trip time before your application even starts processing. A CDN that serves cached responses from an edge node 5ms away reduces that to near-zero.
Understanding how to structure your origin architecture — what gets cached at the edge, how cache invalidation works, and how to protect your origin from traffic spikes — is a core scaling skill for any backend engineer building globally distributed products.
CDN Architecture Fundamentals
A CDN sits between end users and your origin server, caching responses at edge locations (Points of Presence, or PoPs) around the world:
User (Tokyo)
|
v
CDN Edge Node (Tokyo PoP) ← Cache HIT → Return cached response (5ms)
|
| Cache MISS
v
Origin Shield (us-west-2) ← Collapsed request → Single origin call
|
v
Origin Server (us-east-1) ← Processes request, returns responseOrigin Shield is a second-tier caching layer that collapses multiple edge-node cache misses into a single request to your origin. Without it, 50 edge nodes missing simultaneously can send 50 requests to your origin. With it, only one request reaches your origin.
Cache Control Headers — The Foundation of CDN Behavior
Cache control headers tell the CDN and browser how to cache your responses:
import express from 'express';
const app = express();
// Static assets — cache aggressively
app.use('/static', express.static('public', {
setHeaders: (res) => {
// 1 year cache, immutable (content-addressed filenames)
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
}));
// API responses — cache with revalidation
app.get('/api/products', async (req, res) => {
const products = await getProducts();
// Cache for 5 minutes at the CDN, 1 minute in browser
res.setHeader('Cache-Control', 'public, s-maxage=300, max-age=60, stale-while-revalidate=600');
res.setHeader('Vary', 'Accept-Encoding');
res.json(products);
});
// User-specific data — never cache at CDN
app.get('/api/user/profile', authenticate, async (req, res) => {
res.setHeader('Cache-Control', 'private, no-store');
const profile = await getUserProfile(req.user.id);
res.json(profile);
});
// Purge-friendly versioned responses
app.get('/api/config', async (req, res) => {
const config = await getConfig();
const etag = `"${config.version}"`;
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400');
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.json(config);
});CloudFront Origin Architecture with Origin Shield
import {
CloudFrontClient,
CreateDistributionCommand,
} from '@aws-sdk/client-cloudfront';
const client = new CloudFrontClient({ region: 'us-east-1' });
const distribution = await client.send(new CreateDistributionCommand({
DistributionConfig: {
Origins: {
Quantity: 1,
Items: [
{
Id: 'api-origin',
DomainName: 'api.myapp.com',
CustomOriginConfig: {
HTTPSPort: 443,
OriginProtocolPolicy: 'https-only',
},
// Enable Origin Shield — collapses cache misses
OriginShield: {
Enabled: true,
OriginShieldRegion: 'us-east-1', // closest to your origin
},
},
],
},
DefaultCacheBehavior: {
ViewerProtocolPolicy: 'redirect-to-https',
CachePolicyId: 'MANAGED_CACHING_OPTIMIZED', // managed policy
OriginRequestPolicyId: 'MANAGED_ALL_VIEWER',
Compress: true,
AllowedMethods: {
Quantity: 7,
Items: ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'],
},
},
Enabled: true,
HttpVersion: 'http2and3',
IsIPV6Enabled: true,
Comment: 'API distribution with origin shield',
PriceClass: 'PriceClass_All',
},
}));Cache Invalidation Strategies
Cache invalidation is one of the two hard problems in computer science. Here are production-tested approaches:
import {
CloudFrontClient,
CreateInvalidationCommand,
} from '@aws-sdk/client-cloudfront';
const cf = new CloudFrontClient({ region: 'us-east-1' });
// Strategy 1: Path-based invalidation
async function invalidatePaths(distributionId: string, paths: string[]): Promise<void> {
await cf.send(new CreateInvalidationCommand({
DistributionId: distributionId,
InvalidationBatch: {
CallerReference: Date.now().toString(),
Paths: {
Quantity: paths.length,
Items: paths,
},
},
}));
}
// After updating product 123:
await invalidatePaths('E2QWRUHEXAMPLE', [
'/api/products/123',
'/api/products', // list page
]);
// Strategy 2: Versioned URL pattern (preferred — no invalidation needed)
function getVersionedUrl(path: string, version: string): string {
return `${path}?v=${version}`;
}
// Strategy 3: Surrogate keys with Cloudflare
async function purgeByTag(tag: string): Promise<void> {
await fetch(`https://api.cloudflare.com/client/v4/zones/${process.env.CF_ZONE_ID}/purge_cache`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.CF_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tags: [tag] }),
});
}
// Tag responses at origin, purge by tag
// Response header: Cache-Tag: product-123,product-list
app.get('/api/products/:id', async (req, res) => {
const product = await getProduct(req.params.id);
res.setHeader('Cache-Tag', `product-${req.params.id},product-list`);
res.setHeader('Cache-Control', 'public, s-maxage=3600');
res.json(product);
});Separating Cacheable and Non-Cacheable Paths
Structure your API to make CDN caching decisions obvious:
// Routing strategy for CDN-friendly APIs
const router = express.Router();
// PUBLIC paths — CDN caches these
// GET /api/public/products
// GET /api/public/categories
// GET /api/public/posts/:slug
router.get('/public/*', (req, res, next) => {
res.setHeader('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600');
next();
});
// AUTHENTICATED paths — CDN passes through, never caches
// GET /api/private/orders
// POST /api/private/checkout
router.use('/private/*', authenticate, (req, res, next) => {
res.setHeader('Cache-Control', 'private, no-store');
next();
});
// MIXED — vary by auth status
router.get('/api/products/:id', (req, res, next) => {
if (req.headers.authorization) {
// Authenticated — include user-specific pricing, no CDN cache
res.setHeader('Cache-Control', 'private, no-store');
} else {
// Anonymous — CDN can cache base product data
res.setHeader('Cache-Control', 'public, s-maxage=600');
}
next();
});Common Mistakes
Caching responses with Set-Cookie headers. Responses containing Set-Cookie should never be cached at the CDN. Use Vary: Cookie or return auth-dependent data only on private routes.
Missing Vary: Accept-Encoding. Without this, a CDN may serve a compressed response to a client that cannot decompress it, or vice versa.
No origin shield. Without an origin shield, a cache miss in 50 edge nodes simultaneously sends 50 parallel requests to your origin — a mini-DDoS from your own CDN.
Wildcard invalidation (/*). Invalidating everything on every deploy defeats the purpose of a CDN and adds significant latency for the first wave of users after deployment.
Not measuring CDN cache hit rate. You cannot optimize what you do not measure. Always track cache hit vs miss ratio per path.
Best Practices
- Use content-addressed filenames (
main.abc123.js) for static assets withimmutablein Cache-Control so they never need invalidation - Enable Origin Shield in the region closest to your origin server to collapse edge-node cache misses
- Tag responses with
Cache-Tagheaders and use tag-based purging for efficient selective cache invalidation - Separate public (CDN-cacheable) and private (authenticated) API routes at the URL prefix level
- Monitor cache hit rate per path — a hit rate below 70% on public routes indicates a caching problem
- Use
stale-while-revalidatefor API responses to serve stale content while the CDN fetches a fresh copy in the background
Key Takeaways
- CDN edge nodes cache responses at locations near users, reducing round-trip latency from 150–200ms (cross-continental) to under 10ms.
- Origin Shield is a second-tier caching layer that collapses multiple simultaneous edge-node cache misses into a single request to your origin server.
Cache-Control: public, s-maxage=300, stale-while-revalidate=600tells the CDN to cache for 5 minutes and serve stale content for up to 10 minutes while revalidating.- Content-addressed filenames (
file.abc123.js) withimmutablecache headers eliminate the need for invalidation entirely for static assets. - Surrogate keys (Cache-Tag headers) enable surgical cache invalidation by tag rather than requiring wildcard purges on every deployment.
- Responses with
Set-CookieorAuthorization-dependent content must useCache-Control: private, no-storeto prevent CDN caching. - Cache hit rate below 70% on public API routes is a signal that TTLs are too short, cache keys are too granular, or responses have unnecessary variation.
- Wildcard invalidation (
/*) after every deploy negates CDN benefits — prefer targeted path-based or tag-based invalidation strategies.
Advertisement