India's Digital Personal Data Protection Act moved cookie consent from a European concern to a domestic one. If you serve Indian users, run analytics or advertising tags, and have no consent mechanism, you have a compliance gap — and the usual fix, bolting on a banner that does nothing, does not close it.
Consent Mode v2 is Google's mechanism for making tags respond to consent rather than ignoring it. Here is how to implement it properly.
What the DPDP Act actually requires
Reduced to what affects your tag setup:
Consent must be free, specific, informed and unambiguous, given by a clear affirmative action. Pre-ticked boxes and "by continuing you agree" banners do not qualify.
It must be as easy to withdraw as to give. If accepting is one click, so is withdrawing.
You must be able to demonstrate consent was obtained.
Notice must be plain and available in English and the Eighth Schedule languages.
The practical consequence: analytics and advertising storage must default to denied, and only flip to granted after an explicit user action.
Consent Mode v2 signals explained
Four signals matter:
| Signal | Governs |
|---|---|
| analytics_storage | Analytics cookies (GA4) |
| ad_storage | Advertising cookies |
| ad_user_data | Sending user data to Google for advertising |
| ad_personalization | Personalised advertising / remarketing |
Two more — functionality_storage and security_storage — cover storage your site needs to work at all and can normally stay granted.
When a signal is denied, Google's tags do not write cookies. They may still send cookieless pings, which is what allows conversion modelling. This is the key insight: consent mode is not an on/off switch for measurement, it is a degradation path.
Default (denied) vs Update (granted) — the mistake everyone makes
This is the error I see most often, and it silently disables consent entirely.
There are two distinct commands:
// DEFAULT — runs before any tag, on every page load
gtag('consent', 'default', { analytics_storage: 'denied', /* ... */ });
// UPDATE — runs only when the user makes a choice
gtag('consent', 'update', { analytics_storage: 'granted', /* ... */ });
The mistake is firing an update to granted on all pages. It looks like it works — data flows, nothing errors — but it grants consent for every visitor before they have chosen anything. You have a banner for show and no actual consent gate.
default describes the state before the user decides. update reflects a decision the user actually made. If nobody clicked anything, there is nothing to update.
The two-tag GTM pattern
Tag 1 — Consent Default. Trigger: Consent Initialization – All Pages (a special trigger that runs before everything else — not the ordinary "Initialization" trigger).
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied',
'functionality_storage': 'granted',
'security_storage': 'granted',
'wait_for_update': 500
});
</script>
wait_for_update tells Google's tags to hold briefly for a consent decision before acting on the default — which stops you losing the first pageview of users who accept immediately.
Tag 2 — Consent Update. Trigger: a Custom Event, cookie_accept.
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'update', {
'ad_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted',
'analytics_storage': 'granted'
});
</script>
Prefer GTM's built-in Consent Mode template over Custom HTML where you can — GTM warns about gtag commands in Custom HTML because ordering is not guaranteed.
Wiring a cookie banner to cookie_accept
The banner is the part that makes this real. It needs to:
Show on first visit, with Accept and Reject given equal visual weight. A grey "Reject" next to a bright "Accept" is a dark pattern and undermines "freely given".
On Accept:
dataLayer.push({ event: 'cookie_accept' })and store the choice.On Reject: store the choice and push nothing (default denied already applies).
Re-apply the stored choice on subsequent loads, after the default tag.
Offer a way to change the decision later — a footer link is enough.
function accept() {
localStorage.setItem("cc_consent", "granted");
window.dataLayer.push({ event: "cookie_accept" });
hideBanner();
}
// On load, after the default tag has run:
if (localStorage.getItem("cc_consent") === "granted") {
window.dataLayer.push({ event: "cookie_accept" });
}
Gating GA4 and Ads tags
In each tag: Advanced Settings → Consent Settings.
GA4 tags require
analytics_storageAdvertising tags require
ad_storage,ad_user_data,ad_personalizationYour consent tags themselves require no additional consent — a consent tag that gates itself can never run
Use GTM's Consent Overview (the shield icon) to confirm every tag has this configured. Anything unconfigured is a tag that ignores consent.
Verifying in the Consent tab
In GTM Preview, the Consent tab shows on-page consent state. Check:
On first load, before interaction:
analytics_storage: deniedYour GA4 tags appear under Tags Not Fired, blocked by consent
After clicking Accept: state flips to
grantedand the tags fire
If GA4 tags fire before you accept, something is granting consent early — usually an update tag on the wrong trigger.
Modelled data: what you still keep
The common objection is "consent will destroy our data". In practice, with consent mode implemented properly:
Denied users still send cookieless pings, so Google can model conversions
Aggregate trends stay usable even when a meaningful share decline
You keep behavioural modelling in GA4, which requires consent mode to be present
The alternative — no consent mechanism — is not "more data". It is the same data with regulatory exposure attached.
Need a compliant setup? → Talk to us