The Sec-GPC Header Explained, From Request to Response

Global Privacy Control is usually described as a browser setting, which makes it sound like one thing. On the wire it is two. There is a request header named Sec-GPC that rides along with the requests a GPC-enabled browser makes, and there is a boolean property named navigator.globalPrivacyControl that any script on the page can read. Both arrive on the same page load, both mean the same thing, and a site that handles one of them is handling half the signal.

This post is the wire-format reference: what each half carries, what the well-known resource is for, and what a correct response from your site looks like. That last question has a counterintuitive answer, because the thing people go looking for first, a GPC response header, does not exist.

What the Sec-GPC header is

It is a request header. A browser with Global Privacy Control turned on attaches Sec-GPC: 1 to the requests it makes: the document request for the page itself, and the subresource requests that follow. Your origin sees it on the way in. There is nothing to negotiate, nothing to opt into, and no handshake to complete first.

The value carries all of the meaning, and there is only one value worth handling. A value of 1 asserts that the person behind the request is exercising an opt-out of the sale and sharing of their personal information. There is no defined value meaning the opposite, which leads to the single most important property of this header: its absence says nothing at all. A request with no Sec-GPC header came from a browser that is not sending the signal, which might mean the browser does not implement it, or implements it and has it switched off. Absence is not consent and it is not an opt-in.

The Sec- prefix is doing work as well. Headers in that namespace are set by the user agent and cannot be written by page scripts, so a site can treat the header as coming from the browser rather than from anything running inside the page. Header names are case-insensitive in HTTP, so read it however your framework normalizes names, but compare the value against the string 1 rather than testing it for truthiness. Every header value is a string, and every non-empty string is truthy.

The other half: navigator.globalPrivacyControl

The same preference is exposed to JavaScript as a property on the navigator object. Any script running on the page can read navigator.globalPrivacyControl and get a boolean back: true when the signal is on. A browser that does not implement GPC leaves the property undefined, so client code that needs to tell "switched off" apart from "not supported" should check whether the property exists before reading its value.

This is not a second signal, and it does not need to be reconciled with the first. It is the same preference delivered to a different audience. The header is addressed to whatever answers the request: your origin, your edge function, your reverse proxy. The property is addressed to code that has already been shipped to the browser and is making its own decisions there, long after the response was written.

Why both paths matter

A server-rendered decision only covers what the server renders. If your origin reads Sec-GPC and leaves an advertising embed out of the HTML, that embed is genuinely gone. What it covers is exactly the markup your server produced on that request, and no more.

A large share of tracking never passes through that step. A tag injected by a container after the page loads, an inline ad loader that assembles its own request URL in the browser, a third-party widget that pulls in a script of its own: none of them ask your origin anything. The only way that code learns about the opt-out is by reading the property itself. So the rule is symmetrical. Read the header where you render, read the property where you execute, and route both into the same decision. Handle only the header and your client-side tags are uninformed. Handle only the property and you have nothing to say to a cache or an edge function deciding what to serve before any JavaScript exists.

What a correct response looks like

Here is the correction that saves the most time: there is no GPC response header. Nothing in the exchange lets your server write a header back that means "honored". The status line does not carry it either. A 200 OK is a 200 OK whether the page it returns is clean or full of advertising pixels. Searching your response headers for a GPC acknowledgement is searching for something the protocol never defined.

Correctness is behavioural instead, and it has two parts. The first is that the sale and sharing the signal opts out of actually stop. Under § 7025 a qualifying opt-out preference signal is a valid opt-out request for that browser or device and for the profiles associated with it, so the question is whether the requests that carry personal information to third parties stop being made. The second is the status display in § 7025(c)(6): when the signal is active, the business has to show whether it processed it. Trackers correctly suppressed plus silence on the page is an incomplete response, not a passing one.

Put those together and you get the uncomfortable version. A response can be flawless at the HTTP layer and still be a failed response: same status code, same headers, same rendering time, and the same tracker payload as the request that arrived without the signal. The first public CCPA settlement in California turned on that kind of gap. Sephora settled for $1.2 million in August 2022 after the state alleged it did not honor Global Privacy Control opt-out signals and did not cure the violations inside the CCPA's thirty-day cure period.

The .well-known GPC resource

Alongside the header and the property, GPC defines a small JSON document served from a well-known path, conventionally /.well-known/gpc.json. It is a site-level declaration: it states that the site honors the signal, and it carries a timestamp recording when that statement was last updated. Publishing it is cheap, and publishing it is reasonable. Some partners and auditors look for it, and its absence invites a question you would rather not answer.

What matters more is what the file is not. It is a statement of intent, written once, at the level of the whole site, by whoever last edited it. It is not generated per request, it does not observe what happened on any particular page load, and serving it changes nothing about whether an advertising tag fired on your checkout page five minutes ago. Treat it as documentation that sits next to the implementation, never as evidence of the implementation. The declaration and the behaviour are separate artifacts, and only one of them is what a regulator ends up measuring.

Reading the header in practice

The read itself is a one-liner in every layer that can see it. What varies is where the result lands and what you tell the cache about it. The snippets below are illustrative rather than ready to paste; the shape is the point.

At the origin. Read the header off the incoming request and normalize it into a boolean the render path can see.

const gpcOptOut = request.headers["sec-gpc"] === "1";

In a reverse proxy or edge function. The same read, one hop earlier, plus the part that gets skipped: telling the cache that this header changes the response.

const gpcOptOut = request.headers.get("Sec-GPC") === "1";

response.headers.set("Vary", "Sec-GPC");

In framework middleware. Resolve the flag once per request, attach it to the request-scoped context every handler already reads, and set the same response header on the way out.

function gpcMiddleware(req, res, next) {
  res.locals.gpcOptOut = req.get("Sec-GPC") === "1";
  res.setHeader("Vary", "Sec-GPC");
  next();
}

Three details are worth stating plainly. Compare against the string value, for the reason given above. Resolve the flag before anything decides what to render, because a decision made earlier in the request cannot be revisited later. And set Vary: Sec-GPC on any response whose content depends on the header, so that every cache between you and the visitor keeps the two variants apart.

Four ways the header gets misread

  1. A missing header is treated as consent. The logic reads "no signal, therefore permitted", which quietly converts silence into an opt-in. Absence means the browser did not send anything, and whatever legal basis you were relying on before GPC existed is the one still in play.
  2. A response gets cached without varying on the header. This one is invisible in testing and consistent in production: the first visitor of the day populates the cache, and everyone behind that cache gets their variant. A GPC-on request served a cached GPC-off page loads every tracker that page was built with, and your origin logs will show it was never asked.
  3. A proxy strips the header before the origin sees it. Load balancers, WAFs, and CDNs can be configured to forward an allowlist of headers, and a header introduced after that allowlist was written is not on it. The browser sent Sec-GPC: 1 and your application never received it, so the code you wrote is correct and never runs.
  4. Only the header is read, never the property. The server-side path is handled carefully and the client-side path is never wired up, so anything that decides for itself in the browser keeps deciding the way it always did. This is the failure that survives the most thorough backend review, because nothing is wrong in the backend.

Confirming the signal reached the site

Reading your own logs proves the header arrived. It does not prove anything stopped, which is the part that gets enforced. Our free GPC checker runs both halves against a live URL.

The manual version is worth knowing too. Open DevTools, load the page from a browser that is sending the signal, select the document request, and confirm Sec-GPC: 1 in the request headers. Then open the console and read navigator.globalPrivacyControl to confirm the property is exposed to page scripts. Both checks describe what your browser sent, not what the site did with it, so the third step is the one that counts: watch the network panel for requests to advertising and marketing endpoints, and compare them against the same page loaded with the signal off.

Whichever route you take, the result is a reading of one page, in one session, at one moment. Implementations drift: a theme update reintroduces a hardcoded pixel, a new vendor script lands outside the consent layer, a caching rule changes and takes the Vary behaviour with it. Verification belongs on a schedule rather than in a launch checklist.

Where to go next

The wire format is the easy half. Reading the header and the property is a few lines of code in any stack; deciding what has to stop when the flag is true, and making that decision reach every tag on the page, is the work. If that is where you are, the companion piece walks through how to turn the signal into suppression, stack by stack, from tag manager configuration to server-side rendering to a consent platform.

If your question is upstream of all of that, and you want to know how much of your traffic carries the signal in the first place, start with which browsers actually send it. Some send GPC by default to every user, some ship a toggle, some need an extension, and the split explains why the volume of signals arriving at a given site keeps climbing without anyone announcing it.

Check this on your own site

Our free GPC checker loads one URL with the Global Privacy Control signal set and reports whether third-party trackers still fire.

Run a free GPC check