What do the three ranges of http.Cookie.MaxAge mean in Go, and how do you delete a cookie?
answer
- three ranges, three different meanings
- zero is not immediate expiry
- negative means delete right now
- the browser keys on domain, path and name
- delete must repeat the original scope
basics
~10 sIn 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 linesfunc 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
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.
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.
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.
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