A visitor’s browser sends Sec-GPC: 1 on every request, your CCPA routing still treats them as opted in until they interact with a banner they will never open, and an audit has flagged the gap.
Triage: Confirm the Signal Arrives and Is Ignored
- Enable Global Privacy Control in a browser that supports it, or send the header manually with
curl -H 'Sec-GPC: 1'. - Load a page and inspect the document request headers in DevTools → Network. Confirm
Sec-GPC: 1is present on the request. - In the console, read
navigator.globalPrivacyControl. It should betrue— the JavaScript surface mirrors the header. - Inspect the resolved consent state. If marketing categories read granted, the signal is being received and discarded.
Root Cause: GPC Is a Default, Not a Banner Interaction
Global Privacy Control is a browser-level preference expressed as a request header and a matching JavaScript property. Under CPRA it is a legally recognised opt-out for the sale and sharing of personal information, and several other US state regimes treat it similarly; it is not a hint to be reconciled with a banner, it is an instruction that arrives before any banner is shown.
The common implementation error follows from where the check is placed. A client-side script reading navigator.globalPrivacyControl runs after the document has been parsed, so any vendor tag present in the markup has already been discovered and, in the opt-out model, already permitted to run. The check is correct and too late.
The second error is treating GPC as equivalent to a banner refusal that a later acceptance can override. It expresses a standing preference for the browser, not a decision about your site, so an acceptance recorded afterwards needs to be an explicit, informed action rather than a default the visitor never revisited. Where the two conflict, the conservative reading is that the signal stands unless the visitor deliberately overrides it.
Resolution: Resolve at the Edge, Express Everywhere
// edge middleware — runs before the document is generated.
export default function middleware(request) {
const gpc = request.headers.get('sec-gpc') === '1';
const region = request.geo?.region ?? null;
// The resolved policy travels into the document so client code never
// re-derives it, and so a cached response cannot serve the wrong one.
const policy = {
region,
// Under an opt-out regime the signal is the opt-out. Under an opt-in
// regime everything is denied anyway, so GPC changes nothing there.
saleOrShare: gpc ? 'denied' : 'default',
source: gpc ? 'gpc' : 'regional-default',
};
const response = next();
response.headers.set('x-consent-policy', JSON.stringify(policy));
// Vary on the header, or a cache will serve one visitor's policy to another.
response.headers.set('Vary', 'Sec-GPC');
return response;
}
<!-- Inlined into the document from the resolved policy, before any container. -->
<script>
window.__POLICY__ = {{ policyJson }};
gtag('consent', 'default', {
ad_storage: window.__POLICY__.saleOrShare === 'denied' ? 'denied' : 'granted',
ad_user_data: window.__POLICY__.saleOrShare === 'denied' ? 'denied' : 'granted',
ad_personalization: window.__POLICY__.saleOrShare === 'denied' ? 'denied' : 'granted',
analytics_storage: 'granted',
});
</script>
The Vary: Sec-GPC header is the detail most likely to be omitted and most consequential. Without it, a CDN caches one visitor’s rendered policy and serves it to the next, so a GPC visitor can receive a response generated for someone who did not send the header — an opt-out failure caused entirely by caching.
Verification: Two Requests, Two Policies
Fetch the page twice, once with the header and once without, and compare the raw HTML. The GPC response should contain denied defaults and no sale-or-share vendor markup; the other should contain the regional defaults. Any difference in the x-consent-policy header confirms the edge is resolving; identical bodies confirm a caching problem.
The Signal Also Applies to Server-Side Tags
An opt-out expressed by the browser is easy to honour in the browser and easy to forget everywhere else. If any part of your measurement runs through a server-side container, the GPC decision has to travel with the forwarded payload exactly as any other consent state does — otherwise the browser correctly suppresses its own vendor requests while the container quietly forwards the same events to the same vendors.
This is a genuinely common gap because the two systems are usually built by different people at different times. The browser-side implementation is driven by a privacy review; the container is introduced later as a performance project, inherits the event stream, and nobody re-asks the consent question because the events look like the ones that were already approved.
Make the policy a required field on the envelope rather than an optional one, and have the container reject payloads that omit it. A rejected event is a visible failure that gets fixed in a day; a silently forwarded one is an opt-out violation that persists until someone reads the container’s logs against a specific visitor’s expectations.
Recording That You Honoured It
Because a GPC visitor typically never interacts with your banner, there is no consent event to log — which means that by default your records contain no evidence that the signal was received or acted upon. That is an uncomfortable position in an audit, where the absence of a record is difficult to distinguish from an absence of action.
Log the resolution rather than only the interaction. When the edge resolves a policy from a GPC header, record it with the same retention as your consent records: a timestamp, the resolved policy, the source, and enough of a request identifier to correlate it. Keep it aggregate where you can — daily counts of GPC-derived denials per region prove the mechanism works without storing a per-visitor row about someone who asked to be tracked less.
Common Pitfalls
- Omitting
Vary: Sec-GPC. A cache will serve one visitor’s policy to another, which converts a correct implementation into an intermittent failure that depends on cache warmth. - Treating GPC as a banner dismissal. It is a standing preference, not an interaction with your site, and it should not be cleared by the same code that records a banner choice.
- Checking only
navigator.globalPrivacyControl. The property is available after scripts run; the header is available before the response exists. Use the header for the decision and the property only as a client-side confirmation. - Applying it under GDPR as if it changed something. In an opt-in regime everything is denied by default already, so GPC adds nothing — treating it as a signal to deny is harmless but also meaningless, and hides the fact that the opt-in gate is what is doing the work.
Frequently Asked Questions
Is GPC legally binding everywhere?
No, and the variation is exactly why the mapping table for regional routing needs an explicit cell for it. California requires recognition under CPRA and its implementing regulations, and Colorado and Connecticut have adopted comparable positions; other states have not, and outside the US the concept generally has no equivalent because the regimes are opt-in.
The defensible default is to honour it everywhere regardless. Doing so costs you measurement from a small and privacy-motivated cohort, is trivially explicable to a regulator, and removes an entire category of jurisdictional edge case from your routing logic — which is worth more than the marginal data in most organisations.
Can a visitor override GPC by accepting on the banner?
Technically yes, and it should require a deliberate, informed action rather than an incidental one. A visitor who sends the signal and then explicitly opts back in for your site has made a choice you can record; a visitor who dismissed a banner with a single click that happened to mean acceptance has not.
Record which source produced the current state — the source field in the policy above exists for this — so an audit can distinguish a signal-derived denial from a visitor-derived one. It also lets you present the banner honestly: telling someone their browser has already opted them out, and offering to change it, is a better interaction than silently overriding them.
Does GPC apply to analytics as well as advertising?
Its statutory scope is the sale and sharing of personal information, which maps to advertising and data-broking rather than to first-party measurement. Analytics that never leaves your control is generally outside it, which is why the example above denies the three advertising signals and leaves analytics_storage alone.
The line is less clean than it sounds, though, because plenty of analytics products do share data with the vendor for their own purposes. The question to ask per vendor is whether the arrangement constitutes a sale or share under the applicable definition — a contractual question rather than a technical one, and one worth resolving in the inventory rather than in code.