Cybersecurity · September 16, 2026 · intSignal Team

Why GA4 Undercounts Visitors From Some US States (and How to Check)

Share this article

Our own site's traffic looked low, and kept looking low. The tag was installed, tags were firing, and there was no consent banner blocking anything. The cause turned out to be a hold on the Google side that delays GA4's page view in certain US states — long enough that short visits from those states were never counted at all.

We measured it, fixed it, and shipped the fix. Six weeks later we measured again and found Google had quietly changed which states it applies to, and our fix had started costing us data in our most important market. This is both halves of that story, and a script to check your own property.

Why is GA4 undercounting visitors from some US states?

In some US states, Google's served tag configuration holds a cookie-based GA4 hit for about five seconds, waiting for a consent signal, before sending the page view. Visitors who leave within that window are never recorded. A cookieless hit is not held, so it's sent immediately.

The effect is lopsided: engaged sessions still arrive, just late, while quick visits from those states disappear. That's easy to misread as a traffic drop or a bounce-rate problem rather than a measurement gap.

What we measured

We tested from a California IP address, which Google geolocates to California, against our live site in August 2026.

The GTM container loaded at about 400ms, and gtag had downloaded and executed by about 412ms. Then nothing happened for roughly 5.3 seconds. The page view fired at about 6,060ms, with tfd at 6059 and consent reported as granted.

That idle gap is the tell. The tag wasn't slow to load and the page wasn't busy. It was waiting.

Before finding the cause we ruled out the usual suspects. It wasn't a missing tag, a consent management platform (we don't run one), our own cookie-consent code, a trigger misconfiguration, or main-thread work delaying the tag.

Six things that didn't release the hold

The obvious assumption is that the page must be failing to send some consent signal. So we tried supplying one, in every way we could think of. Each of these still waited about 5.7 to 6 seconds:

  1. No consent commands at all.
  2. Analytics consent set to granted by default.
  3. A consent update pushed about 800ms after load.
  4. wait_for_update: 0 on the consent default.
  5. Hand-rolled __tcfapi and __gpp stubs.
  6. Loading the tag early in <head> versus about 550ms later.

None of it made a difference. Nothing on the page released the hold.

One trap worth knowing: a throwaway, unregistered measurement ID fired in about 0.6 seconds — but only because an unregistered property skips consent processing entirely. It looks like a working control and isn't one. Test against the real property.

The fix: cookieless only where Google holds

The breakthrough was noticing the hold only applied to cookie-based hits. A cookieless hit from the same state was sent right away.

Google's Consent Mode supports a region parameter on consent defaults, so the fix was to keep full cookie-based measurement everywhere except the held states, and default those to cookieless:

gtag('consent', 'default', {
  analytics_storage: 'granted',
  ad_storage: 'denied',
});
// Cookieless only where Google holds cookie-based hits.
// This list changes. Match it to the served config, then re-measure.
gtag('consent', 'default', {
  analytics_storage: 'denied',
  region: ['US-CO', 'US-CT', 'US-DE', 'US-MD', 'US-MN', 'US-MT',
           'US-NE', 'US-NH', 'US-NJ', 'US-OR', 'US-TX'],
  wait_for_update: 500,
});

The snippet shows the state list as it stood in September. In August, our version also included US-CA — which matters later.

Measured from California after the change, the page view dropped from about 6,060ms to between 734ms and 1,114ms, sent as cookieless with no _ga cookies set. Short visits from those states now count.

The trade-off is real. Visits from those states become cookieless and rely on GA4's modeling rather than cookies, so you lose some precision on returning users and attribution there. We judged partial data better than none.

Check your own legal position before copying this. We run an opt-out model for a US-focused site, which is why analytics defaults to granted outside the held states. That is a legal decision, not a technical default. Visitors in the EU and UK, for example, generally require opt-in consent before analytics cookies. Set your consent defaults to match your obligations first, and treat the region handling as a measurement tweak on top.

Six weeks later, the list had changed

In September we re-checked before writing this up, expecting to confirm the August result. We found something else.

Our August fix listed 12 states, including California. The served configuration we fetched on September 16, 2026 listed 11. California was gone.

We then timed page views from California, aborting every analytics request so nothing was recorded. With consent granted and no regional override, the page view fired in 131 to 197ms, with cookies set. There was no hold.

So from some point in those six weeks, our fix had been forcing California visitors to cookieless measurement for no reason. For a company whose core market is Southern California, that meant losing returning-visitor and attribution data from exactly the visitors we care most about.

Measured on our real site through the full GTM container, again with every hit aborted:

Before (CA cookieless)After (CA restored)
Page view sent860–940ms924–963ms
Consent state (gcs)G100, cookielessG101, full
_ga cookies set02

Timing barely changed. The gain was measurement quality: California is back to full, cookie-based analytics.

Check whether your region is held

Don't rely on a published list of states, including the one above. Measure from the location you care about. This script loads your GA4 tag with consent granted and times the page view. It aborts every hit, so nothing is recorded in your property.

// check-ga4-hold.mjs — usage: node check-ga4-hold.mjs G-XXXXXXXXXX
// Requires: npm install playwright && npx playwright install chromium
import { chromium } from "playwright";
import http from "node:http";

const id = process.argv[2];
if (!/^G-[A-Z0-9]+$/.test(id ?? "")) {
  console.error("usage: node check-ga4-hold.mjs G-XXXXXXXXXX");
  process.exit(1);
}

const html = `<!doctype html><script>
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('consent', 'default', {
  analytics_storage: 'granted', ad_storage: 'denied',
});
gtag('js', new Date());
gtag('config', '${id}');
</script>
<script async
  src="https://www.googletagmanager.com/gtag/js?id=${id}"></script>`;

const server = http.createServer((_, res) => {
  res.writeHead(200, { "Content-Type": "text/html" });
  res.end(html);
}).listen(4480);
const browser = await chromium.launch();

async function timePageView() {
  const context = await browser.newContext();
  const page = await context.newPage();
  let start = 0;
  let ms = null;
  await page.route(/\/g\/collect/, (route) => {
    const url = route.request().url();
    if (ms === null && url.includes("en=page_view")) {
      ms = Date.now() - start;
    }
    route.abort(); // never let the hit reach Google
  });
  start = Date.now();
  await page.goto("http://localhost:4480/");
  for (let i = 0; i < 150 && ms === null; i++) {
    await page.waitForTimeout(100);
  }
  await context.close();
  return ms;
}

// The first load downloads the tag cold and can take seconds on its
// own, so time it twice and judge on the faster (warm) run.
const runs = [await timePageView(), await timePageView()];
const shown = runs.map((ms) => (ms === null ? "none" : `${ms} ms`));
console.log("page_view after:", shown.join(", "));

const seen = runs.filter((ms) => ms !== null);
if (seen.length === 0) {
  console.log("=> no page_view seen: check the ID and your network");
} else if (Math.min(...seen) >= 4500) {
  console.log("=> HELD: cookie-based hits from your location wait ~5s");
} else {
  console.log("=> not held from your location");
}

await browser.close();
server.close();

Run it with your own measurement ID. A result around 5,000ms or more means cookie-based hits from your location are held. A few hundred milliseconds means they aren't.

Two things matter for a trustworthy result. First, use your real, registered property — an unregistered ID skips consent processing and will always look fast. Second, the result reflects your IP's location, so to test another state you need to run it from a connection that geolocates there.

We tested all three outcomes before publishing. From California it reported not held, at 219 to 318ms. With a five-second hold simulated on the page, it reported held, at 5,217 to 5,236ms. With the tag blocked from loading, it reported that no page view was seen — rather than mistaking the silence for a hold, which an earlier draft of this script did.

Find the region list in Google's served config

You can also see the list Google is serving your property. Fetch your tag and pull out the region strings:

curl -s "https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX" \
  | grep -oE '"[0-9]+":"US-[A-Z]{2}(~US-[A-Z]{2})+"'

On September 16, 2026 that returned the 11 states above, in two numbered fields near two values of 5000.

Match on the pattern, not a field number. The numbering isn't stable either: the field numbers we noted in August don't match the ones we saw in September. A check that looks for a specific field number will quietly break.

The configuration also contains your own detected location, such as "US-CA" on its own. That's where Google thinks you are, not part of the held list — don't let it confuse the result.

What this means if you manage analytics

  • A persistent, unexplained traffic shortfall can be measurement, not marketing. Check when the page view is actually sent before concluding visitors aren't arriving.
  • The hold affects cookie-based hits only. That's the lever: cookieless hits in the held regions are sent immediately.
  • Any hardcoded state list is a liability. Google changed this one without announcement, and a stale entry silently degrades data rather than breaking anything visible. Put a recurring check on it.
  • It isn't simply every state with a privacy law. California has one and was dropped; Virginia and Utah both have one and weren't on the September list. You can't derive the list from legislation — you have to read or measure it.

What we can't tell you

We can only measure from California, so our timings for the other states come from Google's served configuration, not from connections in those states. We don't know exactly when California was removed, only that it happened between August 6 and September 16, 2026. And we don't know Google's reasons for choosing or changing the list. Treat the specific states here as a dated snapshot, and the method as the part that lasts.

Frequently asked

Why does GA4 show fewer users than my server logs?

There are many causes, including ad blockers and consent choices. One that's easy to miss: in some US states, Google holds cookie-based GA4 hits about five seconds before sending, so visitors who leave sooner are never recorded.

It changes, which is the main lesson here. On September 16, 2026, Google's served configuration listed Colorado, Connecticut, Delaware, Maryland, Minnesota, Montana, Nebraska, New Hampshire, New Jersey, Oregon and Texas. California had been on it in August and was not. Check your own configuration rather than relying on any published list.

Does setting wait_for_update to 0 fix it?

No. We tested wait_for_update: 0, a granted default, a delayed consent update, and hand-rolled __tcfapi and __gpp stubs. All still waited about six seconds. What worked was defaulting the held regions to cookieless, because cookieless hits aren't held.

Is defaulting analytics to granted compliant?

That depends on your jurisdiction and your visitors, not on this measurement issue. Many regions, including the EU and UK, generally require opt-in before analytics cookies. Decide your consent model on legal grounds first.

Share this article