v3.5.0 (paths relative to its src/) and
confirmed by the end-to-end suite tests/webpath (make webpath, plan phase 5).
Where the data comes from
Every event a browser sends is accepted by the gateway and replayed into Umami by the replayer as onePOST /api/send (plan section 4.4 step 12):
Umami’s session id is
uuid(website, ip, userAgent, hash(dailySalt, vt), distinctId) and its
visit id is uuid(sessionId, hash(startOfHour(createdAt))) unless a cache token carries an
earlier visit id (lib/collection-session.ts). With the constant address, a session is one
visitor token with one user agent on one website on one UTC day.
The seven metrics
All read routes takestartAt and endAt in unix milliseconds and count only events whose
created_at is in that range. “Pageview events” are rows with event_type 1; custom events
(event_type 2) never count as views and never make a visit.
The metrics bar of the overview page shows Visitors, Visits, Views, Bounce rate and Visit
duration; the Sources, Location and Environment panels show referrers, countries, and
browsers, OS and devices (
docs/reference/umami-contract.md item 11).
Unique visitors are unique per UTC day
Two things change at 00:00 UTC, and either one alone gives a new visitor:- The gateway’s
daily_secretrotates and is destroyed, so the same browser gets a new visitor tokenvt(plan 4.4 step 8). - Umami runs with
SALT_ROTATION=dailyandTZ=UTC, so its base salt changes with the event’s day (lib/crypto.tsgetSalt,startOfDayofcreated_at).
- A person who visits on Tuesday and Wednesday is two visitors. Visitors over a multi-day
range equal the sum of the daily visitors. Measured: one visitor token on three days gives 1
visitor per day and 3 over the three days (
TestWebPath/time_ranges). - A visit that runs across midnight splits into two visits of two visitors, because the session id changes (Umami treats a cache token of another session as drift).
- The same browser with a different user agent (an update, another browser) is another
visitor the same day. Two browsers behind one address are two visitors; one browser that
changes address is a new visitor too, because the address is part of
vt. - A restart of the whole NATS cluster during a day loses
edge_secrets(memory storage), so every returning visitor of that day is counted again (plan 4.4, consequences).
The visit rule
A visit ends when the visitor is idle for more than 30 minutes, with one exception that comes from Umami’s own visit id.- While the gap to the visitor’s previous event on that website is at most 1800 s, the replayer sends Umami’s last cache token, and the visit continues, also across UTC hours.
- When the gap is longer, the replayer sends no cache token. Umami then derives the visit id from the session and the UTC hour of the event. An event in a different UTC hour than the visit’s start therefore starts a new visit, but an event in the same UTC hour joins the visit that started in that hour.
All three rows are measured by
TestWebPath/visit_rule. The same-hour case is Umami’s own
behaviour for events without a cache token, which is how Umami treats timestamped events
(historical skips its 30 minute expiry), so a visit on this platform can last longer than
30 minutes of idle time only inside the UTC hour in which it started. The gap is measured on
gateway acceptance times, so a backlog replayed after an outage forms the same visits as live
delivery.
Bands
Each site has a primary website (id equals the site id) and a quarantine website. Events from an address prefix go to the quarantine website from the event that brings the prefix toIPCLUSTER_THRESHOLD distinct visitor tokens for the site within an hour
(internal/gateway/ipcluster.go) (plan 4.4 step 9). Every metric above is
computed per website, so quarantined traffic is counted separately and never mixed into the
primary numbers.
What is not counted
- Bots: Umami’s
isbotcheck runs on the original user agent; a bot event answers{"beep":"boop"}, stores nothing and is counted asbotonEDGE_STATSand inreplayer_outcomes_total. - Events the gateway refused (
Sec-GPC: 1, bad proof, wrong origin, rate limits); they are counted onEDGE_STATSby outcome. - Visits that hold only custom events: they have no pageview, so Umami’s
/statsdoes not count them as visits, visitors or bounces. - Events whose timestamp is not after a website reset: Umami clamps every read to the
website’s
resetAt(lib/request.ts). The replayer sends whole acceptance seconds, so an event accepted in the same second as a reset, or a backlog accepted before it and replayed after it, is stored but never shown.
Daily aggregates (phase 11)
The operator views readagg.web_daily and agg.web_daily_dim, which the
jobs task aggregate-web (internal/jobs/aggregateweb) fills from Umami’s
own read routes, as the admin, for each active site, both bands and each UTC
day: /stats gives one web_daily row, /metrics with type referrer,
country, device, browser and os gives the web_daily_dim rows.
A run reads today and the 3 days before it every hour; jobs run aggregate-web --from <day> --to <day> reads any range again. Each site day
replaces its rows in one fenced transaction, so re-running changes nothing
and a backfill restores deleted rows.
The research items of plan phase 11 were measured on 2026-11-12 against the
dev Umami (the pinned 3.5.0 image, events sent through /api/send the way
the replayer sends them, three UTC days, six visitors, a bot, a custom event,
own-host and www. referrers, an event at exactly 00:00:00) and are asserted
by TestAggregateWeb (internal/jobs/aggregateweb) on every run.
Consequences for the operator views (
docs/reference/control-plane-api.md
section 8):
- Visitors, visits, bounces and total time over a range are sums of daily rows, which equal Umami’s answer for the same range. A project’s numbers are the sum of its sites; one person on two sites of a project is two visitors, as Umami counts per website.
- Bounce rate is
bounces / visitsand average visit durationtotaltime / visits, the formulas of Umami’s dashboard. - A breakdown value over a range is the sum of its daily values, which equals
Umami’s
/metricsfor the range. - Removed tenants are not read again; the purge step
aggdeletes their rows unlessremoval_keep_aggregateskeeps them.