Field note / fundamentals

publishedupdated 2026-05-01#cdn#networking#performance#cloudfront#caching

CDN Architecture

How Content Delivery Networks work — PoPs, edge caching, cache invalidation, pull vs push, and when to use a CDN.

Overview

A CDN (Content Delivery Network) is a geographically distributed network of servers (Points of Presence / PoPs) that cache content close to users. Primary benefits: reduced latency (serve from edge, not origin), reduced origin load, DDoS mitigation via absorption.


Architecture

WITHOUT CDN:
  User (Tokyo) ──── 150ms RTT ────▶ Origin (US-East)
  User (London) ─── 100ms RTT ───▶ Origin (US-East)
  User (Sydney) ─── 200ms RTT ───▶ Origin (US-East)
  All traffic hits origin → high latency, high cost

WITH CDN (Pull Model):
  ┌───────────────────────────────────────────────────────┐
  │                   Origin Server (US-East)              │
  │               (only serves cache misses)               │
  └───────────────────────────────────────────────────────┘
          ▲              ▲              ▲
          │ cache miss   │ cache miss   │ cache miss
          │ (1st request)│              │
  ┌───────┴──────┐ ┌─────┴──────┐ ┌───┴────────┐
  │  PoP Tokyo   │ │ PoP London │ │ PoP Sydney │
  │  (edge node) │ │            │ │            │
  │  Cached:     │ │  Cached:   │ │  Cached:   │
  │  /image.jpg  │ │  /style.css│ │  /app.js   │
  └──────┬───────┘ └──────┬─────┘ └──────┬─────┘
         │                │              │
  User (Tokyo)     User (London)   User (Sydney)
  5ms RTT          8ms RTT         6ms RTT

CDN REQUEST FLOW (Pull):
═══════════════════════════════════════════════════════
  1. User requests https://cdn.example.com/image.jpg
  2. DNS resolves to nearest PoP (Anycast routing)
  3. PoP checks local cache:
     HIT:  return cached file (← 5ms)
     MISS: forward to origin, cache response, return to user (← 150ms first time)
  4. Subsequent requests for same file → served from edge

CDN CACHE INVALIDATION:
═══════════════════════════════════════════════════════
  Problem: content updated at origin, but CDN still serves old version

  Strategy 1: URL versioning (best)
    /app.js?v=abc123  (content hash in filename/query)
    New deploy changes hash → new URL → cache miss (intentional)
    Old URL still cached (backward compatible)

  Strategy 2: Purge API
    POST https://api.cloudfront.net/invalidations
    { "paths": ["/image.jpg", "/style.css"] }
    Takes 0-60s to propagate to all PoPs
    Has cost (AWS charges per invalidation path)

  Strategy 3: Short TTL
    Cache-Control: max-age=60
    Stale content for at most 60s
    Simple but increases origin load

PUSH vs PULL:
═══════════════════════════════════════════════════════
  Pull CDN:
    - Origin is master, CDN fetches on demand
    - Good for frequently-changing content
    - First-request latency hits origin
    - No storage cost management

  Push CDN:
    - You proactively push content to CDN nodes
    - Good for large static assets (video, software downloads)
    - Content available before first request
    - You manage expiry and storage

Technical Implementation

CloudFront with S3 Origin (AWS CDN)

// AWS CDK: CloudFront distribution with S3 origin
import { Distribution, ViewerProtocolPolicy, CachePolicy } from 'aws-cdk-lib/aws-cloudfront';
import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins';

const distribution = new Distribution(this, 'WebDistribution', {
  defaultBehavior: {
    origin: new S3Origin(assetsBucket),
    viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
    cachePolicy: new CachePolicy(this, 'AppCachePolicy', {
      defaultTtl: Duration.days(365),    // long TTL for versioned assets
      minTtl: Duration.seconds(0),
      maxTtl: Duration.days(365),
      enableAcceptEncodingGzip: true,
      enableAcceptEncodingBrotli: true,
    }),
  },
  additionalBehaviors: {
    '/api/*': {
      origin: new HttpOrigin('api.example.com'),
      cachePolicy: CachePolicy.CACHING_DISABLED,  // never cache API responses
    },
  },
});

Cache-Control Headers

// Express: set cache headers per resource type
app.use('/static', (req, res, next) => {
  // Versioned assets (hash in filename): cache forever
  if (/\.[a-f0-9]{8,}\.(js|css|png|jpg)$/.test(req.path)) {
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
  } else {
    // Non-versioned: 1 hour
    res.setHeader('Cache-Control', 'public, max-age=3600');
  }
  next();
}, express.static('dist'));

// HTML: never cache (so new deploys are picked up immediately)
app.get('/', (req, res) => {
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
  res.sendFile('index.html');
});

// API responses: no CDN caching
app.use('/api', (req, res, next) => {
  res.setHeader('Cache-Control', 'private, no-store');
  next();
});

CDN Cache Invalidation (CloudFront)

import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';

const cloudfront = new CloudFrontClient({ region: 'us-east-1' });

export async function invalidatePaths(paths: string[]): Promise<void> {
  await cloudfront.send(new CreateInvalidationCommand({
    DistributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID,
    InvalidationBatch: {
      CallerReference: Date.now().toString(),  // unique per request
      Paths: {
        Quantity: paths.length,
        Items: paths,  // e.g., ['/index.html', '/api/products/*']
      },
    },
  }));
}

// Call after deploy to invalidate HTML (JS/CSS use versioned URLs)
await invalidatePaths(['/index.html', '/sitemap.xml']);

Interview Preparation

Q: How does a CDN know which PoP to send a user to?

Anycast routing + GeoDNS. With Anycast, the same IP is announced from multiple PoPs — routers automatically route to the nearest one (BGP shortest path). With GeoDNS, the DNS server returns different IP addresses based on the user's geographic location. Most CDNs use a combination of both.

Q: What content should NOT be cached at CDN?

Personalized content (shopping cart, user profile), authenticated responses (anything behind Authorization header), real-time data (stock prices, live scores), POST/PUT/DELETE responses. Use Cache-Control: private, no-store for these.

Q: How do you deploy a frontend app update without users getting stale JavaScript?

Content-hash-based filenames. Webpack/Vite output app.abc123.js where the hash changes with every build. index.html references the new filename. CDN caches app.abc123.js forever (it never changes). On deploy, invalidate only index.html — it's tiny, cheap, and the new index.html references new hashed JS/CSS filenames.

Q: CDN vs Load Balancer — what's the difference?

CDN: caches and serves static/cacheable content from edge nodes close to users. Reduces latency by geographic proximity. Load balancer: distributes traffic across multiple backend servers in one datacenter. Reduces load per server and provides failover. Both can be in the same architecture: CDN → internet → Load Balancer → App Servers.


Learning Resources