A single paid ad blast can tank your CDN cache hit rate from 95% down to 12% in under an hour. The moment an ad platform or email tool tacks on ?utm_source=newsletter&utm_medium=email&fbclid=IwAR..., your CDN treats every incoming click as a completely distinct URL and forwards every single one straight to your origin server.
Let’s fix this by normalizing cache keys and stripping tracking parameters at the CDN edge. Your origin stays fast and protected while client-side marketing analytics keep working without missing a beat.

How Tracking Query Strings Trash Your Cache Hit Ratio
By default, edge CDNs like Cloudflare, Fastly, and CloudFront include the entire raw query string when computing their cache key hash.
Take a normal request path:
GET /pricing/ HTTP/2
Host: example.comWhen someone visits without query params, the CDN hashes example.com/pricing/, serves a clean HIT, and delivers the response in about 15ms. But when marketing launches a campaign or sends an email blast, the incoming traffic looks more like this:
GET /pricing/?utm_source=meta&utm_medium=cpc&utm_campaign=spring_sale&fbclid=12345 HTTP/2
GET /pricing/?utm_source=meta&utm_medium=cpc&utm_campaign=spring_sale&fbclid=67890 HTTP/2
GET /pricing/?utm_source=newsletter&mc_cid=a1b2c3 HTTP/2Because values like fbclid, gclid, and email campaign IDs are unique per user, your CDN hashes each request as a brand-new page. Your origin ends up generating the exact same identical HTML thousands of times over. If you’ve already implemented rules like those in our guide on configuring CDN edge cache rules for WordPress dynamic URLs, query string clutter will quietly wipe out those performance gains.
Edge Cache Key Normalization vs. URL Rewriting
There are two main ways to handle this at the edge:
- Cache Key Normalization (Recommended): The CDN strips the tracking parameters only when calculating the internal cache lookup key, but keeps the original query string intact when passing the response to the browser.
- Edge Request Rewriting: The CDN strips the parameters before passing the request downstream or responding to the visitor.
Cache key normalization is almost always what you want. Client-side trackers (Google Tag Manager, GA4, Meta Pixel) rely on reading window.location.search in the browser. Normalizing the key means the visitor gets a cached response from the edge in milliseconds, while their browser still parses the tracking tags in the URL bar.
These common marketing parameters never change server-rendered HTML and should be stripped from your cache key:
utm_source,utm_medium,utm_campaign,utm_term,utm_content(Google Analytics)gclid,gclsrc,wbraid,gbraid(Google Ads)fbclid(Meta / Facebook)msclkid(Microsoft / Bing Ads)ttclid(TikTok Ads)twclid(Twitter / X Ads)mc_cid,mc_eid(Mailchimp)_hsenc,_hsmi(HubSpot)yclid(Yandex)
Method 1: Normalizing Query Strings with Cloudflare Workers
If you’re on Cloudflare, you can use Transform Rules or a lightweight Worker to sanitize the cache key while leaving functional query strings (like ?page=2 or ?s=search) alone.
Here’s a production-ready Worker script that removes marketing clutter from the cache key and sorts remaining params alphabetically:
addEventListener('fetch', event => { event.respondWith(handleRequest(event.request));
}); const TRACKING_PARAMS = new Set([ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'gclsrc', 'wbraid', 'gbraid', 'msclkid', 'ttclid', 'twclid', 'mc_cid', 'mc_eid', '_hsenc', '_hsmi'
]); async function handleRequest(request) { const url = new URL(request.url); let modified = false; for (const param of Array.from(url.searchParams.keys())) { if (TRACKING_PARAMS.has(param.toLowerCase())) { url.searchParams.delete(param); modified = true; } } // Sort remaining parameters for a consistent cache key url.searchParams.sort(); const cacheKey = new Request(url.toString(), request); const cache = caches.default; // Check CDN edge cache with sanitized key let response = await cache.match(cacheKey); if (!response) { // Fetch from origin if cache miss response = await fetch(request); // Only cache successful GET responses if (request.method === 'GET' && response.status === 200) { const responseToCache = response.clone(); event.waitUntil(cache.put(cacheKey, responseToCache)); } } return response;
}By sorting whatever parameters remain, requests like /shop/?size=m&color=blue and /shop/?color=blue&size=m both hit the exact same cache entry instead of caching twice.
Method 2: Stripping Marketing Parameters in Fastly or Varnish VCL
If you’re running Fastly or self-hosting Varnish, clean up the query string inside vcl_recv before the hashing step.
Drop this snippet into your default.vcl file before the hashing block:
sub vcl_recv { # Strip tracking query strings from the URL if (req.url ~ "(?|&)(utm_[a-z]+|fbclid|gclid|gclsrc|msclkid|ttclid|mc_[a-z]+|_hs[a-z]+)=") { set req.url = regsuball(req.url, "(utm_[a-z]+|fbclid|gclid|gclsrc|msclkid|ttclid|mc_[a-z]+|_hs[a-z]+)=[^&]+&?", ""); set req.url = regsub(req.url, "(?|&)$", ""); } # Clean up dangling question marks if (req.url ~ "?$") { set req.url = regsub(req.url, "?$", ""); }
}Varnish strips the specified query parameters out of req.url right before entering vcl_hash. Every visitor coming in with ad tracking tags will hit the exact same cached object in memory.
Method 3: Normalizing at Origin with an Nginx Reverse Proxy
If your CDN forwards full query strings straight through to your origin, you can handle the cleanup right in Nginx. Pairing this with immutable cache-control headers behind a CDN stops unneeded application runs and database load completely.
Use Nginx’s map module inside /etc/nginx/nginx.conf to filter out junk params:
http { # Strip marketing parameters from args map $args $clean_args { "~(.*)(?:&|^)(?:utm_[a-z]+|fbclid|gclid|msclkid|mc_[a-z]+)=[^&]*(.*)" $1$2; default $args; } # Clean up residual leading/trailing ampersands map $clean_args $final_args { "^&+(.*)" $1; "(.*)&+$" $1; default $clean_args; }
}Then pass $final_args to your upstream application or fastcgi cache in your server block:
server { listen 80; server_name example.com; location / { # Fastcgi cache key using the normalized args fastcgi_cache_key "$scheme$request_method$host$uri$final_args"; # Proxy or PHP-FPM pass include fastcgi_params; fastcgi_param QUERY_STRING $final_args; fastcgi_pass 127.0.0.1:9000; }
}Check the config syntax and reload Nginx:
sudo nginx -t && sudo systemctl reload nginxWhat I Ran: Verifying Edge Cache Hits with cURL
Once your edge rules are live, test them from your terminal with two consecutive requests using different tracking values.
Fire these two cURL commands:
# First request with user A's Facebook click ID
curl -I -s "https://example.com/pricing/?utm_source=facebook&fbclid=IwAR111111111" | grep -iE '(x-cache|cf-cache-status|age)' # Second request with user B's Facebook click ID
curl -I -s "https://example.com/pricing/?utm_source=facebook&fbclid=IwAR222222222" | grep -iE '(x-cache|cf-cache-status|age)'The first request warms up the edge cache and might return a miss:
cf-cache-status: MISS
age: 0The second request has a completely different fbclid, but it should return an instant cache hit:
cf-cache-status: HIT
age: 14On my test landing page, response times dropped from 847ms down to 24ms. The backend server never even woke up for the second visitor.
The Gotcha: Preserving Client-Side Analytics and Attribution
The biggest trap I hit early on was trying to clean URLs by throwing a 301 redirect at incoming traffic.
If someone clicks an ad for example.com/?utm_source=newsletter and your server immediately returns a 301 Moved Permanently to example.com/, the browser drops the query parameters on the spot. By the time your Google Analytics script loads on the target page, window.location.search is empty. Your paid campaign traffic gets dumped into the “Direct” bucket.
Here is how to keep your data accurate:
- Never issue a 301 or 302 edge redirect to strip tracking parameters.
- Normalize the cache key only at your CDN tier. The user’s browser still needs the full query string in the address bar on initial load.
- If you want clean URLs in the browser bar for visual aesthetics, clean them client-side after your analytics tags fire using the MDN History API documentation:
// Clean up tracking query params in the browser without reloading or breaking GA4
window.addEventListener('load', () => { const url = new URL(window.location.href); const cleanParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid']; let cleaned = false; cleanParams.forEach(param => { if (url.searchParams.has(param)) { url.searchParams.delete(param); cleaned = true; } }); if (cleaned) { window.history.replaceState({}, document.title, url.pathname + (url.search ? url.search : '')); }
});This lets GA4 and Meta Pixel capture the campaign data first, then quietly removes the clutter from the address bar without causing a page refresh.
Handling Origin Shields to Prevent Stampedes
Even with clean cache keys, a viral post or high-volume email blast can trigger hundreds of simultaneous requests the exact second an edge cache entry expires. You need request collapsing to keep your backend safe.
To protect your origin during cache revalidations, check out our guide on preventing CDN cache stampedes with origin shield and request collapsing and our step-by-step on configuring CDN origin shield and tiered caching.
Frequently Asked Questions
Does stripping query strings at the edge break Google Ads conversion tracking?
No, as long as you do not redirect with a 301/302. Normalizing the cache key serves the cached HTML payload while keeping the original ?gclid=... URL intact in the browser address bar. Client-side tracking scripts read the parameter directly from window.location after the page loads.
What happens to search and filter queries on ecommerce sites?
Functional params like ?page=2, ?filter_color=black, or ?s=keyword must stay in the cache key. In our Cloudflare Worker and Nginx configurations, we only strip an explicit list of known marketing parameters, so application-level filters still generate separate, correct cache entries.
Can I sort query parameters to improve cache hits further?
Yes. Parameter sorting (canonicalization) makes sure ?product=shirt&size=m and ?size=m&product=shirt share one cache entry. Adding sorting to your Worker stops duplicate caching caused by random query string ordering.
Should I strip tracking query parameters inside WordPress via PHP?
No. Once a request hits PHP, your web server has already spun up workers and burned CPU cycles booting the app. Edge normalization handles this before traffic ever reaches your infrastructure, keeping server load flat.
Next Steps
Now that your CDN is stripping tracking parameters and serving landing pages from edge cache, check our guide on configuring CDN edge cache rules for dynamic URLs and cookies to make sure logged-in users and shopping carts bypass the cache properly without leaking private data.

