┌── POST 09.05 · Cookie Audits & Scanners · 7 min read

Your CMP Says Denied. What Did Google’s Tag Actually Send?

Tag Assistant shows what your page declared, not what it sent. The gcs and gcd parameters on the outgoing request show the real state — and gcs is blind to both signals Consent Mode v2 added. A measured matrix and a console decoder.

TL;DR

Tag Assistant shows you what your page declared. The gcs and gcd parameters on the outgoing request show what Google’s tag actually sent. They are not always the same thing, and one gap matters: gcs carries only ad_storage and analytics_storage, so granting the two signals Consent Mode v2 added leaves it reading G100 — denied — while the request is measurably not denied. Measured here on 5 September 2026, with the full matrix and a console decoder you can paste into your own site.

There is a specific kind of consent bug that survives every check most teams run. The banner works. Tag Assistant shows the right states. The dataLayer looks correct. And the data going out of the browser still does not match what the user chose.

It survives because every standard verification step inspects the same thing: the consent state the page has declared to itself. Google’s own consent mode debugging guide documents exactly one method, Tag Assistant, and Tag Assistant reads the API calls your page made. If your CMP calls gtag('consent', 'update', ...) with the wrong mapping, Tag Assistant faithfully reports the wrong mapping as the state, because that is the state.

The independent check is the request itself. Every hit a Google tag sends carries the consent state it was operating under at the moment it fired, and you can read it from the network tab without trusting any layer above it.

The two parameters

Google documents that these exist, and documents what one of them carries. From the consent mode overview:

The gcs parameter is used to transmit the ad_storage and analytics_storage parameters, indicating the user’s consent choice regarding the storage of advertising and analytics cookies (web) or device identifiers (app).

And, separately, that gcd “is always sent to Google services, regardless of whether consent mode is activated or not.”

What Google does not publish is the value table. There is no official page saying what G100 means, or how to read gcd. So rather than repeat what the SEO pages assert about it — this niche invents things, and the gcd format they describe does not match what the tag currently sends — the values below were measured.

How they were measured

Nine static pages, each loading the real gtag.js from googletagmanager.com against the same GA4 property, each running a different consent sequence: no consent mode at all, defaults denied, defaults granted, and various updates applied 1.2 seconds after page load. Every page read its own outgoing /g/collect URLs back out of the Performance Timeline. Chromium 152.0.7977.82 on macOS, 5 September 2026, gtag container 45je6921za200.

The matrix

Consent sequence gcs gcd npa
No consent mode at all absent 13l3l3l3l1l1 0
Default: all four denied G100 13p3p3p3p5l1 1
Default: all four granted G111 13t3t3t3t5l1 0
Denied, then analytics_storage granted G101 13p3r3p3p5l1 1
Denied, then the three ad signals granted G110 13r3p3r3r5l1 0
Denied, then all four granted G111 13r3r3r3r5l1 0
Denied, then ad_user_data + ad_personalization only G100 13p3p3r3r5l1 0

gcs reads G1 followed by two binary digits: ad_storage, then analytics_storage, 1 for granted. G101 is analytics granted, advertising denied.

In gcd, four letters appear in the order ad_storage, analytics_storage, ad_user_data, ad_personalization. Across these runs: l means the signal was never set, p denied by default, t granted by default, and r denied by default and then granted by an update. The remaining digits, the fifth letter slot and the trailing characters did not vary in a way these scenarios explain, so no meaning is claimed for them here.

One value is missing on purpose. A state for “denied by default, then actively confirmed as denied by the user” is widely asserted online, but the scenario that should produce it — an explicit all-denied update — caused GA4 to send no further hit at all, so there was nothing to read. It is not in the table because it was not observed.

Finding 1: gcs cannot see the v2 signals

Look at the last row again. ad_user_data and ad_personalization were both granted. gcs stayed at G100.

This follows directly from Google’s own definition — gcs transmits ad_storage and analytics_storage, and those two only — but the consequence is easy to miss. The two signals that Consent Mode v2 introduced, the ones the certified-CMP requirement for Google Ads in the EEA is built around, are invisible in the parameter most guides tell you to check.

So “I checked, it says G100, we’re denied” is not a valid conclusion. In that last row the request was carrying two granted advertising signals, and npa — non-personalised ads — had already flipped from 1 to 0 while gcs sat unchanged. If you are verifying a v2 implementation, gcs is the wrong parameter on its own. Read gcd, and read npa as a cross-check.

Finding 2: the first hit is frozen

In every scenario that started denied, the initial page_view went out as gcs=G100 and stayed that way. Granting consent 1.2 seconds later never rewrote it. The grant produced a new hit carrying the new state; the original was already gone.

This is the behaviour that makes wait_for_update matter, and it is why the order and timing of the default and update calls is not a stylistic preference. A returning visitor who consented last week, on a site whose CMP restores that state 300ms into the page load, contributes one denied-state page view on every single load. It will not look like an error anywhere in your reporting. It looks like modelled traffic.

When you check a real site, check the first hit specifically, not whichever one is convenient in the list.

Finding 3: absence of gcd proves nothing

The page with no consent mode whatsoever still sent gcd=13l3l3l3l1l1 — all four signals reading “never set” — which matches Google’s statement that gcd goes out regardless. It also wrote _ga cookies immediately, as you would expect with nothing gating it.

What was missing on that page was gcs. That is the useful tell: no gcs parameter means consent mode is not running on that tag at all. A gcd of all l says the same thing in more detail. If you inherit a site and want to know within ten seconds whether consent mode was ever wired up, this is the check.

Do it on your own site

Paste this into the console before touching the banner, then interact with it. It watches every Google collect request and labels the consent state each one carried.

const SIG = ['ad_storage','analytics_storage','ad_user_data','ad_personalization'];
const GCD = {l:'never set', p:'denied by default',
             t:'granted by default', r:'denied, then granted'};

new PerformanceObserver(list => {
  for (const e of list.getEntries()) {
    if (!/\/collect|\/ccm\//.test(e.name)) continue;
    const q = new URL(e.name).searchParams;
    const gcs = q.get('gcs'), gcd = q.get('gcd');
    console.log(q.get('en') || '(hit)', {
      gcs: gcs
        ? gcs + ' - ad_storage=' + (gcs[2] === '1' ? 'granted' : 'denied') +
              ', analytics_storage=' + (gcs[3] === '1' ? 'granted' : 'denied')
        : 'ABSENT - consent mode is not running',
      gcd: gcd
        ? (gcd.match(/[a-z]/g) || []).slice(0, 4)
              .map((c, i) => SIG[i] + '=' + (GCD[c] || c))
        : null,
      npa: q.get('npa')
    });
  }
}).observe({type: 'resource', buffered: true});

On a correctly wired site with defaults denied, the first line should show G100 with all four signals “denied by default”. After accepting analytics only, the next hit should show G101 with analytics_storage=denied, then granted and the three advertising signals still denied. If ad_user_data moves when the user accepted analytics only, your category mapping is wrong — and that is the failure gcs alone would have hidden from you.

Two caveats on the method. buffered: true replays requests already in the timeline, so you can run it after page load and still see the first hit. And resource timing gives you the URL, not the response — this tells you what was sent, not what Google did with it.

What this does not tell you

This is a check on Google’s tags and nothing else. It says nothing about the Meta pixel, the session-replay script or the chat widget, none of which have a gcs parameter and none of which are governed by these signals. Consent Mode denied is also not the same as a tag being blocked: the requests in the denied rows above were still sent, cookieless, for modelling. If your obligation is that nothing fires before consent, these parameters describe how a hit was labelled, not whether it should have happened.

What they do give you is the one thing the dashboards cannot: evidence from outside your own stack about what left the browser.

If you would rather not read query strings by hand on every template, CookieInspector’s Consent Mode checker captures the same requests across your site and flags the pages where the signals disagree with the banner.

C
About the author
Consent Mode HQ
Editorial team at Consent Mode HQ
Read more by author ↗