CDN Components Explained — How Content Delivery Networks Actually Work
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 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 } }
# 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.
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 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); }
# 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)
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.
#!/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."
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.
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 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');
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
| Aspect | Pull CDN | Push CDN |
|---|---|---|
| How content reaches edge | On-demand: edge fetches from origin on first request per PoP | Proactively: you upload to CDN storage, it propagates to all PoPs |
| First-user latency | Higher — first user in each region triggers a cache miss to origin | Zero — content pre-positioned before first request |
| Setup complexity | Low — point DNS to CDN, set Cache-Control headers, done | Higher — requires upload pipeline integration and storage management |
| Storage cost | Only caches what users actually request | You pay for storage of all pushed assets at all PoPs |
| Best for | Websites, APIs, unpredictable traffic, long-tail assets | Large files, known launch events, video streams, software downloads |
| Cache invalidation | Wait for TTL or call purge API | Re-upload new file + call purge API or use versioned paths |
| Real-world examples | Cloudflare, Fastly default mode, AWS CloudFront | Bunny.net storage, KeyCDN, AWS CloudFront with S3 origin |
| Origin server load | Spiky — bursts on cache misses across many PoPs simultaneously | Minimal — 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.
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.