invisit
All posts

By Miraç Berk16 min read

Why do GA4, Plausible and server logs show different numbers?

Every reason GA4, Plausible, Vercel and server logs disagree, how big each gap gets, how to check it, and a procedure to reconcile one week.

Two soft 3D bar charts of slightly different heights beside a small server rack, joined by a measuring tape, in violet dithered light and shadows.

GA4, Plausible, Vercel Web Analytics and your server logs show different numbers because they count different things in different places. Each defines a visitor and a visit its own way, records either in the browser or on your server, loses different traffic to ad blockers, consent banners and bot filters, starts its day at a different midnight, and finishes processing at a different time.

Below is every cause we know of, how large each tends to be by the vendor’s own documentation or our own tests, how to check it, and a procedure for reconciling two tools over one week, as of November 2026. invisit, the cookieless analytics tool we build, appears where its behaviour answers the question.

Every cause of mismatched analytics numbers at a glance

Sixteen causes explain almost every mismatch. For each, the table gives which count runs higher, how big the gap tends to be and where to check.

Cause Which count runs higher How big How to check
Ad blockers The unblocked tool, and server logs 5 to 25% missed (Plausible); 10 to 30% more events via proxy (PostHog) Page views by browser in both tools
Consent banner A tool that does not wait for consent Your banner’s decline rate Your consent tool’s report
Cookie id vs daily hash Daily-hash tools, on ranges longer than a day Sum of daily visitors divided by the range’s visitors Compare one day at a time
Visit and visitor definitions Depends on timeout, midnight and campaign rules See the definitions table below Compare page views first
Bots Server logs, then the tool with the weakest filter 53% of HTML requests on Cloudflare were not human (December 2025) City and network breakdowns
Internal traffic The tool without exclusions Your team’s share Your own city, browser and pages
Single-page app navigation The tool that follows History API route changes Every in-app navigation after the first page Pages per visit
Time zones and date presets Neither; days shift An evening of traffic moves to another date Same zone, explicit dates
GA4 thresholds and (other) Other tools, where GA4 withholds or folds rows The withheld rows GA4’s data quality icon
Estimated unique counts Either ±1.63% on GA4 sessions at 95% confidence Exact counts in BigQuery
GA4 consent mode modelling GA4, with modelled users added The modelled share Reporting identity
Processing delay The faster tool, for recent days GA4: 24 to 48 hours of processing Compare closed days only
Prefetch and prerender Server logs, and trackers that run in hidden pages One request per page fetched ahead and never opened The Sec-Purpose header
CDN cache hits Analytics, over origin logs Your cache hit ratio for HTML CDN logs
JavaScript that never runs Server logs Visitors who leave before the script runs Landing page logs
Cross-domain journeys GA4 users and sessions, when domains are not linked One extra user and session per crossing Your own domains in the referrer report

Six causes have their own guides on this blog:

Ad blockers

Blocklists such as EasyPrivacy block requests to known analytics domains, so a blocked tool undercounts against an unblocked one and against your logs. Plausible estimates the loss at “typically between 5% and 25%” without a proxy, and PostHog says a proxy “typically increases event capture by 10-30%”; both sell the fix. Our analytics proxy guide shows how to measure your own gap.

A tool that waits for consent counts only visitors who accept, while a cookieless tool without a banner counts everyone, so the gap is the decline rate your consent tool reports. Our cookie banner guide covers when analytics needs consent in the EU, the UK and the US. GA4’s attempt to model the missing visitors is covered below.

GA4 recognises a returning browser by its cookie, while Plausible, Fathom and Vercel recognise a visitor only within one day, so any range longer than a day shows more visitors in the daily-hash tools. Daily visitors should come close; weekly and monthly visitors will not. How cookieless analytics counts visitors explains the hash and how to estimate the multi-day gap.

Bots

The major tools filter bots that name themselves; they differ on the rest, and GA4 lets you neither turn its filter off nor see what it removed. From September 2025, GA4 properties logged Direct sessions from Lanzhou, China and Singapore with almost no engagement, which WIRED measured at 14.7% and 6.6% of visits to US government sites over 90 days. Our bot traffic guide compares each tool’s filter.

Internal traffic

Your own visits, teammates, preview deployments and uptime checks count in any tool you have not told to ignore them, and IP address rules miss most of them. Our internal traffic guide covers the opt-out in each tool.

Single-page app navigation

In a single-page app, only the first page is a real page load. A tool that does not follow route changes records one page view per visit, so its page views fall far below a tool that does. Our SPA page view guide shows what each tracker needs.

What does each tool call a visitor and a visit?

A GA4 user, a Plausible unique visitor and an invisit visit are three different measurements, so compare like with like: page views with page views, sessions with visits, and visitors only one day at a time. As of November 2026, each vendor documents them like this:

Tool What a visitor or user is When a visit or session ends
GA4 Reports show active users: a cookie id with an engaged session, a first_visit or engagement time 30 minutes without activity by default, adjustable; not at midnight, not on a new campaign
Plausible A hash of a daily salt, domain, IP address and user agent; two days means two visitors 30 minutes without activity
Fathom A visitor-day: a signature that rotates at midnight UTC A 24-hour session window, the visitor-day; bounces used a 30-minute window before its 2026 rebuild
Umami A unique session: a hash of website, IP address and user agent with a salt that rotates monthly by default A visit is a hash of the session and an hourly salt, and can run past the hour
Matomo A visitor id in the _pk_id cookie, kept 13 months 30 minutes without activity, a new campaign, or midnight in the website’s time zone
PostHog A distinct id in localStorage and a first-party cookie that expires after 365 days; a daily hash if cookieless 30 minutes without activity, or 24 hours; one session spans tabs
Vercel A hash of the request, valid for a single day “A group of page views by the same visitor”; no timeout documented
invisit No unique visitor metric; visits are built from a hash that changes each UTC day 30 minutes without activity, or 24 hours; a visit counts only if it viewed a page

Sources: GA4 sessions, GA4 users, GA4 vs Universal Analytics, Plausible, Plausible data policy, Fathom, Umami, Matomo visits, Matomo cookies, PostHog sessions, Vercel.

Page views are defined almost the same way everywhere, which makes them the baseline for any comparison. Visitors are the furthest apart: GA4’s reports show only active users, so GA4 can report fewer users than a daily-hash tool reports visitors for the same day. Total users, inactive ones included, is in Explorations.

Session rules move visit counts both ways. A visitor who reads through midnight is two visits in Matomo and one session in GA4, and so is one who clicks back in mid-visit through a link with a new utm_campaign. Fathom’s 24-hour window makes a morning visit and an afternoon return one session, where a 30-minute tool counts two. Bounce rates and funnels inherit these boundaries; see bounce rate across nine tools and funnel analysis.

invisit, the analytics tool we build, reports visits and page views and has no unique visitor count. A visit joins events from one visitor hash with no gap over 30 minutes, for at most 24 hours. The hash includes the UTC date, so at midnight UTC a visit carries on only while the visitor stays on the same page load, which in a single-page app can be the whole session; the next full page load starts a new visit. Compare invisit visits with GA4 sessions or Plausible visits, never with users.

Why do time zones change daily analytics numbers?

Each tool puts events on dates by a clock you choose, so two tools set to different zones file an evening’s traffic under different dates. Single days, and the first and last day of a range, then disagree even when both tools counted correctly.

Tool Whose clock sets the day
GA4 The property’s reporting time zone; a change applies only going forward and can leave a flat spot or spike
Plausible The site’s reporting timezone, in site settings
Fathom The site’s reporting timezone since June 15, 2026, before that the viewer’s browser; visitor hashes still reset at midnight UTC
Matomo The website’s time zone, which also ends visits at midnight
PostHog The project’s time zone, UTC by default
invisit Each viewer’s account time zone; public dashboards use the viewer’s browser zone; the REST API requires a timezone

Sources: GA4, Plausible, Fathom, Matomo, PostHog.

invisit’s per-person zone has the cost Fathom moved away from in June 2026: teammates in Istanbul and New York see different totals for “today” on the same site. Your account’s zone comes from your browser at sign-up and can be changed in account settings; the REST API takes it as a parameter and echoes it in meta.timezone.

Date presets add a second trap. Plausible’s “Last 7 days” leaves out today (Plausible), while invisit’s is today plus the six days before it, so two dashboards on their presets read two different weeks. Type explicit dates into both.

Daily-hash tools also restart visitor counting at midnight UTC whatever zone the report uses, so one local day can span two hashes. The midnight UTC boundary explains the effect on unique visitors.

Why does GA4 hide, sample or estimate some numbers?

GA4 changes some numbers on purpose: it withholds small rows, samples large queries, folds rare values into (other) and estimates unique counts. Each shows up as a gap against tools that count every row.

Data thresholds

GA4 withholds data from reports that include demographics, and drops search query rows with too few users, so that nobody can be identified. You cannot adjust the thresholds; the data quality icon says when they apply, and a wider date range makes them less likely (Google).

Sampling

GA4 samples a query that covers more than 10 million events on a standard property, or up to 1 billion on Analytics 360, and the data quality icon shows the share of data used (Google). This mostly hits Explorations. Plausible applies “limited data sampling” to dashboard views with more than 10 million pageviews (Plausible).

The (other) row

When a table has more rows than its limit, GA4 keeps the most common values and folds the rest into a row called (other). Google treats any dimension with more than 500 values as high cardinality, and reports with secondary dimensions, filters or comparisons are the likeliest to hit the limit (Google). GA4 applies stricter limits to intraday data until the daily data replaces it (Google). A page list in GA4 whose rows add up to less than its total is usually this.

Estimated unique counts

GA4 estimates users and sessions with HyperLogLog++, at precision 14 for users and 12 for sessions (Google), and puts the session count within ±1.63% at 95% confidence, so exact counts from BigQuery can differ by about that much (Google). Fathom says it counts every visitor exactly (Fathom). invisit counts visits with ClickHouse’s uniq function, an approximate count from a sample of up to 65,536 hashes (ClickHouse). A gap of a percent or two on sessions or visits is noise.

With consent mode in its advanced setup, GA4 models the visitors who declined analytics cookies and adds them to its reports, so GA4 can show more users than it observed. A cookieless tool counts those visitors directly, so the two numbers can land close while meaning different things.

Modelling needs consent mode on every page with tags that load before the visitor answers, at least 1,000 events a day with analytics_storage denied for 7 days, and at least 1,000 daily users with it granted on 7 of the previous 28 days. Modelled data appears only under the Blended reporting identity, and the data quality icon then reads “Including estimated user data”. Below those volumes, events from visitors who declined are not reported at all (Google).

To compare observed numbers only, set the reporting identity under Admin, Data display, to Observed. Google says the setting changes neither collection nor processing.

How long until each tool’s numbers stop changing?

A day’s numbers keep changing for hours to days after it ends, so a comparison that includes today or yesterday compares unfinished data.

Tool When the numbers settle
GA4 Realtime in a few minutes, intraday in 2 to 6 hours, daily data in about 12 hours; processing can take 24 to 48 hours and some data arrives up to 7 days late
Plausible “There are no delays with data”; the realtime view refreshes every 30 seconds
Matomo Cloud Today’s reports refresh about every 6 hours; finished days start processing 1 to 2 hours after midnight in the site’s zone; the Visits Log is real time
invisit Each report states how current it is; visit-level metrics are final 24 hours and 30 minutes after a visit ends

Sources: GA4 data freshness, Plausible, Plausible realtime, Matomo Cloud. GA4 also says key event attribution can change for up to 12 days.

Every invisit report says how current it is. The meta.freshness object in each REST API answer holds watermark, the latest minute of the site’s data rolled up into reports; as_of, when the answer was computed; and state: ok, stale when processing is more than 10 minutes behind, degraded when freshness could not be checked, or no_data. The dashboard shows stale data with a “Catching up” badge.

Visit metrics settle later than page views. invisit accepts events up to 24 hours late, and a late event can extend a visit or change whether it bounced. So the visits report, GET /v1/sites/{site_id}/analytics/sessions, merges final visits with visits that can still change, and returns finalized_through and provisional_through to show where one ends. A visit becomes final once it ended more than 24 hours and 30 minutes earlier.

Why do server logs show more traffic than analytics?

Server logs record every request that reaches your server. Browser analytics records a page view only when its script ran in a real browser, for a page someone saw. Three differences make logs higher, and one makes them lower.

Bots that never run JavaScript

Most requests for web pages do not come from people. On Cloudflare’s network on December 2, 2025, humans made 47% of requests for HTML pages and non-AI bots 44%; over the year, AI crawlers averaged 4.2% and Googlebot 4.5% (Cloudflare). Crawlers that only fetch HTML fill your logs and never reach browser analytics, and filtering user agents that contain “bot” removes only the ones that say so.

Pages fetched ahead of a click

Browsers fetch pages before anyone clicks. Chrome marks those requests Sec-Purpose: prefetch, or Sec-Purpose: prefetch;prerender when it renders the page in a hidden tab (Chrome), and a page fetched ahead but never opened is still a line in your log.

We tested this on November 3, 2026 in Chrome 155: a landing page prerendered two pages with Speculation Rules, and the visitor then opened one of them. The server logged three page requests for a visit that saw two pages, and opening the prerendered page sent no new request, so the log alone cannot tell the opened page from the unopened one. invisit and Plausible recorded two page views, while Umami’s script recorded three, sending page views from inside both hidden pages. Which trackers count prerendered pages has the per-tool results and a workaround.

nginx’s default log format does not record the header. Add it and count the share of page loads that were speculative, which is the most your log can overcount from prefetching:

/etc/nginx/conf.d/views.conf
log_format views '$remote_addr [$time_iso8601] "$request" $status '
                 '"$http_user_agent" purpose="$http_sec_purpose"';

server {
    # ...your existing server block
    access_log /var/log/nginx/views.log views;
}
Terminal
awk -F'"' '$2 ~ /^GET \/[^ .]* HTTP/ && $3 ~ /^ 200 / && tolower($4) !~ /bot|crawl|spider|headless/ { n++; if ($6 ~ /prefetch/) s++ } END { printf "page loads: %d, speculative: %d (%.0f%%)\n", n, s, 100 * s / n }' /var/log/nginx/views.log

We ran both in the nginx:alpine image. The one-liner keeps successful GETs for paths without a file extension and drops self-declared bots.

JavaScript that never runs

A visitor who closes the tab before the script loads, a browser with JavaScript off, a script that fails to load and a blocked request all appear in logs only. We found no neutral measurement of the first three. To size them on your own site, compare HTML responses in the log with page views for the same landing pages and day, as in measuring your own analytics gap.

CDN cache hits, which make logs lower

A page served from a CDN cache never reaches your origin, so origin logs miss it while the browser script still counts it. Cloudflare does not cache HTML by default, but a cache rule that caches everything changes that (Cloudflare). If your CDN caches pages, compare analytics with the CDN’s logs, not the origin’s.

How do subdomains and cross-domain journeys change visitor counts?

Moving between your own domains can turn one visitor into two and your own site into a referrer, depending on how each tool identifies people.

To check, look for your own domains in each tool’s referrer report. Every row is a journey the tool split in two.

How to reconcile two analytics tools over the same week

Work from the metric with the fewest definitions to the most, so each step removes one cause before the next.

  1. Pick a closed week, Monday to Sunday, that ended at least two days ago, so GA4 has finished most of its processing. Type the dates into both tools instead of using presets.
  2. Set both tools to the same reporting time zone. In invisit that is your account time zone, or the timezone parameter in the API.
  3. Check that both scripts run on the same pages and hostnames. A page missing from one tool’s top pages is a missing script; a hostname in only one tool is a preview or staging deployment.
  4. Compare page views for the week, then per page. The remaining ratio is mostly blocking, consent and bot filtering.
  5. Break page views down by browser in both tools. A gap concentrated in a few browsers points at blockers or consent; a gap spread evenly points at filters or definitions.
  6. Remove what one tool filters and the other does not: your own visits in both, and any day with a spike from one city.
  7. Compare GA4 sessions with Plausible or invisit visits only once page views line up, and visitors only one day at a time.
  8. Write down the ratio between the tools and check it again next month. A stable ratio is normal; a sudden change means a broken script, a new consent banner, a blocklist update or a bot wave.

Checklist before you call an analytics gap a bug