There is no Session flag and no Persistent flag. A cookie with neither Max-Age nor Expires is a session cookie; add either one and it becomes persistent. That is the whole distinction — and it changes where the value is stored, how long a stolen copy is useful, and whether the cookie needs consent.
The Difference in One Header
These two cookies are identical in every way that matters to your application. The second one will still be on the user's disk next month:
# Session cookie — no lifetime attribute at all
Set-Cookie: session=abc123; Path=/; Secure; HttpOnly; SameSite=Strict
# Persistent cookie — 30 days
Set-Cookie: remember=xyz789; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=2592000| Session cookie | Persistent cookie | |
|---|---|---|
| Lifetime attribute | None | Max-Age or Expires |
| Stored | In memory, per browser profile | On disk, in the cookie database |
| Survives a browser restart | Usually not — but see below | Yes, until it expires |
| Survives a tab close | Yes — it is per profile, not per tab | Yes |
| Readable from disk by malware | Harder | Yes |
| Typical use | Sign-in, CSRF token, cart, flash messages | Remember-me, language, theme, analytics |
| Needs consent | Usually not, when strictly necessary | Usually yes, unless strictly necessary |
Max-Age and Expires
Two attributes make a cookie persistent, and they express the same idea differently:
Max-Ageis a duration in seconds, counted from when the browser received the cookie.Max-Age=3600means one hour from now.Expiresis an absolute HTTP date:Expires=Wed, 21 Oct 2026 07:28:00 GMT.
Prefer Max-Age. Where both are present it wins in every current browser, and being relative it is immune to a client whose clock is wrong — a skewed clock can make an Expires cookie either expire immediately or long outlive its intended window. The only reason to send Expires is compatibility with clients old enough not to understand Max-Age, and setting both is how you get that.
// Express: maxAge is milliseconds, and it emits Max-Age plus Expires
res.cookie('remember', token, {
path: '/',
secure: true,
httpOnly: true,
sameSite: 'lax',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
});
// Omit maxAge/expires entirely and you get a session cookie
res.cookie('__Host-session', sid, {
path: '/', secure: true, httpOnly: true, sameSite: 'strict',
});Session Cookies Are Not a Timer
The tempting mental model is "the session cookie expires when the browser closes, so my session expires then too". Neither half of that is dependable.
- Session restore keeps them alive. Chrome, Firefox and Edge all reopen the previous windows on request, and session cookies come back with them. On Chrome the "Continue where you left off" setting makes this the default behaviour, so the cookie can survive indefinitely across restarts.
- Mobile browsers rarely close. An app that is swiped away is usually suspended, not terminated, and the cookie jar persists.
- The browser closing tells your server nothing. There is no signal. The session record on your side is still open, still valid, and still usable by anyone holding the cookie value.
Choosing a Lifetime
The security question is not "how long is convenient" but "how long does a stolen copy stay useful". Work from the value of the cookie:
| Cookie | Lifetime | Reasoning |
|---|---|---|
| Authenticated session | Session, or a few hours | Highest value; back it with a server-side idle timeout |
| CSRF token | Session | Meaningless once the session ends |
| Remember-me token | 14–30 days | Opt-in only, single-use, and revocable server-side |
| Cart before checkout | 7–30 days | Convenience; holds no credential |
| Language, theme | 6–12 months | Low value, and re-asking is worse than the risk |
| Consent record | 6–12 months | Long enough to honour, short enough to re-ask |
| Analytics identifier | As short as the analysis allows | A long lifetime makes it a persistent identifier |
A remember-me cookie deserves special care, because it is the one place a persistent cookie genuinely carries a credential. Make it a separate token from the session cookie, store a hash of it server-side, rotate it on every use, and let the user revoke it from an active devices screen. Never make it a copy of the session identifier with a longer lifetime.
Deleting a Cookie
There is no delete command. You overwrite the cookie with an expiry in the past, and the browser drops it — but only if every scoping attribute matches the original exactly:
# Same Path, same Domain, same Secure — only then does it replace the original
Set-Cookie: remember=; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=0A mismatch is the usual reason a cookie appears to survive logout. If the original was set with Path=/app and you clear it with Path=/, the browser treats them as two different cookies: it stores a new empty one and leaves the real cookie exactly where it was. The same happens with a Domain that was present on one and absent on the other.
And deleting the cookie is only half of a logout. Invalidate the session server-side at the same time — otherwise anyone who captured the value before logout can keep using it.
Lifetime and Consent
Privacy regimes do not care about the session-versus-persistent distinction directly; they care about purpose. But lifetime is what usually decides which side of the line a cookie falls on:
- A short-lived cookie needed for the service the user asked for — sign-in, cart, CSRF — is normally strictly necessary and exempt from consent.
- A long-lived cookie that can follow the same user across visits looks like a persistent identifier, which needs consent and belongs in a cookie notice with its actual duration listed.
- The stated duration has to match the header. A notice that says "13 months" while the cookie carries
Max-Age=63072000is a two-year cookie, and the header is what an auditor will read.
Common Mistakes
- Treating browser close as session expiry, and shipping no server-side timeout at all.
- Making the remember-me cookie a long-lived copy of the session identifier, so one stolen cookie is a permanent login.
- Clearing a cookie with different
PathorDomainattributes than it was set with, and concluding that logout "sometimes does not work". - Using
Expiresalone and inheriting every client clock problem. - Setting a year-long lifetime by default because it was convenient, then listing a shorter duration in the cookie notice.
- Auditing only your own cookies and ignoring the multi-year ones dropped by an embedded player or tag manager on your domain.
Frequently Asked Questions
1. What actually makes a cookie a session cookie?
The absence of both Max-Age and Expires. There is no Session keyword — a cookie with no lifetime attribute is a session cookie by definition, and adding either attribute converts it to a persistent one. That is the entire distinction as far as the browser is concerned.
2. Do session cookies really disappear when the browser closes?
Not reliably. Chrome, Firefox and Edge all offer session restore, and when a window is reopened that way the session cookies come back with it. Mobile browsers rarely "close" at all. Treat browser lifetime as a convenience, never as your expiry mechanism — the server-side session record is what must actually expire.
3. Which one should I use for a login session?
A session cookie for a plain sign-in, and a persistent one only when the user explicitly asks to stay signed in. Either way the cookie is just a pointer: the authoritative expiry lives server-side, so a cookie that outlives its session record is useless rather than dangerous.
4. If Max-Age and Expires are both set, which wins?
Max-Age wins in every current browser. It is also the safer of the two because it is a relative duration in seconds, so a wrong clock on the client cannot shorten or extend it. Expires takes an absolute HTTP date and exists mainly for very old clients — set both only if you need that compatibility.
5. How do I delete a cookie?
Re-send it with Max-Age=0 (or an Expires date in the past) and exactly the same Path, Domain and Secure attributes as the original. Attribute mismatches are the usual reason a "deleted" cookie survives — the browser treats a different path or domain as a different cookie and leaves the original in place.
6. How do I see the lifetimes my site is actually setting?
Run the page through the Cookie Security Checker — it reports the lifetime of every cookie a page sets, so long-lived and session cookies are visible side by side. In the browser, DevTools → Application → Cookies shows Session in the Expires column for cookies with no lifetime attribute.