{"id":49,"date":"2026-08-01T01:51:00","date_gmt":"2026-07-31T22:51:00","guid":{"rendered":"https:\/\/john-nessime.com\/blog\/?p=49"},"modified":"2026-08-01T01:51:02","modified_gmt":"2026-07-31T22:51:02","slug":"cdn-setup-dynamic-content","status":"publish","type":"post","link":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/","title":{"rendered":"CDN Setup Without Breaking Dynamic Content"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Somebody puts a CDN in front of the site on a Friday. The homepage gets faster, the bill goes down, everyone is pleased. On Monday a customer emails to say they logged in and saw someone else&#8217;s name in the header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is the failure mode worth taking seriously. Not slowness, not downtime \u2014 the CDN cached a page that was personalised for one user and served it to everybody else. And you will not notice, because your browser holds a session and looks fine. The people seeing the broken version are the ones who never contact you.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Getting CDN setup right for a dynamic site is not about finding the right toggle in a dashboard. It is about deciding, per route, what is public and what is personal, and then making the origin say so clearly enough that no edge node has to guess.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Sort your routes before touching any config<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every URL on your site falls into one of three buckets. Write the list out \u2014 actually write it, this takes ten minutes and prevents the incident above.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Static assets.<\/strong> CSS, JS, images, fonts. Identical for everyone, content-addressed if your build is sensible. Cache these hard, for a year.<\/li>\n\n<li><strong>Public dynamic.<\/strong> Generated by the application but identical for every anonymous visitor. Blog posts, product pages, category listings. This is where the real performance win lives, and where most people leave everything uncached out of nervousness.<\/li>\n\n<li><strong>Private dynamic.<\/strong> Anything personalised or authenticated. Carts, dashboards, checkout, admin, most API responses. Never cached at a shared cache, no exceptions.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The middle bucket is the whole game. If you cache only the first bucket you have built an asset CDN and left the expensive work untouched. If you accidentally cache the third, you have a data leak.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The header that separates the buckets<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The single most useful thing to understand is the difference between <code>private<\/code> and <code>no-store<\/code>, because people use them interchangeably and they mean very different things.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Static assets with a content hash in the filename\nCache-Control: public, max-age=31536000, immutable\n\n# Public dynamic: edge caches it, browser revalidates\nCache-Control: public, s-maxage=300, max-age=0, must-revalidate\n\n# Personalised: browser may cache, shared caches must not\nCache-Control: private, no-store\n\n# Genuinely sensitive: nobody stores this anywhere\nCache-Control: no-store<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>private<\/code> means &#8220;a browser cache may keep this, a shared cache may not&#8221;. <code>no-store<\/code> means &#8220;do not write this to disk anywhere&#8221;. For an authenticated dashboard you want both. For a bank statement you want <code>no-store<\/code> alone.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The other key pair is <code>s-maxage<\/code> versus <code>max-age<\/code>. <code>s-maxage<\/code> applies only to shared caches \u2014 the CDN \u2014 and overrides <code>max-age<\/code> there. That lets you cache a product page at the edge for five minutes while telling browsers to revalidate every time, so a price change propagates in five minutes rather than living in someone&#8217;s browser for an hour.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Set caching headers at the origin, per route, and let the CDN follow them. Configuring cache rules only in the CDN dashboard means the policy lives somewhere your application code cannot see and your staging environment does not have.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">Vary, and how it quietly ruins your hit rate<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Vary<\/code> tells caches that the response depends on a request header, so a separate copy is stored per distinct value. Used precisely it is essential. Used carelessly it destroys your cache.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Fine. A handful of encodings, a handful of variants.\nVary: Accept-Encoding\n\n# Also fine if you genuinely serve different content per language\nVary: Accept-Encoding, Accept-Language<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now the one to avoid:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Effectively disables caching. Every browser build is a distinct\n# User-Agent string, so every visitor gets their own cache entry.\nVary: User-Agent<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you need device-specific responses, most CDNs expose a normalised device-type value with two or three possible values. Use that rather than the raw header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Vary: Cookie<\/code> deserves its own warning. It looks like a safe way to separate logged-in from anonymous traffic, and it is not \u2014 every visitor carries slightly different cookies from analytics and consent tools, so you get a near-unique cache key per person. The correct approach is to make the presence of a session cookie bypass the cache entirely, rather than varying on it.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Cookies are where sites actually break<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Two rules, and between them they prevent most CDN incidents.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rule one: a session cookie means bypass<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If a request arrives carrying your session cookie, it should go to origin uncached. Most CDNs let you express this as a cache rule; in nginx, if you are running your own edge, it is a variable:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>map $http_cookie $bypass_cache {\n    default                 0;\n    \"~*wordpress_logged_in\" 1;\n    \"~*woocommerce_items\"   1;\n    \"~*comment_author\"      1;\n    \"~*PHPSESSID\"           1;\n}\n\nlocation \/ {\n    proxy_cache            edge_cache;\n    proxy_cache_bypass     $bypass_cache;   # do not serve from cache\n    proxy_no_cache         $bypass_cache;   # do not write to cache\n    proxy_cache_valid      200 301 302 5m;\n    proxy_cache_use_stale  error timeout updating http_500 http_502 http_503 http_504;\n    proxy_cache_lock       on;\n\n    add_header X-Cache-Status $upstream_cache_status always;\n    proxy_pass http:\/\/origin;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You need <em>both<\/em> directives. <code>proxy_cache_bypass<\/code> stops a logged-in user being served a cached anonymous page. <code>proxy_no_cache<\/code> stops their personalised response being written into the cache for everyone else. Setting only the first is a common and dangerous mistake \u2014 the read path is protected while the write path is wide open.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That <code>X-Cache-Status<\/code> header is not decoration. It is how you verify any of this actually works.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rule two: strip cookies you do not need<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Analytics and consent tools set cookies on every visitor. If your CDN treats any cookie as a bypass signal, your anonymous traffic stops being cacheable the moment you add a tag manager. Configure the CDN to ignore everything except the specific cookies that matter, and be explicit about which those are.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Query strings, and the cache-key explosion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">By default many CDNs include the full query string in the cache key. Then a marketing campaign starts appending <code>utm_source<\/code>, <code>utm_medium<\/code>, <code>fbclid<\/code> and friends, and one page becomes thousands of cache entries that each miss exactly once.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Decide deliberately which parameters affect the response:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Include<\/strong> parameters that change the content \u2014 <code>?page=2<\/code>, <code>?sort=price<\/code>, <code>?category=boots<\/code>.<\/li>\n\n<li><strong>Ignore<\/strong> tracking parameters entirely. They never change what the server renders.<\/li>\n\n<li><strong>Normalise order<\/strong> where the CDN supports it, so <code>?a=1&amp;b=2<\/code> and <code>?b=2&amp;a=1<\/code> are one entry rather than two.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">An allowlist is safer than a blocklist here. New tracking parameters appear constantly; the set of parameters your application actually reads changes rarely.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Keeping dynamic pages cached without going stale<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The usual objection to caching public dynamic pages is that content changes. Two mechanisms solve this, and together they let you cache aggressively without serving stale content for long.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Stale-while-revalidate<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>Cache-Control: public, s-maxage=60, stale-while-revalidate=600, stale-if-error=86400<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Read that as three separate promises. Fresh for sixty seconds. For the following ten minutes, serve the stale copy immediately while fetching a fresh one in the background \u2014 so nobody waits. And if the origin is throwing errors, keep serving the stale copy for a day rather than showing a 502.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That last directive turns your CDN into a partial outage shield. It is one of the highest-value headers on this page and it costs nothing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Purge on change<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Time-based expiry is a blunt instrument. If your application knows when a product price changed, tell the CDN immediately rather than waiting for a TTL.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tag-based purging is the version worth building toward \u2014 tag responses by the entities they contain, then purge by tag when an entity changes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Origin response for a product page\nCache-Tag: product-1234, category-boots, layout-main<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Update product 1234 and you purge one tag, invalidating the product page, every category listing it appears on, and the homepage if it is featured \u2014 without knowing any of those URLs in advance. Header name and support vary by provider, so check what yours calls it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verifying it before you point DNS<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Test with curl, not a browser. Your browser has cookies, a warm local cache and an opinion; curl tells you the truth.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># What headers is the origin actually sending?\ncurl -sI https:\/\/example.com\/products\/boots | grep -i 'cache-control|vary|set-cookie'\n\n# Twice in a row: second request should show a HIT\ncurl -sI https:\/\/example.com\/products\/boots | grep -i 'x-cache|age'\ncurl -sI https:\/\/example.com\/products\/boots | grep -i 'x-cache|age'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now the test that matters most. Request an authenticated page with a real session cookie and confirm it never returns a cache hit:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -sI https:\/\/example.com\/account \n  -H \"Cookie: session=&lt;a-real-session-value&gt;\" \n  | grep -i 'x-cache|cache-control'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You want <code>BYPASS<\/code> or <code>MISS<\/code>, and <code>Cache-Control: private<\/code>. If that ever shows <code>HIT<\/code>, stop and fix it before going further.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The leak test, which is the one people skip: fetch a personalised page with a session, then fetch the same URL with no cookies at all and confirm you get the anonymous version.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># With a session\ncurl -s https:\/\/example.com\/account -H \"Cookie: session=&lt;value&gt;\" | grep -i 'welcome back'\n\n# Immediately after, with nothing. Should NOT show the personalised content.\ncurl -s https:\/\/example.com\/account | grep -i 'welcome back'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the second command finds personalised content, the CDN cached a private response. That is the Monday-morning email, and you have just caught it before your customers did.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Test the edge before DNS points at it<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -sI https:\/\/example.com\/ --resolve example.com:443:&lt;edge-ip&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That sends a real request to a specific edge node with the correct hostname and TLS SNI, without changing DNS. It is how you find out whether the whole thing works before any user does.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Things that break in ways nobody expects<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Client IPs all become the CDN&#8217;s<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Your logs fill with edge node addresses, rate limiting starts throttling everyone at once, and geolocation breaks. The real address arrives in a forwarded header, and your origin has to be configured to trust it \u2014 but only from the CDN&#8217;s address ranges. Trusting <code>X-Forwarded-For<\/code> from anywhere lets any visitor claim any IP they like, which turns a logging annoyance into an authentication bypass.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Redirect loops<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The CDN talks to your origin over HTTP, the origin sees a non-HTTPS request and redirects to HTTPS, which comes back through the CDN, forever. Either terminate TLS at the origin too, or have the origin trust <code>X-Forwarded-Proto<\/code> when deciding whether to redirect.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">POST responses getting cached<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Shouldn&#8217;t happen by default, but a badly written cache rule can do it, and the result is one user&#8217;s form submission response served to the next person. Only cache <code>GET<\/code> and <code>HEAD<\/code>. Ever.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Websockets and streaming<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Websocket upgrades and server-sent events need to pass through untouched. Buffering breaks streaming responses silently \u2014 the connection works, the data just arrives in one lump at the end, which looks like an application bug rather than a proxy setting.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Set-Cookie on a cacheable response<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If a public page returns <code>Set-Cookie<\/code> and gets cached, every subsequent visitor receives that cookie \u2014 potentially somebody&#8217;s session identifier. Most CDNs refuse to cache responses carrying <code>Set-Cookie<\/code>, but not all, and not always. Check your origin is not setting cookies on pages you intend to cache.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rolling it out without a bad week<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Write down the route list.<\/strong> Three buckets, every path accounted for.<\/li>\n\n<li><strong>Set headers at the origin first,<\/strong> with the CDN not yet in front. Verify with curl.<\/li>\n\n<li><strong>Start with everything bypassed<\/strong> except static assets. A CDN that caches nothing still terminates TLS closer to users and is not a failure.<\/li>\n\n<li><strong>Add public dynamic routes one at a time,<\/strong> with short TTLs. Sixty seconds, then extend once you trust it.<\/li>\n\n<li><strong>Run the leak test<\/strong> after every rule change, not just at the end.<\/li>\n\n<li><strong>Watch origin request volume.<\/strong> It should drop noticeably. If it does not, something is bypassing that you think is caching.<\/li>\n\n<li><strong>Keep a purge command to hand<\/strong> and know how long a full purge takes before you need it.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Common mistakes<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Testing only in a logged-in browser, where cached-private-content bugs are invisible.<\/li>\n\n<li>Setting <code>proxy_cache_bypass<\/code> without <code>proxy_no_cache<\/code>, protecting reads but not writes.<\/li>\n\n<li><code>Vary: Cookie<\/code> or <code>Vary: User-Agent<\/code>, which shatters the cache into near-unique entries.<\/li>\n\n<li>Leaving tracking parameters in the cache key.<\/li>\n\n<li>Configuring caching in the CDN dashboard only, so the policy is invisible to your code and absent from staging.<\/li>\n\n<li>Trusting forwarded IP headers from any source rather than only the CDN&#8217;s ranges.<\/li>\n\n<li>Using <code>private<\/code> where you needed <code>no-store<\/code>, or the reverse.<\/li>\n\n<li>Caching everything on day one and finding the problems in production.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Best practices<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Default to no caching, then allow specific routes. Opt-in beats opt-out when the failure mode is a data leak.<\/li>\n\n<li>Own the policy at the origin so it travels with your code and exists in staging.<\/li>\n\n<li>Use <code>s-maxage<\/code> for the edge and <code>max-age<\/code> for browsers. They are different problems.<\/li>\n\n<li>Always set <code>stale-if-error<\/code>. It is free outage insurance.<\/li>\n\n<li>Expose a cache status header and monitor hit ratio per route, not site-wide.<\/li>\n\n<li>Build purge-on-publish into the application rather than relying on TTLs alone.<\/li>\n\n<li>Make the leak test part of CI, so a future config change cannot reintroduce it quietly.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently asked questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Can a CDN cache dynamic pages at all?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes, and that is where most of the benefit is. Dynamic does not mean personalised. A blog post rendered from a database is identical for every anonymous visitor and can be cached at the edge for minutes. Only genuinely per-user content must bypass.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What is the difference between private and no-store?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>private<\/code> permits browser caching but forbids shared caches. <code>no-store<\/code> forbids writing the response to any cache at all. Use both for authenticated pages; use <code>no-store<\/code> alone for genuinely sensitive responses.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why did my hit ratio collapse after adding a cookie banner?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Because the consent tool sets a cookie on every visitor and your CDN treats any cookie as a bypass signal, or you are varying on <code>Cookie<\/code>. Configure the CDN to ignore all cookies except the specific ones that indicate a session.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I verify private pages are not being cached?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Request the page with a real session cookie and confirm the cache status shows a bypass or miss, never a hit. Then request the same URL with no cookies and confirm the personalised content is absent. Do this after every cache rule change.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Should I cache API responses?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Public read-only endpoints, yes \u2014 with short TTLs and a cache key that includes whatever parameters affect the response. Anything requiring an Authorization header should be treated as private by default.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I still need caching at the origin?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. The CDN protects you from repeat requests for the same URL; it does nothing for the first request, for uncacheable routes, or during a purge. Origin-level caching handles those.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping up<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A CDN in front of a dynamic site is a classification problem wearing a performance costume. Sort your routes into public, private and static; express that at the origin with <code>Cache-Control<\/code>; make session cookies a hard bypass on both the read and write path; and keep tracking parameters out of the cache key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then test the way an anonymous visitor arrives, not the way you browse your own site. Two curl commands \u2014 one with a session, one without \u2014 catch the failure that matters, and they take about ten seconds. Run them every time you change a cache rule and the Monday email never arrives.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Want a CDN set up without the surprises?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I work with teams on edge caching for dynamic sites \u2014 route classification, cache headers that survive a redesign, purge-on-publish wired into the application, and the monitoring that tells you when hit ratio quietly drops.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you would rather have this done carefully the first time, you can hire me on Upwork.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/www.upwork.com\/freelancers\/~01f15a912ad84a6620\" target=\"_blank\" rel=\"noreferrer noopener\">Work with me on Upwork<\/a><\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Put a CDN in front of a dynamic site carelessly and it will cache one user&#8217;s personalised page and serve it to everyone \u2014 invisibly, because your logged-in browser looks fine. Here is how to classify routes, set cache headers that hold, and run the two curl commands that catch the leak.<\/p>\n","protected":false},"author":1,"featured_media":50,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[24,107,30],"tags":[113,109,108,71,3,114,111,21,18,4,110,112],"class_list":["post-49","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-web-performance","category-web-security","tag-cache-control","tag-caching","tag-cdn","tag-cloudflare","tag-devops","tag-edge-computing","tag-http-headers","tag-infrastructure","tag-nginx","tag-troubleshooting","tag-web-performance","tag-web-security","entry","has-media"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>CDN Setup Without Breaking Dynamic Content<\/title>\n<meta name=\"description\" content=\"CDN setup for dynamic sites without cached private pages: route classification, Cache-Control and Vary, cookie bypass rules, and the curl leak test.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"CDN Setup Without Breaking Dynamic Content\" \/>\n<meta property=\"og:description\" content=\"CDN setup for dynamic sites without cached private pages: route classification, Cache-Control and Vary, cookie bypass rules, and the curl leak test.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/\" \/>\n<meta property=\"og:site_name\" content=\"John Nessime\" \/>\n<meta property=\"article:published_time\" content=\"2026-07-31T22:51:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-31T22:51:02+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/cdn-setup-dynamic-content-featured.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"800\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"John Nessime\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"John Nessime\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/\"},\"author\":{\"name\":\"John Nessime\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"headline\":\"CDN Setup Without Breaking Dynamic Content\",\"datePublished\":\"2026-07-31T22:51:00+00:00\",\"dateModified\":\"2026-07-31T22:51:02+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/\"},\"wordCount\":2241,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"image\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/cdn-setup-dynamic-content-featured.png\",\"keywords\":[\"Cache Control\",\"Caching\",\"CDN\",\"Cloudflare\",\"DevOps\",\"Edge Computing\",\"HTTP Headers\",\"Infrastructure\",\"Nginx\",\"Troubleshooting\",\"Web Performance\",\"Web Security\"],\"articleSection\":[\"DevOps\",\"Web Performance\",\"Web Security\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/\",\"name\":\"CDN Setup Without Breaking Dynamic Content\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/cdn-setup-dynamic-content-featured.png\",\"datePublished\":\"2026-07-31T22:51:00+00:00\",\"dateModified\":\"2026-07-31T22:51:02+00:00\",\"description\":\"CDN setup for dynamic sites without cached private pages: route classification, Cache-Control and Vary, cookie bypass rules, and the curl leak test.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/#primaryimage\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/cdn-setup-dynamic-content-featured.png\",\"contentUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/cdn-setup-dynamic-content-featured.png\",\"width\":1200,\"height\":800,\"caption\":\"CDN request flow diagram showing anonymous requests served from the edge cache while requests carrying a session cookie bypass to origin, with a route classification table for static, public dynamic and private content\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/cdn-setup-dynamic-content\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"CDN Setup Without Breaking Dynamic Content\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/\",\"name\":\"John Nessime\",\"description\":\"Cloud, DevOps, Data &amp; AI \u2014 Built, Tested, Explained\",\"publisher\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\",\"name\":\"John Nessime\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"contentUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"width\":512,\"height\":512,\"caption\":\"John Nessime\"},\"logo\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\"},\"description\":\"AWS Certified Solutions Architect helping businesses build reliable cloud, data, reporting, and automation solutions. I help startups, agencies, and growing businesses replace manual processes and disconnected data with practical AWS architectures, clean data pipelines, useful dashboards, and maintainable automation.\",\"sameAs\":[\"https:\\\/\\\/john-nessime.com\\\/blog\",\"https:\\\/\\\/www.linkedin.com\\\/in\\\/john-m-nessime\"],\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/author\\\/johnnessime\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"CDN Setup Without Breaking Dynamic Content","description":"CDN setup for dynamic sites without cached private pages: route classification, Cache-Control and Vary, cookie bypass rules, and the curl leak test.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/","og_locale":"en_US","og_type":"article","og_title":"CDN Setup Without Breaking Dynamic Content","og_description":"CDN setup for dynamic sites without cached private pages: route classification, Cache-Control and Vary, cookie bypass rules, and the curl leak test.","og_url":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/","og_site_name":"John Nessime","article_published_time":"2026-07-31T22:51:00+00:00","article_modified_time":"2026-07-31T22:51:02+00:00","og_image":[{"width":1200,"height":800,"url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/cdn-setup-dynamic-content-featured.png","type":"image\/png"}],"author":"John Nessime","twitter_card":"summary_large_image","twitter_misc":{"Written by":"John Nessime","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/#article","isPartOf":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/"},"author":{"name":"John Nessime","@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"headline":"CDN Setup Without Breaking Dynamic Content","datePublished":"2026-07-31T22:51:00+00:00","dateModified":"2026-07-31T22:51:02+00:00","mainEntityOfPage":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/"},"wordCount":2241,"commentCount":0,"publisher":{"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"image":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/#primaryimage"},"thumbnailUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/cdn-setup-dynamic-content-featured.png","keywords":["Cache Control","Caching","CDN","Cloudflare","DevOps","Edge Computing","HTTP Headers","Infrastructure","Nginx","Troubleshooting","Web Performance","Web Security"],"articleSection":["DevOps","Web Performance","Web Security"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/","url":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/","name":"CDN Setup Without Breaking Dynamic Content","isPartOf":{"@id":"https:\/\/john-nessime.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/#primaryimage"},"image":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/#primaryimage"},"thumbnailUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/cdn-setup-dynamic-content-featured.png","datePublished":"2026-07-31T22:51:00+00:00","dateModified":"2026-07-31T22:51:02+00:00","description":"CDN setup for dynamic sites without cached private pages: route classification, Cache-Control and Vary, cookie bypass rules, and the curl leak test.","breadcrumb":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/#primaryimage","url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/cdn-setup-dynamic-content-featured.png","contentUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/cdn-setup-dynamic-content-featured.png","width":1200,"height":800,"caption":"CDN request flow diagram showing anonymous requests served from the edge cache while requests carrying a session cookie bypass to origin, with a route classification table for static, public dynamic and private content"},{"@type":"BreadcrumbList","@id":"https:\/\/john-nessime.com\/blog\/devops\/cdn-setup-dynamic-content\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/john-nessime.com\/blog\/"},{"@type":"ListItem","position":2,"name":"CDN Setup Without Breaking Dynamic Content"}]},{"@type":"WebSite","@id":"https:\/\/john-nessime.com\/blog\/#website","url":"https:\/\/john-nessime.com\/blog\/","name":"John Nessime","description":"Cloud, DevOps, Data &amp; AI \u2014 Built, Tested, Explained","publisher":{"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/john-nessime.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105","name":"John Nessime","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","contentUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","width":512,"height":512,"caption":"John Nessime"},"logo":{"@id":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png"},"description":"AWS Certified Solutions Architect helping businesses build reliable cloud, data, reporting, and automation solutions. I help startups, agencies, and growing businesses replace manual processes and disconnected data with practical AWS architectures, clean data pipelines, useful dashboards, and maintainable automation.","sameAs":["https:\/\/john-nessime.com\/blog","https:\/\/www.linkedin.com\/in\/john-m-nessime"],"url":"https:\/\/john-nessime.com\/blog\/author\/johnnessime\/"}]}},"_links":{"self":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/49","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/comments?post=49"}],"version-history":[{"count":1,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/49\/revisions"}],"predecessor-version":[{"id":51,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/49\/revisions\/51"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/media\/50"}],"wp:attachment":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/media?parent=49"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/categories?post=49"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/tags?post=49"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}