Stripping UTM Parameters from the Cache Key
Problem Statement
A campaign launches, traffic rises, and the origin sees load out of all proportion to the number of pages being requested. Every distinct utm_content value, every fbclid, every gclid creates a separate cache entry for a page whose body is byte-identical. The content is one page; the cache is storing thousands of copies, each cold on first request.
Prerequisite Concepts
- Cache key normalization and query strings — how a key is composed and where normalization fits.
- How CDN cache keys are generated — the components a key contains by default.
- Cache hit, miss and bypass mechanics — why every unique key starts as a miss.
Step-by-Step Resolution
Step 1 — Measure the fragmentation
Count distinct request targets per path and compare against the number of pages you actually publish:
awk '{print $7}' access.log \
| awk -F'?' '{print $1}' \
| sort | uniq -c | sort -rn | head -5
# Then, for the busiest path, count distinct full targets
grep ' /landing/spring-sale' access.log | awk '{print $7}' | sort -u | wc -l
One published page and eleven thousand distinct targets is the shape this procedure fixes.
Step 2 — Strip during key composition
Removal must happen before any other normalization step, so nothing downstream orders or filters parameters that are about to disappear:
sub vcl_recv {
# Campaign and click identifiers never change the response
set req.url = querystring.regfilter(req.url,
"^(utm_[a-z_]+|fbclid|gclid|msclkid|dclid|twclid|mc_[ce]id|igshid|ref|referrer)$");
}
The equivalent in Nginx is to name the surviving parameters explicitly, which is both a strip and an allow-list at once:
location /landing/ {
proxy_cache pages;
# Only these parameters can influence the key; everything else is ignored
proxy_cache_key "$scheme$host$uri?variant=$arg_variant";
proxy_pass http://origin;
}
Step 3 — Leave the URL itself alone
This is the property that separates key stripping from a redirect. The browser must still see the tagged URL, because client-side analytics reads it from location.search. Nothing in the steps above rewrites what the visitor’s address bar contains or what the origin receives — only the string used for the lookup.
Step 4 — Verify
# Warm the entry with a clean URL
curl -sI 'https://example.com/landing/spring-sale' | grep -iE 'cf-cache-status|^age'
# Then request it with a campaign tag — expect a HIT with a climbing Age
curl -sI 'https://example.com/landing/spring-sale?utm_source=newsletter&utm_content=v3' \
| grep -iE 'cf-cache-status|^age'
A HIT with a non-zero Age on the tagged request is the proof. A MISS means the strip is not being applied — most often because the rule sits on a route pattern that does not match the landing path.
Step 5 — Guard against the next parameter
A deny-list is out of date the day a new tool is adopted. Where the platform supports it, invert the rule so that only known-meaningful parameters survive:
sub vcl_recv {
set req.url = querystring.filter_except(req.url, "variant,lang");
set req.url = querystring.sort(req.url);
}
The allow-list carries its own risk, and it is the more serious one: a genuine parameter left off the list means the edge serves one variant to everyone. Verify each excluded parameter by confirming that changing its value does not change the response body.
Expected Output / Verification
After the change, distinct targets for the landing path collapse to the number of genuine variants, and campaign traffic hits immediately rather than warming its own entry:
before: 11794 distinct keys, 3.1% hit ratio on campaign traffic
after: 4 distinct keys, 98.6% hit ratio on campaign traffic
Edge Cases
- An origin that varies on a stripped parameter. If server-side code branches on
utm_source, the stored response no longer matches its key and the wrong content is served. Confirm the origin ignores them before stripping. - A parameter that is both tracking and functional.
refis sometimes used for referral attribution and sometimes for a functional referrer code. Check what the application does with it rather than assuming from the name. - Rules applied to some routes only. A strip configured on the landing path leaves the rest of the site fragmented. Apply it globally and override where a route genuinely needs a parameter.
- Case variance in parameter names.
UTM_Sourceandutm_sourceare distinct names to a case-sensitive filter, and some email tools emit capitalised forms. - Fragments and empty values.
?utm_source=with no value still creates a distinct key on a byte-comparing edge, and is common in malformed campaign links. - Server-side attribution reading the query string. If the origin logs campaign parameters for attribution, it must still receive them even though they no longer influence the key. Stripping for key composition and stripping from the forwarded request are separate operations, and conflating them silently breaks reporting.
- A shield with different rules. If the strip is applied at the edge but not the shield, the shield sees fragmented keys and cannot serve the edge’s misses.
Frequently Asked Questions
Why not redirect to a clean URL instead?
A redirect costs an extra round trip on exactly the traffic you paid to acquire, and it destroys the campaign attribution that client-side analytics reads from the URL. Stripping during key composition achieves the caching benefit with neither cost.
Will analytics still work if the parameter is not in the key?
Yes. The key affects only which stored entry is selected; the browser still receives the URL it requested, including the tags, and any script reading location.search is unaffected.
Should the parameters be forwarded to the origin?
Usually yes, in case server-side attribution depends on them — but only if the origin does not vary its response on them. If it does, the response cannot safely share a key with an untagged request.
Is a deny-list of known tracking parameters enough?
It works until the next tool introduces a new parameter, at which point fragmentation returns silently. An allow-list of the parameters the application reads is stable because it changes only when the application does.
Related
- Sorting query parameters for a stable cache key removes the second-largest source of accidental key variance.
- How to debug CDN cache key mismatches is the isolation procedure for finding what else is in the key.
- Origin shielding and request collapsing absorbs the cold-start traffic that remains after the key is clean.