Skip to content

Most of our search traffic was never human

By Oleg Sidorkin, CTO of Cinevva

Our organic search traffic is growing, and it has been all year. That part is real. But in early July the line jumped in a way that didn't line up with anything we had shipped, and a jump with no cause behind it is worth an afternoon.

We went looking and found a headless-Chrome fleet arriving with a Google referrer.

80.6%of all sessions in the last 30 days were automated
89%of Google organic sessions were fake
1.7%of Google Ads sessions were fake
34.2%of "bounces" turned out to be real readers

The shape matters more than the peak

100%75%50%25%0MarAprMayJunJul89%Share of weekly sessions flagged as automated

Through spring the fake share drifted between a third and two thirds of sessions. That's roughly the background noise any public website lives with.

In late June it climbed and kept climbing. Measured from the first week of April, real sessions grew about 5.5x by mid-July. Automated ones grew about 18.6x.

The growth underneath is ours. The fleet sat on top of it, which is what made this harder to catch than ordinary junk traffic. A fake signal riding a real trend looks like acceleration.

What made it hard to see

We watch these numbers closely, and anything that moves without an explanation gets a query written against it. The reason this one took real digging is that our session rows genuinely could not tell the two populations apart.

Engagement was being inferred from each session's last_activity_at timestamp. On a single-page visit, that field never moves after the first write.

So a bot that loads one page and leaves writes the same row as a person who lands on a guide and reads it for five minutes. Bounce rate, time on site, engagement, all of it came off a field that holds no information for single-page sessions. This isn't unusual. It's the default in most homegrown analytics, and it's the first thing I'd check in yours.

The answer was to stop inferring from the session row and read the exit beacon instead, which reports actual time on page and scroll depth from the client. With evidence instead of inference, the two groups separated immediately.

FlaggedEveryone else
Scrolled at all1.8%56.5%
Belongs to a logged-in account0.05%29%

That gap is our evidence that the classifier describes something real, rather than labeling quiet visitors as robots.

What actually gives a fleet away

The user agent is the worst place to look, because it's the cheapest thing to change. The best signal turned out to be the graphics card.

Headless browsers in a data center have no GPU, so they fall back to software rendering, and WebGL reports the renderer string without being asked twice. SwiftShader, llvmpipe, Mesa OffScreen. Real visitors have real GPUs.

SignalWhat it catchesShare of our evidence
Software GPU rendererNo physical graphics card90.5%
Data center or proxy ASNRented network origin58%
navigator.webdriverAutomation flag left on10.4%

Network origin helped less than you would expect. Roughly four in ten of these sessions exit through genuine consumer ISPs. Residential proxies are cheap now, and any rule assuming bots come from AWS will miss nearly half of them.

The rest came from physical impossibilities in what the client reported about itself. A Chromium user agent always ends with a Safari token matching its AppleWebKit token, frozen at 537.36 since 2013, and this fleet emitted typos, Safari/537.35 and .38. A phone user agent claimed a 1600x1200 screen. Another variant claimed a perfect 1600x1600 square. One claimed 1200x3000, a portrait desktop monitor at a 2.5:1 ratio that nobody manufactures.

Every one of those is somebody's emulator config, and none of them show up unless you go looking.

The fleet adapted every time we flagged it

The first profile was Linux Chrome, a Google referrer, America/New_York, 1920x1080. We wrote a rule.

A variant came back at 800x600 on UTC. We widened the rule.

Then a Chinese cohort appeared riding China Mobile and Unicom residential addresses with pinned browser builds, including Chrome 99 from March 2022 and the Chrome DevTools device-emulation default, a 2015 Nexus 5 running 2018 WeChat. Those exit through consumer ISPs, so every network rule we had was useless against them.

Then a US flavor turned up on Cox and Comcast with a current, entirely plausible Chrome 146 user agent at 1920x1080. Nothing about that fingerprint is wrong, and one session at a time it's indistinguishable from a real visitor.

So the last detector we built doesn't look at individual sessions. It groups the previous 48 hours by country, user agent, screen size, and traffic source, then looks for clusters of at least 20 anonymous sessions where 95% or more never read for three seconds and never scrolled a pixel.

Real visitors don't cluster that tightly. Twenty strangers on an identical browser at an identical resolution who all leave without reading anything are one machine with twenty addresses. Anyone in the cluster who did read or scroll stays human, and sessions attached to a real account are never flagged at all.

Fingerprint rules decay, since beating them only takes a config change. Behavioral rules last longer, because faking the behavior costs the operator real money.

The audit found the opposite error too

While we were pulling fake visitors out of the numbers, we found real ones we had been discarding.

Once we had genuine read times, it turned out 34.2% of what we'd counted as human bounces were people who read for 30 seconds or more, or scrolled past half the page. A third of those "failures" were the content working. Chinese readers arriving from Bing were spending five minutes on a single guide and getting logged as bounces.

The numbers were off in both directions at once, and both errors point the same wrong way. Together they tell you your traffic is large and shallow when it's actually smaller and deeper.

The channel we paid for was the honest one

Share of sessions flagged as automated, by sourceGoogle organicDirectBingReferralAI assistantsGoogle Ads89%80.4%7%3.7%2.8%1.7%025%50%75%100%

Last 30 days. Google organic accounted for 63.7% of all sessions in that window.

Every instinct in growth runs the other way. You assume paid is where you're being defrauded and organic is where you earned it. Ours was inverted.

If we had reallocated budget on the July numbers, we would have moved money away from the channel actually delivering humans and into producing more content for a scraper.

Why it's worth an afternoon

The number itself is only a number. Sitting downstream of it are the pages we write next, the locale we invest in, whether a campaign survives review, and whether the quarter looked good enough to keep doing the same thing. All of that can be moved by a stranger with a cheap proxy pool.

If you run a small site, go look at your GPU renderer strings and your exit beacons before you trust your own growth curve.

And treat our numbers as a floor rather than a verdict. "Human" here only means "not yet caught," the rules were written from fingerprints we happened to observe, and we still don't know who runs this fleet or what they want with a game-creation site.


Related: