Disclosure: this post contains links to paid products and affiliate programs. I may receive compensation if you sign up or buy through them. Pricing shown is verified against the vendor's public pricing page.
For a few weeks my analytics told me a comfortable story: 518 humans had clicked an outbound link in the last 30 days. Then I put that number next to a different one from the same database — 134 page views — and the story fell apart.
Nobody clicks a link on your site more times than they load a page on your site. A ratio above 1.0 is not a suspicious trend to monitor. It is arithmetic proof that the tracker is wrong.
Mine was at 3.9.
The setup
The site is a static build on Cloudflare Pages. Outbound links do not point straight at the destination — they go through a redirect worker at /r/{slug}, which records the click server-side and then 302s the visitor onward. Standard pattern, and it survives ad blockers, which is why I used it.
Each click row stores a source_page so I know which page produced it. The worker fills it like this:
const sourcePage = request.headers.get("Referer") || `cta:${ctaId}`;
The fallback exists for a real reason. Some browsers and privacy settings strip the Referer header, and I did not want those clicks landing in the database as anonymous rows I could never attribute. So if there is no referer, fall back to the CTA id, which I already have.
Downstream, the bot filter decided whether a click was human. Its reasoning, roughly: a real browser on my site sends context, so a row that has a source_page came from a browser, and an empty one is suspect.
-- what "human" used to mean
WHERE source_page IS NOT NULL AND source_page != ''
Read those two snippets together and the bug is obvious. It was not obvious to me for weeks, because they live in different files written on different days.
Why the fallback poisoned the filter
ctaId comes from the query string. The link in the HTML is literally:
<a href="/r/wati?cta=best-whatsapp-wati">Try Wati</a>
A crawler fetching that URL sends no Referer. So the worker takes the fallback branch and writes source_page = "cta:best-whatsapp-wati".
That is a non-empty source_page. Which is exactly what the filter accepted as proof of a browser.
The fallback was not recording "a human whose referer was stripped". It was recording "the value that was already sitting in the href" — a value any crawler following the link reproduces perfectly, because it is in the markup. I had built a bot detector whose evidence of humanity was a string the bot could read off the page.
The fix
One condition. A real browser referer is an absolute URL, so require that shape instead of mere presence:
-- source_page now has to look like a referer, not like something copied from the href
WHERE source_page LIKE 'http%'
Reclassifying the whole table moved 518 "humans" down to 218. Ratio went from 3.9 to 1.6.
1.6 is still wrong, and that is the useful part
The honest reading of 1.6 is "still inflated, just less". Plenty of crawlers do send a Referer, so the new rule catches the lazy ones and misses the polite ones. I did not want to ship a number I would have to re-litigate every month, so the reporting script now prints both figures and the ratio between them:
"reality_check_30d": {
"page_views_js": 134,
"redirect_clicks_human": 218,
"inflation_ratio": 1.6,
"verdict": "inflated"
}
That block is deliberately annoying. As long as the ratio sits above 1.0, every report I read carries a note that the click number is not trustworthy. A metric that announces its own error bar beats a metric that quietly looks fine.
What I would generalise from this
Cross-check server-side counts against a client-side one. Server-side tracking is popular precisely because ad blockers cannot stop it — but that same property means bots cannot be stopped either. The JavaScript page_view event undercounts (blockers, no-JS) while the server-side click count overcounts (bots). Neither is truth, and the gap between them is the diagnostic. One number alone would never have told me anything was wrong.
Write down the invariants your data must satisfy. Not thresholds like "alert if clicks drop 20%" — invariants, things that cannot be true in a working system. Outbound clicks ≤ page views is one. They cost nothing to assert and they fail loudly the moment a fallback quietly changes meaning.
Watch fallback values that come from the request. A default is fine when it encodes something you know independently. It is dangerous when it echoes attacker- or crawler-controlled input back into your data, because downstream code cannot tell the echo from the real thing. My fallback did not fabricate the value — it copied it from the href, which made it indistinguishable from the genuine article.
The bug is rarely inside one file. Both snippets were reasonable in isolation. Neither author — both of them me — was thinking about the other. The defect lived in the seam, and the only thing that surfaced it was comparing two numbers that had no business disagreeing.
I keep a public verified pricing index for automation and messaging tools, which is what the redirect endpoint was tracking clicks on in the first place. If you want the arithmetic-check idea without the write-up, the short version is: find two metrics in your system that must hold an order relationship, and assert it.
Top comments (0)