scf_flagged_event with one or more reasons and does not count it. This page lists each reason with its exact trigger in the source, the real traffic that can trip it, and what to do. Some real visitors are flagged by design: the checks trade a small loss of real traffic for numbers that are hard to inflate.
Source: quality.ts, origin.ts, pow.ts, datacenter.ts and fingerprints.ts in vendor/umami/src/lib/scf, which scripts/sync-umami.sh builds from Umami v3.5.0 and the patches. Layer 1 comes from patch 0003, the flag and layers 2 to 4 from patch 0004.
How reasons combine
- Umami’s own checks run first. A user agent that
isbotrecognizes gets{"beep": "boop"}and nothing is stored. An IP listed in Umami’sIGNORE_IPgets HTTP 403. Neither is a flag reason. - Layers 1 to 3 each add at most one reason. Within layer 1 the checks run in table order and the first failure is the reason. An event can carry up to three reasons, one per layer.
- Layer 4 runs only on clean events. A visitor is counted toward its network’s limit only after passing layers 1 to 3, so blocked scripts cannot push real visitors over the limit.
many_visitors_per_networktherefore never appears with another reason. - Which events. Page views, custom events,
identifycalls and performance events with a website id are checked. Error reports fromdata-errorsare handled before the checks and never flagged. - Same reply. A flagged event gets
{"beep": "boop"}, the reply bots get. It does not say which check failed. - Each layer has its own switch. With none set, the fork behaves like upstream Umami. The switches are listed under each layer below and on Configuration.
What is stored
A flagged row keeps the website id, the time, the reasons, the event type and name, the hostname, the URL path without its query string, the referrer domain, the country, and the browser, OS and device type. It never keeps the IP address or the query string. Each flagged event is also logged as one JSON line from theumami service: {"scf":"event_flagged","websiteId":"…","reasons":["…"]}.
The event type is “custom event” when the event has a name and “page view” otherwise, so flagged identify and performance events are recorded as page views.
There is no way to move a flagged event into the counted totals later. Fixing the cause affects new events only.
Project members see flagged counts per reason under Flagged, not counted on the Website screen (Flagged traffic). SCF operators see them for every project under Traffic quality.
Layer 1: real browser
Switch:SCF_ENFORCE_ORIGIN. Any non-empty value turns it on. The hosted service sets "1".
Browsers attach Origin and the Fetch Metadata headers Sec-Fetch-Mode and Sec-Fetch-Site to the tracker’s request. A script posting from a server usually does not.
Domains are compared after normalizing both sides: lowercase, then scheme, path, port, a trailing dot and a leading www. removed. The Origin host passes when it equals a site domain or is a subdomain of one. A site’s domain field can list several domains separated by commas.
missing_origin
Trigger. The request has no Origin header, the header is the literal string null, or its value is not a parseable URL.
Common legitimate causes
- A page opened from a
file://URL, or running in a sandboxed iframe. Browsers sendOrigin: nullfrom both. - Your own server-side or test code posting to
/ewithout a browser.
not_browser_fetch
Trigger. Origin is present, but Sec-Fetch-Mode or Sec-Fetch-Site is missing.
Common legitimate causes
- A site served over plain HTTP. Browsers send Fetch Metadata only in secure contexts, so Lumenstat requires HTTPS.
- A self-hosted collect endpoint served over plain HTTP.
- A browser too old to send Fetch Metadata.
no_site_domain
Trigger. Origin and Fetch Metadata are present, but the website’s domain field is empty.
Common legitimate causes
- A website created without a domain, for example by hand in Umami’s admin UI. Enrollment always sets one domain per website.
origin_mismatch
Trigger. The Origin host is neither one of the site’s domains nor a subdomain of one.
Common legitimate causes
- The site moved to a new domain, or is also served on a domain that was never registered.
- Preview or staging deployments on another domain, for example a hosting provider’s preview URL.
- Local development on
localhostwith the production snippet.
example.org covers docs.example.org. To keep previews and localhost from sending at all, set data-domains in the snippet.
Layer 2: proof of JavaScript
Switch:SCF_POW_BITS, an integer from 1 to 24: the number of leading zero bits the proof must have. Unset, 0 or any other value turns the check off. The hosted service sets "10".
The tracker sends x-lumenstat-pow: <slot>.<nonce>, where sha256("<website>:<slot>:<url>:<nonce>") starts with at least 10 zero bits and slot is the 10-minute window. The tracker reference has the exact format.
no_proof
Trigger. The x-lumenstat-pow header is missing.
Common legitimate causes
- The page loads a tracker built without the proof of work, such as upstream Umami’s
script.jsor a copy from another Umami install, pointed at Lumenstat withdata-host-url. - On an instance that moved from upstream Umami to this fork: browsers still running upstream’s
/script.jsfrom their cache. Umami serves/script.jswith a one-day cache. - Custom code that posts to
/edirectly instead of using the tracker.
/v/<version>/l.js (see Tracker releases), or call umami.track from it instead of posting yourself. After moving an instance from upstream Umami, expect cached old trackers to clear within a day.
bad_proof
Trigger. One of:
- the header is not
<slot>.<nonce>with a slot of 1 to 10 digits and a nonce of 1 to 12 digits; - the slot is more than one 10-minute window away from the server’s current window;
- the hash of
<website>:<slot>:<url>:<nonce>, using thewebsiteandurlin the payload, has fewer leading zero bits thanSCF_POW_BITS.
- A device clock that is wrong. A clock off by less than 10 minutes always passes. Off by 20 minutes or more always fails. In between, it depends on where in the window the event falls.
- A proxy or extension that rewrites the request body after the tracker computed the proof.
- On a self-hosted instance,
SCF_POW_BITSset above 10. The tracker always works to 10 bits, so most real visitors fail.
SCF_POW_BITS and the server’s clock.
Layer 3: data center
Switch:SCF_DATACENTER_RANGES, the path to a file with one CIDR range per line (# starts a comment). Unset turns the check off. The hosted service mounts deploy/datacenter-ranges.txt at /etc/lumenstat/datacenter-ranges.txt.
The file holds about 8,900 ranges from the providers’ own published lists and from the prefixes announced by hosting-only networks: AWS, Google Cloud, Oracle Cloud, DigitalOcean, Akamai (Linode), Hetzner, OVHcloud, Contabo, Vultr, Scaleway, Alibaba Cloud and Tencent Cloud. node scripts/update-datacenter-ranges.mjs rebuilds it. The IP is checked in memory and never stored.
datacenter_ip
Trigger. The visitor’s IP address falls inside a range in the file.
Common legitimate causes
- A VPN, proxy or privacy service whose servers run on one of the listed providers.
- A corporate gateway or cloud-hosted browser running on one of those networks.
- Your own uptime checks or end-to-end tests that drive a real browser from a cloud machine.
umami service after changing it. If the file cannot be read, Umami logs datacenter_ranges_unreadable and the check flags nothing.
Layer 4: many visitors per network
Switch:SCF_MAX_VISITORS_PER_IP_HOUR, a positive integer: the number of distinct visitors allowed per site, per network, per hour. Unset or not a positive integer turns the check off. It also needs Redis (REDIS_URL). The hosted service sets "150", a generous value because mobile carriers put many people behind one IPv4 network.
A visitor is an IP address plus a user agent. A network is the IP cut to /24 for IPv4 or /48 for IPv6. Visitors are counted in a Redis HyperLogLog per site, network and UTC clock hour. Keys and members are HMACs under the random daily salt (or the app secret when that salt is off), so Redis never holds an IP or a network address. Keys expire after two hours.
many_visitors_per_network
Trigger. The event passed layers 1 to 3, its visitor is new for this site, network and hour, and adding it takes the count above the limit. Visitors already counted that hour keep counting. The count starts over at the next full hour.
Common legitimate causes
- A company, university or government network where many people share one address range.
- Conference or event Wi-Fi during a talk or launch that links to the site.
- A mobile carrier that puts many subscribers behind a few addresses.
SCF_MAX_VISITORS_PER_IP_HOUR. If every visitor lands on the same network, Umami is reading the IP of a proxy: check CLIENT_IP_HEADER on Configuration.
Counting is best effort: if Redis fails, the event is not flagged.
Limits
- No layer proves a human. A script that runs a real browser on a home connection, loaded from the site’s own domain, passes all four. Read Bot and inflation defense for what each layer does not stop.
- Real visitors can be flagged, as listed above. The share of flagged traffic per reason is the signal to read, not single events.