August 31, 2026

Strip UTM Parameters at Cloudflare Without Losing Attribution (Corrected)

cloudflare caching performance woocommerce analytics

Updated October 7, 2026

Correction — 7 October 2026. The Transform Rule this article originally recommended was broken, and I was wrong about attribution. It did not fail on every request — a plain /product/mug/?utm_source=newsletter answered with a 200 was handled correctly — but it failed in cases that matter for paid traffic:

  • A tracking parameter in first position followed by a normal one (?fbclid=x&color=red) reached the server as ?&color=red. WordPress answers that with a 301 — an extra round-trip on the landing.
  • Non-adjacent tracking parameters were only partly removed, because Cloudflare’s regex_replace() replaces only the first match. The article claimed it removed every one. It didn’t.
  • Any redirect issued by the server — apex → www, missing trailing slash, WordPress canonical redirect — was built from the already-stripped URL. The browser followed it and gclid / fbclid / gad_source vanished from the address bar, exactly where GA4 and Google Ads read them.

On one store running Google Ads this showed up in the logs as 21 paid landings over about three weeks — plus every landing on the non-canonical host, which the logs cannot count. The rule is withdrawn. The repo now ships a small Cloudflare Worker that strips the same parameters and puts them back on same-site redirects. The sections below are updated; the full post-mortem is on GitHub: POSTMORTEM.md.

If you deployed the rule from this article: disable it (Rules → Transform Rules → URL Rewrite). If the Super Page Cache plugin created a [DO NOT EDIT] rule with the same regex, switch off its “strip tracking parameters” option instead.

Same Page, a Thousand URLs

You probably run some caching already. A cache plugin, server-level page cache, maybe Cloudflare in front of all of it. And it probably handles marketing parameters correctly. Probably.

Here’s the problem. Every campaign link points at the same page but carries a unique query string:

/product/mug/?utm_source=newsletter&utm_medium=email
/product/mug/?utm_source=facebook&fbclid=AbC123xYz
/product/mug/?gclid=EAIaIQobChMI...

To you that’s one product page. What a cache does with those three URLs depends entirely on its configuration — and both failure modes hurt:

  • The most common one: cache bypass. Plenty of stacks treat any query string as “dynamic — don’t cache” and send every campaign click straight to PHP. Your homepage serves from cache in milliseconds; the exact same page with ?fbclid=... renders from scratch, uncached. Unless a campaign parameter actually changes what appears on the page — and utm_source never does — you’re burning server time re-rendering the identical page, click after click. That’s the whole idea of caching, inverted: this is precisely the work a cache exists to save.
  • The other extreme: cache-key explosion. An over-aggressive setup that includes the full query string in the cache key will happily store a hundred variants of the same page. And because click IDs like fbclid and gclid are unique per click, virtually none of those entries will ever be served twice — every visitor gets a cold miss anyway, and the cache dutifully stores a copy nobody will ever read. You pay for the render and for storing its useless result.

Either way, the pain arrives exactly when you can least afford it: during a newsletter send or a live campaign, when thousands of visitors show up within minutes — all carrying parameters.

And with paid traffic this stops being untidy and starts being expensive. I’ve watched it happen in paid-ads work more than once: every click from Google Ads or Meta arrives with a gclid or fbclid attached, so if parameterized URLs bypass your cache, the visitors you pay the most for are precisely the ones who never hit it. You’re paying real CPCs to serve your slowest, uncached page — to cold traffic, at the moment of highest intent.

Good cache plugins ship an ignore-list for the well-known params. But your stack isn’t one cache — it’s Cloudflare, then a server-level page cache, then whatever your plugin does. Do all of those layers agree on all of the parameters — not just utm_source, but gbraid, wbraid, ttclid, _kx, mc_eid? If you want to answer “probably” with “certainly”, cut the problem off before it reaches any of them.

The Fix: Strip Them at the Edge

I published a small repo for this: cfrules — strip tracking parameters before the origin, keep them in the browser. It’s a tip I’ve been showing around for a while — including during my workshop at WordCamp Europe.

The first version was a single Transform Rule: an If expression that matched known tracking parameters and a Then regex_replace() that removed them. That version is the one that broke (see the correction above), and it can’t be fixed inside a Transform Rule — one regex_replace() per expression, no nesting, and no way to touch the server’s response.

The current version is a Worker — under a hundred lines of plain JavaScript that run at Cloudflare’s edge:

  • it removes marketing parameters (utm_*, gclid, gad_source, fbclid, msclkid, ttclid, …) from GET/HEAD requests in any position and any number, and leaves the parameters you keep byte-for-byte untouched;
  • it asks your server for the clean URL, so /blog/post?id=42&utm_source=newsletter&fbclid=AbC123 reaches the cache and the origin as /blog/post?id=42;
  • if the server answers with a redirect to the same site (www ↔ apex included), it appends the stripped parameters back to the Location header;
  • redirects to other sites — payment gateways, for example — pass through untouched.
browser ── GET /mug?gclid=G&a=1 ──▶ Worker ── GET /mug?a=1 ──▶ server
                                                                 │
browser ◀── 301 /mug/?a=1&gclid=G ── Worker ◀── 301 /mug/?a=1 ───┘
                        ▲ restored

One cache entry per page instead of one per campaign link — and the click ID survives the trip.

It still works on the Free plan, with one difference worth knowing: a Worker counts requests (100,000 per day per account on Free). The repo’s deploy script sets the route to fail open — over the limit, traffic goes straight to the server with the full URL, just without the cache gain — and excludes /wp-content/, /wp-admin/ and friends so static files don’t count. On the Pro plan the same code runs as a Cloudflare Snippet, which only fires on URLs that carry tracking parameters.

”Wait — Won’t This Kill My Attribution?”

Fair question — and the one I originally got wrong.

What I wrote first: this is a rewrite, not a redirect, so the browser keeps the full original URL and analytics never notice. That is true only when the server answers with a 200. When the server answers with a redirect, it builds the new address from the URL it received — the stripped one — and the browser follows it. Parameters gone. I reasoned my way to the claim instead of testing it against real redirects.

The Worker closes that gap: it sees the redirect on the way back and re-attaches the parameters. With that in place, the short answer is: no, as long as your attribution is client-side — and it almost certainly is. Every mainstream tracker reads the URL in the browser:

  • GA4 (gtag.js) reads utm_* from location.href.
  • Meta Pixel picks up fbclid client-side.
  • Google Ads, TikTok, Klaviyo, Mailchimp — same story, JavaScript reading the browser URL.
  • WooCommerce Order Attribution (core since 8.5) is client-side too: sourcebuster.js reads the URL in the browser, stores the source in cookies, and checkout reads the cookies.

There’s also a point most people miss: if you run full-page caching, your server already wasn’t seeing those parameters in any useful way. A cache hit never executes PHP. Any “server-side” attribution that relied on PHP reading the query string was already fiction for every cached visit. Stripping at the edge doesn’t take something away — it makes explicit what was already true: on a cached site, attribution lives in the browser.

Where You Should Be Careful

Honest caveats, because there are a few:

  • Affiliate and referral plugins that read params server-side. The first version stripped ref — the default referral parameter of some affiliate plugins (AffiliateWP, for one). The Worker no longer touches ref and a few other parameters that change page behaviour. It still strips the very generic pp: if your site uses it for anything real, remove it from the list in worker/src/index.js.
  • Test redirects, not just pages. Check a campaign link on the non-canonical host (apex vs www) and on a URL without its trailing slash, and confirm the final address still carries gclid. That’s the test I didn’t run the first time.
  • Server-side personalization. If a landing page changes its content in PHP based on utm_campaign, it will stop seeing it. (On a cached page it already couldn’t do this reliably — another reason that pattern is a trap.)
  • Log-based campaign analytics. Your access logs get cleaner, which is the point — but if you mine logs for UTM data, that data ends here.
  • Rules that run before the Worker. If you already have Transform or Redirect Rules touching the query string, check how they interact under Rules → Overview.

None of these apply to a typical WooCommerce store running GA4 plus a pixel or two. Review the parameter list against your own known query params once, then deploy.

One Worker, Tested

Clone the repo, run the tests (they cover parameter position, non-adjacent parameters, same-site and cross-site redirects, encoding), list your hosts — apex and www — and deploy: github.com/mateusz-zadorozny/cfrules. Then test with a real campaign link that triggers a redirect, not only with ?utm_source=test on the homepage.

Think of a cinema where every guest bought a ticket through a different promo — newsletter, Facebook ad, Google. The usher tears off the promo stub at the door: each guest keeps their half, so marketing still knows exactly which promo sold which seat. But inside, everyone watches the same single screening. Without the usher, you’d be running a private screening per promo code — or worse, performing the film live, from scratch, for every “promo” guest. The first usher had one flaw: when a guest was sent to a different hall, their half of the stub stayed at the door. The new one walks them over and hands it back.

Stripping tracking params is one small piece of a cache setup that actually holds under campaign traffic. If you’d rather have someone tune the whole stack — cache keys, edge rules, server-level caching — that’s the kind of thing I do in WooCommerce Care.

M
Written by

Mateusz Zadorozny

SHIFT64 Founder. WooCommerce performance specialist helping store owners achieve faster load times and better conversions.