[invisit](https://invisit.dev) · [Docs](https://invisit.dev/docs) · [llms.txt](https://invisit.dev/llms.txt) · [llms-full.txt](https://invisit.dev/llms-full.txt)

# How cookieless analytics counts unique visitors, and what it can't

> The four ways analytics tools tell visitors apart, what each can and cannot count across days, and why a deleted salt matters. Checked November 2026.

- Author: Miraç Berk
- Published: 2026-11-03
- Page: https://invisit.dev/blog/cookieless-analytics
- Tags: privacy, cookieless, analytics

![A soft 3D calendar page and a fading key beside a row of small visitor figures, with violet dithered light and shadows.](https://invisit.dev/_astro/cover.CbMmrpX_.png)

Cookieless analytics counts unique visitors by hashing what every page request already carries: the IP address and the user agent, plus the site and a salt that changes every day. The same browser gets the same hash all day and a new one the next, so a cookieless tool can count unique visitors within a day but not across days: someone who visits on Monday and again on Tuesday is two visitors.

The daily hash is one of four designs in use. Below, each is described from the vendors' own documentation as of November 2026, with what it can and cannot measure and why a deleted salt matters. We build invisit, a cookieless analytics tool, so we also say where our design loses.

## The four ways analytics tools tell visitors apart

Analytics tools tell visitors apart with a random id stored in the browser, a hash with a daily salt, a hash with a longer-lived salt, or no identifier at all. Only the last three are cookieless.

| Method                        | Identifier                                 | Links a visitor for            | Can measure                                         | Typical consent position                                               | Examples                                                           |
| ----------------------------- | ------------------------------------------ | ------------------------------ | --------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Cookie or localStorage id     | Random id stored in the browser            | Until it expires or is cleared | Returning visitors, multi-week funnels, attribution | EU: consent, except France under limits; UK: aggregate statistics only | GA4, Mixpanel, PostHog and DataFast by default                     |
| Daily hash                    | Hash of IP, user agent, site, daily salt   | One day                        | Daily unique visitors, visits, same-day funnels     | EU: disputed, France allows it under limits; UK: unsettled             | Plausible, Fathom, invisit, Vercel; PostHog, DataFast as an option |
| Hash with a longer-lived salt | Hash of IP, user agent, site, monthly salt | Up to a month by default       | The above, plus returns within the month            | The same debate, with a longer link to justify                         | Umami                                                              |
| No identifier                 | None; the referrer decides                 | Not at all                     | Page views and visits, not unique people            | Lowest risk; depends on the national reading                           | Simple Analytics, Cloudflare Web Analytics                         |

The consent column summarises [our guide to cookie banners for analytics](/blog/cookie-banner-analytics); it is not legal advice.

## Cookie and localStorage ids: GA4, Mixpanel, PostHog and DataFast

A cookie-based tool generates a random id on the first visit, stores it in a first-party cookie or localStorage, and sends it with every event. The visitor stays the same visitor until the id expires or is cleared, which is what makes returning visitors and long attribution windows possible.

- Google Analytics 4 sets `_ga` and `_ga_<container-id>` for two years by default ([Google's cookie list](https://support.google.com/analytics/answer/11397207)).
- Mixpanel's JavaScript SDK keeps its distinct id and super properties in a cookie that lasts 365 days by default ([Mixpanel's JavaScript docs](https://docs.mixpanel.com/docs/tracking-methods/sdks/javascript)).
- PostHog keeps its state in localStorage plus a first-party cookie, `ph_<project_token>_posthog`, that expires after 365 days ([PostHog persistence](https://posthog.com/docs/libraries/js/persistence)).
- DataFast's default `script.js` sets a `datafast_visitor_id` cookie for 365 days, and DataFast says you may need a cookie banner for it ([DataFast's GDPR guide](https://datafa.st/docs/gdpr-cookieless-tracking)).

Two years is the ceiling, not the norm. Google's own cookie page notes that browsers cap first-party cookies for visitors who do not come back, at 400 days in Chrome and 7 days in Safari. WebKit applies the same rule to localStorage, deleting a site's script-writable storage after seven days of Safari use without user interaction on that site ([WebKit](https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/)).

## How does a daily hash count unique visitors?

A daily hash identifies a visitor on the server. It combines the IP address and user agent from the request with the site and a salt, and hashes the result; nothing is stored in the browser. When the salt changes the next day, the same inputs produce a different hash, so the visitor cannot be linked to the day before.

| Tool                                                                                  | What goes into the hash                                 | When the salt changes         | The old salt                                  |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------- | --------------------------------------------- |
| [Plausible](https://plausible.io/data-policy)                                         | Daily salt, site domain, IP address, user agent         | Every 24 hours                | Deleted: "rotated and deleted every 24 hours" |
| [Fathom](https://usefathom.com/docs/troubleshooting/hashes-salts)                     | IP, user agent, site id and a daily salt, with SHA-256  | Midnight UTC                  | Not documented                                |
| [PostHog cookieless mode](https://posthog.com/tutorials/cookieless-tracking)          | Project, daily salt, IP, user agent, hostname           | Daily                         | Deleted once the day's events are processed   |
| [DataFast cookieless script](https://datafa.st/docs/gdpr-cookieless-tracking)         | Signals such as IP, user agent, site domain, and a salt | About every 24 hours, UTC     | Not documented                                |
| [Vercel Web Analytics](https://vercel.com/docs/analytics#how-visitors-are-determined) | "A hash created from the incoming request"              | Valid for one day, then reset | Not documented                                |
| invisit                                                                               | Site, date, device, browser and OS type, IP address     | Each UTC day                  | Deleted about 25 hours after the day ends     |

In invisit, the tool we build, visitors are told apart with a keyed hash of the site, the date, the device, browser and operating system type and the IP address, mixed with a random salt generated for each UTC day. The salt is deleted automatically once late events for that day can no longer arrive, about 25 hours after the day ends, after which nobody, invisit included, can recompute that day's hashes. Raw IP addresses are never stored, and the site can choose to leave out browsers that send Global Privacy Control or Do Not Track ([privacy guide](https://invisit.dev/docs/guides/privacy.md)).

Plausible states the consequence plainly in its [metric definitions](https://plausible.io/docs/metrics-definitions): "If a person visits from multiple devices or on multiple days, they are counted as separate visitors. Within the same day, a person counts as one unique visitor even if they have multiple sessions."

Here is the mechanism as a sketch, not any vendor's code. It runs as written with Bun or Node:

```ts title="daily-visitor-hash.ts"
import { createHmac, randomBytes } from 'node:crypto'

// One random salt per UTC day, kept only in memory.
const salts = new Map<string, Buffer>()

function visitorHash(site: string, ip: string, userAgent: string, at: Date): string {
  const day = at.toISOString().slice(0, 10)
  let salt = salts.get(day)
  if (!salt) {
    salt = randomBytes(32)
    salts.set(day, salt)
  }
  return createHmac('sha256', salt).update(`${site}\n${day}\n${userAgent}\n${ip}`).digest('hex')
}

const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/130.0.0.0'
const monday = visitorHash('example.com', '203.0.113.77', ua, new Date('2026-11-02T09:00:00Z'))
const mondayEvening = visitorHash(
  'example.com',
  '203.0.113.77',
  ua,
  new Date('2026-11-02T21:00:00Z'),
)
const tuesday = visitorHash('example.com', '203.0.113.77', ua, new Date('2026-11-03T09:00:00Z'))

console.log(monday === mondayEvening) // true: one visitor all day
console.log(monday === tuesday) // false: a new visitor on Tuesday

// Once Monday can receive no more events, its salt goes, and Monday's hashes match nothing.
salts.delete('2026-11-02')
```

## Umami: a hash with a monthly salt

Umami also hashes instead of setting cookies, but its salt rotates monthly by default, so a returning visitor counts once for up to a month. [Umami's visitor identification](https://docs.umami.is/docs/metric-definitions#visitor-identification) hashes the website id, IP address, user agent and "a rotating salt", and self-hosted installations can change the period with `SALT_ROTATION`.

Umami's source code shows what that salt is made of. In [`crypto.ts`](https://github.com/umami-software/umami/blob/559e3c607c23d2c72d9bd4f6481bda428255323c/src/lib/crypto.ts#L56-L78), the rotating salt is a hash of the start of the month, and every session id is hashed together with a hash of the installation's `APP_SECRET` ([where the session id is built](https://github.com/umami-software/umami/blob/559e3c607c23d2c72d9bd4f6481bda428255323c/src/app/api/send/route.ts#L156-L162)). Neither part is random per period, and neither is deleted: whoever holds `APP_SECRET` can recompute any month's session id from an IP address and user agent.

In return, Umami can recognise a returning visitor within the month, which a daily hash cannot. Across months, its docs say, identifiers are "not automatically linked".

## Counting without any identifier: Simple Analytics and Cloudflare

Simple Analytics and Cloudflare Web Analytics keep no visitor identifier at all, not even a hash. They decide from the referrer whether a page view starts a visit, so neither counts unique people.

[Simple Analytics' rule](https://docs.simpleanalytics.com/explained/unique-visits): a page view counts as a visitor when it has no referrer or a referrer from another hostname, while internal clicks, reloads and back or forward navigation do not. Its visitors figure is therefore unique page views, and doubles as its session count. Simple Analytics says hashes of IP addresses combined with dates are "still considered fingerprinting and requires consent under GDPR and PECR", and avoids them.

[Cloudflare Web Analytics](https://developers.cloudflare.com/web-analytics/data-metrics/high-level-metrics/) defines a visit as a page view whose HTTP referer does not match the hostname, and reports visits and page views, not unique visitors. Cloudflare says it uses no client-side state such as cookies or localStorage and does not fingerprint by IP address or user agent ([product page](https://www.cloudflare.com/web-analytics/)). Its [launch post](https://blog.cloudflare.com/free-privacy-first-analytics-for-a-better-web/) warned that you "should expect this number to differ from other analytics tools".

The cost is that someone who arrives from a newsletter in the morning and from Google in the afternoon counts twice. Simple Analytics renamed its funnels to goals in July 2024 because the steps did not always come from the same session ([changelog](https://www.simpleanalytics.com/changelog)).

## What can a daily hash count, and what can't it?

A daily hash counts unique visitors, visits, entry and exit pages, and same-day paths and funnels. It cannot count anything that needs to know the same person came back on a different day.

### Unique visitors in a day and across days

Within one UTC day, a daily hash and a cookie measure the same thing: distinct browsers. Across days they diverge, because a daily hash counts visitor-days. Someone who reads your blog on Monday, Tuesday and Wednesday is one unique visitor for the week in GA4 and three in Plausible, Fathom or invisit. PostHog lists this as the first cost of its cookieless mode: "high unique user counts beyond one day (like weekly or monthly unique users)".

### Returning visitors

A daily hash cannot tell a new visitor from a returning one across days, so it has no honest returning-visitor rate. DataFast documents that its new versus returning breakdowns, visit count filter and User and Journey tabs are "unavailable or less reliable" in cookieless mode.

### Funnels and attribution over several days

Funnels and attribution that span days break with a daily hash. DataFast's guide gives the example of a visitor who first came three months ago, again two weeks ago, and converts today: its cookie ties the three together, while with its cookieless script "each calendar day (after the salt rotates) can look like a different visitor".

invisit builds [funnels](https://invisit.dev/docs/guides/events.md#funnels) of 2 to 8 steps within one visit, and charts revenue from Stripe, Paddle and other providers beside traffic without attributing it to sources, campaigns or visitors. If your business runs on multi-week first-touch attribution, a cookie behind a consent banner does that job and a daily hash does not.

For signed-in users, invisit's [`identify(user.id)`](https://invisit.dev/docs/guides/events.md#identify-signed-in-users) attaches a hash of your own user id to their visits, so one customer's visits on different days share that id. That is visitor-level data again, which [changes the consent picture](/blog/cookie-banner-analytics).

### What happens when the IP address changes

Because the IP address is part of the hash, anything that changes it mid-day turns one person into two visitors, and anything that shares it can turn several people into one.

- Splits: a phone moving from home wifi to mobile data, or a VPN switching servers. iCloud Private Relay keeps its relay address stable "during a browsing session" ([Apple](https://developer.apple.com/icloud/prepare-your-network-for-icloud-private-relay/)), so a return later that day can arrive from a new one. IPv6 privacy addresses rotate on their own clock: [RFC 8981](https://www.rfc-editor.org/rfc/rfc8981) gives temporary addresses a default preferred lifetime of one day, unrelated to midnight UTC.
- Merges: offices, schools and mobile carriers put many people behind one public IPv4 address ([RFC 6888](https://www.rfc-editor.org/rfc/rfc6888) covers carrier-grade NAT). PostHog names the likely collision as two users "with the same IP address (e.g. in a corporate network) and the same user agent", and Apple says a Private Relay address "may be shared among more than one Private Relay user in the same area".

invisit hashes the type of device, browser and operating system rather than the full user agent string, so it merges a little more often: colleagues using Chrome on Windows desktops behind one office address share one hash for the day. A B2B product read from offices leans toward merges; a consumer site read on phones leans toward splits.

### The midnight UTC boundary

Daily salts change on a fixed clock. Fathom's salt rotates at midnight UTC, and its [data journey](https://usefathom.com/data) notes that "a local reporting day can span two UTC salts"; invisit's salt is per UTC day as well. Midnight UTC is 4 pm in California in winter and 5 pm in summer, so an evening reader there gets a new hash partway through their day.

## How far do cookieless counts drift from cookie-based counts?

No single percentage holds for every site, and we found no vendor-neutral measurement worth quoting. The direction is predictable, and you can estimate your own gap before you switch.

- Daily unique visitors: close, with IP splits and merges pulling in opposite directions.
- Weekly and monthly unique visitors: higher with a daily hash, because each person counts once per day they visit. The more often your audience returns, the bigger the gap; a site of one-time visitors from search barely moves.
- Page views: no identity involved, so differences come from ad blockers, bot filtering and where each script loads, not from the counting method.

Cookie counts are not ground truth either. Safari's 7-day cap makes a visitor who stays away longer than a week new again, which inflates cookie-based uniques too. Behind a consent banner in the EU, GA4's basic [Consent Mode](https://developers.google.com/tag-platform/security/concepts/consent-mode) loads no tags until the visitor answers, so people who decline are never counted at all.

To estimate the multi-day gap on your own site, take one week in your current cookie-based tool. Add up the unique visitors of each of the seven days, then divide by the unique visitors it reports for the whole week. That ratio is roughly how much a daily hash will inflate your weekly count; a ratio near 1 means few people return within a week.

## Why does it matter that the salt is deleted?

A visitor hash is only as private as its salt. While the salt or key exists, anyone holding it can recompute a person's hash from an IP address and user agent, or sweep every possible address until one matches. Once the salt is deleted, the stored hashes cannot be linked back to anyone, by the vendor or by whoever obtains the data later.

We measured how cheap that sweep is. A short C program using OpenSSL, on one desktop with a 12-thread Intel Core i5-12600, took a hash built from a known salt, a domain, a Chrome user agent and the address 203.0.113.77, tried all 4,294,967,296 IPv4 addresses, and found the address in 77 seconds with salted SHA-256 and in 108 seconds with HMAC-SHA256, a keyed hash.[^sweep] Checking whether one known person visited takes a single hash.

Fathom's own explainer makes the same point: "If someone knows the salt, they can include it when testing guesses." The hash function is not what protects visitors; the salt's absence is. So ask any vendor what happens to old salts:

- Deleted: Plausible ("rotated and deleted every 24 hours"), PostHog's cookieless mode (deleted "once that day's events have been processed") and invisit (about 25 hours after each UTC day ends).
- Not documented: Fathom, Vercel and DataFast's cookieless script document how often the hash changes, but the pages we read do not say whether old salts are kept.
- Derivable: Umami's salt is computed from the date, and its key, `APP_SECRET`, lasts as long as the installation.

![Timeline chart in UTC: Monday's salt hashes events from Monday 00:00 to Tuesday 00:00, is kept for late events until Wednesday 01:05 and is then deleted; Tuesday's salt does the same a day later, deleted Thursday 01:05.](https://invisit.dev/_astro/salt-lifecycle.CWM-bIRM.png)

*How long one day's salt lives in invisit, with the default lateness and clock-skew allowances.*

## Cookieless counting gives up cross-day identity on purpose

Cookieless counting trades away cross-day identity on purpose. Not knowing that Monday's visitor came back on Tuesday is the privacy property itself: linking the two days would take exactly the kind of persistent identifier that cookie consent rules exist for.

In exchange, nothing is stored on the device, and once the salt is deleted, nobody can rebuild a person's history from the data. You give up multi-day unique visitors, returning-visitor rates, cross-day funnels and long attribution windows; if one of those is central, measure it with a cookie behind consent, or with identity where people already sign in.

## Before you switch to or compare cookieless analytics

- [ ] Compare page views first; they do not depend on identity.
- [ ] Compare unique visitors one day at a time, never over a week or a month.
- [ ] Work out your own ratio of summed daily visitors to weekly visitors in your current tool.
- [ ] List every report that needs a person across days, such as returning visitors, cohorts and multi-week attribution, and decide whether each moves to a cookie behind consent, to signed-in identity, or goes.
- [ ] Ask your vendor how long the salt lives, and whether it is deleted or can be derived from a secret that persists.

The trap to avoid is reading the higher monthly visitor count after a switch from cookies as growth. It is the same people, counted once for each day they came.

[^sweep]: One run each on November 3, 2026, with 12 threads each taking a slice of the IPv4 space. The fixed part of the input (the salt and domain, or the HMAC key pads) was hashed once and reused, as an attacker would. A naive version that formatted and hashed every full message took 589 seconds.
