Override Origin Cache-Control Headers with CDN Edge Rules

by Fahim

Your backend app is probably spitting out Cache-Control: max-age=0, must-revalidate or private, no-cache on assets that should sit at the edge for months. When that happens, your CDN blindly obeys the origin and bounces every single incoming request straight back to your servers, sending response times from 20ms up to 650ms.

We can fix this by setting up edge cache override rules on the CDN and cleaning up the origin reverse proxy. We’ll track down bad upstream headers, rewrite edge TTLs without trashing dynamic user sessions, and stop buggy cache tags from poisoning your edge nodes.

Enterprise rack server with illuminated network cables demonstrating CDN edge cache routing
Enterprise rack server with illuminated network cables demonstrating CDN edge cache routing

The Problem: Origin Headers Killing Edge Hit Rates

Most backend frameworks—Laravel, Express, Django, or legacy WordPress setups—ship with middleware that slaps aggressive anti-caching headers on every response by default. If you didn’t explicitly configure headers on a static or semi-dynamic endpoint, the framework plays it safe and tells downstream clients not to store anything.

By default, CDNs respect standard MDN Cache-Control documentation rules. If your origin sends no-store, the edge drops the response the moment it streams to the client. If it sends max-age=60, the CDN revalidates every 60 seconds across all 200+ edge locations worldwide.

Here’s what happens when 10,000 visitors hit a semi-static blog post or JSON API endpoint set up like that:

  • Bandwidth spikes: Your origin pumps out gigabytes of uncompressed payloads that should have stayed at the edge.
  • CPU throttling: PHP-FPM or Node.js workers redline rendering identical HTML over and over.
  • Global latency: A user in Tokyo waits 380ms for a box in Virginia to regenerate content that hasn’t changed in three weeks.

Even if you already followed our guide to configure CDN origin shield and tiered caching, broken origin headers will bypass the shield completely and hammer your database.

Diagnosing Bad Origin Headers with cURL

Before writing edge rules, check what your origin actually returns when bypassing the CDN. Run cURL directly against the origin server’s public IP address or internal hostname.

Here is the command to inspect raw upstream response headers:

curl -svo /dev/null https://origin.example.com/api/catalog/products  -H "Host: example.com"  -H "Accept-Encoding: gzip"

Check the output carefully. On a misconfigured origin, you’ll usually see something like this:

< HTTP/2 200
< date: Tue, 11 Feb 2025 14:22:01 GMT
< content-type: application/json; charset=utf-8
< cache-control: no-cache, private, max-age=0
< pragma: no-cache
< set-cookie: session_id=abc123def; Path=/; HttpOnly
< x-powered-by: Express

That cache-control: no-cache, private, max-age=0 header tells every intermediary cache to ditch the response. If your backend also attaches an unnecessary Set-Cookie header on public endpoints, standard CDN setups will refuse to store the object at all.

Understanding Edge TTL vs Browser TTL

Overriding headers means separating two caching layers: the edge cache (shared across all visitors) and the browser cache (private to an individual client).

Under the RFC 9111 HTTP Caching specifications, caching directives follow a clear hierarchy:

  1. s-maxage: Sets TTL specifically for shared caches (CDNs and proxies). Browsers ignore this and fall back to max-age.
  2. CDN-Cache-Control or vendor headers (like Cloudflare-CDN-Cache-Control): Overrides both s-maxage and max-age on edge networks that support it.
  3. max-age: Applies to both edge nodes and browser caches if no shared directive is found.
  4. Expires: Legacy fallback when Cache-Control is missing.

When overriding headers at the edge, you can instruct the CDN to cache the response in edge RAM/NVMe for 7 days, but tell the visitor’s browser to revalidate after 5 minutes. That keeps visitor data fresh while dropping origin load down to near zero.

Overriding Cache-Control via CDN Edge Rules

Most CDNs let you set declarative rules matching URL paths, file extensions, or query parameters to override upstream headers. If you use custom edge routing on StackPath or Cloudflare, you can enforce custom edge TTLs in the control panel or via edge workers.

Here is an edge rule setup you can deploy to force caching on static and public API routes regardless of origin headers:

{ "rules": [ { "match": { "path": "/assets/*", "extension": [ "js", "css", "webp", "svg", "woff2" ] }, "action": { "override_origin_ttl": true, "edge_ttl": 2592000, "browser_ttl": 86400, "strip_response_headers": [ "set-cookie", "pragma" ], "force_cache": true } }, { "match": { "path": "/api/public/*" }, "action": { "override_origin_ttl": true, "edge_ttl": 3600, "browser_ttl": 60, "serve_stale_on_origin_error": true } } ]
}

Pay attention to the strip_response_headers directive. If your origin framework sends session cookies on static asset requests, stripping Set-Cookie at the edge lets the CDN safely cache the response without leaking session state across users.

If your application has query parameters that alter analytics tracking rather than rendered content, make sure you also strip marketing query strings at the CDN edge so you don’t fragment your cache keys.

Overriding Headers at the Origin Proxy (Nginx)

If you manage the web server or reverse proxy in front of your app (like Nginx), you don’t need to wait on edge rule propagation. You can enforce clean caching headers before the CDN even sees the upstream response using the Nginx ngx_http_headers_module.

Open your Nginx virtual host config:

sudo nano /etc/nginx/sites-available/example.com.conf

Add directives to ignore upstream caching headers and set your own Cache-Control and s-maxage values:

server { listen 443 ssl http2; server_name example.com; location /static/ { proxy_pass http://127.0.0.1:3000; # Tell Nginx to ignore headers returned by Node/PHP proxy_ignore_headers Cache-Control Expires Set-Cookie; proxy_hide_header Set-Cookie; proxy_hide_header Pragma; # Set public edge caching: 30 days edge, 1 day browser add_header Cache-Control "public, max-age=86400, s-maxage=2592000, immutable" always; } location /api/public/ { proxy_pass http://127.0.0.1:3000; proxy_ignore_headers Cache-Control Expires; # Edge holds for 10 mins, client for 30s, serve stale on failure add_header Cache-Control "public, max-age=30, s-maxage=600, stale-while-revalidate=60, stale-if-error=86400" always; }
}

Test the syntax and reload Nginx:

sudo nginx -t && sudo systemctl reload nginx

This prevents upstream Node.js or Python processes from breaking edge cacheability with default framework headers. To handle origin hiccups gracefully, you should also configure stale-while-revalidate and stale-if-error on Nginx.

Preserving Authenticated User Sessions

The biggest trap with overriding origin headers is caching private user data—like account dashboards, checkout carts, or CSRF tokens—and serving it to anonymous visitors.

Never use a blanket “Override All” rule on root paths like /*. Always add explicit bypass logic. If a request carries an authentication cookie or an Authorization: Bearer header, the CDN must pass the request straight to the origin and skip caching entirely.

Here is an edge worker script (compatible with standard Edge JS runtimes) showing conditional cache overriding with safe session bypass:

export default { async fetch(request, env, ctx) { const url = new URL(request.url); const cookieHeader = request.headers.get("Cookie") || ""; const authHeader = request.headers.get("Authorization"); // 1. Bypass edge cache entirely for logged-in users or auth requests if (cookieHeader.includes("session_id=") || authHeader || url.pathname.startsWith("/account/")) { return fetch(request); } // 2. Clone request to fetch from origin const response = await fetch(request); const newHeaders = new Headers(response.headers); // 3. Override origin headers for public endpoints only if (url.pathname.startsWith("/blog/") || url.pathname.startsWith("/static/")) { newHeaders.set("Cache-Control", "public, max-age=3600, s-maxage=604800"); newHeaders.delete("Set-Cookie"); newHeaders.delete("Pragma"); } return new Response(response.body, { status: response.status, statusText: response.statusText, headers: newHeaders }); }
};

For more granular cookie matching rules across WordPress or REST APIs, check out our guide on how to bypass CDN edge cache for authenticated API routes and cookies.

Real-World Gotcha: The Upstream 304 Revalidation Trap

I hit an annoying edge caching bug in production where overriding headers caused endless 304 loops. The origin returned a 304 Not Modified with an empty body, but retained its original Cache-Control: private, no-cache header.

When the edge node revalidated upstream using an If-None-Match ETag, it merged the 304 headers back into the cached object. Because the 304 response had private, no-cache, the CDN updated its stored cache metadata and flushed the asset across all edge PoPs.

To avoid upstream 304 revalidation issues:

  • Ensure your proxy strips origin cache directives on conditional requests.
  • Configure edge rules to overwrite headers during both cache store and cache revalidate lifecycle hooks.
  • Strip the ETag header at the edge if your origin servers run behind a load balancer with non-synchronized filesystem inodes.

Verifying Edge Cache Hits

Once your edge rules or Nginx directives are in place, test them by sending multiple requests to your CDN endpoint. Skip your local browser for this—local disk caching will obscure what the CDN is actually doing.

Run a repeated cURL check and inspect the cache headers:

curl -I https://example.com/assets/app.css

On the first hit, you should see a cache miss along with your custom downstream header:

HTTP/2 200
date: Tue, 11 Feb 2025 14:35:10 GMT
content-type: text/css; charset=utf-8
cache-control: public, max-age=86400, s-maxage=2592000
x-cache: MISS
age: 0
server: CDN-Edge-Node

Run the exact same command two seconds later:

HTTP/2 200
date: Tue, 11 Feb 2025 14:35:12 GMT
content-type: text/css; charset=utf-8
cache-control: public, max-age=86400, s-maxage=2592000
x-cache: HIT
age: 2
server: CDN-Edge-Node

Check the x-cache: HIT (or cf-cache-status: HIT depending on your CDN) and the incrementing age header. Response latency on that asset dropped from 480ms on the initial miss down to 14ms on the edge hit.

Frequently Asked Questions

What is the difference between s-maxage and max-age?

max-age specifies how long a resource stays fresh for all clients, including browsers and intermediate caches. s-maxage (shared max-age) applies exclusively to shared public proxies like CDNs. If both are present, CDNs respect s-maxage, while browsers ignore it and follow max-age.

Why does my CDN refuse to cache even with an edge rule?

Usually, it’s the Set-Cookie response header. Many CDNs bypass the cache whenever an origin sends a cookie to avoid caching sensitive user sessions. You’ll need to configure your edge rule or reverse proxy to strip Set-Cookie on public paths.

Will overriding Cache-Control break API deployments?

It can if you don’t handle cache invalidation. When deploying new API code or changing payload schemas, trigger an edge cache purge via your CDN’s API in your CI/CD pipeline, or use versioned endpoints (like /api/v2/products or query versioning).

Does Cloudflare or StackPath support CDN-Cache-Control?

Yes. CDN-Cache-Control is supported by modern edge networks. It lets you define caching directives targeted specifically at CDNs without changing the Cache-Control headers delivered to client browsers.

What to Configure Next

Now that your CDN bypasses broken origin headers and caches static payloads properly, make sure your origin server is protected from direct IP bypass. Follow our guide to restrict origin traffic to CDN shield IPs with UFW and Nginx to block all non-CDN traffic.

all_in_one_marketing_tool