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

  1. Enable Global Privacy Control in a browser that supports it, or send the header manually with curl -H 'Sec-GPC: 1'.
  2. Load a page and inspect the document request headers in DevTools → Network. Confirm Sec-GPC: 1 is present on the request.
  3. In the console, read navigator.globalPrivacyControl. It should be true — the JavaScript surface mirrors the header.
  4. Inspect the resolved consent state. If marketing categories read granted, the signal is being received and discarded.
The signal arrives before anything else A request timeline. The Sec-GPC header is present on the document request itself, before the response is generated, so an edge or server reading it can shape the initial HTML. A client-side check of navigator.globalPrivacyControl only becomes available once scripts run, by which time any tag in the markup has already been discovered by the preload scanner. Sec-GPC on the request available before the response is generated — can shape the HTML navigator.globalPrivacyControl available once scripts run — after the scanner has seen the markup banner interaction may never happen at all for a GPC visitor The signal is the earliest input you will ever have. Reading it last wastes that.

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.

Where the decision can be made Three placements for the GPC check. At the edge, before the document is generated, the signal can suppress vendor markup and inline the correct consent defaults. In the server render, it can still shape the HTML but not any resource hints already emitted by a cache. In client-side script, it can only prevent future injections, because the markup has already been parsed. at the edge suppresses vendor markup and inlines the defaults the correct placement in the server render can shape the HTML unless a cache served it workable in client script only future injections the markup is already parsed too late for the opt-out An opt-out signal that arrives with the request should be answered before the response.

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 two-request check Two curl requests to the same URL, one carrying the Sec-GPC header and one not. The responses should differ in the inlined consent defaults and in whether sale-or-share vendor markup is present. Identical responses mean either the middleware is not running or a cache is serving one policy to both. with Sec-GPC: 1 saleOrShare: denied no ad vendor markup defaults inlined as denied without the header saleOrShare: default regional policy applies defaults per jurisdiction identical responses middleware not running, or a cache is serving both check the Vary header Run it against production, not a local server — the cache is part of what you are testing.

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.


Up: Regional Routing for CCPA and Global Privacy Laws