A Set-Cookie header is a one-line contract with the browser, and most of it is silent. Nothing tells you when an attribute was rejected, when a lifetime was truncated, or when the cookie was never stored at all. This page walks through what each attribute does, which combinations browsers refuse, and how to read the report above.

What a Set-Cookie header holds

The structure is simple: a name, a value, then a list of attributes separated by semicolons.

PartExample
name and valuesessionid=9f2b41c7
attributesPath=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600

Only the name and the value ever come back. The browser stores the attributes, uses them to decide when to send the cookie, and then sends a Cookie header containing nothing but name=value pairs. Your server cannot see the Path, the expiry or the SameSite setting of an incoming cookie, which is why a cookie that arrives with an unexpected value usually means two cookies of the same name exist on different paths.

How to read the report

Paste one Set-Cookie line per row. A leading Set-Cookie: is optional, an HTTP status line is ignored, and a Cookie: request header is recognised and analysed differently.

  1. Attributes. Every attribute present, plus three computed rows: the real lifetime in days rather than seconds, the byte size, and a plain sentence saying which hosts and paths the cookie will be sent to.
  2. Findings. Errors first. An error means a browser rejects or truncates something. A warning means it works but is worth changing. Information is context, not a problem.

--decode

Percent-decodes the value, so de%2DAT reads as de-AT and a JSON payload becomes legible. Most frameworks encode cookie values on the way out, which is why raw headers are full of %22 and %3A.

--hardening

Adds the advisory checks on top of the hard rules: a missing HttpOnly, a Domain attribute that widens the cookie to every subdomain, and values shaped like a JWT. Turn it off when you are debugging a specific rejection and want only the reasons the browser itself cares about.

Every attribute, explained

AttributeEffectNotes
ExpiresAbsolute expiry dateMust be an IMF-fixdate, Wed, 21 Oct 2026 07:28:00 GMT. Unparseable values are ignored, turning the cookie into a session cookie.
Max-AgeLifetime in secondsWins over Expires wherever both are present. Max-Age=0 deletes the cookie.
DomainWidens the cookie to subdomainsAbsent means the exact host only, which is the safer default. The leading dot is ignored.
PathRestricts by URL prefixDefaults to the directory of the request, not to /. It is not a security boundary; scripts on the same origin reach every path.
SecureHTTPS onlyRequired by SameSite=None, by Partitioned, and by both name prefixes.
HttpOnlyHidden from JavaScriptThe single most effective thing you can do about session theft through XSS.
SameSiteCross-site behaviourStrict, Lax or None. Missing means Lax in current browsers.
PartitionedCHIPS, one jar per top-level siteRequires Secure. For embedded content that needs its own state per host.
PriorityEviction order under pressureChrome only, Low, Medium or High. Other browsers ignore it.

Anything else is not an attribute. Browsers drop unknown attributes without a word, so a mistyped HttpsOnly or Same-Site looks exactly like a correct header until you notice the cookie behaving as if the attribute were absent. The report flags them for that reason.

SameSite in practice

SameSite decides whether the cookie rides along when another site triggers a request to yours. The three values differ in one dimension, how much cross-site traffic still carries the cookie.

RequestStrictLaxNone
Same sitesentsentsent
Link from another sitenot sentsentsent
Cross-site form POSTnot sentnot sentsent
Iframe, image, fetchnot sentnot sentsent

Two details cause most of the confusion. The first is that Strict breaks the arriving-from-elsewhere case: a user who clicks a link in an email to a page behind a login lands logged out, because the cookie was not sent on that navigation. The usual pattern is a Lax session cookie plus a Strict cookie for the operations that matter.

The second is that "same site" is not "same origin". It compares registrable domains, so app.example.com and api.example.com are the same site and a request between them is not cross-site, despite being cross-origin and therefore subject to CORS. Mixing the two concepts up is how people end up adding SameSite=None to a cookie that never needed it.

Chrome keeps one compatibility exception, sometimes called Lax-allowing-unsafe: a cookie younger than two minutes is still sent on a top-level cross-site POST. It exists to keep older single-sign-on flows working and it is not something to design around.

Why a cookie is silently dropped

Rejection is the frustrating part of cookies, because the response looks fine and nothing appears in the browser. These are the reasons, roughly in the order they show up in real bug reports:

  • SameSite=None without Secure. Rejected since Chrome 80 in February 2020. Typically appears as an integration that works in your own tab and not inside a customer's iframe.
  • Secure over plain HTTP. A local development server on http:// cannot set a Secure cookie. http://localhost is treated as a secure context, other hostnames pointed at 127.0.0.1 are not.
  • A Domain that does not match. A host can set a cookie for itself or for a parent domain, but not for an unrelated one and not for an entry on the Public Suffix List.
  • Broken prefix rules. __Host- with a Domain attribute, or __Secure- without Secure.
  • Size. Above roughly 4096 bytes for name, value and attributes together, the cookie is discarded.
  • An invalid Expires date. Not a rejection exactly, the attribute is just ignored, and the persistent cookie you wanted becomes a session cookie.

Chrome devtools shows all of these: open the Network panel, select the response, and the Cookies tab lists rejected cookies with a strike-through and a tooltip naming the reason. The tool above answers the same question from the header alone, which is what you have when the header came from a log, a curl run or a colleague.

The __Host- and __Secure- prefixes

Both prefixes are enforced by name. The browser sees the prefix and refuses to store the cookie unless the rules hold.

PrefixRequiresWhat it buys
__Secure-Secure, set over HTTPSThe cookie cannot have been written by a plain HTTP response.
__Host-Secure, Path=/, no DomainThe cookie is locked to one exact host and no subdomain can overwrite it.

The attack __Host- closes is cookie tossing. Subdomains share a cookie jar with the parent domain, and a cookie set on blog.example.com for .example.com arrives at example.com looking exactly like one the main app set. The server cannot tell them apart, because the Cookie header carries no Domain. Anyone who controls any subdomain, including a forgotten staging box or a hosted marketing tool, can therefore write cookies your application trusts. Renaming a CSRF token to __Host-csrf costs one line and removes the whole class.

Size and lifetime limits

Cookies are small, and every one of them is sent with every request to the domain, including images and API calls. The numbers worth knowing:

  • About 4096 bytes per cookie, counting name, value and attributes. Over that, browsers drop it.
  • Around 180 cookies per domain in Chrome, with a global cap in the low thousands. Once it fills, cookies are evicted, and Priority decides the order.
  • 400 days maximum lifetime since Chrome 104 in August 2022, matched by Firefox. Longer values are accepted and truncated.
  • Seven days in Safari for cookies written by JavaScript through document.cookie, under Intelligent Tracking Prevention. Cookies set by a Set-Cookie header are not affected.
  • 8 KB per request header line on nginx by default, 8190 bytes on Apache. Cross it and you get a 400 before your application ever runs.

Keep an identifier in the cookie and the state on the server. A session cookie that carries a serialised user object works fine in development, then someone adds a permissions array and every request to the domain grows by two kilobytes. The guide on SameSite, Secure and HttpOnly covers the attribute history in more depth, and if you are deciding where to keep a token in the first place, localStorage vs cookies is the relevant one.

Set-Cookie questions

Is it safe to paste a session cookie into an online cookie parser?

Only into one that parses in your browser. A Set-Cookie line from a real response contains a live session token, so a server-side parser gets a working login to your application, and it will sit in an access log long after you closed the tab. The parsing here is JavaScript running in your tab: the header is never uploaded, never logged, and the page keeps working with the network disconnected. Check the Network panel in devtools while you type if you want proof that nothing leaves.

What does SameSite=Lax mean?

SameSite=Lax sends the cookie with same-site requests and with top-level navigations that use a safe method, so following a link from another site carries the cookie but an image, an iframe, a form POST or a fetch from another origin does not. It has been the default in Chrome since version 80 and in Firefox since 2020, which means a cookie with no SameSite attribute behaves as Lax. Strict drops even the link case, and None sends the cookie everywhere but requires the Secure attribute.

Why is my cookie not being set?

In order of how often it happens: SameSite=None without Secure, which browsers reject outright; a Domain attribute that does not match the response host; the response came over plain HTTP while the cookie asked for Secure; a __Host- or __Secure- prefix whose rules are broken; an Expires date in the past; or the cookie is larger than about 4 KB. None of these produce an error you can catch, the cookie simply never appears. Chrome devtools shows the reason under Network, then the request, then the Cookies tab, where rejected cookies are listed with a strike-through.

What is the difference between Secure and HttpOnly?

Secure controls the transport: the cookie is only sent over HTTPS, so it cannot be read by anyone watching a plain HTTP request. HttpOnly controls the reader: the cookie is hidden from document.cookie, so JavaScript on the page cannot see it and an XSS payload cannot exfiltrate it. They protect against different attackers and both belong on a session cookie. Neither prevents the cookie being sent on cross-site requests, which is what SameSite is for.

How long can a cookie last?

400 days, in practice. Chrome capped cookie lifetimes at 400 days in version 104, released in August 2022, and Firefox followed. A Max-Age of 63072000 (two years) is therefore accepted, stored, and quietly truncated to 400 days. Safari is stricter for cookies set by JavaScript: its Intelligent Tracking Prevention caps those at seven days, which is why a "remember me" that works in Chrome can expire within a week on iOS.

What is the __Host- prefix?

A cookie name starting with __Host- is a contract enforced by the browser: the cookie must have Secure, must have Path=/, and must have no Domain attribute. Break any of those and the cookie is rejected. The payoff is that a subdomain cannot overwrite it, which closes the cookie-tossing attack where a compromised or third-party subdomain writes a cookie the main domain then trusts. For CSRF tokens it is close to free security and still barely used.

What is a Partitioned cookie?

Partitioned marks a cookie as CHIPS, Cookies Having Independent Partitioned State. The cookie is stored in a jar keyed to the top-level site rather than shared across every site that embeds you, so an embedded widget can keep state per host without being a cross-site tracker. It requires Secure, and reading it from a different top-level site returns nothing. It is the supported way to keep third-party iframes working as browsers narrow unpartitioned cross-site cookies.

Can JavaScript read an HttpOnly cookie?

No, and that is the point. document.cookie skips them, the browser still sends them, and a frontend needing the value should ask an API.

What causes "400 Request Header Or Cookie Too Large"?

The Cookie header the browser sends grew past what the server accepts. nginx allows 8 KB per header line by default through large_client_header_buffers, and Apache 8190 bytes through LimitRequestFieldSize. Because every cookie on the domain is sent with every request, a handful of analytics and consent cookies plus a fat session gets there faster than people expect. Paste the Cookie header into this tool and it shows the byte cost of each pair, which is usually enough to identify the one to move into server-side storage.

What is the difference between the Set-Cookie and Cookie headers?

Set-Cookie is the response header and carries the attributes. Cookie is the request header and carries only name=value pairs, so the server never sees Path or expiry.

Do cookies work across subdomains?

Only if you ask for it. Without a Domain attribute a cookie is sent to the exact host that set it and nowhere else. With Domain=example.com it goes to example.com and every subdomain, including staging, the blog and anything a third party runs for you. The leading dot in ".example.com" has been meaningless since RFC 6265; both forms behave the same. A cookie cannot be widened to a parent domain that does not match the response host, so shop.example.com cannot set a cookie for example.org, and no site can set one for a public suffix such as .co.uk.

Are third-party cookies still usable?

Partly, and it depends on the browser. Safari has blocked third-party cookies by default since 2020 and Firefox partitions them through Total Cookie Protection since 2022, so in both a cross-site cookie is either absent or isolated per top-level site. Chrome reversed its plan to remove them and kept them available, while pushing CHIPS as the supported route for embedded content that needs state. Building anything new on unpartitioned third-party cookies is a bad bet; Partitioned with Secure is the version that keeps working.