codekredo

← All notes

Counting visitors without cookies, from server logs

Most visitor statistics rely on a script in the page and an identifier stored in the browser. Our sites take a different route: once a day, a small program reads the web server's logs and keeps nothing but daily totals. This note explains how that works and where its limits are.

29 October 2026

Why not a tracking script?

A typical analytics setup adds a script to every page. The script runs in the visitor's browser, stores an identifier there and sends each page view to an analytics service – often a third party. In the EU, storing identifiers in the browser that aren't strictly necessary generally requires consent, which is one reason so many sites show a banner before you can read anything.

We wanted something simpler. The web server already writes a log line for every request: it needs that to deliver the site, find errors and fend off attacks. If the statistics come from those logs, nothing extra is collected in the browser, no script has to load, and no data leaves our server for this purpose.

What a log line contains

Each request leaves one line in the access log. It records:

That's enough to answer the questions we care about: which pages are read, roughly how many people read them, and how they found the site. The logs themselves are rotated daily and deleted after ten days at the latest.

From log lines to visitors

Counting page views is easy: every successful request for a real page is one view. Counting visitors is harder without an identifier. Our approach is deliberately modest: for each day, the IP address and the browser identifier are combined into a hash value, and every distinct value counts as one device.

key = hashlib.sha256(f"{ip}|{user_agent}".encode()).hexdigest()
visits[(day, key)].append(path)   # in memory only

The hash only exists in memory while the logs are being read. What gets written to disk afterwards are totals per day: page views per page, the estimated number of visitors and where visits came from. There's no list of visitors, no IP address and no browser identifier in the stored data.

Because the key changes every day, the same person reading on Monday and Tuesday counts twice over the week. That's why our reports call the number an estimate.

Leaving out bots

A large part of all requests isn't from people at all: search engine crawlers, link previews in messengers, uptime monitors and, above all, scanners that probe every site for login pages and configuration files. Counting them would make the numbers meaningless. So a device only counts if it passes a few checks:

Search engine crawlers are counted separately. Many scanners pretend to be Googlebot, so a crawler only counts as genuine if a reverse DNS lookup of its address leads to the search engine's domain and that host name resolves back to the same address – the method the search engines themselves recommend.

host = socket.gethostbyaddr(ip)[0]
genuine = host.endswith(('.googlebot.com', '.google.com')) and \
    ip in {a[4][0] for a in socket.getaddrinfo(host, None)}

Where visitors come from

For the referrer, only the domain name is kept – “google.com” or “example.org”, never the full address of the page. Known search engines are grouped by name. That's enough to see whether people arrive through search, through a link on another site or by typing the address, without storing what someone searched for or which page linked to us.

What it can't do

On the other hand, it isn't affected by ad or script blockers, it adds no weight to the pages, and it works the same way for every visitor. For small sites that mainly want to know which pages are useful, that trade-off works well for us.

How it runs

The whole thing is a single Python script that only uses the standard library. It runs once a day on the server, recounts every day still in the logs and sends us a short weekly summary. It covers both www.codekredo.com and xcombos.codekredo.com, with a separate report for each site.

Our privacy policy describes the setup and its legal basis. This note explains our approach; it isn't legal advice for your own site.

More notes