Writing · September 5, 2026

Most of my traffic was my own server

I launched this site with no analytics script and read the raw access logs instead. Sixty percent of the traffic turned out to be the server talking to itself.

There is no analytics script on this site. No cookies, no consent banner, nothing to disclose. Apache already writes down every request it serves, so once a week I pull the logs down to my laptop and run GoAccess over them.

The first report said 2,522 requests in a week. For a site nobody knows exists, that seemed like a lot. It was.

Where it actually went

  • 1,507 were self-requests and scanners whose user agent never says "bot"
  • 1,394 were an old WordPress install on the same domain, and probes for its login page
  • 66 were static assets and JavaScript chunks
  • 50 were Next.js prefetches of my own pages
  • 728 lines survived all of it

The single largest source was a backup plugin on that WordPress install, calling its own admin endpoint 1,028 times. Forty-one percent of my website's traffic was my website, talking to itself, on a schedule.

Then it gets worse

Of the 728 that survived, 260 hit the homepage. Those came from 202 distinct addresses, all with ordinary Chrome and Safari user agents, at most seven requests each. Not one went on to a second page.

The interior pages, over the same week: twelve on the work page, ten on about, nine on writing, seven on the resume.

Real people who land on a homepage go somewhere. Two hundred visitors who arrive, look, and leave without a single click are not an audience. They are a scan wearing a browser's clothes, and no user agent filter will catch them, because they are not lying about anything a filter can check. The shape gives it away, not the string.

The honest number for the week is somewhere near fifty pageviews, and most of those were me, checking that the deploy worked.

Four different problems wearing one number

They are worth separating, because only one of them is what people mean by "bot traffic".

Infrastructure counted as audience. The backup cron was not deceiving anyone. It was doing its job. It just was not a person.

A naming accident. My host leaves the previous site's logs in a directory with a .bak suffix. My glob matched it, and I folded in 1,431 requests to a blog that stopped existing fifteen years ago. Those lines parse perfectly, which is exactly what makes them dangerous.

My own bug. Next.js fetches a payload for a route during client-side navigation, on top of rendering it. Those are 200s on real paths. Every navigation was counted twice, by me, in the filter I wrote to make the numbers trustworthy.

Actual scanners. The smallest of the four, and the only one I had planned for.

The part that is not a technique

I have spent years telling stakeholders that a number going up is not the same as the thing you wanted going up. It is bracing to run that check on your own site and find that your largest traffic source is a cron job, your second largest is a blog you forgot about, and a chunk of the rest is a mistake you introduced while trying to be careful.

None of this is an argument against analytics products. A hosted script would have filtered most of it out before I ever saw it, which is the whole reason to pay for one.

It is an argument for looking at the raw data once. Not forever, and not instead. Once, early, before the dashboard has taught you what to expect. A dashboard can tell you what it counted. It cannot tell you what it threw away, or what it quietly counted twice.

← All writing