skip to content

What do the three ranges of http.Cookie.MaxAge mean in Go, and how do you delete a cookie?

level: middleimportance: should knowfreq 60%

answer

  1. three ranges, three different meanings
  2. zero is not immediate expiry
  3. negative means delete right now
  4. the browser keys on domain, path and name
  5. delete must repeat the original scope

basics

~10 s

In http.Cookie, a positive MaxAge writes Max-Age in seconds, zero writes no Max-Age attribute at all (a session cookie), and a negative MaxAge writes Max-Age=0, which tells the browser to delete the cookie immediately.

solid answer

~50 s

`http.Cookie.MaxAge` is a tri-state `int`, and the zero value is the trap: `MaxAge > 0` emits `Max-Age=<n>` in seconds, `MaxAge == 0` emits **no** `Max-Age` attribute, giving a session cookie that lives until the browser closes, and `MaxAge < 0` emits the literal `Max-Age=0`, which is the delete instruction. So deleting is a *set*: you send the same `Name` with an empty `Value`, `MaxAge: -1`, and the **same `Path` and `Domain`** you originally used, because the browser identifies a stored cookie by that triple. A logout handler that sets `Path: "/"` on login but omits it on delete leaves the original cookie in place and the user stays logged in. `Expires` is the older absolute-time mechanism; Go writes it as well if you set it to a valid time, and a past `Expires` is the alternative delete idiom.

code

go · 11 lines
go
func logout(w http.ResponseWriter, r *http.Request) {
	http.SetCookie(w, &http.Cookie{
		Name:     "session",
		Value:    "",
		Path:     "/", // must match the Path used when it was issued
		MaxAge:   -1,  // Go writes: Max-Age=0
		HttpOnly: true,
		Secure:   true,
	})
	w.WriteHeader(http.StatusNoContent)
}

go deeper

for a junior

Remember that deleting a cookie means sending another Set-Cookie, and that MaxAge is a count of seconds. Learn the three ranges as a table rather than guessing from the zero value.

for a middle

Explain what net/http writes for each range, why the zero value produces a session cookie, and why the deletion cookie must repeat the original Path and Domain to overwrite the stored entry.

for a senior

Show how you would confirm a logout actually cleared state: compare the raw Set-Cookie lines from login and logout, and back the cookie with server-side invalidation so a stale copy is useless.

for a principal

Own the decision between short-lived cookies with renewal and long MaxAge values, and be able to justify the operational cost of each when a credential has to be revoked quickly.

## MaxAge is a tri-state int, not a duration The field is declared as a plain `int` on `http.Cookie`, and `net/http` gives its three ranges three different meanings when it formats the header: | Field value | What Go writes | Effect | |---|---|---| | `MaxAge > 0` | `Max-Age=<n>` | the cookie lives `n` seconds from now | | `MaxAge == 0` | *nothing* | no lifetime attribute: a session cookie | | `MaxAge < 0` | `Max-Age=0` | delete the cookie now | Two things trip people up here. First, the value is **seconds**, not a `time.Duration` — writing `MaxAge: int(time.Hour)` sets a lifetime of 3.6 billion seconds, because a `Duration` counts nanoseconds. Write `MaxAge: 3600`, or `int(time.Hour.Seconds())`. Second, the zero value does not mean "expire immediately"; it means "say nothing about lifetime". Since `MaxAge` is zero unless you set it, every cookie you create without thinking about lifetime is a session cookie that the browser drops when it closes. That is a perfectly reasonable default for a login session, but it should be a decision, not an accident. ## Deleting is just another Set-Cookie There is no delete call. HTTP has no way for a server to reach into a browser's cookie store, so the only mechanism is to send a `Set-Cookie` that overwrites the existing entry with one that is already expired: ```go http.SetCookie(w, &http.Cookie{ Name: "session", Value: "", Path: "/", MaxAge: -1, }) ``` Go formats that as `session=; Path=/; Max-Age=0`. ## The part that actually goes wrong: identity A browser does not store cookies by name alone. It stores them by **(domain, path, name)**. A cookie named `session` scoped to `Path=/` and one named `session` scoped to `Path=/admin` are two independent entries, and both can be sent on the same request. So the deletion cookie has to be built with the *same* `Path` and `Domain` fields as the one you are trying to remove. This is where logout handlers quietly fail. Login sets `Path: "/"`; logout is written later, in a different file, and leaves `Path` empty; Go then emits no `Path` attribute; the browser defaults the path to the directory of the URL that produced the response — `/logout`'s directory, `/` — and *may* happen to match, or, if the endpoint is `/api/auth/logout`, will scope the deletion to `/api/auth/` and leave the real cookie untouched. The user stays logged in and nothing anywhere reports an error. The flag fields are different: `Secure`, `HttpOnly` and `SameSite` are not part of the identity triple, so mismatching them does not prevent the overwrite. It is still good practice to send the deletion with the same flags, because a `Secure` cookie can only be overwritten over an appropriate connection. ## Expires, and why both exist `Expires time.Time` is the original mechanism: an absolute timestamp. Go writes it only when the time is valid for a cookie (roughly, a year of 1601 or later), formatted in the fixed HTTP date form. `MaxAge` came later and is relative, which sidesteps clock skew between server and client. Go happily writes both if you set both, and browsers prefer `Max-Age` when both are present. The two delete idioms are therefore `MaxAge: -1` and `Expires: time.Unix(0, 0)` (or any past time); the first is clearer and is what most Go code uses. Setting `Expires` to the zero `time.Time` does not delete anything — Go simply omits the attribute, because the zero time is not a valid cookie expiry. ## Verifying it Because `http.SetCookie` returns nothing, the cheapest verification is to format the struct yourself and look at it. `(*http.Cookie).String()` produces exactly the header value `SetCookie` would add, so a unit test can assert the whole line, and a `httptest.NewRecorder()` plus `rec.Result().Cookies()` lets a handler test assert the parsed cookie the client would see. Comparing that string against what the browser's cookie inspector shows after logout is the fastest way to find a path mismatch.

  • Your logout handler sets MaxAge to -1 but users stay logged in. What do you check first?
    The `Path` and `Domain` on the deletion cookie against the ones used at login. Browsers key cookies on (domain, path, name), so a deletion with a different scope creates and expires a *second* cookie while the original survives. Compare the two raw `Set-Cookie` lines side by side.
  • How do http.Cookie.MaxAge and http.Cookie.Expires differ in what Go emits?
    `MaxAge` is relative seconds and Go writes `Max-Age=<n>`; `Expires` is an absolute `time.Time` written as an HTTP date, and only when the time is valid for a cookie. Go writes both if both are set, and browsers prefer `Max-Age`. Relative lifetimes avoid client clock skew, so prefer `MaxAge`.
  • Does leaving MaxAge at 0 make a cookie that expires immediately?
    No — it makes Go omit the attribute entirely, producing a session cookie that survives until the browser is closed. Immediate deletion needs a negative `MaxAge`, which Go writes as `Max-Age=0`. Confusing the two is the classic mistake, because the zero value reads like "zero seconds".

saying these in an interview costs you the question

  • Thinks MaxAge 0 expires the cookie immediately
  • Assigns a time.Duration to MaxAge instead of seconds
  • Deletes without repeating the original Path and Domain
  • Looks for a DeleteCookie function in net/http
  • Believes the server can remove a cookie without a response header