SameSite answers one question: when a request to your site was started by a different site, should the browser attach this cookie? Getting the answer wrong in one direction leaves you open to cross-site request forgery. Getting it wrong in the other breaks single sign-on, embedded widgets and payment redirects — usually for a subset of users, on a subset of browsers, with nothing in your logs.
What "Same Site" Actually Means
The word site here is narrower than "origin" and wider than "host". A site is the registrable domain — the public suffix plus one label. So example.com, app.example.com and www.example.com are all one site, and requests between them are same-site. example.co.uk and example.com are two different sites, even though they look related.
Scheme counts too. Modern browsers treat http://example.com and https://example.com as different sites ("schemeful same-site"), which is one more reason a cookie that behaved fine on a plain-HTTP staging box can vanish once part of the flow moves to HTTPS.
https://app.example.com:8443 to https://example.com is cross-origin but same-site, and the cookie is sent regardless of the SameSite value.The Three Values
SameSite=Strict
The cookie is never attached to a request that originated on another site — not on a form post, not on an image load, and not even when the user clicks a normal link. It is the strongest setting and the one with the most visible side effect: a user arriving from an email, a search result or a chat message lands on your page without their session cookie, and the first render looks logged out.
SameSite=Lax
A deliberate middle ground. The cookie is sent on top-level navigations that use a safe method — the user clicking a link, essentially — and withheld from everything else cross-site: form POSTs, fetch and XMLHttpRequest calls, iframes, images and script tags. That combination blocks the classic CSRF shape while keeping ordinary inbound links working, which is why it became the browser default.
SameSite=None
The cookie is attached to every cross-site request, which is the pre-2020 behaviour made explicit. You need it for cookies that must work inside someone else's page or across a redirect chain: embedded widgets, third-party analytics, federated login, hosted checkout flows. It is rejected outright unless the cookie is also marked Secure.
| Cross-site request | Strict | Lax | None |
|---|---|---|---|
| User clicks a link to your site | Not sent | Sent | Sent |
Cross-site form POST | Not sent | Not sent | Sent |
fetch from another origin | Not sent | Not sent | Sent |
Your page inside an iframe | Not sent | Not sent | Sent |
<img> or <script> load | Not sent | Not sent | Sent |
| Any same-site request | Sent | Sent | Sent |
Writing the Attribute
SameSite is a single token on the Set-Cookie header. Order does not matter, and the value is case-insensitive — but an unrecognised value is not: browsers fall back to the default rather than guessing, so a typo like SameSite=lax; is fine while SameSite=Laxx silently means "no attribute".
# A session cookie locked to your own site
Set-Cookie: __Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Strict; Max-Age=7200
# A cookie that has to work inside a third-party embed
Set-Cookie: widget_state=xyz; Path=/; Secure; SameSite=None; Max-Age=86400Frameworks expose the same three values as an option:
// Express / cookie-parser
res.cookie('__Host-session', token, {
path: '/',
secure: true,
httpOnly: true,
sameSite: 'strict', // 'strict' | 'lax' | 'none'
maxAge: 7200 * 1000,
});And nginx can stamp it on a response from an upstream you cannot change:
proxy_cookie_flags ~ secure httponly samesite=lax;SameSite=None when Secure is missing. The cookie is not stored with a safer value — it is not stored at all. Nothing appears server-side, so the symptom is an intermittent logout or a widget that works for you and not for anyone else.The Lax Default, and Why It Is Not the Same as Declaring Lax
Since 2020 every major browser has treated a cookie with no SameSite attribute as Lax. That default is the right one, so omitting the attribute is not a vulnerability. It is still worth writing:
- An explicit value documents intent. "No attribute" is indistinguishable from "nobody thought about it", which is what an auditor will assume.
- Defaults have changed before and can change again. A written value pins the behaviour to your decision rather than to the browser's current policy.
- Some older clients, embedded webviews and API consumers still apply the pre-2020 behaviour of sending the cookie everywhere. The attribute is the only thing that tells them not to.
There is one more wrinkle worth knowing: Chrome exempts brand-new cookies from the default for the first two minutes, so a cookie set during a redirect can still arrive on a cross-site POST shortly afterwards. This "Lax-allowing-unsafe" carve-out exists for legacy SSO flows and is not something to design against.
Choosing a Value Per Cookie
This is a per-cookie decision, not a per-site one. Work from what the cookie is for, not from which value sounds safest:
| Cookie | Value | Why |
|---|---|---|
| Session / auth token | Strict | Never needed on a cross-site request; the highest-value target you have |
| CSRF token | Strict | Only ever read by your own same-site form posts |
| "You are logged in" hint | Lax | Lets the landing page render correctly after an inbound link click |
| Theme, locale, consent | Lax | Low value, and the default already matches |
| Embedded widget state | None | The page runs in someone else's frame; requires Secure |
| Federated login / hosted checkout | None | The flow crosses sites by design; scope it to that one cookie |
Strict on the session cookie makes inbound links look logged out, do not weaken it to Lax. Add a second, non-sensitive Lax cookie that carries nothing but a boolean. The landing page reads it, renders the logged-in shell, and the real session cookie arrives on the next same-site request.SameSite and Third-Party Cookie Restrictions
SameSite=None asks the browser to send the cookie cross-site. It does not guarantee the browser will agree. Third-party cookie policy is a separate, stricter layer that sits on top:
| Browser | Missing attribute | None without Secure | Third-party cookies |
|---|---|---|---|
| Chrome | Treated as Lax | Rejected | Restricted, phasing out |
| Firefox | Treated as Lax | Rejected | Partitioned by top-level site |
| Safari | Treated as Lax | Rejected | Blocked by ITP |
| Edge | Treated as Lax | Rejected | Restricted, phasing out |
The practical consequence: a flow that depends on SameSite=None surviving in an iframe is already broken in Safari and increasingly fragile elsewhere. If you own both ends, move the cookie onto the top-level site — a subdomain of the page the user is actually looking at — instead of relying on third-party delivery.
Debugging a Missing Cookie
SameSite failures are quiet by design, so work top-down rather than guessing. In each case the question is which of the three layers dropped it:
- Was it ever set? DevTools → Network → the response → Cookies tab. A rejected cookie is flagged there with the reason, including
SameSiteNoneInsecure. - Is it in the jar? Application → Cookies, filtered to the setting domain. If it is absent, the browser refused it; if it is present, the problem is on the send side.
- Is the request actually cross-site? Compare the registrable domain of the initiating page with the request target — and check the scheme, because HTTP to HTTPS counts as cross-site.
- Is it a third-party restriction? Retry with third-party cookies allowed for that site. If it works, the SameSite value is correct and the blocker is browser policy.
- Confirm the header from outside the browser. A cookie audit shows what the server sends before any client-side script gets involved.
Common Mistakes
- Setting
SameSite=Noneon every cookie to fix one broken embed, which re-exposes the session cookie to cross-site requests. - Adding
NonewithoutSecureand reading the resulting silent drop as a server bug. - Choosing
Strictfor the session cookie, seeing the logged-out landing page, and reverting the whole change instead of adding aLaxcompanion. - Treating
Laxas complete CSRF protection and removing tokens from state-changing endpoints. - Assuming subdomains are cross-site. They are not —
Domain, not SameSite, is the attribute that controls subdomain scope. - Setting the value on your own cookies and ignoring the tag manager, chat widget and video embed that also write cookies on your domain.
Frequently Asked Questions
1. What happens if I never set SameSite at all?
Every current browser applies SameSite=Lax for you. Your cookies are not suddenly unsafe, but the behaviour is a browser default rather than your decision — it is invisible in the header, it is not obvious to whoever audits the site next, and it changes if the default changes. Write the value out even when it matches the default.
2. Why does Strict log my users out when they click a link from email?
That is exactly what Strict is defined to do. The click is a cross-site navigation, so no cookie is attached, and your server sees an anonymous request. The user is still logged in — the second request, now same-site, carries the cookie. The usual fix is a Strict session cookie plus a small Lax companion cookie that only says "a session exists here", so the landing page can render as logged-in.
3. Is SameSite=Lax enough to stop CSRF?
Lax blocks the classic cross-site POST, which removes most opportunistic CSRF. It is not a complete defence: it still allows cross-site top-level GET navigations, so any state-changing endpoint reachable by GET remains exposed, and it does nothing on browsers that ignore the attribute. Keep CSRF tokens on state-changing routes and treat SameSite as a second layer.
4. My SameSite=None cookie is being dropped. What is wrong?
Almost certainly a missing Secure attribute — None without Secure is rejected outright by every current browser, with nothing logged on your side. If Secure is present and the cookie still disappears, you are hitting third-party cookie restrictions instead: Safari blocks them via ITP, Firefox partitions them, and Chrome restricts them.
5. Does SameSite apply to subdomains?
No. SameSite works on the registrable domain (the "site"), not the host, so app.example.com and www.example.com are the same site and requests between them are never cross-site. That is a different boundary from the Domain attribute, which controls which hosts receive the cookie at all. Use Domain to control scope; use SameSite to control cross-site behaviour.
6. How do I see which SameSite value my site is actually sending?
Run the domain through the Cookie Security Checker — it reads every Set-Cookie header the page returns and reports the SameSite value per cookie, including the ones set by third-party scripts. In the browser, DevTools → Application → Cookies shows the same column, and the Network panel flags cookies that were dropped.