A visitor accepts cookies on www.example.com, clicks through to shop.example.com, and is shown the banner again — while your analytics records two consent events for one person and your vendors initialise twice with different states.

Triage: Establish Where the Record Lives

The symptom has one of two causes, and a two-minute inspection separates them.

  1. Accept on the main host. Open Application → Storage and find where the decision was written: a cookie, localStorage, or both.
  2. Read the cookie’s Domain column. A value of www.example.com is host-scoped; .example.com is shared across subdomains.
  3. Navigate to the second host and inspect the same storage. If the record is absent, you have a scoping problem; if it is present but the banner still appears, you have a reading problem.
  4. Check whether the consent platform is configured with the same identifier on both hosts — many platforms scope their own record per configured domain.
Two failure shapes, one symptom The banner reappearing on a second host has two causes. In the scoping case, the record was written host-scoped so it does not exist on the second host at all. In the reading case, the record is present and shared but the second host reads a different key, a different platform configuration, or localStorage rather than the cookie. The Application panel distinguishes them in one look. scoping — record absent Domain: www.example.com the second host cannot see it fix: widen the cookie domain reading — record present Domain: .example.com but the second host reads elsewhere fix: align the key or the platform config Look at the Domain column first. It answers which of the two you are in before any code is read.

Browser storage is partitioned in ways that rarely match how a business thinks about its site. localStorage is scoped to an exact origin, so www.example.com and shop.example.com have entirely separate stores with no mechanism to share between them. Cookies are more permissive: a cookie set with Domain=.example.com is sent to every subdomain, which is why the consent record belongs there rather than in local storage.

The complication is that a shared cookie is a wider disclosure than a host-scoped one. Every subdomain now receives the record on every request — including subdomains you do not control tightly, such as a status page on a third-party platform or a marketing microsite run by an agency. That is an argument for keeping the cookie small and non-identifying, not for abandoning the approach: a category bitmask and a policy version disclose nothing a visitor’s own decision has not already established, while a full consent record with an identifier is something you would rather not broadcast.

The second, subtler cause is platform configuration. Most consent platforms scope their stored record by the domain configured in their console, so two hosts registered as separate properties will each maintain their own decision no matter how your cookies are scoped. The platform adapter is where this surfaces, because it is the only place that knows which record is authoritative.

What each storage mechanism can share A comparison of storage mechanisms by scope. localStorage is scoped to an exact origin and cannot be shared between subdomains at all. A host-scoped cookie is limited to the host that set it. A domain-scoped cookie is sent to every subdomain, which is what makes sharing possible and also what widens the disclosure. A note records that the third option is the only workable one and that its payload should therefore be minimal. localStorage exact origin only — cannot be shared, at all cookie, host-scoped the host that set it, and nothing else cookie, Domain=.example.com every subdomain — the only sharing mechanism The workable option is also the widest. Keep the payload to a bitmask and a version.

Write the decision to a domain-scoped cookie alongside whatever the platform stores, and have every host read that cookie first:

// consent-cookie.js — the shared record. Small, non-identifying, versioned.
const KEY = 'consent';

export function writeConsent(state, policyVersion) {
  const value = JSON.stringify({ v: policyVersion, c: state });
  // Domain must start with a dot's modern equivalent — a bare apex domain,
  // which browsers treat as covering subdomains.
  document.cookie = [
    `${KEY}=${encodeURIComponent(value)}`,
    'Domain=example.com',
    'Path=/',
    'SameSite=Lax',
    'Secure',
    `Max-Age=${60 * 60 * 24 * 180}`,
  ].join('; ');
}

export function readConsent(currentPolicyVersion) {
  const raw = document.cookie.split('; ').find((c) => c.startsWith(KEY + '='));
  if (!raw) return null;
  try {
    const { v, c } = JSON.parse(decodeURIComponent(raw.split('=')[1]));
    // A record captured under an older policy is not consent to the current one.
    return v === currentPolicyVersion ? c : null;
  } catch { return null; }
}

The version check is the part that is easy to omit and expensive to omit. Without it, a policy change — a new vendor category, a changed purpose — silently inherits consent that was given for a different set of processing, which is the stale-record failure an audit is most likely to find.

Where the consent platform maintains its own record, treat the cookie as the shared source and reconcile on load: read the cookie, and if it disagrees with the platform’s state, push the cookie’s value into the platform rather than the reverse. The cookie is the thing both hosts can see.

Verification: Accept Once, Navigate, No Banner

Clear all storage, accept on the first host, and navigate to the second. The banner must not appear, the vendors must initialise with the accepted categories, and the Application panel should show one cookie with Domain=example.com rather than two host-scoped records.

The check that catches a partial fix Three states after accepting on the first host and navigating to the second. A correct implementation shows one shared cookie and no banner. A partial fix shows the cookie present but the banner still displayed, meaning the second host is not reading it. A failed fix shows two separate host-scoped cookies, meaning the domain attribute did not take effect. correct one cookie, apex domain no banner on host two partial cookie present banner still shown — not read failed two host-scoped cookies the Domain attribute was rejected The middle state is the common one, and it is a code problem rather than a cookie problem.

There is a standing temptation to put more in the shared record — the full platform payload, a visitor identifier, a timestamp with a session reference — because it is convenient to have it available everywhere. Resist it, for two reasons that compound.

The first is disclosure. A domain-scoped cookie is transmitted to every subdomain on every request, including ones operated by third parties on your behalf. Anything in it is effectively shared with those operators whether or not they need it, and an identifier in particular converts a consent record into a tracking mechanism — which is an unfortunate thing to discover in a privacy review of your consent implementation.

The second is size. Cookies are sent on every request to every matching host, so a large record is a per-request tax on your own origin as well as a disclosure. A bitmask and a version fit comfortably in a few dozen bytes; a serialised platform payload can run to several hundred, on every asset request that does not have Domain scoping of its own.

Keep the detailed record wherever the platform puts it, per host, and let the shared cookie carry only what both hosts must agree on: which categories were granted, and under which policy version.

  • Widening the cookie without auditing the subdomains. Enumerate what example.com actually resolves to before setting an apex-scoped cookie; the list is usually longer than anyone remembers.

Common Pitfalls

  • Setting Domain to a host rather than the apex. Domain=www.example.com covers www and its own subdomains, not siblings. The apex is what shares across siblings.

  • Relying on localStorage with a postMessage bridge. It works, requires an iframe on every page, and fails entirely under storage partitioning. The cookie is simpler and more durable.

  • Omitting the policy version. A record without one cannot be invalidated when the policy changes, so an old decision silently authorises new processing.

  • Assuming the platform’s own cookie is shareable. Most consent platforms scope their record to the property configured in their console, so widening your cookie does not widen theirs. Reconcile explicitly on load rather than expecting the platform to notice.


Frequently Asked Questions

Does this work across entirely different domains?

No, and increasingly it cannot. Cookies cannot be shared between unrelated registrable domains, and the mechanisms that used to bridge them — a third-party iframe holding shared storage — are blocked or partitioned by every current browser. A consent decision on example.com genuinely does not transfer to example.net.

Treat separate domains as separate consent contexts and ask again, which is also the position most regulators take: consent is given in relation to a specific controller and context, and inferring it across properties a visitor may not associate with each other is difficult to defend. Where the properties are genuinely one service, consolidating them onto subdomains of one domain solves the problem properly.

What about a subdomain we do not control?

A domain-scoped cookie is sent to it regardless, which is a reason to audit the list of subdomains before widening the scope. A marketing microsite on an agency’s platform, a status page, a legacy host nobody has decommissioned — all of them will receive the consent record on every request.

If any of those are uncomfortable, the mitigation is the same one as for size: put nothing in the cookie you would mind them seeing. A record saying “analytics and marketing were accepted under policy 4” discloses a decision the visitor made about your site, which those hosts arguably ought to honour anyway.

Should the server read this cookie too?

It is worth doing, and it is the main practical benefit of using a cookie rather than any other mechanism. Because the cookie arrives with the request, a server-rendered page can emit the correct state in its initial HTML — no banner for a returning visitor who accepted, no layout shift when the gated content appears, and correct consent defaults inlined before any container boots.

Treat it as a hint rather than as authority. The client-side platform remains the source of truth because it is the only thing that can observe a change made in another tab, so the server’s job is to render the likely state and let the client correct it if it has since changed.


Up: Syncing Consent States Across Multiple Vendors