system design

CDN Explained: How Content Delivery Networks Work (2026)

A CDN serves content from servers near your users — cutting latency from 300ms to 20ms. Covers edge nodes, cache headers, Cloudflare vs CloudFront, and DDoS protection.

By Akash Sharma·20 min read
#cdn
#system design
#performance
#caching
#cloudflare
#web
#infrastructure
#security

A user in Tokyo requests your website. Your servers are in Virginia. The data has to travel 10,000 km — even at the speed of light through fiber, that's roughly 100ms for the round trip. Add DNS lookups, TCP handshakes, and TLS negotiation: your page takes 400ms to load.

Another user in New York gets it in 40ms.

A Content Delivery Network (CDN) is a globally distributed network of servers that caches and delivers your content from locations physically close to each user, eliminating most of that cross-continental latency. Instead of every user hitting your origin server in Virginia, they hit a nearby edge server — one in Tokyo, one in London, one in São Paulo — that already has a cached copy.

CDNs fix the physics problem of distance. No amount of code optimization overcomes the speed of light.

What Is a CDN and How Does It Work?

A Content Delivery Network is a network of servers (called edge servers or PoPs — Points of Presence) placed in data centers around the world. When a user requests content, the CDN routes them to the nearest edge server instead of your origin server.

plaintext
Without CDN:
Tokyo user → Virginia origin → ~300ms
 
With CDN:
Tokyo user → Tokyo edge server → ~20ms

         (cached copy of your content)

The edge server stores a cached copy of your content. Your origin server only gets called when the cache is empty or expired — a cache miss. Every subsequent request from that region hits the cache — a cache hit. The goal is to make cache misses rare.

This works because most content doesn't change per-user. A JavaScript bundle is the same for everyone. A product image is the same for everyone. A blog post HTML is the same for everyone. CDNs exploit that fact by serving one cached copy to millions of users globally.

CDN Architecture: Edge Nodes and PoPs

Points of Presence (PoPs) are the physical locations where CDN servers live. Major CDN providers operate hundreds of them: Cloudflare runs in 330+ cities across 120+ countries. Each PoP contains multiple edge servers — redundant machines that share the cache for that location.

How Requests Get Routed to the Nearest Edge

CDNs use anycast routing to send each user to the nearest PoP automatically. With anycast, every PoP announces the same IP address to the internet. When your DNS resolves cdn.example.com, BGP (Border Gateway Protocol) routes the request to whichever PoP is topologically closest — not geographically closest, but closest in terms of network hops and latency.

plaintext
User in Singapore

    ▼ DNS resolves cdn.example.com

  BGP routing: which PoP responds fastest?

    ├── Singapore PoP (5ms) ← WINNER
    ├── Hong Kong PoP (18ms)
    └── Tokyo PoP (70ms)

The user gets the Singapore PoP. This routing is transparent — the user doesn't configure anything. It happens at the network layer before a single HTTP byte is sent.

Origin Pull vs Origin Push

CDNs populate their cache in two ways:

Origin pull (most common): On the first request for content, the edge server fetches it from your origin, caches it, and serves it. Subsequent requests hit the cache. You don't pre-populate anything — the CDN fills itself lazily as users request content.

Origin push: You proactively upload content to CDN storage (like Cloudflare R2 or AWS S3 as a CloudFront origin). Good for large files, video, or assets where you want zero cache misses from day one.

Most web apps use origin pull for dynamic content and origin push for large static assets like video files.

CDN Caching: Cache-Control Headers

You control CDN caching behavior through HTTP response headers. Understanding these is the difference between a CDN that actually helps and one that's a transparent proxy doing nothing.

Cache-Control Directives

python
# FastAPI example — cache headers per route type
 
@app.get("/api/products")
def get_products(response: Response):
    response.headers["Cache-Control"] = "public, max-age=300, s-maxage=3600"
    # max-age=300: browsers cache for 5 minutes
    # s-maxage=3600: CDN caches for 1 hour (overrides max-age for shared caches)
    # public: safe to cache in CDN (not user-specific)
    return {"products": [{"id": 1, "name": "Widget", "price": 29.99}]}
 
@app.get("/api/user/profile")
def get_profile(response: Response):
    response.headers["Cache-Control"] = "private, no-store"
    # private: browser may cache, but CDN must not
    # no-store: don't persist anywhere (most restrictive)
    return {"user": {"id": 42, "name": "Alice"}}
 
@app.get("/static/config.json")
def get_config(response: Response):
    response.headers["Cache-Control"] = "public, max-age=86400, stale-while-revalidate=3600"
    # stale-while-revalidate: serve stale content for 1 hour while fetching fresh
    # Eliminates cache-miss latency spikes during revalidation
    return {"feature_flags": {"dark_mode": True}}

Key directives:

  • public — CDN can cache this response
  • private — CDN must not cache; browser may
  • no-store — cache nothing, anywhere
  • max-age=N — browser caches for N seconds
  • s-maxage=N — CDN caches for N seconds; overrides max-age for shared caches while browser still uses max-age
  • stale-while-revalidate=N — serve stale content while fetching fresh in background

Surrogate-Control and CDN-Specific Headers

Some CDNs support headers beyond the HTTP spec:

http
# Fastly and Varnish — overrides Cache-Control for CDN only
Surrogate-Control: max-age=86400
 
# Cloudflare CDN-Cache-Control (ignores browsers entirely)
CDN-Cache-Control: max-age=3600

These let you cache content in the CDN for a long time while telling browsers to revalidate frequently — useful for content that updates infrequently but where you want users to always get the latest version from the browser.

The Vary Header

Vary tells the CDN to store separate cache entries for different request header values:

http
Vary: Accept-Encoding

This creates one cache entry for gzip-encoded responses and another for brotli. Essential for compressed assets.

http
Vary: Accept-Language

This creates separate cache entries per language — but be careful. Vary: Cookie or Vary: Authorization will effectively disable caching because every user has different cookie/auth values. This is a common CDN performance mistake.

Cache Invalidation

Content changes. You have three tools:

Versioned URLs — Add a content hash to asset filenames. When content changes, the URL changes. The CDN caches the old URL forever and the new URL gets freshly cached:

html
<!-- Busts cache automatically on deploy — CDN caches each version forever -->
<script src="/app.a3f8c912.js"></script>
<link rel="stylesheet" href="/styles.d94b221a.css">

Webpack, Vite, and Next.js do this automatically with content hashing. Set Cache-Control: public, max-age=31536000, immutable for these — they never change, so cache them for a year.

API purge — For content where URLs don't change but content does, use the CDN's purge API:

bash
# Cloudflare: purge specific URLs after a CMS publish
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"files": ["https://example.com/api/products", "https://example.com/blog/"]}'
 
# Purge by cache tag (Cloudflare Enterprise / Fastly)
curl -X POST ".../purge_cache" \
  -d '{"tags": ["product-catalog", "homepage"]}'

Cache tags let you group related content and purge it all at once — a Fastly Instant Purge completes in under 150ms globally.

Short TTLs — For content that changes frequently, use a short s-maxage (60–300 seconds). Slightly stale but never catastrophically old. Simpler than manual purging.

What to CDN and What Not To

Not everything benefits from CDN caching. Getting this wrong either wastes origin requests or, worse, serves the wrong user's data to someone else.

Cache These

Content TypeWhyRecommended TTL
JS/CSS bundles (hashed)Same for all users, never change1 year (immutable)
Images, fonts, videosBinary, same for all users7–30 days
Static HTML (JAMstack)Same for all users until deploy5 min–1 hour + purge on deploy
Public API responsesProduct listings, prices, public data1–60 minutes
OpenGraph imagesGenerated per-page, not per-user24 hours

Do Not Cache These

Content TypeWhy Not
Authenticated endpointsUser A's data must not go to user B
Shopping cart / session dataPer-user, changes frequently
Payment and checkout flowsSecurity-sensitive, must be fresh
Real-time data (stock prices, live scores)Stale data is wrong data
WebSocket connectionsCDNs don't proxy WebSocket streams
Write endpoints (POST/PUT/DELETE)Side effects must reach origin

Sometimes Cache These (With Care)

APIs with a Vary key: If your API response varies by language or region but not by user identity, you can cache it. Use Cache-Control: public, s-maxage=300 and Vary: Accept-Language.

Search results: If search results are the same for all users (e-commerce catalog search), cache short-TTL (30–60 seconds). If personalized, don't.

Edge Side Includes (ESI): Some CDNs (Varnish, Fastly) support ESI — a markup that lets you cache most of a page but dynamically insert user-specific fragments. A cached product page with a dynamically inserted "Welcome back, Alice" header. Complex to implement but powerful.

CDN Providers Compared

ProviderBest ForPricing ModelPoPsKey Differentiator
CloudflareMost web apps, DDoS protectionFree tier; paid from $20/mo330+ citiesWorkers edge compute, Zero Trust, free SSL, largest DDoS mitigation network
AWS CloudFrontAWS-native apps (S3, EC2, ALB)Pay-per-GB ($0.0085–$0.12/GB by region)600+ PoPsTight AWS integration, Lambda@Edge, Origin Shield
FastlyHigh-traffic media, APIs needing fast purgePay-per-GB; custom enterprise90+ PoPsInstant Purge (under 150ms), VCL programmability, real-time log streaming
AkamaiEnterprise, media streaming, high complianceEnterprise contracts4,000+ PoPsOldest CDN (1998), largest edge footprint, dominant in video/media streaming
Bunny CDNCost-sensitive projectsFrom $0.005/GB120+ PoPsCheapest major CDN, simple pricing, no minimums

Which to choose:

  • Starting out or general web app: Cloudflare free tier covers most use cases, including DDoS protection, WAF, and SSL.
  • Already on AWS: CloudFront integrates natively with S3, ALB, and Lambda. No cross-cloud egress fees.
  • High-traffic APIs or media: Fastly's real-time purge and VCL programmability give fine-grained control.
  • Enterprise / compliance / streaming: Akamai's footprint and SLA track record dominate in regulated industries.
  • Budget-first: Bunny CDN at $0.005/GB is roughly 10x cheaper than CloudFront for pure bandwidth.

Note: Vercel's Edge Network is built-in for Vercel deployments and requires no configuration — but it's not a standalone CDN you can use independently.

CDN for APIs and Dynamic Content

CDNs aren't just for static assets. A significant performance win comes from caching API responses, if you do it carefully.

Caching API Responses

Public, read-heavy API endpoints are excellent CDN candidates:

python
@app.get("/api/products")
def list_products(category: str = None, response: Response = None):
    # Cache public product listings for 5 minutes at CDN
    response.headers["Cache-Control"] = "public, s-maxage=300, stale-while-revalidate=60"
    return db.query_products(category=category)

Five minutes of CDN caching means your database sees 1 query per PoP per 5 minutes instead of thousands of queries per second. For a product catalog with 300 edge nodes, that's a 99.9%+ reduction in database load for read traffic.

Cache Keys and Query Strings

By default, CDNs treat ?page=1 and ?page=2 as separate cache keys — correct behavior. But watch for query parameters that don't affect content:

plaintext
/api/products?category=shoes&utm_source=email
/api/products?category=shoes&utm_medium=social

These are the same response but different cache keys, fragmenting your cache. Configure your CDN to strip marketing parameters from the cache key:

plaintext
# Cloudflare Cache Rules — ignore utm_* params for cache key
Cache key: URL path + relevant query params only
Strip: utm_source, utm_medium, utm_campaign, utm_content, fbclid, gclid

Vary Headers for API Personalization

If your API returns different responses based on Accept-Language:

http
Cache-Control: public, s-maxage=3600
Vary: Accept-Language

The CDN caches one copy per language code. This is safe and effective. Never use Vary: Cookie or Vary: Authorization on public endpoints — these defeat caching entirely.

What About Personalized API Responses?

If an endpoint returns user-specific data, set Cache-Control: private, no-store. Full stop. No exceptions. Serving user A's cart to user B is a data breach.

For endpoints that mix public and private data, split them: one public endpoint (CDN-cacheable) and one authenticated endpoint (never cached).

CDN Security

Modern CDNs are not just performance tools — they sit at your network perimeter and absorb a significant portion of security threats before they reach your infrastructure.

DDoS Mitigation

A distributed denial-of-service attack floods your servers with traffic to make them unreachable. A CDN absorbs this at the edge:

  • Cloudflare's network has a capacity of over 280 Tbps — more than enough to absorb most DDoS attacks before they reach your origin
  • Traffic from attack sources gets dropped at the edge PoP, never forwarded to your servers
  • Your origin server only sees legitimate, CDN-filtered traffic

Without a CDN, a 10 Gbps DDoS attack can saturate your datacenter uplink. With Cloudflare in front, the same attack is distributed across 330 cities and absorbed.

Web Application Firewall (WAF)

CDN WAFs inspect HTTP traffic for attack patterns:

  • SQL injection: ' OR 1=1 -- in query parameters
  • XSS: <script> tags in form inputs
  • Path traversal: ../../etc/passwd in URL paths
  • Log4Shell, Spring4Shell: Known CVE payload patterns
  • Bot signatures: Automated scanners, credential stuffers

Cloudflare's WAF uses rules updated by their threat intelligence team. When a new CVE drops, WAF rules protect your app within hours — before you've even patched the vulnerable library.

SSL/TLS Termination at the Edge

With a CDN:

plaintext
Browser ──[HTTPS]──▶ CDN Edge ──[HTTP or HTTPS]──▶ Your Origin

TLS negotiation (which adds 1–2 round trips) happens between the user and the nearest edge server — 20ms away instead of 300ms. Your origin can receive plain HTTP internally over a private network, simplifying your infrastructure. Or use HTTPS between CDN and origin for end-to-end encryption.

CDNs also handle certificate provisioning and renewal automatically. Cloudflare issues free Universal SSL certificates. You never touch a certificate.

Bot Protection

CDNs distinguish humans from bots using:

  • Browser fingerprinting (JavaScript challenges)
  • CAPTCHAs for suspicious traffic patterns
  • Rate limiting by IP, ASN, or user agent
  • Known bad IP reputation lists

This stops credential stuffing, content scraping, and inventory hoarding (a major problem for e-commerce) at the edge without touching your application code.

Origin IP Protection

By routing all traffic through a CDN, your origin server's real IP address is hidden from the public internet. If an attacker discovers your origin IP and attacks it directly, they bypass the CDN. Protect against this:

  1. Block all inbound traffic except from CDN IP ranges
  2. Use a shared secret header that CDN adds and your origin validates
nginx
# Nginx: only accept requests with Cloudflare's secret header
if ($http_cf_connecting_ip = "") {
    return 403;
}

Debugging CDN Issues

When caching behaves unexpectedly, these tools and techniques let you diagnose it quickly.

Reading Cache Response Headers

Every CDN-served response includes headers that tell you what happened:

bash
curl -I https://example.com/api/products
 
# Cloudflare response headers
CF-Cache-Status: HIT          # Served from Cloudflare cache
CF-Ray: 89ab12cd34ef5678-SIN  # Which edge server responded (SIN = Singapore)
Age: 187                       # Seconds since this entry was cached
Cache-Control: public, max-age=300, s-maxage=3600
 
# AWS CloudFront headers
X-Cache: Hit from cloudfront
Via: 1.1 abc123.cloudfront.net (CloudFront)
 
# Fastly headers
X-Cache: HIT
X-Cache-Hits: 3               # Times served from this PoP's cache
Fastly-Debug-Path: (D cache-sin18821-SIN 1719000000.000)

CF-Cache-Status values:

  • HIT — served from edge cache
  • MISS — not in cache, fetched from origin
  • EXPIRED — was cached but TTL elapsed, re-fetched from origin
  • BYPASS — caching explicitly disabled for this request
  • DYNAMIC — Cloudflare determined content is dynamic, not cached
  • REVALIDATED — served stale while revalidating in background

Testing from Multiple Regions

Your local cache state is not the global cache state. Test from different regions to verify edge caching:

bash
# Use a hosted curl service or VPN to test from specific regions
# Or use Cloudflare's diagnostic tool: example.com/cdn-cgi/trace
 
curl https://example.com/cdn-cgi/trace
# Returns: ip, loc (country), colo (which Cloudflare datacenter you hit)

A MISS in Singapore doesn't mean a MISS in Frankfurt — each PoP maintains its own cache. First request to each PoP always misses; subsequent requests from users in that region hit.

Purging Cache During Debugging

bash
# Cloudflare: purge a single URL to force fresh fetch
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"files": ["https://example.com/api/products"]}'
 
# Verify: next request should show CF-Cache-Status: MISS then HIT

Common CDN Debugging Scenarios

Problem: Cache-Control: private set but CDN is still caching Check: Is CDN-Cache-Control or Surrogate-Control overriding it? Some CDNs have page rules that override headers.

Problem: Low cache hit rate despite correct headers Check: Query string fragmentation. Run curl -I "https://example.com/api/products?utm_source=x" — if the CDN isn't stripping tracking params, each UTM variant is a separate cache miss.

Problem: Users getting stale content after deploy Check: Are you purging cache on deploy? Does your CI/CD pipeline call the purge API after deployment? If using versioned URLs for JS/CSS, only HTML pages need purging.

Problem: CF-Cache-Status is always DYNAMIC Check: Cloudflare's automatic detection marked the content as dynamic. Add an explicit Cache Rule in the Cloudflare dashboard to override for that URL pattern.

Cache Hit Rate: The Key Metric

A healthy CDN setup has a cache hit rate above 90%. This means:

  • 90%+ of requests are served from edge, not origin
  • Origin gets 10x less traffic
  • Users worldwide get consistent sub-50ms response times

Monitor cache hit rate in your CDN dashboard. If it drops:

SymptomLikely CauseFix
Hit rate < 50%Query string fragmentationStrip non-functional params from cache key
Hit rate < 70%TTL too shortIncrease s-maxage
Hit rate drops after deployNo cache purge on deployAdd purge step to CI/CD
Hit rate 0%Cache-Control: private on all routesAudit response headers

Frequently Asked Questions

What is a CDN and how does it improve performance?

A CDN (Content Delivery Network) is a network of geographically distributed servers that caches copies of your content close to users. Instead of every request traveling to your origin server (potentially across continents), users are served from a nearby edge server. This reduces latency from hundreds of milliseconds to under 20ms for cached content, reduces load on your origin server, and provides redundancy if your origin goes down.

How does a CDN decide where to cache content?

CDNs use anycast routing at the DNS layer — every PoP announces the same IP address, and BGP routes each user to the topologically nearest PoP. Content is cached using origin-pull: the first user to request content from a given region triggers a fetch from your origin; that response is cached at the edge. Subsequent users in that region get the cached copy. Cache duration is controlled by your Cache-Control response headers, specifically s-maxage for CDN TTL.

What is the difference between Cloudflare and AWS CloudFront?

Cloudflare is a standalone CDN and security platform that works with any hosting provider. It offers a generous free tier, includes DDoS protection and WAF at all paid tiers, and provides Cloudflare Workers for edge computing. AWS CloudFront is AWS-native — it integrates seamlessly with S3, EC2, ALB, and Lambda@Edge, and has no cross-service egress fees within AWS. Choose Cloudflare for general web apps or if you want the free tier; choose CloudFront if you're already deep in the AWS ecosystem and want tight integration.

Should I put my API behind a CDN?

Yes, for public read endpoints. Product listings, blog posts, price feeds, and public search results are all good CDN candidates — set Cache-Control: public, s-maxage=60 and your database load drops dramatically. Never cache authenticated or user-specific endpoints (Cache-Control: private, no-store). The risk is serving user A's data to user B, which is a data breach. A common pattern is to split one endpoint into a public cached version and an authenticated uncached version.

How do I purge cached content from a CDN?

Use the CDN's purge API after publishing changes. Cloudflare, CloudFront, and Fastly all provide REST APIs for this. Trigger purges from your CI/CD pipeline on deploy. For static assets, use content-hashed filenames (app.a3f8c912.js) instead of purging — when content changes, the filename changes, and the CDN caches both versions indefinitely. Cache tags (available on Cloudflare Enterprise and Fastly) let you group related content and purge it all with a single API call, which is useful for CMS-driven sites.

What CDN headers should I set for optimal caching?

For static assets with hashed filenames: Cache-Control: public, max-age=31536000, immutable. For public API responses: Cache-Control: public, s-maxage=300, stale-while-revalidate=60. For user-specific data: Cache-Control: private, no-store. The key distinction is s-maxage (CDN TTL) vs max-age (browser TTL) — s-maxage overrides max-age for shared caches like CDNs, so you can cache long at the CDN while making browsers revalidate more often. Avoid Vary: Cookie or Vary: Authorization — these fragment the cache and effectively disable CDN caching.

Can a CDN protect against DDoS attacks?

Yes. DDoS mitigation is one of the primary reasons many teams use a CDN beyond performance. Cloudflare's network capacity exceeds 280 Tbps, enough to absorb virtually any volumetric attack. Traffic from identified attack sources is dropped at the edge PoP closest to the attacker — it never reaches your origin. CDN WAFs also block application-layer attacks (Layer 7 DDoS) that mimic legitimate traffic patterns. However, your origin IP must be protected too: if attackers discover your real server IP, they can bypass the CDN and attack it directly. Restrict inbound traffic to CDN IP ranges only.


Key Takeaways

  • CDNs serve content from edge servers near users, reducing latency from hundreds of milliseconds to under 20ms
  • Anycast routing automatically directs each user to the nearest PoP — transparent to users and application code
  • s-maxage controls CDN cache TTL; it overrides max-age for shared caches while browsers still use max-age
  • Cache static assets with hashed filenames (1-year TTL, immutable); cache public API responses with short TTLs (60–300 seconds)
  • Never cache authenticated or user-specific responses — use Cache-Control: private, no-store
  • Modern CDNs also provide DDoS mitigation (280+ Tbps capacity), WAF, SSL termination, and edge computing
  • Target >90% cache hit rate — if lower, check query string fragmentation, TTL values, and response headers
  • CF-Cache-Status: HIT/MISS headers tell you exactly what the CDN did with each request

A CDN is one of the highest-ROI infrastructure changes you can make. It reduces latency, cuts origin load, and adds a security layer — often for free on Cloudflare's base tier.

Related reading: Load Balancing Strategies · Rate Limiting Your API · Redis Caching Explained

Enjoyed this article?

Get weekly insights on backend architecture, system design, and Go programming.