Three signals, one consent stack. In production, getting CCPA and GDPR consent right in 2026 means orchestrating Google Consent Mode v2, Global Privacy Control, and the IAB Global Privacy Platform so they all reflect the same underlying user decision. Each one is separately documented; the interaction between them is where most implementations break.
This is a code-first walkthrough of the unified pattern. It is for the privacy engineers and tag managers who own the consent tagging layer and need to ship something that survives both a CPPA sweep and a DPO review. Companion posts cover the legal detail: GDPR Cookie Consent in 2026, the CCPA Cookie Banner Requirements pillar, and the GPC compliance guide.
TL;DR. The unified pattern runs in order: (1) the site's Consent Mode default is set in
<head>before any tag, with region-specificdeniedvalues for EEA, UK, and US opt-out states. (2) The CMP reads theSec-GPCheader (server) andnavigator.globalPrivacyControl(client) on every request. (3) When GPC is present, the CMP auto-denies and writes the opt-out to the first-party cookie and, if authenticated, to the account. (4) The CMP callsgtag('consent', 'update', ...)to flip Consent Mode. (5) The CMP also updates the GPP string via the__gpp()API so downstream ad-tech vendors receive the signal. (6) Server-side GTM reads the consent state from the event payload and applies it to server-fired tags. Full code, race conditions, and common failure modes below.
The three signals in one mental model
Google Consent Mode v2 is what Google's own tags read. If you use Google Ads, GA4, Floodlight, or Google Tag on EEA/UK traffic, Consent Mode v2 has been mandatory since March 6, 2024. The four boolean signals it takes:
ad_storage: storage of advertising cookies and identifiersanalytics_storage: storage of analytics cookies and identifiersad_user_data: sending user data to Google for advertising purposesad_personalization: using data for personalized advertising and remarketing
Global Privacy Control (GPC) is what the user sends to your site. The user's browser (Brave, DuckDuckGo, Firefox with setting, or any Chrome/Safari user with an extension) sets the Sec-GPC: 1 HTTP header and exposes navigator.globalPrivacyControl === true. Twelve US states now require businesses that sell or share personal information to honor it.
IAB Global Privacy Platform (GPP) is what your CMP sends out to ad-tech partners. It replaces the deprecated IAB US Privacy (USP) string and carries region-specific consent information as base64-encoded section data. Google Ad Manager, and most programmatic ad-tech, now read GPP from bid requests.
The three signals relate as follows: GPC is input from the user, Consent Mode is output to Google, GPP is output to the rest of the ad-tech ecosystem. A well-orchestrated implementation keeps all three consistent with the user's single underlying consent decision.
Consent Mode v2 defaults: the block that has to fire first
The single most common Consent Mode v2 failure is a race condition: a Google tag fires before the consent default is set. When that happens, Google treats the tag as if consent were granted, which produces either measurement errors or, in the EEA, DMA compliance violations.
The fix is to inline the gtag('consent', 'default', ...) block directly in <head>, before the CMP script, before gtag.js, and before any GTM container.
Minimal defaults, global:
<head>
<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',
wait_for_update: 500
});
</script>
<!-- CMP script here, synchronous or async -->
<!-- gtag.js / GTM here -->
</head>
The wait_for_update: 500 tells Google tags to wait up to 500ms for a consent update before firing with the default (denied) state. This is what gives the CMP a chance to load, read GPC, call its own gtag('consent', 'update', ...), and flip the signals before tags actually commit to a consent path.
Region-specific defaults
A single global denied default is aggressive; it will reduce measurement in regions where consent is not legally required. The refinement is region-specific defaults, which Consent Mode v2 supports via an array of ISO 3166-1 alpha-2 country codes or ISO 3166-2 subdivision codes.
Pattern: denied in EEA/UK; US opt-out states denied for ad_user_data and ad_personalization; granted elsewhere.
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
// EEA + UK: deny all by default
gtag('consent', 'default', {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
region: [
'AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IS',
'IE','IT','LV','LI','LT','LU','MT','NL','NO','PL','PT','RO','SK','SI',
'ES','SE','GB'
],
wait_for_update: 500
});
// US UOOM states: deny ad_user_data and ad_personalization by default
gtag('consent', 'default', {
ad_user_data: 'denied',
ad_personalization: 'denied',
region: [
'US-CA','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
});
// Global baseline: granted
gtag('consent', 'default', {
ad_storage: 'granted',
ad_user_data: 'granted',
ad_personalization: 'granted',
analytics_storage: 'granted'
});
</script>
The more-specific region wins. A user in Germany (EEA set) gets the first block's all-denied default. A user in California (UOOM set) gets the second block's ad-specific denial. Everyone else gets the global granted default.
Helper flags
Two additional helpers that improve measurement quality when storage is denied:
gtag('set', 'url_passthrough', true);
gtag('set', 'ads_data_redaction', true);
url_passthrough preserves gclid and similar URL parameters across navigation when cookies are denied, which helps attribution for opted-in users. ads_data_redaction strips ad identifiers from pings when ad_storage is denied, which is the correct behavior under most privacy regimes.
Basic vs. Advanced mode
Two operational modes:
Basic mode: Google tags do not load until the user grants consent. No cookie-less pings. Simplest to defend to a DPO; worst for measurement accuracy because you lose everything on denial.
Advanced mode: Google tags load immediately and fire "cookie-less pings" to Google servers with consent signals in the URL. On denied, the pings still include signals like page URL, IP (server-observed), and timestamp. Google uses these to feed conversion modeling for denied-consent users. Best for measurement; more controversial under ePrivacy Art. 5(3).
The EEA question. Cookie-less pings still involve access to the user's terminal equipment (the browser's execution context) and transmission of information derived from that access. Article 5(3) requires consent for access to terminal equipment. Whether cookie-less pings fall within Art. 5(3) has not been decided by any major DPA, but the conservative position is that they do, and Advanced mode is safer behind prior consent for EEA traffic.
Most privacy-conservative implementations use Basic mode in the EEA (or gate Advanced mode behind consent) and Advanced mode in the US where ePrivacy does not apply.
The CMP update: flipping Consent Mode on user action
Once the CMP has loaded, read GPC, and rendered the banner (if the user hasn't already opted out via GPC), the user interacts: accept, reject, or customize. On that interaction, the CMP calls gtag('consent', 'update', ...).
On accept:
gtag('consent', 'update', {
ad_storage: 'granted',
ad_user_data: 'granted',
ad_personalization: 'granted',
analytics_storage: 'granted'
});
On reject or GPC auto-opt-out:
gtag('consent', 'update', {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied'
});
On granular (user consents only to analytics, not advertising):
gtag('consent', 'update', {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'granted'
});
The order matters. The default block sets the baseline; the update block flips specific signals. Do not re-issue all four signals with granted on every update; only update the ones that the user's choice changes.
GPC detection: server and client
GPC detection happens twice: on the server (for request-scoped decisions, first-party cookie setting, downstream API gating) and on the client (for CMP and Consent Mode flipping).
Server: Next.js middleware
// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const gpc = request.headers.get('sec-gpc');
const response = NextResponse.next();
if (gpc === '1') {
response.cookies.set('user_opt_out', '1', {
httpOnly: true,
sameSite: 'lax',
secure: true,
path: '/',
maxAge: 60 * 60 * 24 * 365, // 1 year
});
response.headers.set('x-user-opted-out', '1');
}
return response;
}
Server: Express
app.use((req, res, next) => {
if (req.get('sec-gpc') === '1') {
req.userOptedOut = true;
// persist to session or DB keyed to user/session
}
next();
});
Server: Rails
class ApplicationController < ActionController::Base
before_action :detect_gpc
private
def detect_gpc
if request.headers['Sec-GPC'] == '1'
session[:user_opted_out] = true
@user_opted_out = true
end
end
end
Server: Laravel
// app/Http/Middleware/DetectGpc.php
public function handle(Request $request, Closure $next)
{
if ($request->header('Sec-GPC') === '1') {
$request->attributes->set('user_opted_out', true);
// persist to session
session(['user_opted_out' => true]);
}
return $next($request);
}
Client: before CMP init
<script>
// Inline before CMP script loads
window.__gpcDetected = (
typeof navigator !== 'undefined' &&
navigator.globalPrivacyControl === true
);
</script>
Most modern CMPs (OneTrust, Cookiebot, Didomi, Usercentrics, CookieYes) auto-detect GPC natively and will auto-apply the opt-out. The inline detection above is a safety check for CMPs that don't, and useful for custom logic you write around the CMP.
The CMP callback: unified flip
When the CMP completes (either because the user interacted or because GPC auto-opt-out was applied), it invokes the consent update. The pattern below shows a CMP-integrated callback that updates Consent Mode and the GPP string together.
Cookiebot example:
window.addEventListener('CookiebotOnConsentReady', function () {
const c = window.Cookiebot.consent;
gtag('consent', 'update', {
ad_storage: c.marketing ? 'granted' : 'denied',
ad_user_data: c.marketing ? 'granted' : 'denied',
ad_personalization: c.marketing ? 'granted' : 'denied',
analytics_storage: c.statistics ? 'granted' : 'denied'
});
// GPP string update is automatic in Cookiebot's current version;
// for custom logic, inspect __gpp API below
});
OneTrust example:
function onOneTrustGroupsUpdated() {
const groups = OnetrustActiveGroups || '';
const marketing = groups.includes('C0004'); // targeting cookies
const analytics = groups.includes('C0002'); // performance cookies
const functional = groups.includes('C0003'); // functional cookies
gtag('consent', 'update', {
ad_storage: marketing ? 'granted' : 'denied',
ad_user_data: marketing ? 'granted' : 'denied',
ad_personalization: marketing ? 'granted' : 'denied',
analytics_storage: analytics ? 'granted' : 'denied'
});
}
// OneTrust fires this after initial load and after every preference change
window.OptanonWrapper = onOneTrustGroupsUpdated;
The GPP string: outbound signal to ad-tech
The IAB Global Privacy Platform (GPP) replaces the deprecated USP string. Google Ad Manager reads GPP from bid requests; Microsoft, The Trade Desk, and other major ad-tech vendors increasingly do too.
GPP strings are base64-encoded and carry a header section plus one or more jurisdiction-specific sections. Current section ID assignments from the IAB Tech Lab GPP spec:
| Section ID | Jurisdiction |
|---|---|
| 2 | IAB Europe TCF v2.2 |
| 3 | GPP header (required) |
| 5 | IAB Canada TCF v2 |
| 7 | US National (MSPA) |
| 8 | US California |
| 9 | US Virginia |
| 10 | US Colorado |
| 11 | US Utah |
| 12 | US Connecticut |
| 13 | US Florida |
| 14 | US Montana |
| 15 | US Oregon |
| 16 | US Texas |
| 17 | US Delaware |
| 18 | US Iowa |
| 19 | US Nebraska |
| 20 | US New Hampshire |
| 21 | US New Jersey |
| 22 | US Tennessee |
| 23 | US Minnesota |
| 24 | US Maryland |
| 25 | US Indiana |
| 26 | US Kentucky |
| 27 | US Rhode Island |
Most CMPs generate the GPP string automatically based on the user's consent and detected region. To read the current GPP state from JavaScript:
window.__gpp('getGPPData', function (data, success) {
if (!success) return;
console.log('GPP String:', data.gppString);
console.log('Applicable Sections:', data.applicableSections);
// e.g., [8] for California
});
To subscribe to changes (useful for downstream code that needs to respond to preference updates):
window.__gpp('addEventListener', function (evt, success) {
if (!success) return;
if (evt.eventName === 'sectionChange' || evt.eventName === 'signalStatus') {
// Respond to the GPP change
}
});
Server-side GTM: propagating consent
The most common advanced-stack mistake is failing to propagate consent from the browser container to the server-side container. The browser knows about the opt-out; the server-side container fires tags anyway because it never received the signal.
The pattern: include the consent state in every event payload sent to the server-side GTM endpoint.
In the browser container, client template:
const consentState = {
ad_storage: getConsentMode('ad_storage'),
analytics_storage: getConsentMode('analytics_storage'),
ad_user_data: getConsentMode('ad_user_data'),
ad_personalization: getConsentMode('ad_personalization'),
gpc: navigator.globalPrivacyControl === true ? 1 : 0
};
sendRequest(sgtmEndpoint, {
events: [...],
consent: consentState
});
In the server-side container, in a tag:
const consent = eventData.consent;
if (consent?.ad_storage !== 'granted') {
// Skip Meta Conversions API, TikTok Events API, etc.
return;
}
The server-side tracking guide covers the broader architecture.
Common failure modes
From audits, in rough order of frequency:
1. Consent Mode default set after gtag init. The gtag('consent', 'default', ...) call must come before any gtag('config', ...) or gtag('event', ...) call. In <head>, before GTM and before gtag.js. If this block runs after the config, Google tags commit to granted before they see the default.
2. No region-specific defaults. The single global denied default is legally safe but costs measurement. The pattern shown above (region-aware, more-specific wins) is the operational standard.
3. wait_for_update omitted or too short. Default wait_for_update is 0, meaning tags fire immediately with the default state. A slow-loading CMP can miss the window. 500ms is a common choice; 2000ms is on the aggressive end. Tune based on CMP load time.
4. CMP and Consent Mode out of sync. Some CMPs block tags entirely (category-based tag suppression) while also calling gtag('consent', 'update', ...). This is fine as long as the two are aligned. When they're not (CMP blocks tag, Consent Mode says granted; or vice versa), diagnosis gets hard. Rule: pick one gating mechanism as authoritative and keep the other synced.
5. Server-side GTM without consent propagation. Covered above. Adding the consent payload to the browser-to-server event and reading it in server tags is a one-sprint fix.
6. Ignoring Sec-GPC on first-party endpoints. Any analytics ingestion API, custom event endpoint, or server-side tag proxy that you run has to read the header too. Otherwise you're in violation of CCPA § 7025 even when your CMP is perfect.
7. GPP string stale after preference change. When the user reopens the banner and changes their choice, the GPP string has to regenerate and the __gpp('addEventListener', ...) event has to fire. Some CMP integrations update the Consent Mode state but not the GPP string.
8. Consent Mode granted sent before banner is dismissed. Caused by gtag init running before the CMP handshake. Symptom: users see the banner, but Google Analytics is already recording them as consented. Fix: move consent defaults above everything else.
The orchestrated flow end to end
Putting it together, the compliant pattern for a single pageview:
- Browser sends request. Includes
Sec-GPC: 1if user has it enabled. - Server middleware detects GPC. Sets
user_opt_out=1first-party cookie. Setsx-user-opted-out: 1response header if relevant downstream. - Server renders page. Includes the Consent Mode defaults block in
<head>, before the CMP script and gtag.js. Region-specific defaults set. - Browser parses the page. Consent Mode defaults commit. Tags queue, waiting for either 500ms or a
gtag('consent', 'update', ...)call. - CMP script loads. Reads
navigator.globalPrivacyControl. Reads theuser_opt_outcookie. If either indicates opt-out, auto-denies. - CMP renders banner or not. If opted out via GPC, skips the banner and renders a small confirmation per California's § 7025(c)(6) display requirement.
- CMP calls
gtag('consent', 'update', ...). Flips Consent Mode to reflect the user's state. - CMP updates GPP string. Via
__gpp()API; downstream ad-tech reads it on outbound bid requests. - Queued tags fire. Google Ads, GA4, Floodlight each respect the Consent Mode signals. Basic mode blocks; Advanced mode sends cookie-less pings.
- Browser-side events flow to sGTM. Event payload includes the consent state.
- Server-side GTM reads consent. Decides whether to fire Meta CAPI, TikTok Events API, LinkedIn Conversions API, etc.
- If user reopens banner and changes preferences. Steps 7 through 11 re-fire.
__gpp('addEventListener', ...)subscribers receive the update.
Every step has a corresponding regulatory obligation. A gap at any step is a gap in the consent chain.
FAQ
Why does the wait_for_update matter?
It gives the CMP time to load and update Consent Mode before Google tags commit. Without it, tags fire immediately with the default (usually denied for EEA), and then can't be "un-fired" when the CMP later says granted. The tag has already committed to the default path.
Should I use Basic or Advanced mode in the EEA?
Basic is safer under ePrivacy Art. 5(3). Advanced sends cookie-less pings pre-consent that arguably constitute access to terminal equipment. No DPA has formally ruled, but the privacy-conservative position is to use Basic in the EEA or gate Advanced behind consent. In the US, Advanced is the common choice.
Does GPC override the Consent Mode default?
GPC doesn't override the default; it triggers the CMP's update flow. The CMP sees GPC, auto-denies in its own state, and calls gtag('consent', 'update', ...) with denied values. From Google's perspective, it's still an update. The difference is that the user didn't click anything; the browser's signal did the work.
Can I skip the GPP string if I only target US traffic?
Technically you can, but Google Ad Manager, OpenRTB 2.6, and most programmatic partners expect the GPP string now. The legacy USP string was deprecated January 31, 2024. Implementing GPP is the forward-compatible choice.
How do I test that my Consent Mode defaults fire correctly?
Open DevTools, disable cache, reload the page, and inspect the collect requests to Google Analytics and Google Ads. Look for gcs=G100 (denied) or gcs=G111 (granted) in the query string. If you see gcs=G111 before the CMP has loaded, your default is broken.
What about Microsoft Consent Mode?
Microsoft UET and Microsoft Clarity have their own consent APIs with similar structure. Covered in Microsoft UET Consent Mode and Microsoft Clarity Consent API. The orchestration pattern is identical; only the API surface differs.
Is TCF v2.3 mandatory in 2026?
TCF participants (CMPs and ad-tech vendors in the IAB ecosystem) have until February 28, 2026 to adopt v2.3. As a publisher using a TCF-compliant CMP, you will get v2.3 support automatically when your CMP updates. No code change required on your side.
How do I handle users who change preferences mid-session?
The CMP should re-fire its update callback whenever preferences change. That callback re-calls gtag('consent', 'update', ...) with the new state and updates the GPP string via __gpp(). Google tags receive the update immediately; server-side tags receive it on the next event. For consent withdrawal, ensure the session's cookies are cleared (if the user denies consent for a previously-granted category, stale cookies should be deleted).
What's the single biggest measurement gap I can fix?
Setting the Consent Mode defaults in <head> before the CMP script and before gtag.js. This one change typically restores 10-30% of lost measurement because it stops tags from committing to the wrong default before the CMP can update them.
Where to go from here
If your consent stack is using Consent Mode v2 but not GPC or GPP, or has a single denied default globally, the highest-leverage changes are: (1) inline the region-specific defaults in <head> with wait_for_update, (2) wire GPC detection in server middleware and client-side before the CMP, and (3) ensure server-side GTM receives the consent state in every event payload. Most implementations I see are missing one or more of these.
For the legal context, see the GDPR Cookie Consent pillar and the CCPA Cookie Banner Requirements pillar. For GPC specifically, including the new 2026 display requirement, see the GPC compliance guide.
If you want a second pair of eyes on your current Consent Mode / GPC / GPP orchestration, Consenteo's engineering team has built this stack on 200+ corporate sites. Get in touch for a review against the failure modes in this post.
Keep reading
More from the Consenteo Knowledge Hub on this topic.
Global Privacy Control (GPC) in 2026: The Complete Compliance and Implementation Guide
GPC is now mandatory in twelve US states and the subject of a coordinated CA-CO-CT enforcement sweep. A practitioner's guide to the spec, the statutes, the January 2026 § 7025(c)(6) display rule, and the middleware code that actually implements it.
Microsoft Clarity Consent API Explained
How to wire the Microsoft Clarity Consent API correctly so Clarity respects user consent state under GDPR and CCPA, with the common implementation mistakes to avoid.
Microsoft UET Consent Mode: What It Is and How to Use It
Microsoft UET Consent Mode gates Bing Ads and Microsoft Advertising conversion tracking on user consent state. How it works, how to wire it, and how it relates to Google Consent Mode v2.
