CDN Caching Beginner

CDN Cache Policy Design: Rules, TTLs, and Content Classification

PS
Priya Sharma — Content Delivery Specialist
••11 min read•Updated Sep 3, 2026

A CDN cache policy defines which content to cache, where to cache it, and for how long using classification rules and TTLs. An effective policy categorizes assets by change frequency, sets tiered TTLs from seconds to months, and applies Cache-Control directives to maximize hit ratios above 90% while guaranteeing content freshness and reducing origin load.

Designing a cache policy without a systematic framework is the fastest way to create stale content, cache poisoning, or an origin that collapses under load. In production environments serving millions of requests per day, I have seen a single overly-broad Cache-Control: public, max-age=86400 rule accidentally cache personalized cart responses, while an overly-conservative no-store default drove origin CPU to 85% during a flash sale. A well-architected policy prevents both extremes by making caching intentional rather than accidental.

This guide provides a beginner-friendly but technically precise framework for building CDN cache policies from scratch. You will learn how to classify every content type on your site, choose Time-To-Live (TTL) values that balance freshness and performance, write enforceable cache rules at the edge, and validate them with real monitoring. The approach works across Cloudflare, Fastly, Akamai, AWS CloudFront, and any standards-compliant CDN because it is grounded in HTTP caching semantics, not vendor-specific magic.

How Should You Classify Content for CDN Caching?

Content classification is the foundation of every cache policy, because different content types have fundamentally different freshness requirements and cacheability. A classification exercise forces you to inventory every URL pattern, API endpoint, and asset type, then assign it to a caching tier based on how often it changes, whether it is personalized, and how expensive it is to generate. Without this taxonomy, you cannot write deterministic rules.

Immutable and Versioned Static Assets

Immutable static assets are files that never change without changing their URL, and they are the most cache-friendly content on your site. Examples include hashed JavaScript bundles like app.a3f8c1.js, versioned CSS, fonts, and images with content hashes or version query strings. Because the URL itself acts as a cache key version, these assets can be cached for a year at both the edge and the browser with complete safety.

For immutable assets, set Cache-Control: public, max-age=31536000, immutable at the origin and configure the CDN to honor it with an edge TTL of one year. The immutable directive tells browsers not to revalidate during the TTL, eliminating conditional requests and improving Core Web Vitals like Largest Contentful Paint. This pattern is why modern build tools hash filenames — it decouples deployment frequency from cache efficiency.

Semi-Static and Periodically Updated Content

Semi-static content changes on a predictable schedule but is identical for all users, making it ideal for short-to-medium TTLs with background revalidation. This category includes product catalog images without hashes, marketing pages, blog posts, category listings, and site-wide JSON configuration files. The content is public and cacheable, but you must tolerate a bounded staleness window.

A practical approach for semi-static content is to use a 1-hour edge TTL with stale-while-revalidate and stale-if-error to hide origin latency from users. For example, a blog post can be served stale for 10 minutes while the edge asynchronously refreshes it, ensuring visitors never wait for origin. If your CMS publishes on a schedule, complement TTLs with precise purge calls via cache invalidation webhooks, so updates propagate in seconds rather than hours.

Dynamic, Personalized, and Private Content

Dynamic and personalized content must be handled with the most restrictive rules, because caching it incorrectly leaks private data between users. This includes authenticated HTML, shopping carts, account dashboards, checkout APIs, and any response with Set-Cookie, Authorization headers, or user-specific JSON. The default for this tier should be Cache-Control: private, no-store or no-cache unless you have explicitly designed a safe cache key.

There are exceptions where dynamic content can be cached safely using key segmentation. For example, an API endpoint like /api/recommendations can be cached for anonymous users with a cache key that includes Accept-Language and geo, but must bypass cache when a session cookie is present. Similarly, GraphQL responses can be cached at the edge if you normalize the query and include the viewer ID in the cache key, otherwise you risk serving one user's data to another. Always audit Vary headers and CDN cache-key settings for this tier.

API and Edge-Computable Responses

API responses occupy a spectrum from highly cacheable to never-cacheable, and classification must consider both volatility and computational cost. Read-heavy, public APIs such as product search, inventory availability, or weather data benefit enormously from even a 5–30 second edge TTL, which can collapse thousands of origin requests per second into one. This micro-caching pattern is essential for performance and origin protection during traffic spikes.

For APIs, use Cache-Control: public, max-age=10, s-maxage=30, stale-while-revalidate=60 combined with surrogate keys for efficient purging. A product API can tag responses with Surrogate-Key: product-123 category-shoes, allowing a single purge to invalidate all related cache entries when inventory changes. If you use CDN fundamentals like edge compute, you can also cache POST responses or assemble personalized fragments at the edge, but only after rigorous key design.

Expert tip: Start classification by exporting your top 100 URL patterns from access logs and labeling each with: public/private, change frequency, personalization, and origin cost. This 30-minute exercise catches 90% of policy errors before you write a single rule.

How Do You Design TTLs and Cache Rules That Balance Freshness and Performance?

TTL design is the act of translating content classification into concrete time values and HTTP directives that govern how long a response lives at each cache layer. A well-designed TTL hierarchy uses short browser TTLs for control, longer edge TTLs for hit ratio, and grace periods for resilience, ensuring users see fresh content without overwhelming your origin. The goal is not the longest possible TTL, but the longest safe TTL for each class.

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.99% (multi-PoP)99.9% (single DC)10x fewer outages

Understanding TTL Layers: Browser, Edge, and Origin Shield

Every CDN cache policy manages at least three TTL layers, and conflating them is a common source of bugs. The browser TTL (max-age) controls how long a user's browser serves from local cache without contacting the CDN. The edge TTL (s-maxage or CDN override) controls how long the PoP keeps the object. The shield or tiered-cache TTL controls how long a regional parent cache retains it. You can tune each independently for optimal behavior.

A proven pattern is to keep browser TTLs shorter than edge TTLs for HTML and API responses, so you retain the ability to purge or update at the edge without waiting for browsers to expire. For example, set max-age=60, s-maxage=300 for a product listing page: browsers revalidate after one minute, but the edge serves hits for five minutes, cutting origin requests by 80%. This distinction is critical when you reason about DNS and edge routing, because a user may hit different PoPs on subsequent requests.

Core Cache-Control Directives You Must Master

HTTP Cache-Control directives are the language your origin uses to instruct CDNs and browsers, and misusing them silently breaks policies. The essential directives are public vs private (who may cache), max-age and s-maxage (how long), no-store and no-cache (do not cache or must revalidate), immutable (never revalidate during TTL), and stale-while-revalidate / stale-if-error (serve stale during refresh or origin failure).

For CDN cache policy design, prefer explicit s-maxage for edge control and use Vary sparingly, because Vary: * or broad Vary: Cookie fragments cache and destroys hit ratio. Instead, configure the CDN to include only the necessary request attributes in the cache key via custom cache-key rules. Also, never rely on Expires alone — it is a fallback for HTTP/1.0 clients and is overridden by max-age when both are present.

Content ClassExamplesBrowser TTL (max-age)Edge TTL (s-maxage)Cacheability
Immutable
Hashed Static
app.abc123.js, style.9f2e.css, logo.woff21 year (31536000) + immutable1 yearpublic, immutable
Versioned Static/images/hero-v3.jpg, /fonts/inter.woff21 month (2592000)1 month – 1 yearpublic
Semi-StaticBlog posts, marketing pages, category HTML60 – 300s3600s (1h) + SWR 600spublic, must-revalidate
Dynamic CacheablePublic API, search, product feed0 – 10s10 – 60s + SIE 86400spublic, short TTL
Private
Personalized
Cart, account, checkout, auth HTML0 / no-storeBypass (0s)private, no-store

Designing Grace Periods for Resilience

Grace periods determine how a CDN behaves when content is stale or the origin is unavailable, and they directly impact availability during deploys and outages. stale-while-revalidate allows the edge to serve a slightly stale response while fetching a fresh copy in the background, hiding revalidation latency from the user. stale-if-error allows serving stale content when the origin returns 5xx, preventing a complete outage.

Configure grace periods proportionally to business tolerance for staleness. For semi-static pages, a stale-while-revalidate=600 (10 minutes) and stale-if-error=86400 (1 day) provides fast pages and resilience without risking long-term staleness. For critical pricing or inventory APIs, keep grace periods short — stale-while-revalidate=5 — to avoid overselling. These directives are your safety net and should be part of every cache policy, not an afterthought.

Cache Key and Vary Design

The cache key defines what makes a cached response unique, and an incorrect key either leaks data or fragments cache unnecessarily. By default, most CDNs key on host + path + query string, but you often need to include or exclude specific headers, cookies, or query parameters. A well-designed key includes only what changes the response body.

For example, include Accept-Encoding and Accept-Language for compressed and localized content, but strip marketing query parameters like utm_source from the key to avoid cache fragmentation. Never include Cookie in the key for public content, and always include it (or bypass cache) for private content. If you support Vary, ensure the CDN respects it or translates it into cache-key behavior, otherwise you will serve gzipped content to clients that cannot decode it.

How Do You Implement and Validate a Production Cache Policy?

Implementation translates TTL design into enforceable edge rules, origin headers, and purge workflows that operate reliably under real traffic. A production-ready policy is declarative, version-controlled, and observable — you can see hit ratios, origin load, and staleness in dashboards and roll back bad rules safely. Implementation without validation is just hope.

Writing Cache Rules at the Edge and Origin

Cache rules should be defined in two places: origin response headers as the source of truth, and CDN edge rules as an enforceable override and safety net. Origin headers ensure correct behavior even if CDN configuration is bypassed, while edge rules let you fix legacy applications without redeploying. This dual-layer approach is the most resilient pattern I recommend to teams.

At the origin, set precise headers per content class. At the edge, create ordered rules that match URL patterns and apply TTLs, cache-key modifications, and bypass conditions. Order matters — place specific bypass rules for /api/cart/* and /account/* before general cache rules for /*, otherwise a broad rule will incorrectly cache private paths. Always add a final default rule that sets a conservative TTL (e.g., 60s) rather than leaving behavior undefined.

Origin Headers — Nginx / Node.jsHTTP
# Nginx: immutable assets (hashed filenames)
location ~* \.[a-f0-9]{8}\.(js|css|woff2)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
  add_header Surrogate-Key "static-immutable";
}

# Semi-static HTML - short browser, longer edge
location = /blog/* {
  add_header Cache-Control "public, max-age=60, s-maxage=3600, stale-while-revalidate=600, stale-if-error=86400";
  add_header Surrogate-Key "blog";
  add_header Vary "Accept-Encoding";
}

# Private - never cache
location ~ ^/(cart|checkout|account)/ {
  add_header Cache-Control "private, no-store, max-age=0";
}
Edge Rules — Cloudflare / Fastly VCLEdge Config
// Cloudflare Cache Rule (dashboard or API)
{
  "description": "Bypass private paths",
  "expression": "(http.request.uri.path contains \"/cart\") or (http.cookie contains \"session\")",
  "action": "bypass_cache"
}
{
  "description": "Cache public API - 30s edge",
  "expression": "http.request.uri.path wildcard \"/api/products/*\"",
  "cache_ttl": { "mode": "override_origin", "ttl": 30 },
  "cache_key": { "custom_key": { "query_string": { "include": ["id", "lang"] } } }
}

// Fastly VCL - strip utm params from cache key
sub vcl_recv {
  if (req.url ~ "[?&]utm_") {
    set req.url = regsuball(req.url, "[?&]utm_[^&]+", "");
  }
  if (req.http.Cookie ~ "session=") {
    return(pass); // bypass cache for authenticated users
  }
}

Testing, Monitoring, and Purging

Every cache policy must be tested with real HTTP requests before and after deployment, because header typos and rule ordering errors are invisible until they cause incidents. Use curl -I and curl -H "Cache-Control: no-cache" to inspect Cache-Control, Age, X-Cache, and Surrogate-Key headers from multiple PoPs. Automate this with synthetic checks that assert TTLs per URL class in CI.

Monitoring should track edge hit ratio, origin request rate, cache status by content class, and time-to-first-byte. A healthy policy for a content-heavy site typically achieves 85–95% edge hit ratio; a sudden drop often indicates a bad deploy that added a cookie or query param to the cache key. Complement TTLs with a cache invalidation strategy using surrogate keys or tag-based purging, so you can purge product-123 across all endpoints in under 2 seconds. For cost optimization, higher hit ratios directly reduce origin egress and compute bills — a 10% hit ratio improvement can cut origin costs by 30% on image-heavy sites.

Validation — CLIbash
# Inspect cache headers from edge
curl -sI https://www.example.com/blog/caching-guide | grep -i -E "cache-control|age|x-cache|surrogate"

# Force revalidation and compare Age reset
curl -sI -H "Cache-Control: no-cache" https://www.example.com/api/products?id=123

# Purge by surrogate key (Fastly / Cloudflare Enterprise)
curl -X POST -H "Fastly-Key: $FASTLY_API_TOKEN" \
  https://api.fastly.com/service/$SERVICE_ID/purge/product-123

# Cloudflare purge by tag
curl -X POST https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache \
  -H "Authorization: Bearer $CF_TOKEN" \
  -d '{"tags": ["product-123"]}'

Common Pitfalls and How to Avoid Them

The most damaging cache policy errors stem from over-caching private data, under-caching public data, and ignoring query-string variance. Over-caching happens when a rule like Cache-Control: public, max-age=3600 is applied globally and captures /account responses that contain personal information. Under-caching happens when every HTML response is marked no-store out of fear, forcing the origin to render every page view. Both are avoidable with precise path matching.

Query-string handling deserves special attention, because CDNs treat /product?id=1 and /product?id=1&utm_source=twitter as different cache keys by default. This fragments cache and artificially lowers hit ratio, especially for marketing campaigns. Normalize keys by allow-listing only meaningful parameters like id and page at the edge. Finally, document your policy in version control and review it during code review — cache rules are infrastructure as code and should be treated with the same rigor as firewall rules.

Checklist before going live: 1) Verify private paths bypass cache with authenticated curl tests. 2) Confirm immutable assets have content hashes and 1-year TTLs. 3) Measure hit ratio per content class for 24 hours. 4) Test purge latency for surrogate keys. 5) Set alerts for origin 5xx and hit-ratio drops below 80%.

Frequently Asked Questions

What is the difference between max-age and s-maxage in a CDN cache policy?

max-age controls how long a browser caches a response, while s-maxage controls how long a shared cache like a CDN edge PoP keeps it. Using both lets you set a short browser TTL for quick client updates and a longer edge TTL for high hit ratios. If s-maxage is absent, CDNs fall back to max-age, so always set s-maxage explicitly for HTML and API responses.

How long should I cache HTML pages on a CDN?

Cache public HTML for 60 seconds to 1 hour at the edge depending on update frequency, with a 60-second browser TTL and stale-while-revalidate of 5–10 minutes. This balances freshness for editors with performance for readers. For news or e-commerce category pages that change frequently, prefer 60–300 seconds edge TTL plus surrogate-key purging on publish, rather than long TTLs alone.

Should I cache API responses at the edge?

Yes, cache public, read-heavy API responses even for 5–30 seconds to protect your origin during traffic spikes. This micro-caching collapses thousands of concurrent requests into one origin fetch. Only cache APIs that are safe to share across users, use a precise cache key that includes relevant query parameters, and bypass cache when authentication cookies or tokens are present.

How do I prevent caching personalized content by mistake?

Set Cache-Control: private, no-store on all authenticated or user-specific responses, and create edge bypass rules that match session cookies, Authorization headers, and private path patterns like /cart/* before any general cache rules. Test with two different user sessions via curl to verify that one user's response is never served to another, and monitor for accidental Set-Cookie on cacheable responses.

When should I use purge vs. short TTLs for content updates?

Use short TTLs (30–300 seconds) when slight staleness is acceptable and you want simplicity, and use purge with surrogate keys when you need updates to propagate in seconds, such as price or inventory changes. The best policies combine both: a moderate TTL for natural expiry plus instant purge on critical events. Avoid relying solely on long TTLs without a purge mechanism, as it leaves you unable to fix bad content quickly.

A thoughtful CDN cache policy is not a one-time configuration but a living system that evolves with your content and traffic. Start with clear classification, apply tiered TTLs with grace periods, enforce rules at both origin and edge, and validate continuously with headers and hit-ratio dashboards. When you get this right, users experience consistently fast pages, your origin stays resilient under load, and your team can deploy confidently knowing freshness is guaranteed.

References & Standards: Cloudflare documentation · RFC 7234