- A consent banner is a UI element, not a control. If the OnConsentChange event doesn't tear down active tag subscriptions and block sGTM clients server-side, PHI ships before the user finishes reading the banner.
- Client-side consent state (localStorage, cookie flags) is spoofable and doesn't survive a page reload race condition. Consent must be checked at the point of transmission, not just at the point of tag firing.
- Google Consent Mode v2's 'denied' state still allows pings for modeling unless you explicitly disable ad_storage AND ad_user_data AND ad_personalization, and even then GA4 will send cookieless hits by default.
- The fix is a server-side consent gate: a single source of truth (a consent record tied to session ID, not cookie) that every downstream tag, pixel, and CAPI call reads before it builds a payload.
A hospital system client had a consent banner. OMS-certified CMP, IAB TCF strings, the works. Legal signed off. Compliance signed off. Then I opened DevTools on their oncology landing page, clicked "reject all," and watched a Meta Pixel PageView event fire 400 milliseconds later with the full URL path (/conditions/breast-cancer/treatment-options) sitting in the referrer field of a Facebook Graph API call.
The banner worked exactly as designed. It just wasn't wired to anything that mattered.
This is the gap in almost every healthcare consent implementation I've reviewed: the banner is a UI layer bolted onto a tracking stack that was never built to listen to it. Consent Mode is configured in the CMP's dashboard. The GTM container has no corresponding checks. Or the checks exist client-side, but a hardcoded script tag in the <head> fires before the CMP even loads. The banner shows up first visually, but the network requests it's supposed to block are already in flight.
Consent Isn't a Popup. It's a Data Contract.
Most teams treat "consent architecture" as choosing a CMP vendor (OneTrust, Cookiebot, Osano) and dropping their script into GTM. That's the easy 20% of the problem. The hard 80% is making sure every single downstream request, from every tag, in every environment, actually checks consent state before it builds a payload.
Think of it as a contract with three parties who all need to agree before data moves:
- The user grants or denies a scope of consent (analytics, ad storage, personalization).
- The consent management layer records that decision and exposes it as a state.
- Every tag, pixel, and server endpoint reads that state before constructing or sending a request.
Step 3 is where healthcare implementations fall apart. Not because teams don't care, but because most tag managers were built for e-commerce, where the cost of a missed consent check is a GDPR fine. In healthcare, the cost is a HIPAA violation with PHI already sitting on Meta's or Google's servers, unrecoverable.
A cookie flag or localStorage value that says analytics_consent: denied can be read, but it can also be delayed, raced, or simply ignored by a script that doesn't check for it. If your only consent enforcement lives in the browser, a hardcoded <script> tag added by a developer six months ago for a UX experiment bypasses it completely. Enforcement has to happen at the network layer, not just the UI layer.

Where the Banner Lies to You
Google Consent Mode v2's Default Behavior
Consent Mode v2 has two states most teams don't understand: denied and granted, and a third behavior most teams don't know exists, cookieless pings.
When a user rejects cookies and ad_storage is set to denied, Google Tag (gtag.js) doesn't stop talking to Google. It sends a cookieless ping, stripped of the ad click ID and first-party cookie, but still containing the page URL, timestamp, and a modeled conversion estimate. For a healthcare site where the URL itself is the PHI (/conditions/hiv-treatment/schedule-consultation), the cookieless ping still leaks it.
To actually stop this, you need all three signals denied, not just ad_storage:
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied'
});
And even with all four denied, GA4 will still send a request to google-analytics.com/g/collect with gcs=G100 (the "consent denied but pinged anyway" flag) unless you've set wait_for_update and confirmed no analytics tag fires before consent resolves.
The Race Condition Nobody Tests
Here's the sequence that breaks most implementations:
- Page loads. GTM container loads. Base tags configured to fire on "All Pages" start their trigger evaluation.
- CMP script loads asynchronously (usually from a CDN, adding 200-800ms of latency).
- Before the CMP finishes rendering the banner and setting default consent state, a base tag with no consent condition already fired.
- User later clicks "reject all." Too late. The first request already went out.
I tested this on the same oncology site: reloaded the page 10 times with network throttling set to "Slow 3G" in DevTools to exaggerate the load gap. In 4 of 10 loads, the Meta Pixel base code fired before the CMP banner rendered. The fix wasn't a smarter banner. It was moving the default consent state assignment to inline <head> script, before any GTM container loads at all:
<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': 2000
});
</script>
<!-- GTM loads after this -->
</head>
wait_for_update: 2000 tells Google tags to hold for 2 seconds for a consent update before defaulting to denied behavior. That buys the CMP time to load and set the real state. It's not elegant, but it closes the race condition that a pure client-side setup can't otherwise avoid.
On a fast connection in your office, the CMP loads instantly and you'll never see the race condition. Throttle to "Slow 3G" in DevTools (Network tab → Throttling dropdown) and reload 10 times. If any tag fires before the banner renders, you have the same gap.
Server-Side Consent Gating: The Actual Fix
Client-side consent checks are necessary but not sufficient. The architecture that actually holds up under audit puts the consent check in your server-side container (sGTM), not just the browser.
Here's the shape of it:
- Consent state is captured client-side by the CMP and written to a first-party cookie, scoped to your domain, not a third-party CMP domain.
- That cookie is passed to the sGTM client as part of every event payload (via the dataLayer, then the GTM web container, then the sGTM endpoint).
- The sGTM container has a server-side variable that reads the consent cookie and evaluates it before any downstream tag (Meta CAPI, Google Ads Enhanced Conversions, GA4 server tag) is permitted to build a request.
- Tags without an explicit consent check are blocked at the container level, not left to trigger conditions that a developer might forget to add.
The critical difference: in a client-only setup, a new tag added by someone unfamiliar with your consent requirements will fire by default. In a server-gated setup, a new tag has to be explicitly granted permission to run, or it does nothing. You've flipped the default from "runs unless blocked" to "blocked unless permitted."
// sGTM server-side variable: Consent Check
function getConsentState(eventData) {
const consentCookie = eventData.headers['x-consent-state'];
if (!consentCookie) return 'denied'; // fail closed, not open
const parsed = JSON.parse(consentCookie);
return parsed.analytics_storage === 'granted' ? 'granted' : 'denied';
}
That last line matters more than it looks. If the consent cookie is missing, malformed, or hasn't loaded yet, the default is denied. Most implementations I've audited default to granted when consent state is ambiguous, because "fail open" is what happens when nobody explicitly coded the fallback. Fail closed instead. A missed conversion event is recoverable. A PHI leak isn't.
Even if you've got a signed BAA with your analytics vendor (rare, and only available for a narrow set of HIPAA-eligible tools), that BAA covers what happens to data once it's received. It doesn't stop the data from being sent in the first place if your consent gating never actually engaged. The BAA and the consent architecture solve two different problems. You need both.
What to Check This Week
Skip the vendor comparison for now. Test what you already have.
Open DevTools, reject all cookies, reload the page. Filter the Network tab for
google,facebook,doubleclick. If any request fires after rejection, your consent gating is cosmetic regardless of what the CMP dashboard reports.Throttle to Slow 3G and reload 10 times. Count how many times a tag fires before the CMP banner finishes rendering. Any count above zero is a race condition, not an edge case.
Export your GTM container JSON and search for tags with no
consentSettingsblock. Any tag missing this is running unconditionally, independent of what the user clicked.Check whether your consent state is enforced server-side or only client-side. If a new hire adds a hardcoded script tag directly to the HTML next month, does anything stop it from firing before consent resolves? If the answer is no, your architecture depends on nobody making a mistake, which is not an architecture.
A consent banner that doesn't gate the actual data path is a legal document, not a technical control. The fix isn't a better banner. It's moving the enforcement point from the browser, where it can be raced and bypassed, to the server, where a request either gets built or it doesn't.