skip to content

When an HTTP 'Set-Cookie' header carries both Max-Age and Expires, which one wins, and how do you correctly delete a cookie you set earlier?

level: middleimportance: must knowfreq 48%

answer

  1. both present → Max-Age wins, Expires ignored
  2. Max-Age relative seconds; Expires absolute GMT date
  3. delete = Max-Age=0 or Expires in the past
  4. identity = name + Domain + Path; must match to delete
  5. clearing the cookie is not invalidating the session

basics

~20 s

Max-Age wins when both are present. To delete, resend Set-Cookie with the same name and the same Domain and Path, plus Max-Age=0 (or an Expires date in the past). If the scoping attributes do not match, you create a second cookie instead of removing the first.

solid answer

~50 s

Per RFC 6265, if both are present **`Max-Age` takes precedence over `Expires`** — clients that support `Max-Age` ignore `Expires` entirely. Sending both is a legacy pattern for very old user agents; today it is just redundancy, and if the two disagree the `Expires` value is dead text.\n\n`Max-Age` is a relative count of seconds from receipt, so the browser converts it once against its own clock; `Expires` is an absolute `HTTP-date` in GMT, compared repeatedly against a possibly skewed client clock. Prefer `Max-Age`.\n\nDeletion is expiry: resend `Set-Cookie` for the **same name with the same `Domain` and `Path`**, with `Max-Age=0` or `Expires` set to a past date such as `Thu, 01 Jan 1970 00:00:00 GMT`. There is no delete verb. The classic bug is omitting `Path` on deletion when the cookie was set with `Path=/app`: name plus domain plus path is the cookie's identity, so a mismatched delete simply creates and expires a *different* cookie while the original keeps being sent.

code

http · 1 line
http
HTTP/1.1 200 OK\nSet-Cookie: sid=abc; Domain=example.com; Path=/app; Max-Age=86400; Secure; HttpOnly\n\nHTTP/1.1 204 No Content\nSet-Cookie: sid=; Domain=example.com; Path=/app; Max-Age=0; Secure; HttpOnly

go deeper

for a junior

Know that Max-Age beats Expires and that you delete a cookie by setting Max-Age=0 with the same name.

for a middle

Explain relative-versus-absolute and clock skew, and be precise that the delete must reproduce Domain and Path because identity is name plus domain plus path.

for a senior

Diagnose the duplicate-cookie and stale-logout failure modes, and prescribe a single centralised cookie-attribute helper used by both set and clear paths.

for a principal

Set the policy: server-side session invalidation as the authority, cookie clearing as best-effort cleanup, and a standard for cookie naming and scoping across services.

## Two ways to express a lifetime\n\n```\nSet-Cookie: a=1; Max-Age=86400\nSet-Cookie: b=2; Expires=Sat, 13 Sep 2026 10:00:00 GMT\n```\n\n`Max-Age` is **relative**: an integer number of seconds from the moment the response is received. `Expires` is **absolute**: an `HTTP-date`, always in GMT, in the `Day, DD Mon YYYY HH:MM:SS GMT` form. Note the comma inside that date — it is the reason multiple `Set-Cookie` headers can never be comma-folded into one line.\n\n## Precedence\n\nIf both attributes appear on the same cookie, **`Max-Age` wins**. RFC 6265 states that a user agent supporting `Max-Age` must ignore `Expires`. `Expires` came from the original Netscape cookie draft; `Max-Age` arrived with RFC 2109 and had patchy support in browsers of that era, which is why the belt-and-braces habit of sending both persists in old frameworks. Every current browser honours `Max-Age`, so sending both only creates a chance for the two to drift apart in your code and confuse the next reader. Pick `Max-Age`.\n\n## Why Max-Age is the better attribute\n\nA client with a badly wrong clock will mis-evaluate `Expires`: set an hour ahead, it may expire the cookie instantly; set a year behind, it may keep it far too long. `Max-Age` is converted to an absolute instant *at receipt* using the same clock, so absolute skew cancels out and only drift during the cookie's life matters. It also avoids date-formatting bugs — a malformed `Expires` value is ignored, silently turning your persistent cookie into a session cookie.\n\n## Deleting a cookie\n\nThere is no "delete cookie" mechanism in HTTP. You expire it:\n\n```\nSet-Cookie: sid=; Path=/; Max-Age=0\nSet-Cookie: sid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT\n```\n\n`Max-Age=0` (and, per the grammar, any non-positive value) means "expire immediately". The value you send is irrelevant — an empty string is conventional.\n\n### Matching the scope is the whole game\n\nA cookie's identity is the triple **(name, Domain, Path)**. The deletion `Set-Cookie` must reproduce the same `Domain` and `Path` the cookie was created with, or the browser treats it as a different cookie: it creates that different cookie, immediately expires it, and your original survives untouched and keeps being sent. Symptoms are exactly the classic bug report "logout doesn't log me out", or "I get logged out on /app but not on /".\n\nCommon mismatches:\n\n- Cookie set with `Path=/app`, deleted with the default path. The default path is derived from the request URI's directory, so the *same* delete code produces different paths on different endpoints.\n- Cookie set with `Domain=.example.com` (host and all subdomains), deleted without `Domain` (host-only). Those are two distinct cookies and both can exist simultaneously with the same name.\n- Cookie set on `Secure`, deleted over plain HTTP by a service that terminates TLS oddly. `Secure` is not part of identity, but a browser will refuse a non-secure overwrite of a `Secure` cookie from an insecure origin.\n\nThe defence is to centralise cookie writing in one place — one function that owns name, `Domain`, `Path`, `Secure`, `HttpOnly`, `SameSite` — and have both the set and the clear paths call it, so the attributes cannot drift. If you already have duplicate same-named cookies in the wild, you must emit a delete for **each** scope combination; you cannot see the attributes of incoming cookies, because the `Cookie` request header carries only names and values.\n\n## Expiry is client-side and cooperative\n\nExpiring a cookie only stops a cooperating browser from sending it. It does not invalidate the credential. Anyone who has copied the value can keep replaying it, and a client with a manipulated clock may keep the cookie longer than you asked. Logout must therefore invalidate the server-side session or token as well as clearing the cookie; treating `Max-Age=0` as a security action is a real vulnerability, not a style nit.

  • A user reports that logout does not clear their session cookie. What do you check first?
    Compare the Domain and Path on the Set-Cookie that created the cookie with the one that clears it — they must match exactly, since name plus domain plus path is the cookie's identity. A default path derived from the logout endpoint's directory is the usual culprit. Then confirm the server also invalidated the session record, because clearing the cookie alone leaves a copied value replayable.
  • Why can two cookies with the same name be sent in one request?
    Because they differ in Domain or Path — for example one host-only cookie and one set on .example.com, or one on Path=/ and one on Path=/app. Both match the request, so both appear in the Cookie header, and the server sees only names and values with no way to tell which is which. It is a strong argument for a single, centrally-owned cookie scope.

saying these in an interview costs you the question

  • Saying Expires wins over Max-Age, or that the later attribute in the header wins
  • Deleting a cookie without reproducing its Domain and Path and assuming it worked
  • Believing there is a dedicated delete-cookie mechanism in HTTP
  • Treating Max-Age=0 on logout as sufficient security, with no server-side invalidation
  • Sending both Max-Age and Expires with different values and expecting some merge

context