How to check if a site honors global privacy control

Request the same page twice, once with Sec-GPC: 1 and once without, and diff what comes back. A site that honours the signal drops the advertising cookie and the tracker; one that ignores it returns byte-identical responses. Both publish the same /.well-known/gpc.json, so the file proves nothing.

Why check this

Run this on staging before any release that changes tags, consent logic or the ad stack, and after an infrastructure change that puts a new proxy in front of the site. It answers a narrower question than "do we support GPC": it answers whether anything the site does is different when the signal arrives.

This procedure tests a site you control. Two requests to somebody else's service cannot tell you what their systems did with the signal, and a public verdict built on one page load is not a finding.

The failure it catches is a signal that is read and then dropped. The header reaches the application, a handler logs it, and the advertising cookie is still set on the response. Nothing in the code review shows that, because the code does read the header.

Prerequisites

import { createServer } from 'node:http';
const port = Number(process.argv[2]);
const mode = process.argv[3];
const AD = 'ad_id=7c1f; Max-Age=31536000; Path=/';
createServer((req, res) => {
  const gpc = req.headers['sec-gpc'] === '1';
  const honour = mode === 'honours' && gpc;
  const path = req.url.split('?')[0];
  console.log(`${mode} ${req.method} ${path} sec-gpc=${req.headers['sec-gpc'] ?? '(absent)'}`);
  if (path === '/.well-known/gpc.json') {
    res.setHeader('content-type', 'application/json');
    return res.end(JSON.stringify({ gpc: true, lastUpdate: '2026-09-12' }));
  }
  if (path === '/tracker.js') {
    res.setHeader('content-type', 'text/javascript');
    return res.end('fetch("/beacon");');
  }
  if (path === '/beacon') return res.end('ok');
  if (!honour) res.setHeader('set-cookie', AD);
  res.setHeader('content-type', 'text/html; charset=utf-8');
  res.end(`<!doctype html><title>${mode}</title>` +
    (honour ? '<p>no tracker</p>' : '<script src="/tracker.js"></script>'));
}).listen(port, '127.0.0.1', () => console.log(`${mode} fixture on http://127.0.0.1:${port}/`));
node fixture.mjs 9642 honours & node fixture.mjs 9643 ignores &
import { launch } from 'puppeteer-core';
const browser = await launch({
  executablePath: 'C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe',
  headless: true,
});
console.log(await browser.version());
for (const port of process.argv.slice(2)) {
  const page = await browser.newPage();
  const seen = [];
  page.on('request', (r) => seen.push(new URL(r.url()).pathname));
  await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle2' });
  console.log(`port ${port}  navigator.globalPrivacyControl: ${await page.evaluate(() => navigator.globalPrivacyControl)}`);
  console.log(`  requests: ${seen.join(' ')}`);
  console.log(`  cookies:  ${(await page.cookies()).map((c) => c.name + '=' + c.value).join(' ') || '(none)'}`);
  await page.close();
}
await browser.close();

Steps

  1. Step 1.

    Take the baseline. No signal, headers only.

    curl -s -D - -o /dev/null http://127.0.0.1:9642/
    
    HTTP/1.1 200 OK
    set-cookie: ad_id=7c1f; Max-Age=31536000; Path=/
    content-type: text/html; charset=utf-8
    Date: Sat, 12 Sep 2026 08:01:34 GMT
    Connection: keep-alive
    Keep-Alive: timeout=5
    Content-Length: 72

    An advertising cookie with a one-year lifetime, and a 72-byte body.

  2. Step 2.

    Repeat the request with the signal on the honouring origin.

    curl -s -D - -o /dev/null -H 'Sec-GPC: 1' http://127.0.0.1:9642/
    
    HTTP/1.1 200 OK
    content-type: text/html; charset=utf-8
    Date: Sat, 12 Sep 2026 08:01:34 GMT
    Connection: keep-alive
    Keep-Alive: timeout=5
    Content-Length: 54

    No set-cookie line, and the body is 18 bytes shorter because the tracker tag is gone. Both changes are visible without a browser.

  3. Step 3.

    Send the same signal to the origin that ignores it.

    curl -s -D - -o /dev/null -H 'Sec-GPC: 1' http://127.0.0.1:9643/
    
    HTTP/1.1 200 OK
    set-cookie: ad_id=7c1f; Max-Age=31536000; Path=/
    content-type: text/html; charset=utf-8
    Date: Sat, 12 Sep 2026 08:01:34 GMT
    Connection: keep-alive
    Keep-Alive: timeout=5
    Content-Length: 72

    Identical to the baseline, cookie included. That is the finding.

  4. Step 4.

    Read the terminal running the ignoring fixture. No command: this is the server's own log.

    ignores fixture on http://127.0.0.1:9643/
    ignores GET / sec-gpc=(absent)
    ignores GET / sec-gpc=1
    …
    ignores GET / sec-gpc=1
    ignores GET /tracker.js sec-gpc=1
    ignores GET /beacon sec-gpc=1
    ignores GET /favicon.ico sec-gpc=1

    The header arrived on every request, subresources included. The site read it and changed nothing, which is a different defect from a site that never received it.

  5. Step 5.

    Load both origins in a browser that sends the signal, and compare what each one pulled.

    node sitecheck.mjs 9642 9643
    
    Chrome/153.0.8010.37
    port 9642  navigator.globalPrivacyControl: true
    requests: / /favicon.ico
    cookies:  (none)
    port 9643  navigator.globalPrivacyControl: true
    requests: / /tracker.js /beacon /favicon.ico
    cookies:  ad_id=7c1f

    Same browser, same signal, two outcomes. The tracker and its beacon are absent on one origin and present on the other.

  6. Step 6.

    Read the declaration each origin publishes.

    for p in 9642 9643; do curl -s http://127.0.0.1:$p/.well-known/gpc.json; echo; done
    
    {"gpc":true,"lastUpdate":"2026-09-12"}
    {"gpc":true,"lastUpdate":"2026-09-12"}

    Both say yes. The GPC specification puts this resource at /.well-known/gpc.json with a boolean gpc member and an RFC 3339 lastUpdate. It is a claim about the origin, not evidence about it.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | set-cookie gone under the signal | Storage behaviour changes | Check that the rest of the stack agrees: beacons, server-side tags, logs. | | Two identical responses | The signal changes nothing | Open the handler that reads the header and follow what it does after the log line. | | Fewer requests in the browser run | Tags are gated on the signal | Confirm the gate is server-side too, or a client with scripting off keeps the tags. | | gpc.json present, behaviour unchanged | The declaration is wrong | Fix the behaviour or withdraw the file. The file is checked by third parties. |

Common mistakes

Sign: The well-known file says gpc true, so the site is recorded as compliant.Cause: Both fixtures above serve the identical file and only one changes anything. The resource is a self-declaration published by the same origin whose behaviour is in question. Test the behaviour and treat the file as a claim to verify.
Sign: Only the document request is checked.Cause: In the fixture log the signal arrived on /tracker.js, /beacon and /favicon.ico as well. A tracker loaded as a subresource receives the header too, so a handler that checks the signal only on the HTML route leaves every beacon unguarded.
Sign: The check is run against a third-party site and reported as a compliance result.Cause: Two requests show what one endpoint returned to one client in one moment. Opt-out processing can happen in a backend you cannot see, and a cookie removed at the edge is not proof either. Run this against a site you operate.

What to check next

FAQ

What is GPC testing?

Sending a request with Sec-GPC: 1 to a site you operate and measuring what changes: cookies set, tags loaded, beacons fired, records written. The signal is meaningful only through the behaviour it alters, so the test is a before-and-after, not a header read.

What does "GPC signal detected" mean?

It means a tool saw Sec-GPC: 1 on the request or navigator.globalPrivacyControl set to true. Detection says the signal arrived. It says nothing about whether the site acted on it, which is what steps 1 to 5 measure.

Is there a GPC inspector?

Browser extensions display the signal state, and the steps above do the same work with curl and a scripted browser, which gives output you can diff and attach to a ticket. Both read the signal; neither can read what a remote backend did with it.

Does a site have to publish gpc.json?

The specification defines the resource as the way an origin declares that it abides by the signal. Publishing it while behaving as the port 9643 fixture does makes a documented claim that the behaviour contradicts, so publish it after the behaviour is in place.

Verified

Verified by Maks Vernycurl 8.21.0Node 22.23.2Brave 1.95.101

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate12 minpublished updated Maks Verny