How we identify visitors with no cookies, and the midnight UTC flaw

7 min read Original article ↗

Every analytics vendor says cookieless now. Very few explain what their implementation actually does, and almost none tell you what it cannot do. This post explains ours, including the part that does not work, because if you are going to trust numbers from a tool, you should know exactly how those numbers are made.

Why we wanted nothing on the device

When we started building Zenovay, we made one decision early: there should be a tracking mode that stores nothing in the browser. No cookies, no localStorage, no IndexedDB, nothing that survives the tab. It is a per site setting and it is off until you switch it on. In the default mode the tracker does use a first party cookie and browser storage. If the storage free property matters to you, check the toggle rather than assuming it.

Not because storage is evil. Because storage is a commitment. The moment you write an identifier to someone’s device you take on storage and deletion obligations, and usually a banner. We wanted a mode where there is nothing stored to delete. That does not make the lawful basis question disappear. Whether a given visit needs consent is a separate rule under a separate law, it still applies, and it belongs to the site owner rather than to us.

That decision sounds clean until you try to answer the most basic question in analytics: is this pageview from the same person as the one five minutes ago? Without a stored identifier, the honest answer is that you have to guess. So the real design question is: how do you guess well, and how do you make the guess expire?

The identity function

Here is the whole mechanism. When an event arrives at our edge, the server computes:

visitor_key = sha256(
  ip_subnet      // not the full IP
  + user_agent   // the browser's own header
  + daily_salt   // rotates at midnight UTC
)

Sessions are a 30 minute window on top of that key. The hash happens server side, on Cloudflare Workers, before analytics records are persisted. In cookieless mode, the tracker sends random, window-scoped visitor and session IDs so events within the open tab can be grouped. Those IDs are not stored in the browser, and ingestion replaces the visitor identity with the daily server hash before persistence. No cookie, localStorage, or IndexedDB entry is created on the device.

Three ingredients, each chosen for a reason.

The IP subnet, not the full IP. Using the full address would make the key more precise and more invasive at the same time. The subnet is deliberately blurry. It also absorbs some of the churn from mobile networks, where the last octet can change between requests while the subnet stays put.

The user agent. It separates the two people behind one home router better than nothing does. It is a weak signal on its own, and that is fine. It is one input of three.

The daily salt. The salt changes at midnight UTC, so a key is only valid for the day it was made, and it is scoped to a single site. Combined with a server side secret that never leaves our infrastructure, that means nobody holding a copy of the database can turn an id back into a person. Now the limit, stated precisely: the rotation is derived from the date, not from a random value we destroy, so we could recompute an old key ourselves if we set out to. We would rather the stronger sentence were true, so we are moving the rotation to a random daily value that is discarded. Until that ships, this is the accurate description.

The flaw

That salt rotation is also a flaw, and we want to be precise about it instead of hiding it in a footnote.

At midnight UTC, every visitor key on the planet changes. The same person, on the same network, in the same browser, becomes a brand new visitor. Which means:

  • Unique visitor counts across multi day ranges are overcounted. A daily reader shows up as seven people in a weekly view.
  • A returning visitor rate across days is not something we can honestly compute in cookieless mode, so we do not pretend to.
  • A five day consideration journey, where someone reads your pricing page on Monday and signs up on Friday, looks like two unrelated people.

Timeline showing a visitor before 00:00 UTC becoming an unknown new visitor after the salt rotates at midnight

If your business depends on cross day visitor stability, this design will annoy you. That is not a bug report we can fix. It is the price of the property described above, and we think the price is worth stating plainly.

What still works, and why revenue attribution survives

Everything inside a day works normally: sessions, journeys, funnels, live view, and channel attribution for the visit that is happening right now.

The interesting case is revenue. If identity dies at midnight, how can we tell you which channel produced a paying customer three weeks after the first visit?

Because at conversion, the guessing stops. When someone signs up or pays, they identify themselves to your product, and your product can tell us. From that moment the journey is anchored to a real account, and the attribution question becomes answerable again, backwards from the conversion. The anonymous phase stays anonymous. The paying phase is precise. We think that is the right place for the boundary: precision arrives exactly when a person chooses a relationship with you.

What we rejected

Heavier fingerprinting. Canvas entropy, font enumeration, audio context quirks. It would make the key more stable and it would make us a surveillance company with a nicer landing page. No.

A weekly salt. Better continuity, softer privacy story. Once you rotate weekly, you are one config change away from monthly, and one more from never. Daily is a bright line and bright lines survive product pressure.

A localStorage fallback. Storing a key only when some heuristic decides it is fine would mean the honest sentence on our landing page needs an asterisk. The sentence is worth more than the continuity.

Collisions, because honesty goes both ways

The flaw above splits one person into many. Two identical browser and network signatures merging into a single visitor for one day

The opposite failure exists too: two people in the same office, on the same subnet, with identical browser versions, can merge into one visitor for a day. We accept that. Under counting and over counting are both distortions, but they are bounded, they expire daily, and neither of them requires writing to anyone’s device.

If you need cross day stability anyway

Cookieless is a mode, not a dogma, and every site in Zenovay has a toggle. It is off until you turn it on. Turn it off and tracking uses a first party cookie, visitors stay stable across days, and the consent obligations that come with that are yours to handle, as they would be with any tool. Session replay, heatmaps and cross domain tracking are separate features with their own storage and consent implications, and enabling them is not the same as running in this mode.

The point

We wrote this because most cookieless claims are marketing sentences, and marketing sentences do not have failure modes. Real mechanisms do. Ours splits people at midnight UTC, merges some office colleagues, and refuses to answer questions it cannot answer honestly. Knowing that, you can decide what our numbers mean.

If you have built identity systems and see a sharper tradeoff we missed, we genuinely want to hear it.