Skip to content

CHIPS and partitioned cookies: the post-third-party-cookie identity model

· 21 min read
Copyright: MIT
CHIPS wordmark over a Set-Cookie line with the Partitioned attribute highlighted in orange

A third-party cookie has one defining property: it is sent on a request to a host that is not the page the user is looking at. An analytics SDK on news.example sets a cookie on tracker.example, and that same tracker.example cookie rides along when the user later visits shop.example that also embeds the SDK. One host, one cookie, every site the user touches. That single shared jar is the mechanism that built cross-site tracking, and it is the thing browsers spent the last decade trying to kill without breaking the embedded chat widget, the CDN-pinned session, and the SSO iframe that all depend on a cookie surviving across an origin boundary.

CHIPS is the answer that shipped. Not a privacy framework, not a sandbox of replacement APIs, just one cookie attribute and a change to how the browser keys its cookie store. Set Partitioned and the browser stops giving you one cookie jar per host. It gives you one jar per host per top-level site. The tracker on news.example and the tracker on shop.example are now two cookies with the same name that can never see each other. This post is about exactly how that double-keying works, what it lets through, what it blocks, and why the spec that was meant to be a small piece of a much larger Privacy Sandbox outlived the rest of it.

The sections below cover the double-keyed cookie jar and how the partition key is computed, the exact Set-Cookie syntax and why Secure is mandatory, the cross-site ancestor chain bit that changed the key in 2025, the per-partition storage budget, how this lines up with general storage partitioning, the cross-browser state, and what changed when Google walked away from deprecating third-party cookies entirely.

A normal cookie is keyed on what the spec calls the host key: the domain or hostname that set it, plus path and a few flags. When the browser builds the Cookie header for an outgoing request, it walks its store, finds every cookie whose host key matches the request target (subject to SameSite, Secure, path, and the rest), and attaches them. The top-level site the user is on never enters that calculation. That is precisely why a third-party cookie follows the user everywhere: the store has no notion of “which site was this cookie for.”

CHIPS adds a second key. A cookie set with the Partitioned attribute is stored against both its host key and a new partition key. The Privacy Sandbox documentation defines it plainly: a cookie’s partition key is the site, meaning the scheme and registrable domain, of the top-level URL the browser was visiting when the request that set the cookie was made. If support.chat.example is embedded in retail.example and sets a partitioned cookie, the partition key is ("https", "retail.example"). The same widget embedded in news.example sets a cookie under partition key ("https", "news.example"). Two cookies, same host, same name, two jars.

On retrieval the browser does the obvious thing. The IETF draft by Dylan Cutler of Google, first published November 2022, describes the algorithm: if a stored cookie’s partition key is null it is an old-style unpartitioned cookie and the partition check is skipped; otherwise the cookie is only included in the request if its partition key is same-site with the partition key derived from the current top-level document. A retail.example partition cookie simply is not a candidate when the top-level site is news.example. Nothing blocks it after the fact with a policy check; it never enters the set the browser considers in the first place.

Unpartitioned third-party cookie top: news.example top: shop.example tracker.example id=abc (one jar) Partitioned cookie under CHIPS top: news.example tracker id=abc top: shop.example tracker id=xyz two jars, never compared *Top: one host key means one cookie that rides every top-level site. Bottom: the partition key splits it into independent per-site jars that cannot observe each other.*

The privacy property falls straight out of this. Cross-site tracking works because a tracker can recognise the same cookie value on two unrelated sites and stitch the visits into one profile. Under CHIPS the cookie value on news.example and the cookie value on shop.example are different storage entries the tracker set independently, and there is no request on which both are present. The browser never hands the tracker a stable identifier that spans the two sites. What survives is per-site state, which is what the legitimate use cases needed in the first place.

The Partitioned attribute and why Secure is not optional

The opt-in is a single attribute on the Set-Cookie response header. The canonical form, from both MDN and the Privacy Sandbox docs, is:

Set-Cookie: __Host-id=value; Secure; Path=/; SameSite=None; Partitioned;

Four things are doing work there and it is worth being precise about which are required and which are recommended. Partitioned is the opt-in itself. Secure is mandatory: the CHIPS proposal states the user agent must reject any cookie set with Partitioned that does not also include Secure, and the IETF draft makes this normative in its store-model algorithm, where a partitioned cookie whose secure-only flag is false is ignored entirely. There is no partitioned cookie over plain HTTP. The reasoning in the draft’s security section is that partitioned state is meant to be a clean, isolated identity primitive, and an unencrypted one would reintroduce the network-injection problems the rest of the cookie hardening work spent years closing off.

SameSite=None is the practical pairing rather than a hard requirement of the attribute. A partitioned cookie that needs to be sent in a cross-site context, which is the whole point of an embedded third party, has to declare SameSite=None or SameSite’s default lax behaviour will keep it off cross-site requests before partitioning ever enters the picture. The two attributes solve different problems. SameSite decides whether a cookie is sent on cross-site requests at all; Partitioned decides which jar it is read from once it is allowed to be sent. The SameSite and partitioning post covers the SameSite half of this in detail, and CHIPS sits directly on top of it.

__Host- is recommended, not required. The prefix is an older cookie-hardening convention that forces a cookie to be Secure, to have Path=/, and to carry no Domain attribute, which binds it to the exact host that set it rather than the registrable domain. For a partitioned cookie that you do not need to share across subdomains, the prefix closes a small attack surface for free. The MDN guidance and the Chrome docs both recommend it without making it a condition of partitioning.

Set-Cookie: __Host-id Secure SameSite=None Partitioned recommended binds to host required cookie ignored without it needed for cross-site send the opt-in: double-keys the cookie *The attribute roles in a CHIPS Set-Cookie line. Secure is enforced; SameSite=None and __Host- are practical companions; Partitioned is the switch.*

There is a deliberate asymmetry in the model worth flagging. A site cannot promote an existing unpartitioned cookie into a partition, and it cannot read a partitioned cookie from outside its partition by dropping the attribute. The Partitioned flag is a property of the stored cookie at write time. Setting a partitioned cookie creates a fresh entry under the current partition key; it does not reach into or merge with the global unpartitioned jar. That one-way wall is the reason partitioning can be trusted as an isolation boundary rather than a soft hint.

JavaScript gets the same control. document.cookie accepts the attribute in a write string, and the CookieStore API was extended with a partitioned boolean on CookieInit, a partitioned field on the CookieListItem you read back, and a partitioned option on the delete path, so script-set cookies participate in the same double-keying as header-set ones.

What the partition key actually contains, and the ancestor bit

For the first two years of CHIPS the partition key was exactly what the early spec said: the site of the top-level document, scheme and registrable domain, nothing else. Nesting depth did not matter. A cookie set by a third party three iframes deep still landed in the top-level site’s partition. That is clean and easy to reason about, and it is also where a subtle security gap lived.

Consider the frame chain A → B → A. The top-level site is A. It embeds a cross-site iframe from B. That B frame then embeds an iframe back to a document on A. Under the original key, the innermost A frame and the top-level A page share a partition, because both resolve to top-level site A. So a cross-site B document could embed an arbitrary credentialed endpoint of A, in the inner frame, and that endpoint would receive A’s partitioned cookies. B did not get the cookie values, but it got to drive authenticated requests into A’s own partition without A ever granting access through the Storage Access API or CORS. A confused-deputy gap, narrow but real.

Chrome’s fix, shipped in M125 in April 2025, adds a cross-site ancestor chain bit to the partition key. The bit is set when any document between the current frame and the top-level document is cross-site to the top-level site. With the bit in the key, the inner A frame in A → B → A no longer shares a partition with the top-level A page, because its ancestor chain passed through cross-site B and flipped the bit. Same top-level site, different partition. The cross-site embed can no longer reach the top-level site’s credentialed partition by bouncing through itself.

top-level: A (https://a.example) iframe: B (cross-site) iframe: A (same site as top, but nested under B) partition key = (https, a.example, ancestor_bit=1) → different partition from the top-level a.example page → cannot read A's top-level partitioned cookies *The ancestor bit splits the inner same-site frame off from the top-level partition because its chain runs through a cross-site B.*

Chrome’s own measurement put the affected traffic at roughly 0.2% of first-party partitioned cookies used in A → B → A contexts, which is why the change shipped without a long deprecation runway. The important point for anyone reasoning about partition identity now is that the key is no longer just the top-level site. It is the top-level site plus a boolean that records whether the path to the current frame crossed a site boundary. This also aligned cookie partitioning with how Chrome already keyed other partitioned storage, which is the subject of a later section. The exact in-memory representation of the bit is an implementation detail of Chrome’s CookiePartitionKey; the observable behaviour is the partition split shown above.

Partitioning multiplies cookie storage. One unpartitioned cookie is one entry. The same cookie set as partitioned across a thousand visited top-level sites is a thousand entries. Left unbounded that is a denial-of-service vector against the user’s own disk and a way to fingerprint by filling storage, so the spec caps it.

The IETF draft puts the limit as a recommendation: a user agent may limit a domain’s cookies to 10 kilobytes per top-level partition. The CHIPS repo adds the empirical justification, that Chrome data suggests ten cookies per partition satisfies around 99% of existing cross-site cookie use cases. Chrome’s shipped numbers, per the Privacy Sandbox docs, are a maximum of 180 cookies per partition with a 10 KiB per-partition-per-embedded-site budget. The byte limit is the binding one in practice; a single embedded third party gets at most 10 KiB of partitioned cookie storage within any one top-level site’s partition, independent of what the same third party stores in other partitions.

That independence matters and it is deliberate. The draft’s privacy section is explicit that partitioned cookies must use separate per-domain limits per partition, because a shared limit would be a cross-partition side channel. If filling storage in the news.example partition could evict or constrain cookies in the shop.example partition, a tracker could probe one partition’s state by observing eviction in another, reconstructing exactly the cross-site signal partitioning is meant to destroy. Per-partition budgets keep the partitions from leaking through their own quota accounting.

tracker.example storage, per top-level partition (cap: 10 KiB / 180 cookies each) news shop blog forum budgets are independent — no eviction side channel *Each partition carries its own quota for the same embedded host. Filling one cannot evict another, which is what keeps the quota from becoming a tracking signal.*

Clearing follows the same partition-scoped logic. When a third party sends Clear-Site-Data, the browser clears that third party’s cookies in the current top-level site’s partition only; the draft is explicit that it must not touch the same third party’s cookies in other partitions. A widget logging a user out on shop.example does not log them out of the widget on news.example, because those are genuinely separate sessions now.

The retrieval algorithm, step by step

It helps to walk the actual decision the browser makes when it assembles the Cookie header for an outgoing request, because the partition check is one branch inside a longer pipeline and it is easy to misattribute behaviour to partitioning that is really SameSite or Secure doing the work. The following is a faithful description of the retrieval logic in the IETF draft, written as illustrative pseudocode rather than a copy of any one engine’s source. It shows where the partition key enters and nothing more.

for cookie in cookie_store:
if not host_key_matches(cookie, request.host): continue # wrong domain
if cookie.secure_only and request.scheme != "https": continue # Secure gate
if not path_matches(cookie, request.path): continue # path scope
if not same_site_ok(cookie, request, navigation): continue # SameSite gate
if cookie.partition_key is not None: # the CHIPS branch
req_pk = (top_level_site, cross_site_ancestor_bit)
if cookie.partition_key != req_pk: continue # wrong partition
include(cookie)

The order matters for reasoning about behaviour. By the time the partition branch runs, the cookie has already passed the host, scheme, path, and SameSite filters. So a partitioned cookie that is missing from a request might be missing for any of those reasons, not because partitioning excluded it. The single most common confusion is a developer setting Partitioned without SameSite=None, watching the cookie vanish on cross-site requests, and blaming the partition logic, when the cookie never reached the partition branch at all because SameSite’s lax default dropped it two steps earlier. The two gates are independent and both have to pass.

The partition comparison itself is a strict equality on the full key, top-level site and ancestor bit together. There is no notion of a partition being a superset or a parent of another. A cookie set in the (retail.example, bit=0) partition is invisible in (retail.example, bit=1) despite the matching site, which is exactly the A → B → A isolation. This strictness is what makes the partition a clean boundary to reason about: either the keys are byte-for-byte the same and the cookie is a candidate, or they are not and it does not exist for this request. There is no fuzzy matching to exploit.

For anyone diffing what a real browser sends against what an HTTP client sends, this is the practical takeaway. A partitioned cookie’s presence on the wire is fully determined by the top-level browsing context, which a bare HTTP client does not have. There is no top-level site, no ancestor chain, no partition key to compute, so a scripted client reconstructing a session has to know which partition a captured cookie belonged to and replay it only in the matching context. The cookie value alone is not enough; the partition it was bound to is part of its identity now. Tooling that captures cookies from a live browser, like the request-capture flow discussed in the session and cookie management across a proxy fleet post, has to record the partition key alongside the value or the replay will silently send the cookie into the wrong jar. Chrome DevTools surfaces this directly: under Application, Storage, Cookies, the details panel shows a Partition Key column, and unpartitioned cookies display an empty key while partitioned ones carry their top-level site.

Where CHIPS sits in storage partitioning

CHIPS did not arrive alone. Chrome partitions most of its client-side storage by a StorageKey, and cookies were the last major piece to be brought into line. The storage partitioning docs list what is keyed this way: Local Storage and Session Storage, IndexedDB, Cache Storage, the Origin Private File System, and the Storage Buckets API. Each of those is scoped so that an embedded third party gets a different storage instance per top-level site, the same shape as a partitioned cookie jar.

The StorageKey is composed of the frame’s origin, the top-level site, and the same cross-site ancestor chain bit that CHIPS adopted in 2025. That adoption was the point. Before M125, cookie partitioning keyed only on top-level site while the StorageKey already carried the ancestor bit, so an A → B → A inner frame got one partition for its cookies and a different one for its IndexedDB. Adding the bit to the cookie partition key made the two consistent, so a piece of embedded code now sees the same partition boundary across cookies and structured storage. For anyone building or detecting cross-site state, that alignment is the useful invariant: per-partition identity is now a single boundary, not two slightly different ones.

The contrast with cookies is that the storage APIs are partitioned by default, with no opt-in attribute. There is no Partitioned-equivalent flag on IndexedDB. A third-party iframe’s IndexedDB has been partitioned by top-level site in Chrome for some time regardless of what the developer asks for. Cookies are the exception that kept an opt-in, because the unpartitioned third-party cookie was load-bearing for so much of the web that flipping it to partitioned-by-default would have broken things in ways the storage APIs did not. The opt-in is the migration path: set Partitioned, get a cookie that survives third-party-cookie blocking, and do not depend on the legacy shared jar that browsers are free to take away.

Cross-browser state

CHIPS is one of the rare privacy features that landed with all three engines on side. Chrome shipped it in version 114, with Edge following at 114 on the same Chromium base. The standards positions were filed early and were positive across the board. Mozilla labelled its position positive in August 2022, and Firefox shipped support in version 141. WebKit’s position, filed the same month with John Wilander and Ted O’Connor assigned, was support, tagged with a performance concern to work through rather than an objection, and Safari shipped it in 26.2. MDN’s compatibility data now records partitioned cookies as a Baseline feature newly available across the latest browser versions as of December 2025.

The reason all three converged is that CHIPS matched where each was already heading. Safari’s Intelligent Tracking Prevention had been partitioning and then outright blocking third-party cookies for years; an explicit opt-in attribute gave embedded services a sanctioned way to keep per-site state instead of having it silently dropped. Firefox’s Total Cookie Protection, the productised name for its dynamic state partitioning, already double-keyed third-party cookies by top-level site automatically. CHIPS is close to a standardised, opt-in version of what Firefox does implicitly. The MDN guidance states the practical difference directly: Firefox partitions third-party cookie storage by default, while CHIPS makes partitioning an explicit, interoperable opt-in that works the same way across engines. For a developer the recommendation is to use the Partitioned attribute rather than relying on any one browser’s default partitioning, because the attribute is the portable contract.

What changed when Privacy Sandbox went away

CHIPS was conceived as one component of the Privacy Sandbox, the suite Google built to replace third-party-cookie functionality before deprecating the cookies themselves. The plan was that the unpartitioned third-party cookie would be removed from Chrome and developers would migrate to partitioned cookies for per-site state, to the Topics API for interest signals, to the Protected Audience API for remarketing, and to Attribution Reporting for conversion measurement. That deprecation never happened.

Google delayed the removal repeatedly, and in July 2024 it abandoned the plan, saying it would not phase out third-party cookies in Chrome and would instead offer a user choice prompt. By April 2025 even the prompt was dropped, leaving Chrome’s existing cookie controls in place. In October 2025 Google retired the remaining Privacy Sandbox advertising APIs, Topics, Protected Audience, and Attribution Reporting, on both Chrome and Android. The sandbox that CHIPS was supposed to be part of is gone.

CHIPS outlived it, and the reason is structural. Topics and Protected Audience were attempts to rebuild ad-tech functionality inside the browser, and they carried Google-specific design and adoption baggage that made the antitrust and industry pushback fatal. CHIPS asked for none of that. It is a cookie attribute with a clear security story, positive positions from Mozilla and Apple, and a shape that matched what those browsers already shipped. It does not depend on third-party cookies being removed to be useful. The instant any browser blocks third-party cookies, by default like Safari and Firefox or by user setting in Chrome, a Partitioned cookie is the supported way for an embedded service to keep working. That value does not evaporate because Chrome decided to keep the legacy cookie alive.

So the current state is a split. The unpartitioned third-party cookie is not being killed in Chrome, which means the cross-site identity it enables persists for any site and any user who has not turned it off. At the same time CHIPS is Baseline, interoperable, and the default-safe choice for new embedded state, because two of the three major engines block the unpartitioned cookie out of the box and the third lets users do the same. A developer shipping an embedded widget today writes Partitioned not because Chrome forced the migration but because it is the only cookie that behaves consistently across all four browsers. The deprecation that was supposed to make CHIPS mandatory fell through; the interoperability that made CHIPS portable is what kept it.

Closing: a small attribute that did its one job

CHIPS is worth studying precisely because it is narrow. It changed one thing, the key the cookie store uses, from a single host key to a host key plus a partition key plus, since 2025, a cross-site ancestor bit. Everything else falls out of that. The privacy property, no stable cross-site identifier, is a consequence of the keys never being compared across top-level sites. The security property, no confused-deputy reach into a top-level partition, is the ancestor bit. The denial-of-service mitigation is the per-partition 10 KiB budget, kept independent so the quota itself does not leak. None of it required new cryptography or a new request flow, just a more specific answer to the question “whose cookie is this.”

The contrast with the rest of the Privacy Sandbox is the lesson. The APIs that tried to relocate an entire advertising mechanism into the browser collapsed under their own ambition and the scrutiny that came with it. The attribute that did one legible thing, with a security story every engine could agree on, is in all four browsers and graded Baseline. Third-party cookies are still alive in Chrome, so CHIPS is not the forced replacement it was meant to be. It is something more durable instead: the one piece of identity plumbing that every browser ships the same way, sitting ready for the day any given user turns the old cookie off.


Sources & further reading

Further reading