All articles
UTM Governance

How broken links and bad UTMs waste paid media spend

Nobody budgets for the landing page being broken, because nobody thinks of the landing page as production. The ad platform does not care either way — it will spend at exactly the same rate into a page that no longer works.

Daniel OkaforMarketing Engineering Lead8 min read

The deploy went out at 17:40 on a Friday. It was a good deploy. It fixed a rendering bug on the product template, and it moved seasonal collections from /collections/spring-sale to /c/spring-sale — with a redirect, because whoever wrote it was being careful.

return 301 https://www.acme.com/c/spring-sale;

Nobody noticed until Monday. And here is the part that makes this expensive rather than merely annoying: nothing was down. The page loaded all weekend. It loaded quickly. Every uptime check was green, the ads kept serving, people kept buying things.

That Location is a literal string. It has no query string on it. Every click that arrived carrying utm_source=meta&utm_medium=paid_social was handed a brand new URL with none of that attached, and the analytics tag on the far side had nothing to read. One command would have shown it:

$ curl -sL -o /dev/null -w '%{url_effective}\n' \
    'https://go.acme.com/spring?utm_source=meta&utm_medium=paid_social'

https://www.acme.com/c/spring-sale

The parameters went in. They did not come out. Nobody ran the command, because there was no reason to — nothing was broken.

The arithmetic nobody runs

Start with the simpler failure, the one everyone can picture: the destination is just gone. A 404, a sold-out product page pulled by the CMS, an expired certificate. Then price the weekend.

LineWorkingResult
Campaign budgetstated up front£40,000 over 14 days
Daily pace£40,000 / 14£2,857
Hourly pace£2,857 / 24£119
Broken Fri 17:40, found Mon 09:15elapsed63.5 hours
Media spent into a dead page63.5 × £119£7,560
Clicks that bought, at £1.20 CPC£7,560 / £1.206,300
Orders not placed, at 2% CVR6,300 × 0.02126
Revenue not made, at £180 AOV126 × £180£22,680
Worked example — substitute your own numbers

Thirty thousand pounds, from a config change, over a weekend, on a campaign whose entire budget was forty.

Now be honest about which number is soft. The last one, by a distance — some of those 126 people would have come back on their own, and a few platforms throttle delivery when a landing page starts failing. Don't lean on it.

The £7,560 is not soft at all. That is money that left the account and bought a screenful of nothing. It's as certain as any number in marketing gets, and it's the one nobody calculates, because a landing page isn't treated as production. No on-call rotation. No rollback plan. Frequently no monitor.

The failure that costs more

Now back to Friday, where nothing was broken.

All 6,300 clicks landed. All 126 orders were placed. Not a penny of media was wasted. The campaign worked exactly as designed — and it showed up in the report as almost nothing, because every session arrived with no campaign information attached.

This is where it gets genuinely expensive. Those sessions don't get filed under 'direct' and quietly ignored. GA4 uses last non-direct click attribution by default: a session with no campaign data gets credited to the most recent non-direct source already known for that user. Organic search. An email from three weeks ago. Whatever was there before.

So the £22,680 doesn't vanish from the report. It gets awarded to a channel that did not earn it.

The second-order cost is the real one

A 404 costs you a weekend of media. A stripped parameter costs you the next planning cycle. Paid social looks dead, so it gets cut. Organic looks unstoppable, so it gets funded. Both conclusions are wrong, both are supported by clean-looking data, and nobody will question either of them — because there is no gap in the report to notice. The number isn't missing. It's on the wrong row.

The mechanics of how parameters get eaten — nginx, Apache, interstitials, geo-redirects, partner click wrappers — are covered properly in the complete guide to campaign link tracking. What matters here is that the cost of the invisible version is larger than the cost of the obvious one, and it accrues to a budget line that will never be traced back to a redirect rule.

The 200 that lies to your monitor

The standard defence is an uptime check, and the standard uptime check asks one question: did it return 200? That is not sufficient, and the gap between it and the truth is where most of this money lives. A soft 404 is a page with no content that says everything is fine.

$ curl -s -o /dev/null -w '%{http_code}\n' \
    'https://www.acme.com/collections/spring-sale'
200

$ curl -s 'https://www.acme.com/collections/spring-sale' \
    | grep -io 'no longer available'
no longer available

Status says healthy. Body says dead. Your monitor is green and your ads are pointing at a page that apologises.

It happens constantly, for boring reasons:

  • Single-page apps render their own 404 client-side. The server returned 200 for the shell before the router ran. Every route on the site is 'up'.
  • Ecommerce platforms serve a friendly 'this product is unavailable' page rather than an error — on purpose, because it's better for the customer and worse for you.
  • A CDN returns its own branded error page with a 200, or the origin 500s and the CDN serves a cached 200 from an hour ago.
  • The page renders, but a geo rule sends half your audience to the wrong currency. Perfectly healthy. Completely useless.

A check worth having asks three questions instead of one: is the final URL the one I expected, are the parameters still on it, and does the body contain the thing that proves the page is real — a price, a product name, the form. Anything else is theatre.

These get treated as two separate problems — links break, and separately, someone is sloppy with naming. They're the same problem. A malformed UTM is one of the ways a link breaks.

Here are four links for one campaign:

?utm_source=Facebook&utm_medium=Social&utm_campaign=Spring Sale
?utm_source=facebook&utm_medium=paid-social&utm_campaign=spring_sale
?utm_source=fb&utm_medium=cpc&utm_campaign=springsale26
?utm_source=facebook.com&utm_medium=Paid_Social&utm_campaign=spring%20sale

Look at the first one. There's a raw space in Spring Sale. That isn't a tidiness issue — it is a functional defect. Paste that into a plain-text email, an SMS, or most chat clients, and the auto-linker terminates the URL at the space. The recipient gets a tappable link ending in utm_campaign=Spring and the loose word Sale sitting next to it as text. If the space had landed anywhere in the path rather than the query, they'd get a 404.

The fourth one has a different bug wearing a suit: utm_source=facebook.com is a hostname, not a source, and it will sit in your report forever as a fifth row that no filter catches because it looks plausible.

One link, built once:

?utm_source=facebook&utm_medium=paid_social&utm_campaign=spring_sale_2026&utm_content=hero_video

Better: don't put the parameters in front of a human at all

The most reliable fix for a UTM that gets mangled in transit is to stop showing it to anyone. Put the parameters on the redirect's destination, not on the link you paste. go.acme.com/spring has nothing to truncate, nothing to wrap, and no space to break on — the parameters get applied on the far side of the hop, where no email client can reach them. Which domain you run that redirect on is a separate decision with its own consequences.

Four rows that should be one

The slow leak is less dramatic than the weekend outage and, over a year, more expensive.

UTM values are case-sensitive strings stored exactly as typed, so those four links produce four rows. Split a campaign four ways and each fragment lands somewhere between noise and rounding error — below whatever threshold makes a human look twice. The campaign that actually worked is now four things that individually look like they didn't.

Nobody catches this in-flight. It gets caught in a spreadsheet at the end of the quarter, by someone merging rows by hand three days before the readout, hoping they found all of them. They didn't. There is no way to know whether they did.

And unlike a broken link, this one is not fixable after the fact. You cannot go back and re-tag a click that happened in March.

Conventions don't work; constraints do

Every team that has this problem already has a naming convention. That's the part people find surprising. The document exists. It is usually good. It is in the wiki, it was written by someone competent, and it is read exactly once, during onboarding.

Because that is not how links get built. Here is how links get built: someone opens last month's campaign, copies the URL out of it, pastes it into the browser bar, and edits it by hand at 16:50 on the day of launch. That is the mechanism. It is how spring_sale becomes spring_sale_v2_FINAL, and no document has ever intervened in it.

A convention asks people to remember. A constraint makes the wrong thing impossible to type. The difference in outcome is total:

Convention (a document)Constraint (a builder)
Medium should be one of: email, cpc, paid_social…Medium is a dropdown. There is no text field
Please use lowercaseThe field lowercases on input
Do not use spacesThe validator refuses to create the link
Follow the campaign naming formatThe campaign is picked from ones that exist
Reuse last quarter's structureTemplates, applied by default
Ask marketing ops if unsureNothing to be unsure about

This is unglamorous and it is most of the value. A UTM builder with templates and validation is not an exciting product surface — it's a dropdown and a regex. It's also the only thing that has ever reliably stopped this, because it moves the fix from the moment of analysis, where it's too late, to the moment of creation, where it's free.

Alerts someone actually acts on

An alert routed to a mailbox nobody opens is not a monitor. It's a record, written in advance, of the fact that you could have known.

The Friday deploy is the case that matters, so design for it. The break happens outside working hours. It is silent. Nothing pages anyone, because nothing is down.

What a monitor has to do to be worth having

  • Check the final URL after redirects, not the link you pasted in.
  • Assert the parameters survived — not just that something returned 200.
  • Assert on body content, so a soft 404 cannot pass.
  • Alert on the redirect chain changing shape, even when it still ends in 200. A new hop that appeared on its own means somebody changed something.
  • Run while the campaign is live, not only before it launches. Friday 17:40 is not before launch.
  • Cover the mobile chain separately. App interstitials and geo rules often only fire on one.

And route it somewhere a person actually is. Slack, or a webhook into whatever you already page from — wherever your team already gets interrupted. The test of an alerting setup is not whether it fires. It's whether anyone was going to see it at 18:00 on a Friday.

Where to start

Take the five campaigns spending the most right now. Run the full chain on each real URL and check the parameters come out the other end. Fifteen minutes.

Most teams find something. Usually old, usually months in place, and usually on the campaign everyone had already concluded didn't work.

Check a link before you ship it.

Paste any URL and get a readiness score, the full redirect chain, UTM detection and a social preview. No signup.

Get the next one by email

Roughly monthly. No filler.

Campaign tracking and link governance, roughly monthly.

How broken links and bad UTMs waste paid media spend | TrackLink