How to check if third party cookies are blocked
Load a page that makes a cross-site request and read Network.responseReceivedExtraInfo for the blocked cookies. In a fresh Chrome 152 profile nothing was blocked. With third-party restriction on, the same SameSite=None; Secure cookie came back with reason ThirdPartyPhaseout, while the Partitioned cookie was still set and still sent.
Why check this
Every answer to this question that reads document.cookie is guessing. A missing cookie has at least four causes: the browser blocked it, the attribute combination was invalid, the path did not match, or the response never set it. Only the browser knows which, and it says so in the protocol.
Run this whenever a login, a payment step or an embedded widget crosses a site boundary, and again after any browser update. The failure it catches is an embedded checkout that works on your machine and signs users out on a colleague's, because one profile restricts third-party cookies and the other does not.
Prerequisites
- Node 22. Start both files below in separate shells. The page is on
localhost:9624and the cookie is set by127.0.0.1:9625, a different site, so this is a genuine third-party cookie. - Loopback counts as a trustworthy origin, which is why a
Securecookie is accepted here over plain HTTP. On a public HTTP origin it would be rejected before any third-party rule applied. - Chrome 152 driven over the DevTools Protocol. The figures are one capture on one machine on 2026-09-12.
// tpc.mjs the third party. It sets three cookies and reports what came back.
import { createServer } from 'node:http';
const SET = [
'tp_none=1; Path=/; SameSite=None; Secure',
'tp_lax=1; Path=/; SameSite=Lax',
'tp_chips=1; Path=/; SameSite=None; Secure; Partitioned',
];
createServer((req, res) => {
const path = req.url.split('?')[0];
console.log(JSON.stringify({ url: req.url, cookie: req.headers.cookie ?? null }));
if (path === '/set') {
res.writeHead(204, { 'set-cookie': SET, 'cache-control': 'no-store' });
return res.end();
}
if (path === '/frame') {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
return res.end(`<!doctype html><meta charset="utf-8"><script>
parent.postMessage({ frameCookie: document.cookie }, '*');</script>`);
}
res.writeHead(204, { 'cache-control': 'no-store' });
res.end();
}).listen(9625, '127.0.0.1', () => console.log('third party listening on 127.0.0.1:9625'));
// main.mjs the top-level page: set, then ping, then read document.cookie inside an iframe
import { createServer } from 'node:http';
const T = 'http://127.0.0.1:9625';
const page = `<!doctype html><meta charset="utf-8"><title>Third party cookies</title>
<h1>Third party cookie fixture</h1>
<script>
window.result = (async () => {
await fetch('${T}/set', { mode: 'no-cors', credentials: 'include' });
await fetch('${T}/ping', { mode: 'no-cors', credentials: 'include' });
const frameCookie = await new Promise((ok) => {
addEventListener('message', (e) => ok(e.data.frameCookie), { once: true });
const f = document.createElement('iframe');
f.src = '${T}/frame';
document.body.append(f);
});
return { frameCookie };
})();
</script>`;
createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
res.end(page);
}).listen(9624, 'localhost', () => console.log('main site listening on localhost:9624'));
// cookies.mjs loads the fixture and reports every cookie decision Chrome made
import { open } from '../../scripts/browser/session.mjs';
const extra = process.argv.slice(2);
const s = await open({ args: extra });
try {
console.log('chrome ' + (await s.browser.version()) + (extra.length ? ' flags: ' + extra.join(' ') : ' flags: (none)'));
await s.cdp.send('Network.enable');
const respExtra = [];
const reqExtra = [];
s.cdp.on('Network.responseReceivedExtraInfo', (e) => respExtra.push(e));
s.cdp.on('Network.requestWillBeSentExtraInfo', (e) => reqExtra.push(e));
const urls = new Map();
s.cdp.on('Network.requestWillBeSent', (e) => urls.set(e.requestId, e.request.url));
await s.goto('http://localhost:9624/');
console.log('frame read of document.cookie: ' + JSON.stringify(await s.page.evaluate(() => window.result)));
console.log('\n--- Set-Cookie, as Chrome judged it ---');
for (const e of respExtra) {
const u = urls.get(e.requestId) ?? '(unknown)';
if (!u.includes('9625')) continue;
for (const c of e.blockedCookies ?? [])
console.log(`blocked ${c.cookieLine}\n reasons: ${JSON.stringify(c.blockedReasons)}`);
for (const c of e.exemptedCookies ?? [])
console.log(`exempted ${c.cookie.name} reason: ${c.exemptionReason}`);
if (e.cookiePartitionKey) console.log(`partitionKey ${JSON.stringify(e.cookiePartitionKey)}`);
}
console.log('\n--- cookies on the outgoing request to /ping ---');
for (const e of reqExtra) {
const u = urls.get(e.requestId) ?? '';
if (!u.includes('/ping')) continue;
for (const c of e.associatedCookies ?? [])
console.log(`${c.blockedReasons?.length ? 'not sent' : 'sent '} ${c.cookie.name}=${c.cookie.value}` +
` partitionKey=${c.cookie.partitionKey ? JSON.stringify(c.cookie.partitionKey) : 'none'}` +
`${c.blockedReasons?.length ? ' reasons: ' + JSON.stringify(c.blockedReasons) : ''}`);
}
console.log('\n--- the cookie jar ---');
const { cookies } = await s.cdp.send('Storage.getCookies');
for (const c of cookies)
console.log(`${c.name}=${c.value} domain=${c.domain} sameSite=${c.sameSite ?? 'unset'} secure=${c.secure} partitionKey=${c.partitionKey ? JSON.stringify(c.partitionKey) : 'none'}`);
} finally { await s.close(); }
Steps
- Step 1.
Run the fixture in a fresh profile with no flags, which is the browser's own default.
node cookies.mjschrome Chrome/152.0.7977.76 flags: (none) frame read of document.cookie: {"frameCookie":"tp_none=1; tp_chips=1"} --- Set-Cookie, as Chrome judged it --- blocked tp_lax=1; Path=/; SameSite=Lax reasons: ["SchemefulSameSiteLax"] partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true} partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true} partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true} --- cookies on the outgoing request to /ping --- sent tp_none=1 partitionKey=none sent tp_chips=1 partitionKey={"topLevelSite":"http://localhost","hasCrossSiteAncestor":true} --- the cookie jar --- tp_none=1 domain=127.0.0.1 sameSite=None secure=true partitionKey=none tp_chips=1 domain=127.0.0.1 sameSite=None secure=true partitionKey={"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}Third-party cookies are not blocked in this profile. The only rejection is
tp_lax, and its reason isSchemefulSameSiteLax: aSameSite=Laxcookie cannot be set from a cross-site response at all, restriction or no restriction. That row would appear on a browser with every privacy setting off. - Step 2.
Read the same visit from the server, which sees only what arrived.
cat tpc.logthird party listening on 127.0.0.1:9625 {"url":"/set","cookie":null} {"url":"/ping","cookie":"tp_none=1; tp_chips=1"} {"url":"/frame","cookie":"tp_none=1; tp_chips=1"}Two of the three cookies came back on the next cross-site request and on the iframe. The server cannot tell why the third is missing. That is the whole reason the check runs in the browser.
- Step 3.
Run the same fixture with third-party cookie restriction turned on.
node cookies.mjs --test-third-party-cookie-phaseoutchrome Chrome/152.0.7977.76 flags: --test-third-party-cookie-phaseout frame read of document.cookie: {"frameCookie":"tp_chips=1"} --- Set-Cookie, as Chrome judged it --- blocked tp_none=1; Path=/; SameSite=None; Secure reasons: ["ThirdPartyPhaseout"] blocked tp_lax=1; Path=/; SameSite=Lax reasons: ["UserPreferences"] partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true} partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true} partitionKey {"topLevelSite":"http://localhost","hasCrossSiteAncestor":true} --- cookies on the outgoing request to /ping --- sent tp_chips=1 partitionKey={"topLevelSite":"http://localhost","hasCrossSiteAncestor":true} --- the cookie jar --- tp_chips=1 domain=127.0.0.1 sameSite=None secure=true partitionKey={"topLevelSite":"http://localhost","hasCrossSiteAncestor":true}tp_noneis now blocked withThirdPartyPhaseout.tp_chipscarriesPartitioned, so it is set, stored with a partition key and sent on the next request. Partitioned is not a milder form of blocked. It is a separate jar, one per top-level site, and the vendor cannot join it to what it saw on another site. - Step 4.
Compare the two blocked reasons for
tp_lax, which never changed.node cookies.mjs --test-third-party-cookie-phaseout | grep -A1 tp_laxblocked tp_lax=1; Path=/; SameSite=Lax reasons: ["UserPreferences"]The identical cookie line was rejected as
SchemefulSameSiteLaxin step 1 and asUserPreferencesin step 3. The restriction check runs first and reports its own reason, so the attribute problem underneath it disappears from the output. A test that asserts on one reason string passes or fails depending on a browser setting that has nothing to do with the bug.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| No blocked lines for your cookie | This profile allows third-party cookies | Repeat with restriction on before calling the feature safe. |
| reasons: ["ThirdPartyPhaseout"] | The browser restricted it as a third-party cookie | Add Partitioned, or move the state to the top-level site. |
| reasons: ["UserPreferences"] | A profile setting blocked it, ahead of any other check | Do not read the attribute set from this. Re-run with defaults. |
| reasons: ["SchemefulSameSiteLax"] | The cookie was never valid cross-site | Fix the attributes. No privacy setting is involved. |
| partitionKey on the cookie in the jar | A CHIPS cookie, stored per top-level site | Confirm the vendor can work without joining across sites. |
| partitionKey in the response line only | The partition context of the request | Ignore it. It says nothing about the cookie. |
Common mistakes
What to check next
- How to check SameSite cookie attribute: the attribute that decides whether a cookie is eligible cross-site at all.
- How to check cookies on a website: the full inventory, before you ask which of them cross a site boundary.
- How to check if cookies are secure and HttpOnly:
Secureis a precondition forSameSite=None, so a missing flag looks like blocking. - How to check if cookies are set without consent: the same capture, run before the banner is answered.
- How to check which third parties receive data: a blocked cookie does not stop a request, and the request may still carry an identifier.
FAQ
How to check third party cookies without writing a fixture?
Open DevTools, Network tab, select a cross-site request, and read the Cookies panel. Rows that were rejected are shown with a reason and a warning triangle. The fixture exists so the answer is reproducible, not because the panel is wrong.
How to check if third party cookies are enabled in this browser?
Run a cross-site request and look for a ThirdPartyPhaseout or UserPreferences entry in the blocked list. Anything shorter, including a settings screenshot, describes the configuration rather than the behaviour, and enterprise policy and Incognito both override the setting.
How to check if third party cookies are enabled with JavaScript?
Embed an iframe from another site that sets a cookie and reports document.cookie back through postMessage, as main.mjs does. Treat the result as a signal, not proof: HttpOnly and a mismatched path both produce the same empty string.
How to check third party cookies in Chrome versus Edge?
The method is identical, because both expose the same protocol. The verdicts are not. Blocking is a browser and profile property, so a matrix of the browsers your users have is the only useful result.
Does the Partitioned attribute mean the cookie is blocked?
No. It is stored in a separate jar per top-level site. In the restricted run the partitioned cookie was set and sent while the plain one was blocked, so a single site keeps working and cross-site joining stops.
Verified
Verified by Maks Vernynode 22.23.2Chrome 152.0.7977.76
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.
Related on this site
advanced15 minpublished updated Maks Verny