Ask a team how many cookies their site sets and the answer is usually three or four. Run an audit and it is thirty. The difference is everything written by a tag manager, an analytics library, a chat widget or an embedded player — cookies that are on your domain, appear under your name in any compliance review, and were never in your codebase.
Step 1 — Capture What the Server Sets
Start with the half you can see from outside the browser. These are cookies delivered by a Set-Cookie response header, and they include anything set by an upstream you proxy:
# Landing page
curl -sI https://example.com | grep -i '^set-cookie:'
# Follow redirects — login and consent flows set cookies on 302s
curl -sIL https://example.com/login | grep -i '^set-cookie:'
# Some vendors only set cookies when they think you are a real browser
curl -sIL -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' \
https://example.com | grep -i '^set-cookie:'A cookie audit does the same sweep and grades each cookie for Secure, HttpOnly, SameSite, prefix and lifetime, which saves reading the attributes by eye. Either way, treat this as the baseline — not the answer.
Step 2 — Capture What Scripts Set
This is where the cookies you did not know about live. Most vendor cookies are written by JavaScript calling document.cookie after the page loads, so they never appear in a response header and no server-side check will find them.
Use a fresh browser profile with no extensions — an existing profile carries cookies from previous visits and will attribute them to this page. Then walk the real flows, not just the home page: landing, consent banner (accept and reject separately), sign-in, search, product page, cart, checkout.
// Paste in the console after walking a flow. Script-set cookies included.
console.table(
document.cookie.split('; ').filter(Boolean).map((c) => {
const [name, ...rest] = c.split('=');
return { name, bytes: c.length, value: rest.join('=').slice(0, 24) + '…' };
})
);document.cookie deliberately omits HttpOnly cookies — that is the point of the flag — so cross-check against DevTools → Application → Cookies, which lists every cookie in the jar with its full attributes, domain and expiry.
Step 3 — Attribute Each Cookie to a Script
An inventory without owners is a list you cannot act on. For each unfamiliar cookie, find what set it. Chrome DevTools gives you two routes:
- Network → Cookies tab on a response shows cookies delivered by header, alongside the request that carried them. The initiator column names what triggered the request.
- A
document.cookiebreakpoint catches script writes as they happen. In Sources, add a breakpoint on cookie modification, reload, and read the call stack — it names the library directly.
Common prefixes shortcut most of the work: _ga and _gid are Google Analytics, _fbp is the Meta pixel, _hj is Hotjar, intercom- is Intercom, __cf is Cloudflare. If a name resolves to nothing, that itself is the finding — a cookie nobody can attribute is a script nobody is maintaining.
Step 4 — Record the Facts That Drive Decisions
Build one row per cookie. These are the columns that actually change what you do next:
| Column | Why it matters |
|---|---|
| Name and domain | Distinguishes a cookie on your domain from one on a vendor's |
| Set by | Server header, or the specific script — decides how you can fix it |
| Purpose | Strictly necessary, or something that needs consent |
| Lifetime | A multi-year cookie is a persistent identifier |
| Secure / HttpOnly / SameSite | The security grade, and whether you can raise it |
| Set before consent? | The single most common compliance finding |
| Owner | Who decides whether it stays |
Keep the table in the repository rather than a spreadsheet. It then reviews like code, and a new cookie shows up as a diff instead of a surprise.
Step 5 — Decide: Keep, Fix, or Remove
Every row gets one of three outcomes, and the honest default is more aggressive than most teams expect. Scripts accumulate; almost every site is carrying a tag from a campaign that ended two years ago.
| Finding | Action |
|---|---|
| Nobody can name the script that sets it | Remove the script |
| Vendor is no longer used | Remove the tag and the cookie with it |
| Analytics or marketing cookie set before consent | Gate the script behind the consent signal |
| Multi-year lifetime on a tracking cookie | Shorten it, or drop the vendor |
Missing Secure on a proxied cookie | Stamp it at the edge (Step 6) |
Missing Secure on a script-written cookie | Update the library, or ask the vendor |
| Duplicate analytics from two vendors | Pick one |
| Third-party cookie a flow depends on | Move it first-party before browsers finish blocking it |
Step 6 — Remediate What You Can Reach
Cookies that arrive through your infrastructure can be corrected at the edge. This works for anything set by a Set-Cookie header from an upstream you proxy — and not for script-written cookies, whose headers never pass through your server at all:
# nginx 1.19.3+ — stamp attributes onto cookies from this upstream
proxy_cookie_flags ~ secure httponly samesite=lax;
# Target a single vendor cookie by name
proxy_cookie_flags _vendor_id secure samesite=lax;For script-written cookies the lever is the script itself. Content Security Policy is the blunt, reliable version of that: a vendor whose script cannot load cannot set a cookie.
Content-Security-Policy: script-src 'self' https://cdn.trusted-vendor.com; report-uri /csp-reportDeploy it in report-only mode first and read the reports for a week — you will find scripts loading other scripts, which is usually how a cookie you cannot attribute got there. Configuring CSP covers the rollout in detail.
Step 7 — Make It Repeat
A one-time audit has a short shelf life, because third-party cookies appear without any code change: a tag manager container is edited outside your repository, and vendors add cookies in their own updates.
- Re-run the header audit on every release and treat a new cookie as a change that needs an owner.
- Keep CSP reporting on permanently — a new third-party script shows up there before its cookie shows up anywhere else.
- Require the inventory row to be added in the same pull request that adds a tag, so the table cannot drift from reality.
- Re-walk the flows manually each quarter. Script-written cookies still need a browser to find.
What a Finished Audit Looks Like
You are done when every cookie in a fresh profile, after walking every flow, can be named, attributed to a script, justified by a purpose, and has a lifetime and attribute set someone chose deliberately. Anything left unexplained is either a script to remove or a question for a vendor — and both are better outcomes than an unexplained cookie.
Frequently Asked Questions
1. What counts as a third-party cookie?
Two different things share the name. A cookie set by a script from another vendor but stored on your domain is first-party by the browser's definition and yours by responsibility. A cookie set on another domain entirely — an ad network, an embedded player — is third-party in the browser sense and subject to blocking and partitioning. An audit has to cover both, because only the first shows up in your own response headers.
2. Why does the audit find cookies my server never sets?
Because most cookies on a modern site are written by JavaScript, not by a Set-Cookie header. Tag managers, analytics libraries, chat widgets and A/B testing tools all call document.cookie after the page loads. A header-only check misses them entirely, which is why the browser-side pass in Step 2 is not optional.
3. Can I just block third-party cookies with a header?
No header does that. Content Security Policy can stop the script from loading in the first place, which prevents the cookie as a side effect, and Permissions-Policy restricts specific browser features — but neither has a "no cookies" directive. Controlling third-party cookies means controlling the scripts and embeds that set them.
4. A vendor cookie is missing Secure and HttpOnly. What can I do?
In order of preference: update the library, since this is often fixed in a newer version; ask the vendor; rewrite the Set-Cookie header at your reverse proxy if the cookie comes from an upstream you proxy; or drop the script. A script-written cookie cannot be rewritten at the proxy, because the header never passes through it.
5. How often should I re-run this?
Every release that changes dependencies or tags, and on a fixed schedule regardless — quarterly is a reasonable floor. Third-party cookies appear without a code change, because a tag manager container is edited outside your repository and vendors add cookies in their own updates.
6. What is the fastest way to get a baseline?
Run the page through the Cookie Security Checker — it reads every Set-Cookie header the response returns and grades Secure, HttpOnly, SameSite, prefix and lifetime per cookie. That covers the server-set half in seconds; pair it with a DevTools pass for the script-written half.