Mid-level 6 min · March 05, 2026

CDN Cache-Control Headers — Why Black Friday Had 0% Hits

Missing Cache-Control headers caused 0% CDN cache hits, maxing origin CPU during peak traffic.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • CDNs reduce latency by caching content at edge servers near users
  • Origin server stores authoritative copy; edge servers serve cached copies
  • Anycast DNS routes users to the nearest Point of Presence (PoP)
  • Cache hit ratio above 80% is the target; below means misconfiguration
  • Most CDN failures come from incorrect Cache-Control headers or query string fragmentation
  • Push CDN pre-positions large files; Pull CDN is the default for websites
Plain-English First

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.confNGINX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 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 Expires
Never 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.
Production Insight
Missing Cache-Control headers cause 100% of requests to hit origin, defeating the CDN.
A single missing header during a traffic spike can DDoS your own origin server.
Rule: always set explicit Cache-Control headers on every response, even for dynamic content.
Key Takeaway
The origin server's job is not to serve every user.
It exists to serve edge servers that haven't cached yet.
Design Cache-Control headers to keep origin traffic minimal for static assets.
Choosing Cache-Control Directives
IfContent is identical for all users (static assets, public APIs)
UseUse 'public, max-age=N' where N is appropriate TTL (1 year for versioned files).
IfContent is user-specific (profile pages, account settings)
UseUse 'private, no-store' to prevent CDN caching altogether.
IfContent changes frequently but is the same for all users (HTML, product listings)
UseUse 'public, max-age=300, stale-while-revalidate=60' for freshness with performance.

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.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 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 Ratio
A 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).
Production Insight
Cache misses in a new region cause a 'thundering herd' against origin when many users arrive simultaneously.
If origin can't handle the burst, it cascades into a full outage.
Rule: pre-warm edge servers for new deployments or use staggered rollouts.
Key Takeaway
Edge servers are where speed happens.
Cache hit ratio is the north-star metric.
Below 80% means your CDN is expensive middleware, not a performance multiplier.
When to Pre-Warm an Edge PoP
IfLaunching a product in a new geographic region
UseUse CDN's pre-warm feature to push popular assets to that PoP before go-live.
IfRunning a flash sale with expected high traffic
UsePre-warm product images and pricing CSS at all PoPs 30 minutes before start.
IfNormal daily traffic with existing users
UseNo pre-warming needed; natural cache misses will fill the edge over time.

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.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/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.
Production Insight
Push CDN requires explicit invalidation when content changes; forget it and users see stale files for TTL duration.
Pull CDN can cause origin meltdown when a new asset goes viral across multiple PoPs simultaneously.
Rule: use Push for known events, Pull for organic traffic, and always monitor cache hit ratio.
Key Takeaway
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.
Monitor your cache hit ratio to validate your choice.
Which CDN Model to Use
IfStandard website with varied traffic, lots of small files
UsePull CDN — lower operational overhead, good for long-tail assets.
IfLarge downloadable files or streaming media with predictable demand
UsePush CDN — pre-position large files, avoid cold-start latency for first users.
IfHybrid: static site with occasional large releases
UseUse Pull CDN as default, and preload new release assets via push or CDN API pre-warm.

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.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 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 Needs
Configure 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.
Production Insight
A single missed invalidation during a price update caused a 20% revenue drop over 24 hours as users saw old prices.
The fix was implementing automated cache purging via webhook from the CMS, but the company lost trust.
Rule: automate invalidation from the content update source, never rely on manual TTL expiry for critical changes.
Key Takeaway
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 doing it, start today.
Choosing an Invalidation Strategy
IfContent changes rarely and can tolerate some staleness (e.g., blog images)
UseSet long TTL (1 year) and rely on natural expiry. No automation needed.
IfContent changes frequently and must be instant (e.g., product prices)
UseUse short TTL (5 minutes) or implement cache purging via API on content update.
IfContent filenames change with each deployment (hash-based)
UseSet infinite TTL. No invalidation needed. Only HTML needs short TTL.

CDN Security: DDoS Protection, WAF, and TLS Termination at the Edge

A CDN isn't just a performance tool — it's also your first line of defence. By terminating TLS connections at the edge, the CDN handles encryption handshakes before they ever reach your origin. This offloads CPU-intensive crypto work and protects your origin from direct exposure to the internet.

Most enterprise CDNs include a Web Application Firewall (WAF) that runs on edge servers. WAF rules inspect incoming requests for SQL injection, XSS, and other OWASP Top 10 attacks — before they reach your application. The rules can be customised per path: strict rule set for login pages, relaxed for static assets.

DDoS mitigation is another core CDN security feature. The distributed nature of edge PoPs means that an attack targeting one region can be absorbed by hundreds of servers globally. CDN providers use traffic scrubbing centres to filter out malicious traffic while passing legitimate requests to origin. Cloudflare's 'Always Online' feature can even serve a cached version of your site when origin is down.

But security at the edge has trade-offs. Offloading TLS termination means your origin receives unencrypted HTTP requests from the CDN (in many setups). You must ensure the link between CDN and origin is secure — either via HTTPS between edge and origin, or by restricting origin access to CDN IP ranges only. This is called 'origin pull' security and is one of the most commonly misconfigured areas.

origin_security_for_cdn.confNGINX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Nginx configuration for origin server behind a CDN
# Restricts access to CDN IP ranges only
# Ensures TLS between CDN and origin (optional but recommended)

server {
    listen 443 ssl http2;
    server_name origin.theforge.io;

    # Only allow requests from the CDN's public IP ranges
    # (Cloudflare IPs as of 2026, check official list regularly)
    allow 173.245.48.0/20;
    allow 103.21.244.0/22;
    # ... add all CDN IP ranges
    deny all;

    ssl_certificate     /etc/ssl/certs/origin.crt;
    ssl_certificate_key /etc/ssl/private/origin.key;

    location / {
        proxy_pass http://app_backend:3000;

        # Trust the CDN's forwarded headers
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $http_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;

        # Prevent CDN from forwarding malicious headers
        proxy_set_header X-XSS-Protection "1; mode=block";
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    }
}
Output
# With this config:
# - Only CDN edge servers can reach origin
# - Direct internet requests to origin IP are rejected (403)
# - TLS is enforced between CDN and origin
# - Origin sees the real user IP via X-Forwarded-For
# - HSTS header is added to all responses
Mental Model: The Castle Analogy
  • The outer wall (CDN edge) absorbs arrows (DDoS) and inspects all visitors (WAF).
  • Only pre-approved messengers (CDN IP ranges) are allowed through the gate to the inner keep (origin).
  • The outer wall handles the handshake (TLS) so the inner keep doesn't waste energy on encryption.
  • If the outer wall falls, the inner keep is still protected by moats (origin security groups).
Production Insight
Without restricting origin access to CDN IPs, attackers can bypass the CDN and directly target origin IP.
A single leaked origin IP can lead to DDoS bypassing all CDN protection.
Rule: always firewall origin to accept traffic only from CDN IP ranges, and use TLS between CDN and origin.
Key Takeaway
A CDN is your first line of defence.
Restrict origin access to CDN IPs and enable HTTPS end-to-end.
Use WAF rules for application-layer filtering before it hits your code.
CDN Security Decisions
IfCDN provider supports origin pull HTTPS
UseEnable HTTPS between CDN and origin for end-to-end encryption.
IfOrigin must allow direct admin access
UseUse a separate admin subdomain (admin.example.com) not proxied through CDN.
IfYou need to block specific countries or user agents
UseConfigure WAF rules at CDN level, not in application code.
● Production incidentPOST-MORTEMseverity: high

The Missing Cache-Control Header That Took Down a Black Friday Sale

Symptom
During a Black Friday flash sale, the e-commerce site became unresponsive. Origin server CPU hit 100%, database connections maxed out. CDN logs showed zero cache hits for all product images even though they were static assets — every request hit origin.
Assumption
The team assumed the CDN would cache all static assets by default. They had configured CDN caching rules in the provider dashboard but had not set Cache-Control headers on the origin server. The CDN provider was honoring origin headers, which were absent, so it treated everything as uncacheable.
Root cause
The origin server's Nginx configuration had no Cache-Control headers for static assets. The CDN provider's policy defaulted to 'no cache' when origin returns no caching directives. Result: every user request for a product image caused a full round-trip to origin.
Fix
Added Cache-Control: public, max-age=86400 to the static asset location block in Nginx. Then purged the (empty) CDN cache to force fresh headers. Within 5 minutes, cache hit ratio jumped to 94% and origin load dropped to 5%.
Key lesson
  • Never assume the CDN caches anything without explicit Cache-Control headers from origin.
  • Test cache behaviour with curl -I to inspect response headers before relying on the CDN in production.
  • Add a monitoring dashboard for CDN cache hit ratio and alert when it drops below 80%.
Production debug guideQuick diagnostic steps for the most common CDN production issues5 entries
Symptom · 01
Users report slow page loads despite CDN being enabled
Fix
Check Cache-Control headers on static assets with curl -I. If missing or set to 'private', the CDN is bypassed. Fix by adding 'public, max-age=N' on origin.
Symptom · 02
Cache hit ratio is below 70%
Fix
Inspect cache key fragmentation: are unique query strings (utm_*, session_id) being included? Configure CDN to ignore or normalize query parameters for static paths.
Symptom · 03
Stale content is served after a deployment
Fix
Verify if cache-busting filenames (content hash) are used. If not, trigger a CDN purge for the changed URLs. Confirm purge completion via CDN provider's API or UI.
Symptom · 04
Some users see other users' personal data
Fix
Immediately disable CDN caching for any URL that returns user-specific content. Audit all API endpoints for missing 'Cache-Control: private, no-store'. Never trust 'Vary: Cookie' alone.
Symptom · 05
Origin server CPU spikes coincide with traffic bursts to a new geographic region
Fix
Check if the CDN has edge servers in that region. If not, the first wave of users will all cause cache misses. Consider pre-warming the CDN or adding a PoP in that region.
★ CDN Debugging Cheat SheetCommands and actions to diagnose CDN issues in under 60 seconds
Is the CDN actually serving my content?
Immediate action
Run curl with verbose headers and check for CDN-specific headers (e.g., X-Cache, CF-Cache-Status, X-Served-By).
Commands
curl -I https://cdn.example.com/static/app.js
Look for X-Cache: HIT or CF-Cache-Status: HIT
Fix now
If MISS or no CDN header, verify DNS points to CDN, then check origin Cache-Control headers.
Why is the cache hit ratio so low?+
Immediate action
Check CDN analytics for top URLs causing cache misses.
Commands
Dig into CDN logs: grep for 'MISS' or query provider API for top uncached URLs
Check for query string variations: curl 'https://.../file.jpg?v=1' vs 'https://.../file.jpg?v=2'
Fix now
Configure CDN to ignore query parameters for static assets, or add cache key rules to normalize.
Users see old content after deployment+
Immediate action
Purge the CDN cache for the changed URLs.
Commands
curl -X POST 'https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache' -H 'Authorization: Bearer TOKEN' -d '{"files":["URL"]}'
Verify purge: curl -I URL and ensure Cache-Control max-age restarted
Fix now
Implement content hashing in filenames to avoid manual purges.
API responses are being cached when they shouldn't be+
Immediate action
Check response headers on an API endpoint.
Commands
curl -I 'https://cdn.example.com/api/user/profile'
If Cache-Control is missing or 'public', immediately add 'Cache-Control: private, no-store' on origin for that endpoint
Fix now
Audit all API routes and apply correct caching policy in application code or reverse proxy.
Pull CDN vs Push CDN
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

1
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.
2
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.
3
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.
4
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.
5
Secure your origin
restrict traffic to CDN IPs, enable HTTPS between CDN and origin, and use WAF rules at the edge for application-layer filtering.

Common mistakes to avoid

4 patterns
×

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.
×

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.
×

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.
×

Not restricting origin access to CDN IP ranges

Symptom
Attackers bypass CDN DDoS protection by directly targeting origin IP. Origin becomes overwhelmed even though CDN is active.
Fix
Firewall your origin to accept traffic only from known CDN IP ranges. Most CDN providers publish a list of IP ranges. Use security groups or iptables to allow only those ranges, and deny all other inbound traffic.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Walk me through exactly what happens — at the network and application la...
Q02SENIOR
Our CDN cache hit ratio dropped from 92% to 41% overnight after a fronte...
Q03SENIOR
A product manager asks: 'Why can't we just cache all API responses at th...
Q01 of 03SENIOR

Walk 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.

ANSWER
1. DNS resolution: Browser resolves the CDN domain. The authoritative DNS returns an IP via Anycast, directing to the nearest edge PoP (lowest latency). 2. Browser opens TCP/TLS connection to that edge IP. 3. HTTP request sent to edge server with the image URL. 4. Edge server checks its local cache using the URL (and any configured cache key) as the lookup. 5. If cache HIT: edge returns the image immediately. 6. If cache MISS: edge forwards request to the origin server (using the configured origin address). The origin returns the image with Cache-Control headers. Edge caches it based on those headers, then serves to user. 7. Response headers include CDN-specific cache status headers (e.g., X-Cache: HIT/MISS). The entire round-trip from Singapore to London adds ~150-200ms on a miss; a hit takes ~5-10ms.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between a CDN edge server and an origin server?
02
Does using a CDN mean my website is always faster?
03
How does a CDN know which edge server is 'closest' to a user?
04
Can I use a CDN to protect against DDoS attacks?
🔥

That's Components. Mark it forged?

6 min read · try the examples if you haven't

Previous
Caching Strategies
4 / 18 · Components
Next
Message Queues