Home System Design CDN Components Explained — How Content Delivery Networks Actually Work

CDN Components Explained — How Content Delivery Networks Actually Work

In Plain English 🔥
Imagine your favourite pizza place has only one kitchen in New York. If you order from Los Angeles, they'd have to ship a pizza across the country — cold, late, and sad. A CDN is like opening mini pizza kitchens in every major city so your pizza arrives hot and fast no matter where you live. The original kitchen (your origin server) still makes the recipe, but the local kitchens (edge servers) serve most customers. That's a CDN in one bite.
⚡ Quick Answer
Imagine your favourite pizza place has only one kitchen in New York. If you order from Los Angeles, they'd have to ship a pizza across the country — cold, late, and sad. A CDN is like opening mini pizza kitchens in every major city so your pizza arrives hot and fast no matter where you live. The original kitchen (your origin server) still makes the recipe, but the local kitchens (edge servers) serve most customers. That's a CDN in one bite.

Every time a user in Tokyo loads a website hosted in Dublin, their browser is waiting for data to travel thousands of miles under oceans and through dozens of routers. At the speed of light, that still takes 150–200ms — and that's before the server even starts processing. For a video stream, an e-commerce checkout, or a multiplayer game, that latency is the difference between a great experience and an abandoned session. Netflix, Cloudflare, and Amazon didn't become giants by ignoring this problem — they solved it with CDNs.

A Content Delivery Network solves the physical distance problem by pre-positioning your content closer to your users. Instead of every request travelling back to one origin server, a global mesh of edge servers handles the heavy lifting locally. This cuts latency, reduces bandwidth costs, absorbs traffic spikes, and adds a layer of protection against DDoS attacks — all at the same time.

By the end of this article you'll be able to draw the full CDN request lifecycle on a whiteboard, explain what each component does and why it exists, understand the trade-offs between push and pull CDNs, and answer the tricky CDN questions that come up in system design interviews. Let's build the mental model from the ground up.

The Origin Server — The Single Source of Truth

Every CDN story starts at the origin server. This is your actual application server — the one running your Node.js API, your Django app, or your S3 bucket. It holds the canonical, authoritative copy of every asset: HTML pages, images, JavaScript bundles, videos, API responses.

The origin server's job in a CDN architecture is NOT to serve every user request. That would defeat the whole point. Its job is to serve content to edge servers when they don't have a cached copy, and to handle requests that genuinely can't be cached — like a personalised checkout page or a real-time bank balance.

This distinction matters enormously. A poorly designed CDN setup routes too much traffic back to the origin, creating a bottleneck. A well-designed one keeps the origin cache-hit ratio (the percentage of requests served without hitting origin) as close to 100% as possible for static assets, while intelligently routing dynamic requests.

Think of the origin as a master chef who trains local chefs. The master chef doesn't cook every meal — they set the recipe, train the team, and only get called in for something truly bespoke.

origin_server_cache_headers.conf · NGINX
12345678910111213141516171819202122232425262728293031323334353637383940414243
# Origin server Nginx configuration
# This tells CDN edge servers HOW to cache each type of content

server {
    listen 80;
    server_name origin.theforge.io;

    # Static assets — cache aggressively at the edge for 1 year
    # The CDN will serve these without touching origin on repeat requests
    location ~* \.(js|css|png|jpg|woff2|svg)$ {
        root /var/www/static;

        # 'public' means CDN edge servers ARE allowed to cache this
        # 'max-age=31536000' = 1 year in seconds
        # 'immutable' tells the browser: don't even check for updates
        add_header Cache-Control "public, max-age=31536000, immutable";

        # ETag lets CDN validate freshness without re-downloading
        etag on;
    }

    # HTML pages — short cache, must check origin for updates
    location ~* \.html$ {
        root /var/www/html;

        # 'stale-while-revalidate' serves cached copy instantly
        # while refreshing it in the background — best of both worlds
        add_header Cache-Control "public, max-age=300, stale-while-revalidate=60";
    }

    # API responses — personalised data must NOT be cached at CDN level
    # 'private' means only the user's browser can cache it, not edge servers
    location /api/user/ {
        proxy_pass http://app_backend;
        add_header Cache-Control "private, no-store";
    }

    # Public API data (e.g. product catalogue) — safe to cache at edge
    location /api/products/ {
        proxy_pass http://app_backend;
        add_header Cache-Control "public, max-age=600"; # 10 minutes
    }
}
▶ Output
# When a CDN edge server requests /static/app.js from origin:
# HTTP/1.1 200 OK
# Cache-Control: public, max-age=31536000, immutable
# ETag: "d41d8cd98f00b204e9800998ecf8427e"
# Content-Type: application/javascript
# Content-Length: 84231
#
# Edge server stores it locally. Next 10,000 users in that region
# get the file FROM THE EDGE — origin never sees those requests.
⚠️
Watch Out: Cache-Control vs ExpiresNever rely on the old 'Expires' header for CDN caching. 'Cache-Control' always wins when both are present, and modern CDNs often ignore 'Expires' entirely. Stick to 'Cache-Control: public, max-age=N' for CDN-cacheable content and 'Cache-Control: private, no-store' for anything user-specific.

Edge Servers and PoPs — Your CDN's Global Muscle

Edge servers are the beating heart of a CDN. They sit inside Points of Presence (PoPs) — physical data centres strategically placed in cities around the world. Cloudflare has 300+ PoPs. Akamai has 4,000+. The denser the network, the shorter the physical path to any user on earth.

When a user requests content, DNS routing (usually Anycast DNS) directs them to the nearest PoP automatically — not nearest by map distance, but nearest by network latency, which accounts for real-world internet topology. A user in Singapore might actually be routed to a PoP in Hong Kong if that path has lower latency.

Each edge server maintains its own local cache. When a request arrives, the edge server checks its cache first. A cache HIT means the content is served immediately — no origin involved, no ocean crossed. A cache MISS means the edge server fetches the content from origin, stores it locally, and serves it. Every subsequent request for that content is a HIT.

Edge servers also do more than cache. Modern CDN edges run TLS termination (handling HTTPS handshakes locally so the encryption overhead doesn't add to origin round-trips), HTTP/2 and HTTP/3 multiplexing, image optimisation, and increasingly, full serverless compute via edge functions.

cloudflare_edge_worker.js · JAVASCRIPT
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
// Cloudflare Worker — code that runs ON the edge server, not your origin
// This runs in 300+ cities simultaneously with ~0ms cold start

adaddEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request));
});

async function handleRequest(incomingRequest) {
    const requestUrl = new URL(incomingRequest.url);

    // --- PERSONALISED CACHING STRATEGY ---
    // We can't cache the full page for logged-in users,
    // but we CAN cache the static shell and inject user data at the edge

    const isStaticAsset = requestUrl.pathname.startsWith('/static/');
    const isApiRequest  = requestUrl.pathname.startsWith('/api/');

    if (isStaticAsset) {
        // Let the CDN cache handle this — fetch from cache or origin
        const cachedResponse = await caches.default.match(incomingRequest);

        if (cachedResponse) {
            // CACHE HIT: add a header so we can monitor hit rates in logs
            const hitResponse = new Response(cachedResponse.body, cachedResponse);
            hitResponse.headers.set('X-Cache-Status', 'HIT');
            return hitResponse;
        }

        // CACHE MISS: fetch from origin and store at this edge location
        const originResponse = await fetch(incomingRequest);
        const responseToCache = originResponse.clone(); // clone before consuming body

        event.waitUntil(
            caches.default.put(incomingRequest, responseToCache) // store asynchronously
        );

        const missResponse = new Response(originResponse.body, originResponse);
        missResponse.headers.set('X-Cache-Status', 'MISS');
        return missResponse;
    }

    if (isApiRequest) {
        // For API requests, add geographic context so origin
        // can serve localised data without client needing to send it
        const modifiedRequest = new Request(incomingRequest, {
            headers: {
                ...Object.fromEntries(incomingRequest.headers),
                'X-User-Country':  incomingRequest.cf.country,   // e.g. 'JP'
                'X-User-City':     incomingRequest.cf.city,      // e.g. 'Tokyo'
                'X-User-Timezone': incomingRequest.cf.timezone   // e.g. 'Asia/Tokyo'
            }
        });
        return fetch(modifiedRequest); // pass through to origin with enriched headers
    }

    // Default: pass through everything else unchanged
    return fetch(incomingRequest);
}
▶ Output
# First request from Tokyo user (Cache MISS):
# GET /static/hero-image.webp
# X-Cache-Status: MISS
# Response time: 180ms (edge → origin in Dublin → back to Tokyo)
#
# Second request from different Tokyo user (Cache HIT):
# GET /static/hero-image.webp
# X-Cache-Status: HIT
# Response time: 8ms (served from Tokyo PoP local storage)
#
# API request enriched at edge:
# X-User-Country: JP
# X-User-City: Tokyo
# X-User-Timezone: Asia/Tokyo
# (Origin can now serve localised pricing/content automatically)
⚠️
Pro Tip: Monitor Your Cache Hit RatioA CDN cache hit ratio below 80% means you're paying for CDN infrastructure but not getting the speed benefits. Check for overly aggressive cache-busting, missing Cache-Control headers on static assets, or URL query strings that are creating unnecessary cache key variations (e.g. ?session_id=xyz making every URL unique).

Pull CDN vs Push CDN — Choosing the Right Delivery Model

There are two fundamentally different ways a CDN can get content from your origin to its edge servers. Understanding which model fits your use case is one of the most important CDN architecture decisions you'll make.

A Pull CDN is lazy — in a good way. Edge servers only fetch content from your origin when a user requests it and it's not already cached. The first request to each PoP causes a cache miss and hits origin. Every subsequent request is a cache hit. This is the default model for most CDNs (Cloudflare, Fastly) and works brilliantly for websites with lots of assets and unpredictable traffic patterns.

A Push CDN is proactive. You explicitly upload content to the CDN's edge servers before anyone requests it. You control exactly what's cached and where. This is ideal for large files you know will be popular — like a new movie upload on a streaming service, or a software release binary. You push once, the file is globally pre-positioned, and the first user in any region gets a cache hit.

The trade-off is control vs automation. Push gives you precision but requires you to manage invalidation. Pull is hands-off but means early users in each region always experience that first-hit latency penalty.

cdn_push_deploy_pipeline.sh · BASH
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
#!/bin/bash
# Push CDN deployment script — used in a CI/CD pipeline after a new build
# This pre-warms edge servers so users NEVER see a cache miss on launch day

set -e # Exit immediately if any command fails

CDN_PROVIDER_API="https://api.bunny.net"
CDN_API_KEY="${BUNNY_API_KEY}"         # Loaded from CI/CD secrets, never hardcoded
STORAGE_ZONE="theforge-assets"         # Your CDN storage zone name
BUILD_DIR="./dist"                      # Local build output directory

echo "=== Starting Push CDN Deployment ==="
echo "Build directory: $BUILD_DIR"
echo "Target storage zone: $STORAGE_ZONE"

# Step 1: Upload all new static assets to the CDN storage zone
# The CDN then distributes these to ALL edge PoPs proactively
for asset_file in $(find "$BUILD_DIR" -type f); do

    # Derive the remote path by stripping the local build prefix
    remote_path="${asset_file#$BUILD_DIR/}"

    echo "Uploading: $remote_path"

    # Determine content type based on file extension for correct headers
    content_type="application/octet-stream" # safe default
    case "$asset_file" in
        *.js)    content_type="application/javascript" ;;
        *.css)   content_type="text/css" ;;
        *.html)  content_type="text/html" ;;
        *.webp)  content_type="image/webp" ;;
        *.woff2) content_type="font/woff2" ;;
    esac

    # PUT the file to CDN storage — CDN propagates it to all PoPs
    curl --silent --show-error \
        --request PUT \
        --header "AccessKey: $CDN_API_KEY" \
        --header "Content-Type: $content_type" \
        --data-binary "@$asset_file" \
        "$CDN_PROVIDER_API/storage/v2/$STORAGE_ZONE/$remote_path"
done

# Step 2: Purge old cached versions so users get the new files immediately
# Without this step, edge servers serve stale content until TTL expires
echo "=== Purging stale CDN cache ==="
curl --silent --show-error \
    --request POST \
    --header "AccessKey: $CDN_API_KEY" \
    --header "Content-Type: application/json" \
    --data '{"async": true}' \
    "$CDN_PROVIDER_API/pullzone/theforge-zone/purgeCache"

echo "=== Push CDN Deployment Complete ==="
echo "Assets pre-positioned at all edge PoPs. First users globally get cache HITs."
▶ Output
=== Starting Push CDN Deployment ===
Build directory: ./dist
Target storage zone: theforge-assets
Uploading: js/app.a1b2c3d4.js
Uploading: css/styles.e5f6g7h8.css
Uploading: images/hero.webp
Uploading: fonts/inter.woff2
=== Purging stale CDN cache ===
=== Push CDN Deployment Complete ===
Assets pre-positioned at all edge PoPs. First users globally get cache HITs.
🔥
Interview Gold: When Would You Choose Push Over Pull?Use Push CDN when: (1) you have large files (>10MB) that would be slow to pull from origin on first request, (2) you have a known traffic event (product launch, live stream) and can't risk cache-miss latency for the first wave of users, or (3) your origin can't handle the spike of simultaneous cache-miss requests. Pull CDN covers everything else — it's simpler and self-managing.

Cache Invalidation — The Hardest Problem in CDN Architecture

Phil Karlton famously said there are only two hard things in computer science: cache invalidation and naming things. CDN cache invalidation is where that joke stops being funny and starts costing companies money.

The problem is this: you've set a 24-hour TTL on your product images. You discover one image has the wrong price printed on it. That image is now cached at 300+ edge locations worldwide. Every user will see the wrong price for up to 24 hours unless you explicitly invalidate the cache.

The two main invalidation strategies are TTL-based expiry (set a reasonable TTL and let it expire naturally — simple but not instant) and explicit purge (call the CDN API to immediately evict specific assets — instant but requires integration work).

The smartest strategy combines both: use cache-busting filenames (e.g. app.a1b2c3.js where the hash changes on every build) with a long TTL. Since the filename changes with every deployment, you never need to invalidate — the old URL simply expires naturally, and all new traffic hits the new URL. Your HTML file, which references the new filename, gets a short TTL so it updates quickly. This is how most mature frontend build pipelines work.

cdn_cache_invalidation_api.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
// CDN Cache Invalidation Service
// Programmatic cache purging after a content update
// Real-world scenario: a product image was updated on the CMS

const https = require('https');

// --- CONFIGURATION ---
const CDN_ZONE_ID   = process.env.CDN_ZONE_ID;   // Your CDN zone identifier
const CDN_API_TOKEN = process.env.CDN_API_TOKEN;  // Never hardcode credentials

/**
 * Purges specific URLs from CDN cache.
 * Call this from a CMS webhook when content is updated.
 *
 * @param {string[]} urlsToInvalidate - Full CDN URLs that need immediate cache eviction
 * @returns {Promise<object>} - Purge job status from CDN provider
 */
async function purgeUrlsFromCdnCache(urlsToInvalidate) {

    console.log(`Purging ${urlsToInvalidate.length} URL(s) from CDN cache...`);

    // Cloudflare Cache Purge API payload format
    const purgePayload = JSON.stringify({
        files: urlsToInvalidate  // Accepts up to 30 URLs per request
    });

    return new Promise((resolve, reject) => {
        const options = {
            hostname: 'api.cloudflare.com',
            path: `/client/v4/zones/${CDN_ZONE_ID}/purge_cache`,
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${CDN_API_TOKEN}`,
                'Content-Type':  'application/json',
                'Content-Length': Buffer.byteLength(purgePayload)
            }
        };

        const apiRequest = https.request(options, (apiResponse) => {
            let responseBody = '';

            apiResponse.on('data', chunk => { responseBody += chunk; });

            apiResponse.on('end', () => {
                const parsed = JSON.parse(responseBody);

                if (parsed.success) {
                    console.log('Cache purge successful:', parsed.result);
                    resolve(parsed.result);
                } else {
                    // Cloudflare returns structured errors — log them clearly
                    console.error('Cache purge failed:', parsed.errors);
                    reject(new Error(parsed.errors[0].message));
                }
            });
        });

        apiRequest.on('error', reject);
        apiRequest.write(purgePayload);
        apiRequest.end();
    });
}

// --- EXAMPLE USAGE ---
// Triggered by a CMS webhook when a product image is updated
async function handleProductImageUpdate(updatedProductId) {
    const affectedUrls = [
        `https://assets.theforge.io/products/${updatedProductId}/hero.webp`,
        `https://assets.theforge.io/products/${updatedProductId}/thumbnail.webp`,
        // Also purge the product page HTML so it reflects the new image
        `https://www.theforge.io/products/${updatedProductId}`
    ];

    try {
        await purgeUrlsFromCdnCache(affectedUrls);
        console.log(`Product ${updatedProductId} is now live at all edge nodes.`);
    } catch (error) {
        console.error('Failed to purge CDN cache. Users may see stale content:', error.message);
        // In production: alert on-call team, fallback to shorter TTL, or retry with backoff
    }
}

handleProductImageUpdate('forge-keyboard-pro-2024');
▶ Output
Purging 3 URL(s) from CDN cache...
Cache purge successful: { id: 'a3b4c5d6e7f8a1b2c3d4e5f6a7b8c9d0' }
Product forge-keyboard-pro-2024 is now live at all edge nodes.

# After purge completes (typically 1-5 seconds globally):
# All 300+ Cloudflare PoPs evict the stale image from local cache
# Next request to each PoP causes a cache MISS → fetches fresh image from origin
# Subsequent requests are cache HITs with the correct image
⚠️
Pro Tip: Content-Hashed Filenames Eliminate Most Invalidation NeedsConfigure your build tool (Webpack, Vite, Parcel) to include a content hash in asset filenames: 'app.[contenthash].js'. Each build produces a unique filename, so you set max-age=31536000 and never purge. Old files expire naturally. Only your HTML (which references the new filenames) needs a short TTL. This is the industry-standard approach used by Google, Facebook, and every major CDN-backed frontend.
AspectPull CDNPush CDN
How content reaches edgeOn-demand: edge fetches from origin on first request per PoPProactively: you upload to CDN storage, it propagates to all PoPs
First-user latencyHigher — first user in each region triggers a cache miss to originZero — content pre-positioned before first request
Setup complexityLow — point DNS to CDN, set Cache-Control headers, doneHigher — requires upload pipeline integration and storage management
Storage costOnly caches what users actually requestYou pay for storage of all pushed assets at all PoPs
Best forWebsites, APIs, unpredictable traffic, long-tail assetsLarge files, known launch events, video streams, software downloads
Cache invalidationWait for TTL or call purge APIRe-upload new file + call purge API or use versioned paths
Real-world examplesCloudflare, Fastly default mode, AWS CloudFrontBunny.net storage, KeyCDN, AWS CloudFront with S3 origin
Origin server loadSpiky — bursts on cache misses across many PoPs simultaneouslyMinimal — origin only serves during your controlled push uploads

🎯 Key Takeaways

  • The origin server's job is NOT to serve every user — it's to serve edge servers that haven't cached yet. Design your Cache-Control headers to keep origin traffic as low as possible for static assets.
  • Content-hashed filenames + long TTLs eliminate the cache invalidation problem for static assets entirely. Every senior frontend engineer uses this pattern. If you're not, start today.
  • Pull CDN is the right default for most web applications. Only switch to Push CDN when you have large files, predictable high-demand events, or an origin that can't absorb cache-miss spikes.
  • CDN cache hit ratio is your north-star metric. Below 80% means your CDN is expensive middleware, not a performance multiplier. The culprits are almost always: missing Cache-Control headers, query string fragmentation, or over-broad cache exclusion rules.

⚠ Common Mistakes to Avoid

  • Mistake 1: Caching user-specific API responses at the CDN — Symptom: User A sees User B's account data, shopping cart, or personalised content (catastrophic privacy/security breach) — Fix: Always set 'Cache-Control: private, no-store' on any response that contains session-specific or user-specific data. Only cache responses that are identical for every user. Audit your Cache-Control headers on every API route before enabling CDN caching.
  • Mistake 2: Not using cache-busting filenames, then setting long TTLs — Symptom: After a deployment, users continue seeing old JavaScript, CSS, or images for hours or days because the cached filename hasn't changed — Fix: Configure your build tool to append a content hash to every asset filename (e.g. main.3f7a2b1c.js). With unique filenames per build, you can safely set a 1-year TTL. Old files simply stop being requested; no manual purging needed. This is the single most impactful CDN caching practice you can adopt.
  • Mistake 3: Ignoring query string handling, causing cache fragmentation — Symptom: CDN cache hit ratio is unexpectedly low (below 60%) even for static assets. Inspection reveals thousands of cache entries for the same file: image.jpg?v=1, image.jpg?v=2, image.jpg?cb=1234567 — Fix: Configure your CDN to either ignore all query parameters for static asset paths, or define a specific list of query parameters that should be included in the cache key. In Cloudflare this is 'Cache Key rules'; in Nginx+proxy_cache it's 'proxy_cache_key'. Strip or normalise tracking parameters (utm_source, fbclid, etc.) from the cache key entirely.

Interview Questions on This Topic

  • QWalk me through exactly what happens — at the network and application layer — when a user in Singapore requests an image from a CDN-backed website hosted in London. Include DNS resolution, edge server behaviour, and what determines whether it's a cache hit or miss.
  • QOur CDN cache hit ratio dropped from 92% to 41% overnight after a frontend deploy. What are the top three things you'd investigate first, and what specific CDN metrics or logs would you look at to diagnose the cause?
  • QA product manager asks: 'Why can't we just cache all API responses at the CDN to make the app faster?' How do you explain the risks, and what specific patterns (e.g. surrogate keys, Vary headers, stale-while-revalidate) would you propose to safely cache as much as possible without serving incorrect data?

Frequently Asked Questions

What is the difference between a CDN edge server and an origin server?

The origin server is your actual application or web server — it holds the authoritative copy of all your content and handles dynamic requests. Edge servers are the CDN's distributed cache servers, placed in cities around the world. Edge servers serve cached copies of your content to nearby users so requests never need to travel back to the origin. The origin only gets involved on a cache miss — the first request for a piece of content at a given edge location.

Does using a CDN mean my website is always faster?

A CDN makes static assets (images, JS, CSS, fonts, videos) dramatically faster for geographically distant users. However, it doesn't automatically speed up dynamic content like personalised API responses or database queries — those still hit your origin. The performance gain depends heavily on your cache hit ratio and how well your Cache-Control headers are configured. A misconfigured CDN can actually slow things down by adding a network hop without caching anything useful.

How does a CDN know which edge server is 'closest' to a user?

Most CDNs use Anycast DNS routing. When a user's browser resolves your CDN domain, the DNS system returns the IP address of the edge PoP with the best network path to that user — not necessarily the physically nearest one, but the one reachable with the lowest latency given current internet routing conditions. Some CDNs supplement this with GeoDNS (routing by geographic IP lookup) or BGP Anycast, where the same IP address is announced from multiple PoPs and the internet's routing protocol automatically selects the best path.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousCaching StrategiesNext →Message Queues
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged