Most explanations of JavaScript cookies versus HTTP cookies stop at where the cookie is written: a script assigns to document.cookie, a server sends a Set-Cookie header, and the browser files both in the same jar. That's true and almost useless, because it skips the part that decides whether your consent banner comes back next week.

A JavaScript cookie is one written by script running in the page, through document.cookie or the newer cookieStore API. An HTTP cookie is one written by a server in a Set-Cookie response header. Both are ordinary cookies with the same attributes, but only the server-written kind can carry HttpOnly, only the server-written kind can use the newest name prefixes, and Safari deletes the script-written kind after seven days of browser use without interaction on your site while honouring the expiry on the server-written kind. The browser draws all of those lines at the moment of writing. Once the cookie is stored, the only lasting trace of how it got there is the HttpOnly flag.

The short version

A consent management platform (CMP) writes its consent cookie with JavaScript by default, so on Safari that cookie lives at most seven days of browser use without a click on your site. Writing the same cookie from your own server in a Set-Cookie header removes that cap, and the cookie stays readable by the banner script as long as you leave HttpOnly off. Chrome and Edge cap both kinds at 400 days, Brave now caps both at six months, and Firefox publishes no general cap. European law does not distinguish between the two mechanisms at all.

Side by side: what changes between document.cookie and Set-Cookie

The table lists every difference that matters for a site owner or developer. Rows with the same value in both columns are there on purpose, because the sameness is the thing people get wrong.

PropertyJavaScript cookie (document.cookie, cookieStore)HTTP cookie (Set-Cookie header)
Written byAny script running in the page, including third-party scriptsThe server that answered a request, for its own host
Domain it belongs toThe page's domain, whoever loaded the scriptThe responding host, or a parent domain named in Domain=
HttpOnlyCannot set it, cannot read cookies that have itOptional; hides the cookie from all page scripts
Name prefixes__Secure- and __Host- on HTTPS pages; __Http- and __Host-Http- are rejectedAll four prefixes accepted
Safari (ITP)Deleted after seven days of Safari use without user interaction on the site; 24-hour expiry on a landing page reached through a decorated link from a classified domainStated expiry honoured when the response comes from your own host; seven-day cap when it comes through third-party CNAME or IP cloaking
Chrome, Edge, other Chromium browsersExpires and Max-Age capped at 400 daysSame 400-day cap
BraveCapped at six months (seven days before the 1.83 release)Capped at six months
FirefoxNo general cap published; storage purged for tracker-listed domains without recent interactionSame
Where you see it in DevToolsApplication > Cookies, with an empty HttpOnly columnApplication > Cookies, plus the Set-Cookie line in the Network response headers
Typical consent-banner useThe default for almost every CMPNeeds an endpoint on your own domain that answers with the header

Two of those rows carry the whole argument. The HttpOnly row explains why security teams insist on server-written session cookies, and the Safari row explains why Safari visitors keep seeing your banner. Everything else is detail that becomes useful when you sit down to write the cookie.

HttpOnly is the only trace that survives the write

In March 2022 a developer filed a WebKit bug about a React web app installed to the iPhone home screen. The app set its authentication cookie with a 28-day expiry, Secure and SameSite=Strict, and users were being logged out after seven days. The cookie had been written client-side, from JavaScript, because that is how a single-page app tends to do it. John Wilander, the WebKit engineer behind Intelligent Tracking Prevention (ITP), explained that the seven-day limit was simply the expiry mechanism of the cookie applied at write time, and that turning it into an interaction-based timer would need new plumbing. His reason is the most useful sentence in this whole subject:

after a cookie has been created, there is nothing to distinguish a cookie created in JavaScript and one created by a server (except that only servers can create HttpOnly cookies ...)

In other words, the cookie jar doesn't record provenance. The browser makes its decision as the cookie comes in, and after that the two kinds are identical unless the server chose to mark its own cookie HttpOnly. The thread is still open on WebKit Bugzilla, and it reads as a small case study in why session and consent cookies should not share a writing method with your marketing tags.

WebKit did build the plumbing later. Its current reference describes script-written cookies as deleted after seven days of browser use with no interaction, on the same timer as localStorage and IndexedDB, rather than having their expiry date rewritten. The practical difference is that Web Inspector may now show a long expiry on a cookie Safari intends to remove.

Because provenance is not stored, telling the two apart on a live site means looking for the evidence the write left behind. Three checks cover nearly every case:

  • Open Application > Cookies in Chrome DevTools. A tick in the HttpOnly column proves a server wrote the cookie. An empty column proves nothing on its own.
  • Reload with the Network panel open and search the response headers for Set-Cookie. If the cookie name appears there, a server wrote it on that request, whatever the flags say.
  • Run document.cookie in the console. A cookie that shows up in DevTools but not in that string is HttpOnly, and therefore server-written.
  • Check the name. A cookie starting with __Http- or __Host-Http- can only have come from a Set-Cookie header, because browsers that understand those prefixes reject script attempts to set them.

That last check is new. Chrome 140 shipped the __Http- and __Host-Http- prefixes in 2025 so that a server can tell its own cookies from ones injected by an extension or a cross-site scripting bug; support elsewhere is still uneven, so treat the prefix as a signal rather than proof. The older __Host- prefix, by contrast, works from JavaScript on an HTTPS page and says nothing about who wrote the cookie. The DevTools cookie audit walkthrough covers the panels in more depth.

Which browsers honour the expiry you set?

Chrome, Edge and Firefox keep the expiry you ask for, up to a ceiling, and treat script-written and server-written cookies alike. Safari and, until recently, Brave are the two that look at how the cookie was written before deciding how long it lives. As of September 2026 the WebKit reference still describes the split in the same terms it has used since 2020.

Safari's rule sits in the Tracking Prevention in WebKit document. Cookies created in JavaScript are deleted along with all other script-writable storage after seven days of browser use with no user interaction on the site, where interaction means a click, tap or keyboard entry and scrolling does not count. A separate rule caps script-written cookies at 24 hours on a landing page the visitor reached through a link carrying a click identifier from a domain that ITP has classified as a tracker. Server-written cookies from your own host keep their stated expiry, with one exception: if a first-party subdomain resolves through a CNAME, or to an IP address, that belongs to a third party and differs from where your pages are served, any cookie set in that response is capped at seven days. WebKit publishes a seven-row table of the cloaking combinations; the short reading is that the subdomain has to point at your own infrastructure, or at the same edge network your main host uses.

Chrome's ceiling is the one most developers have met by accident. Since Chrome 104 in August 2022, any cookie asking for more than 400 days is stored with an expiry of exactly 400 days from the moment it was set; the cookie is not rejected, just shortened. Chrome 118 went back and applied the same cap to cookies already in storage. The number comes from the draft revision of the cookie standard, chosen because 400 days is a little over 13 months and a site visited once a year still works. Nothing in the rule looks at document.cookie versus Set-Cookie. A two-year _ga and a two-year Set-Cookie session both come out at 400 days, and both get their clock reset every time the site rewrites them.

Brave is the browser that changed its mind. It copied Safari's seven-day cap on script-written cookies in 2019, kept it through years of complaints from developers whose dark-mode preferences kept resetting, and then opened an issue in July 2025 to remove it. The issue closed against the 1.83 release. Script-written cookies in Brave now get the same six-month ceiling as HTTP cookies, and Brave points users who want stronger clearing at its Forgetful Browsing and Shred features instead.

Firefox tried the seven-day cap on its Nightly channel during the Firefox 68 cycle in 2019 and never shipped it to release. Its tracking protection works differently: storage belonging to domains on Mozilla's tracker list is purged when the visitor has not interacted with them recently, and that purge doesn't care how a cookie was written. For a first-party consent cookie on a site that is not on the list, Firefox does what Chrome does, minus the published 400-day ceiling. The cookie duration explainer has the browser-by-browser lifetimes if you need them for a policy.

The same consent cookie, written both ways

Suppose the banner has just recorded analytics granted and marketing denied, and you want that choice to survive for a year. Here is the write from the page:

const state = encodeURIComponent(JSON.stringify({ analytics: true, marketing: false }));
document.cookie = "cc_consent=" + state + "; Max-Age=31536000; Path=/; Secure; SameSite=Lax";

The promise-based version does the same job with named options instead of a string to assemble, and it has been usable across current browsers since Firefox 140 joined Chrome and Safari 18.4 in mid-2025:

await cookieStore.set({
  name: "cc_consent",
  value: state,
  expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
  path: "/",
  sameSite: "lax"
});

Either way, Safari reads that as script-written storage and puts it on the seven-day timer, and Chrome accepts the year because it is under 400 days. Now the same cookie from the server. The banner sends the choice to an endpoint on your own domain, and the endpoint answers:

HTTP/1.1 204 No Content
Set-Cookie: kk_consent=%7B%22analytics%22%3Atrue%2C%22marketing%22%3Afalse%7D; Max-Age=31536000; Path=/; Domain=example.com; Secure; SameSite=Lax

Three things changed and one thing did not. The write happened in a response rather than in the page, so Safari honours the 365 days provided example.com really is your host. A Domain= attribute appeared, because a response from consent.example.com would otherwise set a host-only cookie that www.example.com never receives. Encoding changed too: a raw JSON object contains characters the header syntax does not allow, so the value is URL-encoded. What did not change is the absence of HttpOnly: this cookie is meant to be read by the banner on the next page load, so it stays visible to scripts, and the SameSite attribute is the same Lax in both versions.

In PHP the equivalent is a single setcookie() call with the options array, and every framework has its own wrapper for the same header.

Can your banner script still read a server-set cookie?

Yes, as long as the server leaves HttpOnly off. The Safari exemption depends on who wrote the cookie, not on whether scripts can read it, so a Set-Cookie header without HttpOnly gets the full expiry and stays readable from document.cookie. Only cookies that carry secrets, such as a session identifier, should be hidden from scripts. A consent state is not a secret; it is a preference the page needs in order to decide which tags to load.

That trade-off is where a lot of advice goes wrong. Articles that recommend HttpOnly for every cookie are thinking about login sessions, where the value is a bearer token and a cross-site scripting bug would hand it to an attacker. Apply the same flag to a consent cookie and the banner can no longer see it, which means every page either has to render the consent state server-side or ask an endpoint before it knows whether to show the banner. Both are workable, and both add a round trip the JavaScript version never needed. Leaving the cookie readable keeps the banner logic simple and costs nothing, because the worst a hostile script could do with it is grant itself consent it was not given, which it could do just as easily by ignoring the cookie altogether. The real defence against that is blocking scripts until consent exists, not hiding the flag that records it.

Kukie.io's implementation is a useful illustration of the pattern rather than a special case. On the Agency plan, the banner posts the visitor's choice to a subdomain you point at its cookie server, and the response sets kk_consent for a year without HttpOnly; the standard JavaScript cookie _cc_consent is still written as a fallback, and the banner checks the HTTP cookie first on load. The HTTP consent cookies documentation has the DNS steps. Whichever CMP you use, the WebKit cloaking rule above means you should confirm what the consent subdomain resolves to before assuming Safari will honour the year.

Two things don't help, in case you were about to try them. Moving the choice into localStorage lands it in the same script-writable bucket that Safari clears on the same timer, and the legal treatment of local storage and session storage is no lighter than for cookies. Rewriting the JavaScript cookie on every page view refreshes the expiry date but not Safari's interaction counter, which only resets when the visitor clicks, taps or types on your pages.

Does the law care how the cookie was written?

No, it doesn't. Article 5(3) of the ePrivacy Directive regulates storing information on, or gaining access to information already stored in, a user's terminal equipment. It doesn't mention cookies by name, let alone the API that wrote them. The European Data Protection Board (EDPB) settled any remaining doubt in version 2.0 of its Guidelines 2/2023 on the technical scope of Article 5(3), adopted on 7 October 2024:

The ePD does not place any upper or lower limit on the length of time that information must persist on a storage medium to be counted as stored, nor is there an upper or lower limit on the amount of information to be stored.

Read alongside the previous paragraph of the same guidance, which counts cookie storage set through browser protocols as storage initiated by the party giving the instruction, that means a one-day script cookie and a 365-day server cookie are the same act in law. What decides whether consent is needed is the purpose of the cookie, not its mechanism or its lifetime. The cookie that records the consent choice itself is generally treated as strictly necessary, since the site cannot honour a refusal without remembering it, and that treatment does not change when it arrives in a header. It does depend on the cookie containing only the choice, an identifier for the consent record and a timestamp, and not doubling as a tracking identifier.

The other legal fact worth keeping straight is that neither cookie is your proof of consent. Safari deleting a script-written consent cookie after a week removes the visitor's stored preference; it does not remove the record that consent was given, which lives in your consent log. The cookie's job is to stop the banner asking again. The log's job is to answer a regulator. Conflating the two is how sites end up either re-prompting people needlessly or believing that a long cookie expiry is a compliance record, and it is neither.

document.cookie vs Set-Cookie: quick answers

Can JavaScript set an HttpOnly cookie?

No. HttpOnly can only be set in a Set-Cookie response header, and a cookie that carries it is invisible to document.cookie and to the cookieStore API. A script can still trigger a request that carries the cookie, it just cannot read or overwrite the value.

Is a cookie set by document.cookie a first-party cookie?

Yes. A script-written cookie belongs to the domain of the page it runs on, even if the script was loaded from a third-party host. First-party status is about the domain the cookie is attached to, not about who wrote it.

Does the cookieStore API avoid Safari's seven-day rule?

No. cookieStore is a script-writable interface like document.cookie, so Safari treats the cookies it writes as script-written storage. The promise-based API is easier to use, but it does not change how Intelligent Tracking Prevention classifies the result.

Do Chrome's 400 days apply to JavaScript cookies too?

Yes. Chrome caps the Expires and Max-Age of every persistent cookie at 400 days from the time it is written, whichever way it was written. Chromium-based browsers such as Edge apply the same limit.

Does Brave still cap JavaScript cookies at seven days?

Not any more. Brave introduced a seven-day cap on script-written cookies in 2019, then removed it in the 1.83 release after concluding it caused more annoyance than protection. Script-written cookies in Brave now share the six-month cap that applies to HTTP cookies.

Is a Set-Cookie header from a CNAME subdomain treated as first-party in Safari?

Only if the subdomain resolves to your own infrastructure. Safari checks where a first-party subdomain's CNAME or IP address points; if it lands on a third-party host that differs from the one serving your pages, cookies from that response are capped at seven days.

Write the consent cookie from your own domain

If your Safari visitors accept the banner and see it again the following week, the cookie recording their choice is almost certainly being written by script. Kukie.io can set that cookie from a subdomain on your own domain instead, in a Set-Cookie header the banner still reads, while keeping the JavaScript cookie as a fallback. If you first want to know which cookies your site sets today, and how, the free cookie scanner lists them without an account.

Create a free account