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.
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.
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.
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
# 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 responseprivate— CDN must not cache; browser mayno-store— cache nothing, anywheremax-age=N— browser caches for N secondss-maxage=N— CDN caches for N seconds; overridesmax-agefor shared caches while browser still usesmax-agestale-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:
# 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=3600These 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:
Vary: Accept-EncodingThis creates one cache entry for gzip-encoded responses and another for brotli. Essential for compressed assets.
Vary: Accept-LanguageThis 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:
<!-- 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:
# 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 Type | Why | Recommended TTL |
|---|---|---|
| JS/CSS bundles (hashed) | Same for all users, never change | 1 year (immutable) |
| Images, fonts, videos | Binary, same for all users | 7–30 days |
| Static HTML (JAMstack) | Same for all users until deploy | 5 min–1 hour + purge on deploy |
| Public API responses | Product listings, prices, public data | 1–60 minutes |
| OpenGraph images | Generated per-page, not per-user | 24 hours |
Do Not Cache These
| Content Type | Why Not |
|---|---|
| Authenticated endpoints | User A's data must not go to user B |
| Shopping cart / session data | Per-user, changes frequently |
| Payment and checkout flows | Security-sensitive, must be fresh |
| Real-time data (stock prices, live scores) | Stale data is wrong data |
| WebSocket connections | CDNs 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
| Provider | Best For | Pricing Model | PoPs | Key Differentiator |
|---|---|---|---|---|
| Cloudflare | Most web apps, DDoS protection | Free tier; paid from $20/mo | 330+ cities | Workers edge compute, Zero Trust, free SSL, largest DDoS mitigation network |
| AWS CloudFront | AWS-native apps (S3, EC2, ALB) | Pay-per-GB ($0.0085–$0.12/GB by region) | 600+ PoPs | Tight AWS integration, Lambda@Edge, Origin Shield |
| Fastly | High-traffic media, APIs needing fast purge | Pay-per-GB; custom enterprise | 90+ PoPs | Instant Purge (under 150ms), VCL programmability, real-time log streaming |
| Akamai | Enterprise, media streaming, high compliance | Enterprise contracts | 4,000+ PoPs | Oldest CDN (1998), largest edge footprint, dominant in video/media streaming |
| Bunny CDN | Cost-sensitive projects | From $0.005/GB | 120+ PoPs | Cheapest 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:
@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:
/api/products?category=shoes&utm_source=email
/api/products?category=shoes&utm_medium=socialThese are the same response but different cache keys, fragmenting your cache. Configure your CDN to strip marketing parameters from the cache key:
# 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, gclidVary Headers for API Personalization
If your API returns different responses based on Accept-Language:
Cache-Control: public, s-maxage=3600
Vary: Accept-LanguageThe 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/passwdin 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:
Browser ──[HTTPS]──▶ CDN Edge ──[HTTP or HTTPS]──▶ Your OriginTLS 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:
- Block all inbound traffic except from CDN IP ranges
- Use a shared secret header that CDN adds and your origin validates
# 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:
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 cacheMISS— not in cache, fetched from originEXPIRED— was cached but TTL elapsed, re-fetched from originBYPASS— caching explicitly disabled for this requestDYNAMIC— Cloudflare determined content is dynamic, not cachedREVALIDATED— 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:
# 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
# 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 HITCommon 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:
| Symptom | Likely Cause | Fix |
|---|---|---|
| Hit rate < 50% | Query string fragmentation | Strip non-functional params from cache key |
| Hit rate < 70% | TTL too short | Increase s-maxage |
| Hit rate drops after deploy | No cache purge on deploy | Add purge step to CI/CD |
| Hit rate 0% | Cache-Control: private on all routes | Audit 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-maxagecontrols CDN cache TTL; it overridesmax-agefor shared caches while browsers still usemax-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/MISSheaders 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.
Related Posts
Continue reading with these related posts
DNS (Domain Name System) Explained: How It Works, Record Types, Security, and Cloud
Complete guide to how DNS works: resolution process, record types, TTL, DNSSEC, DNS over HTTPS, Route 53, Cloudflare, troubleshooting with dig, and common DNS mistakes engineers make.
Caching Strategies Explained: Cache-Aside, Write-Through, Read-Through & Write-Behind (2026)
Cache-aside, read-through, write-through, write-behind — when to use each caching strategy, with Python and Node.js implementations, invalidation patterns, and cache hit-ratio metrics.
Redis Caching Explained: How It Works, Patterns, and Code Examples
Redis is an in-memory data store used as a cache to reduce database load and latency. Learn cache-aside, TTL, stampede prevention, eviction policies, and complete Python/Node.js code examples.