Visit counters: bots that execute JavaScript
On the morning of November 4, 2026, the VosPosts visit counter displayed an impressive figure: 3,352 “human” visitors in one day, followed by another 2,272 before 1 a.m., on a site that normally receives around 400 visits daily. No article had been shared and no link had gone viral. They were simply bots, and our counter mistook them for readers.
Here is how we discovered it, why the old rule “if JavaScript runs, it must be human” no longer works, and how to find these fake visitors in your server logs.
The trap: “JavaScript ran, so it must be human”
Many homemade counters, including ours, rely on a simple idea: the server records the requested page, then a small browser script sends a second request to “certify” the visit. Older bots merely downloaded HTML without executing anything, so the second request never came and they stayed out of the statistics.
That filter is now full of holes. Today, many bots execute JavaScript just like a real browser:
- Search engines render pages to understand them: Googlebot uses an up-to-date Chromium, and Baiduspider-render, Applebot, Bytespider (ByteDance) and meta-webindexer also appear.
- The AdSense advertising bot (Mediapartners-Google) visits pages to analyze their content.
- Modern scrapers control real headless browsers: headless Chrome, Puppeteer, Playwright or lighter engines such as Lightpanda, designed for automation and AI agents.
To a counter based on script execution, all these visitors look like readers. Most strikingly, the problem did not begin with the attack: examining a perfectly ordinary day, November 3, we found that 446 of the 621 visits counted as human actually came from bots. More than seven out of ten.
Signs of a headless-browser scraper
The November 4 spike had all the hallmarks of organized scraping. None of these clues proves anything alone; together, they leave little doubt.
A different IP address for every page
Every page request came from a different IP, spread across the globe. This is the signature of residential proxies: networks that route requests through household connections, often without their owners knowing. As a result, blocking IP addresses is pointless: there are thousands, and they belong to real homes.
Browsers outdated by years
User agents cycled through a short list: Chrome 118–120, Firefox 120 and 121, and Edge 119 and 120. These versions date from late 2023. Yet Chrome and Firefox update automatically roughly every four weeks; our real readers arrived with Chrome 148–156. One visitor using a three-year-old browser is plausible, but thousands in one night indicate a fixed user-agent list copied into a scraping tool.
Amid that rotation, Lightpanda/1.0 even appeared without any disguise.
Behavior that was too consistent
- Desktop computers only, no mobile devices, although most real traffic comes from phones;
- no referrer: nobody arrived from Google, social media or another website;
- 2,340 distinct searches in the site's internal search engine, designed to extract as many pages as possible;
- 24,772 requests that day, accounting for 42% of all server traffic.
The hidden risk: advertising
A headless browser executes everything, including loading ad placements: that day, the bot loaded our ad blocks 2,939 times. For an AdSense publisher, this matters: bot-generated impressions count as invalid traffic, which Google deducts from earnings and which, if repeated, can jeopardize an account. An inflated counter is not merely a matter of ego.
Spotting these bots in your logs
Apache or Nginx access logs in “combined” format contain everything needed. A few commands will help sort things out. First, the most common user agents:
awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn | head -30Then check the distribution of Chrome versions. On a normal site, most traffic clusters around the four or five latest releases; a large group of old versions stands out immediately:
grep -oE 'Chrome/[0-9]+' access.log | sort | uniq -c | sort -t/ -k2 -nHourly request volume pinpoints the start and end of a wave:
awk '{print substr($4, 2, 14)}' access.log | sort | uniq -cFinally, for a suspicious signature, compare the number of requests with the number of distinct IPs. If the figures are nearly equal, you have found your proxy network:
grep -c 'Chrome/119.0.0.0' access.log
grep 'Chrome/119.0.0.0' access.log | awk '{print $1}' | sort -u | wc -lA useful detail: since 2022, Chrome has sent only its major version number followed by .0.0.0. So Chrome/119.0.0.0 is a normal format; the number 119 is what gives the bot away.
What we implemented
Stop counting, without blocking
First layer: the counter decides server-side whether a visit deserves to be counted. A visit is no longer recorded as human if the user agent is empty, identifies itself as a bot (bot, spider, crawler, HTTP tools such as curl or Python, headless browsers), or claims a Chrome or Firefox version more than two and a half years old. The threshold is recalculated based on the date, avoiding manual version updates:
$jours = (time() - mktime(0, 0, 0, 12, 5, 2023)) / 86400; // Chrome 120, décembre 2023
$version_actuelle = 120 + (int)floor($jours / 28); // une version toutes les 4 semaines
if (preg_match('/Chrome/(d+)./', $ua, $m) && (int)$m[1] < $version_actuelle - 32) {
return true; // navigateur trop ancien pour être un lecteur réel
}One important safeguard: a signed-in user is always counted, regardless of their browser. The filter targets strangers, not loyal members who forgot to update an old computer.
Reject scrapers, allow search engines
Second, more aggressive layer: the server now returns a 403 error for scraper signatures, with an explicit exception for legitimate crawlers. In Apache, it takes only a few lines:
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} (Lightpanda|HeadlessChrome) [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Chrome/1(18|19|20).0.0.0 [OR]
RewriteCond %{HTTP_USER_AGENT} Firefox/12[01].
RewriteCond %{HTTP_USER_AGENT} !(bot|spider|crawl|google|bing|facebook) [NC]
RewriteRule ^ - [F]The first three conditions are joined by [OR]; the last is added with an implicit “and”: one of the signatures is blocked unless the user agent claims to be a search engine or social network. After deployment, we checked both sides: the scraper receives a 403, while a recent Chrome, an iPhone, Googlebot, Bingbot and the AdSense bot still get their pages.
Blocking Googlebot by mistake can cost far more than inflated statistics. Test every rule against search-engine user agents before deploying it to production.
Limitations to keep in mind
A user agent can be forged in one line. Once this scraper claims to be Chrome 155, these rules will no longer catch it. This is not permanent protection but basic hygiene: it removes lazy bots, which make up the majority, and makes the figures readable again. Going further requires combining signals (no referrer, one page per IP, no mouse movement or scrolling) or using a specialized bot-detection service.
The real lesson lies elsewhere: a counter that lies only once a year during a spectacular spike probably lies quietly on every other day too. If you have never inspected your access logs, spend ten minutes running the commands above on an ordinary day. The result may surprise you.
French
German
English
Spanish
Hindi
Italian
Japanese
Korean
Norwegian
Chinese

