flowsk.com
How-to

How to keep UTMs through a 301 redirect

Most redirect configurations silently drop the query string, and every campaign that passes through one lands as direct traffic. Here is the fix for nginx, Apache, Cloudflare, Rails and Next.js.

Jul 21, 2026· 3 min read ·How-to
Quick answer

nginx: append $is_args$args · Apache: QSA flag · Cloudflare: preserve query string

Redirects drop the query string unless you explicitly preserve it. A campaign that hits http:// instead of https://, or a non-www host, or an old URL, arrives at your landing page with no UTMs at all — and lands in direct traffic.

This is the single most common cause of unexplained direct traffic on sites that use vanity or legacy URLs.

You launch a campaign. The URL works when you click it. A week later the campaign shows almost no traffic, and direct is up by suspiciously the same amount.

Somewhere in your redirect chain, the query string is being dropped.

Why it happens

A redirect tells the browser to go to a new URL. Whether the query string comes along depends entirely on how the redirect is configured, and most default configurations drop it.

Chains are longer than people think. A single campaign click can hit:

http://yoursite.com/promo?utm_source=facebook&utm_campaign=spring
  → https://yoursite.com/promo?utm_source=facebook&utm_campaign=spring   (http→https)
  → https://www.yoursite.com/promo                                        (non-www→www)  ← dropped here
  → https://www.yoursite.com/promo/                                       (trailing slash)

Three hops, and the second one loses everything. The visitor lands on the right page and your analytics records direct traffic.

Finding it

curl -IL "http://yoursite.com/promo?utm_source=test&utm_campaign=test"

Watch the Location header on each hop. The moment ?utm_source=test disappears, you have found the culprit.

Do this for every entry point you advertise, not just the canonical one. The broken hop is nearly always on a URL nobody tests because “the site works.”

The fixes

nginx$is_args$args appends the query string if one exists:

# Wrong — drops everything after the ?
return 301 https://www.yoursite.com$request_uri;

# Right
return 301 https://www.yoursite.com$uri$is_args$args;

Careful: $request_uri does include the query string, so the first line is actually fine for a straight host swap. It breaks when you rewrite the path and build a new URI by hand — which is exactly when people forget.

Apache — the QSA (query string append) flag:

# Wrong
RewriteRule ^promo$ /offers [R=301,L]

# Right
RewriteRule ^promo$ /offers [R=301,L,QSA]

Cloudflare Rules — there is an explicit “preserve query string” toggle in redirect rules. It is off by default in some rule types. Check it.

Rails

# Wrong
get "/promo", to: redirect("/offers")

# Right
get "/promo", to: redirect { |params, request| "/offers?#{request.query_string}" }

Next.jsnext.config.js redirects preserve the query string by default, but a manual NextResponse.redirect in middleware does not unless you build the URL from the original:

const url = request.nextUrl.clone()
url.pathname = "/offers"     // keeps searchParams
return NextResponse.redirect(url)

The chains worth auditing

Redirect Frequently drops?
httphttps Rarely (usually $request_uri)
non-www → www Often
Trailing slash normalisation Often
Legacy path → new path Almost always
Vanity URL → landing page Almost always
Link shortener Depends on the provider
Consent-banner bounce Sometimes

The last four are the ones campaigns actually use, and the ones nobody tests.

Fragments never survive

https://yoursite.com/promo#utm_source=facebook   ← never works

Fragments are not sent to the server at all, so no redirect can preserve them and no server-side capture can read them. Never put campaign data after a #.

The structural fix

Fix your redirects — but also make yourself immune to the next one.

Capture the campaign parameters server-side, on the first request that lands, and write them to the visitor record immediately. Whatever survives the chain is recorded permanently at that moment, before any client-side script, consent banner or subsequent navigation can lose it.

Client-side capture is later and more fragile: by the time JavaScript runs, a consent flow or a framework navigation may already have rewritten the URL.

Testing before you launch

Before any campaign goes live, run the exact URL — with its UTMs, from the actual ad — through the UTM inspector. It follows the redirects for real and shows which parameters survive to the final destination.

Two minutes, and it catches the failure that would otherwise take a fortnight and a confusing report to discover.

Frequently asked questions

How do I know if this is happening to me?

Take a campaign URL, add the UTMs, and open it with curl -IL. Watch the Location header on each hop. If the query string disappears at any step, you have found it.

Which redirects usually break it?

http→https, non-www→www, trailing slash normalisation, legacy path redirects, and link shorteners. Most sites have at least two of these chained.

Do link shorteners drop UTMs?

Some do. Test yours specifically — put the UTMs on the destination URL rather than relying on the shortener to forward them, and you avoid the question.

What about the fragment?

Fragments (#...) are never sent to the server, so a redirect cannot preserve them. Never put campaign data after a hash.

Is there a way to be immune to this?

Capture the campaign parameters server-side on the first request that lands, before any redirect chain finishes. Whatever survives to that point is recorded permanently.

Test your redirect chain

The UTM inspector follows the redirects for real and shows you exactly which hop drops your parameters.

Test a campaign URL

Stop guessing which ad made the sale.

Flowsk Signals stitches the anonymous click to the email to the purchase — first-party, server-side, de-duplicated. One snippet, $29/mo, and every conversion comes with a receipt you can inspect.

Keep reading