Why doesn't Google Consent Mode v2's ad_personalization/analytics_storage signal update the way I expect?
Short answer
Consent Mode v2 signals only take effect on each tag's next request, not retroactively on hits already fired, and the most common cause of a signal appearing not to update is calling gtag('consent', 'update', {...}) before the base tag library (gtag.js or GTM) has actually loaded and registered a listener for it - the call gets pushed into the dataLayer array, but nothing is listening yet to act on it.
Why this happens
dataLayer.push() calls are just array entries until something reads them; gtag.js and GTM both process the queue once they've initialized, which is why a default consent call needs to run synchronously before either library loads, and an update call needs to run after the visitor actually interacts with the banner. A race where the CMP's accept/decline handler fires the update call before GTM has finished initializing produces exactly this symptom - the push happens, but too early for anything to act on it.
A second common cause is a typo or mismatch in the parameter names themselves - analytics_storage, ad_storage, ad_user_data, and ad_personalization are the four recognized keys, and a misspelled or nonstandard key name silently does nothing rather than throwing a visible error.
A third is confusing gtag.js-level consent handling with GTM's own Consent Overview - a GTM container needs each tag's Consent Settings configured (see the related tag-governance page) for tags added through GTM's UI to actually respect an incoming signal; the dataLayer push alone isn't sufficient for those tags.
Fix it
- 1
Confirm the default call runs before anything else loads
The
consent,defaultcall needs to execute synchronously in the page's initial script (abeforeInteractive-equivalent placement), before gtag.js, GTM, or any vendor tag has a chance to load.window.dataLayer = window.dataLayer || []; function gtag(){ dataLayer.push(arguments); } gtag('consent', 'default', { analytics_storage: 'denied', ad_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied', wait_for_update: 500 }); - 2
Confirm the update call fires from the actual banner interaction handler
Check that your CMP's accept/decline button handler calls
gtag('consent', 'update', {...})with the exact four parameter names above - a typo in any key silently no-ops rather than erroring. - 3
Check the wait_for_update timing against your banner's own render delay
If
wait_for_updateis shorter than however long it actually takes your banner to render and receive a click, tags can fire on the default (denied) state before the update ever arrives - increase the value or confirm the banner renders faster than the timeout. - 4
Separate gtag.js-level consent from GTM's per-tag Consent Settings
If you're using GTM, confirm each relevant tag has Consent Settings configured under Advanced Settings - a correct dataLayer push updates the signal, but a GTM tag without Consent Settings configured doesn't check that signal at all.
- 5
Read resolved state from GTM Preview or Tag Assistant, not just the raw dataLayer array
The dataLayer array shows you what was pushed; GTM Preview's Consent tab shows you the resolved state GTM is actually applying per tag, which is the signal that actually matters for whether a tag fires.
How to verify it worked
Open GTM Preview mode, interact with the consent banner, and check the Consent tab for the exact resolved state of each signal after your choice - a passing result matches what you clicked.
In the browser console, type dataLayer after interacting with the banner and confirm a consent, update entry appears with the exact key names and values you expect.
If using GTM, confirm in the tag's own debug entry that it lists the correct Consent Settings and shows fired/not-fired matching the current consent state.
Related
Still stuck, or want this checked against your specific setup?
We scope every engagement in discovery, before implementation - no assumptions about your stack.