Response header
< Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=LaxSet-Cookie Header: Attributes, Examples and Browser Rules
Set-Cookie syntax and every attribute: Expires, Max-Age, Domain, Path, Secure, HttpOnly, SameSite, Partitioned, __Host- prefixes, with Express and Django.
- Direction
- Response
- Category
- Cookies
- Spec
- RFC 6265 §4.1
- JS can read it
- No: forbidden response header
On this page
TL;DR:
Set-Cookietells the browser to store a name=value pair and replay it inCookieon matching requests. For a session cookie useHttpOnly; Secure; SameSite=Lax; Path=/, and prefer the__Host-name prefix.
Syntax
HTTP/1.1 200 OK
Set-Cookie: __Host-sid=a1b2c3d4; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400
The browser stores it and later sends:
GET /dashboard HTTP/1.1
Host: example.com
Cookie: __Host-sid=a1b2c3d4
One cookie per header. Send several Set-Cookie lines, never one comma-joined line.
Attributes
| Attribute | Effect |
|---|---|
Max-Age=N | Lifetime in seconds from now. 0 or negative deletes the cookie. Takes precedence over Expires |
Expires=<HTTP-date> | Absolute expiry in GMT. Dates in the past delete the cookie |
Domain=example.com | Also sends the cookie to all subdomains. Omit it to restrict to the exact host that set it |
Path=/ | URL path prefix the cookie applies to. It is not a security boundary |
Secure | Only sent over HTTPS (browsers also treat localhost as secure for this purpose) |
HttpOnly | Hidden from document.cookie, still sent on requests. Limits damage from XSS, does not prevent it |
SameSite=Strict|Lax|None | Controls sending on cross-site requests. None requires Secure |
Partitioned | Opts into CHIPS: the cookie is keyed to the top-level site that embedded it |
Expiry. A cookie with neither Max-Age nor Expires is a session cookie, but “deleted when the browser closes” is unreliable: Chrome’s “Continue where you left off” and similar restore features keep session cookies alive. Use an explicit short Max-Age for anything with security meaning. RFC 6265bis limits lifetime to 400 days and Chrome clamps longer values.
Pick expiry dates well in the future when you need an absolute date, for example:
Set-Cookie: theme=dark; Path=/; Expires=Wed, 01 Dec 2027 12:00:00 GMT; SameSite=Lax
A copy-pasted Expires from an old tutorial is already in the past, and the cookie is deleted the moment it is set.
SameSite and “site”
“Site” means the registrable domain (eTLD+1): app.example.com and api.example.com are same-site, even though they are different origins. example.com and example.co.uk are not. A request is cross-site when the top-level site differs.
When SameSite is absent, Chromium-based browsers treat the cookie as Lax, with a short exception that allows it on top-level cross-site POSTs for the first two minutes. Firefox and Safari do not apply that Lax default, though Safari blocks third-party cookies outright and Firefox partitions them. Always write the attribute explicitly. SameSite=None; Secure is required for cookies in third-party contexts (embedded widgets, cross-site iframes), and many browsers now block or partition those anyway. Details in SameSite.
Prefixes and Partitioned
Name prefixes are enforced by the browser:
Set-Cookie: __Host-sid=abc; Path=/; Secure; HttpOnly; SameSite=Lax
Set-Cookie: __Secure-pref=1; Domain=example.com; Secure
__Secure- requires Secure and an HTTPS origin. __Host- additionally requires Path=/ and no Domain, so a compromised or unrelated subdomain cannot overwrite your session cookie. A cookie violating the prefix rules is silently rejected.
Partitioned (CHIPS) gives embedded third-party widgets cookie state that is separate per top-level site:
Set-Cookie: __Host-widget=xyz; Path=/; Secure; SameSite=None; Partitioned
Limits that bite
- About 4096 bytes per cookie (name plus value plus attributes); browsers drop larger ones silently. Practical per-domain count limits are around 180 in Chrome and Firefox.
- All matching cookies are sent on every request, including static assets. Large cookies produce
431 Request Header Fields Too Largeor nginx400 Request Header Or Cookie Too Large. - The
Secureflag cannot be set from an HTTP page, and an HTTP response cannot overwrite aSecurecookie in current browsers.
By stack
Express’s maxAge is milliseconds, Django’s max_age and Flask’s max_age are seconds, and Next.js cookies().set takes seconds. Mixing these up is the most common reason a cookie lasts 1000 times too long or too short.
from flask import make_response
@app.route('/login')
def login():
response = make_response('Logged in')
response.set_cookie(
'sessionId', 'abc123',
max_age=86400, secure=True, httponly=True, samesite='Lax'
)
return response
nginx can add cookies and pass them through, and proxy_cookie_flags rewrites flags on upstream cookies:
proxy_cookie_flags ~ secure httponly samesite=lax;
Debug a cookie that does not stick
curl -si https://example.com/login -d 'user=a&pass=b' | grep -i '^set-cookie'
curl -s -c jar.txt -b jar.txt https://example.com/login -d 'user=a&pass=b'
cat jar.txt
In Chrome DevTools, Application, then Cookies shows stored cookies; the Network panel marks rejected ones with a warning icon on the Set-Cookie header and a reason such as “This attempt to set a cookie via a Set-Cookie header was blocked because it had the Secure attribute but was not received over a secure connection.” Typical causes: Secure over HTTP, SameSite=None without Secure, Domain not matching the host, a __Host- rule violation, or a size over 4096 bytes. See Cookie not sent.
Related
- Cookie, Cache-Control, Vary
- Expires, Max-Age, Secure, HttpOnly, Domain, Path
- Cookie security guide and the Cookie builder
- Cookies vs localStorage vs sessionStorage: where to keep state and auth tokens in the browser.
In practice
Express.js
// Session cookie (no maxAge: removed when the browser session ends)
res.cookie('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/'
})
// Persistent cookie (30 days). Express maxAge is in MILLISECONDS
res.cookie('rememberMe', userId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000
})
// Clear: name, path and domain must match the original
res.clearCookie('sessionId', { path: '/' })Next.js App Router
import { cookies } from 'next/headers'
export async function POST() {
const cookieStore = await cookies()
cookieStore.set('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 // seconds
})
return new Response(null, { status: 204 })
}Django
from django.http import JsonResponse
def login_view(request):
response = JsonResponse({'status': 'ok'})
response.set_cookie(
'rememberMe',
value=user_id,
max_age=30 * 24 * 60 * 60, # seconds
httponly=True,
secure=True,
samesite='Strict',
)
return responseFrequently asked questions
What is Set-Cookie?
Set-Cookie is a response header that asks the browser to store a cookie and send it back in a Cookie header on later matching requests. Each header carries one cookie plus optional attributes such as Max-Age, Path, Secure, HttpOnly and SameSite.
Which attributes should a session cookie have?
HttpOnly so JavaScript cannot read it, Secure so it only travels over HTTPS, SameSite=Lax (or Strict) to limit cross-site sending, and Path=/. Adding the __Host- name prefix additionally requires Secure, Path=/ and no Domain, which prevents sibling subdomains from overwriting it.
Does Max-Age or Expires win when both are set?
Max-Age wins. RFC 6265 section 5.3 says a user agent that sees both uses Max-Age. Old Internet Explorer ignored Max-Age, which is why some frameworks still emit both.
How long can a cookie last?
RFC 6265bis caps the lifetime at 400 days, and Chrome enforces that cap: a longer Max-Age or Expires is clamped. Other browsers may apply their own limits, for example Safari restricts the lifetime of cookies set from JavaScript.
Can I set multiple cookies in one response?
Yes, send one Set-Cookie header per cookie. Set-Cookie is the exception to the rule that repeated fields can be comma-joined (RFC 9110 section 5.3), because the Expires date contains a comma. Proxies and frameworks must keep them as separate lines.
How do I delete a cookie?
Send Set-Cookie with the same name, Domain and Path and either Max-Age=0 or an Expires date in the past. If any of name, Domain or Path differ, the browser treats it as a different cookie and the original stays.
Sources
Related
Clear-Site-Data Header: Logout and Wipe Browser Data
Clear-Site-Data tells the browser to delete cookies, storage and cache on logout. Directives, quoting rules, HTTPS-only limits, browser gaps, Express and nginx.
Partitioned Cookies (CHIPS): The Partitioned Attribute
The Partitioned cookie attribute (CHIPS) gives third-party embeds a separate cookie jar per top-level site. Syntax, Secure requirement, and browser support.
Cookie Prefixes: __Host- and __Secure- Rules
Cookie prefixes make the browser enforce attributes. The exact __Host- and __Secure- rules from RFC 6265bis, how to set them, and why a cookie vanishes.
Authentication-Info Header
Learn how Authentication-Info provides additional authentication data in responses to successful requests. Covers digest authentication and session info.