How to Preserve UTM Parameters Through Redirects Without Losing Attribution
A practical guide to forwarding campaign query strings through redirects, testing every hop, and keeping GA4 attribution useful.
You can build a clean campaign URL and still lose attribution before the visitor reaches the landing page.
The usual culprit is not GA4, the ad platform, or the frontend tag. It is a redirect that sends the browser to the right page while quietly dropping the query string.
This matters for developers because redirects often live in infrastructure: Nginx, Apache, edge middleware, short-link tools, CMS routing, or application code. If one hop in the chain forgets ?utm_source=..., the final page cannot recover it.
This guide walks through redirect patterns that preserve UTM parameters without creating duplicate keys, unsafe destinations, or analytics noise.
The Failure Mode
Start with a tagged campaign URL:
https://go.example.com/spring-sale?utm_source=linkedin&utm_medium=social&utm_campaign=spring_sale
A broken redirect might return:
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/spring-sale
The redirect is valid HTTP. The user lands on the right page. But the destination URL no longer contains utm_source, utm_medium, or utm_campaign, so GA4 sees a pageview without the campaign context you expected.
The correct redirect preserves the query string:
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/spring-sale?utm_source=linkedin&utm_medium=social&utm_campaign=spring_sale
Status code correctness and attribution correctness are separate checks. A 301, 302, 307, or 308 can all preserve or drop parameters depending on how the Location header is built.
Choose the Redirect Type First
Use a permanent redirect such as 301 or 308 when the source URL has permanently moved. Use a temporary redirect such as 302 or 307 for short-lived campaigns, experiments, or links whose destination may change.
Do not use a permanent redirect just because it works in a quick test. Browsers, crawlers, and intermediate systems may cache permanent redirects, which makes later campaign changes harder to reason about.
The query-forwarding rule should be intentional either way: if the campaign URL arrives with tracking parameters, every redirect hop that owns the route should preserve them.
Server Examples
Nginx
For a destination with no existing query string, append the incoming query safely with $is_args$args:
location = /spring {
return 302 https://www.example.com/spring-sale$is_args$args;
}
$is_args expands to ? only when the incoming request has a query string. $args contains the raw incoming query string. That avoids producing a trailing ? for untagged visits.
If the destination already has parameters, do not add a second question mark:
location = /spring {
return 302 https://www.example.com/spring-sale?region=us&$args;
}
That simple version is fine only if you have tested the empty-query case and accept the trailing &. For stricter output, split tagged and untagged cases or move the logic into application/edge code where URL parsing is clearer.
Apache
With mod_rewrite, QSA appends the original query string to the replacement URL:
RewriteEngine On
RewriteRule ^spring$ https://www.example.com/spring-sale [R=302,L,NE,QSA]
QSA means query string append. It is especially important when the replacement URL includes its own query string, because without explicit handling the original parameters may be replaced.
Application Code
In application code, parse URLs instead of concatenating strings:
app.get("/spring", (req, res) => {
const incoming = new URL(req.originalUrl, "https://go.example.com");
const destination = new URL("https://www.example.com/spring-sale");
incoming.searchParams.forEach((value, key) => {
destination.searchParams.append(key, value);
});
res.redirect(302, destination.toString());
});
This preserves duplicate keys exactly as received. If you prefer one value per UTM key, use set() for known campaign keys instead of append():
for (const key of ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]) {
const value = incoming.searchParams.get(key);
if (value) destination.searchParams.set(key, value);
}
The important part is that URL encoding is handled by URL and URLSearchParams. Do not build redirect URLs by stitching raw request values into a string.
Decide Your Duplicate-Key Policy
Duplicate parameters are easy to create:
https://www.example.com/spring-sale?utm_source=newsletter&utm_source=linkedin
Different tools may choose the first value, the last value, or expose both. That ambiguity is bad for attribution.
Pick a policy before launch:
- Source-owned UTMs win: the incoming campaign parameters replace destination defaults.
- Destination-owned UTMs win: the landing page keeps its predefined campaign values.
- Reject duplicates: the redirect refuses or normalizes links with conflicting campaign keys.
For most campaign redirects, source-owned UTMs are easiest to reason about because the distributed URL defines the campaign. There are exceptions, especially when a destination uses query parameters for product filters, locales, or experiments.
Multi-Hop Redirects
Campaign links often pass through several systems:
ad platform -> short link -> branded redirect domain -> landing page
Attribution survives only if every hop preserves the query string. If the short link keeps the parameters but the branded redirect drops them, the final page still loses.
Trace the whole chain:
curl -sSIL \
'https://go.example.com/spring?utm_source=linkedin&utm_medium=social&utm_campaign=spring_sale'
Read every Location header. Confirm:
- The expected number of redirects occurs.
- Each hop stays on an expected hostname.
- HTTPS does not downgrade to HTTP.
- Required UTM parameters are still present.
- Existing destination parameters are merged intentionally.
- Duplicate UTM keys are absent or match your policy.
To follow the redirect chain and limit loops:
curl -sSIL -L --max-redirs 10 \
'https://go.example.com/spring?utm_source=linkedin&utm_medium=social&utm_campaign=spring_sale'
An unexpectedly long chain is not just inefficient. It increases the chance that one system will strip or rewrite parameters.
Open-Redirect Safety
Avoid patterns like this:
res.redirect(req.query.next);
That can turn a tracking route into an open redirect, where an attacker sends users through your domain to a malicious site.
Use an allowlist for destinations:
const allowedDestinations = {
spring: "https://www.example.com/spring-sale",
pricing: "https://www.example.com/pricing"
};
const target = allowedDestinations[req.params.slug];
if (!target) return res.sendStatus(404);
const destination = new URL(target);
Then copy only the parameters you intend to support. Many teams preserve utm_*, gclid, fbclid, and a few product parameters while rejecting unrelated keys. The right list depends on your analytics and privacy requirements.
Production Checklist
Before a campaign goes live, test these cases:
/spring?utm_source=linkedin&utm_medium=social&utm_campaign=spring_sale
/spring?utm_source=newsletter
/spring
/spring?utm_campaign=spring%20sale
/spring?utm_source=one&utm_source=two
Verify at two layers.
First, inspect network behavior with curl or browser developer tools. The final URL should contain the expected parameters, encoded correctly, with no surprise hostnames or loops.
Second, test the analytics result. In GA4 DebugView or realtime reports, confirm the visit is attributed to the expected source, medium, and campaign. A correct final URL is necessary, but analytics configuration can still affect what appears in reports.
Managing Redirects at Scale
A few hand-written rules are easy to audit. Hundreds of campaign routes, vanity domains, migration URLs, and partner links need a process.
Treat redirects as managed configuration:
- Store source URL, destination URL, status code, owner, and query-forwarding policy.
- Test representative tagged and untagged URLs after bulk changes.
- Monitor failed destinations, redirect loops, and unexpected hostnames.
- Keep campaign links stable even when the landing page changes.
- Document whether
utm_*, ad click IDs, and product parameters are forwarded.
If you use a redirect management platform, verify query forwarding explicitly. Do not assume every tool preserves parameters by default. For a deeper walkthrough, see this guide on how to preserve UTM parameters through redirects.
Top comments (1)
that's very helpful!
thanks!