Here is a small puzzle worth thinking about. You log in to a website, click around for ten minutes, and the site keeps treating you as the same person the whole time. Yet HTTP, the protocol your browser uses to talk to that site, has no built-in memory. Each request the browser sends arrives as if the server had never seen you before. So how does the site know it is still you on the next click?
The answer, most of the time, is cookies. A cookie is a tiny piece of text that a server hands to your browser and asks it to keep. From then on, your browser quietly attaches that text to every request it sends back to that same site. That little round-trip is what gives a “stateless” protocol the appearance of memory. In this article we will look at exactly how that works at the HTTP level, the headers involved, and the attributes that control how a cookie behaves.
Why HTTP needs cookies in the first place
HTTP is described as stateless. That word sounds technical, but the idea is simple: the server does not automatically remember anything between one request and the next. Every request is independent. Think of it like talking to a clerk who has perfect amnesia. You tell them your name, they help you, and the moment you walk away they forget you completely. Walk back two seconds later and you have to introduce yourself again.
That design is actually a strength. It keeps servers simple and lets huge sites spread requests across many machines without each one needing to track who you are. But it leaves a real problem: shopping carts, logins, language choices, and “stay signed in” all need the server to recognise you across multiple requests.
Cookies solve this with a clever, lightweight trick. Instead of the server keeping a memory of you, it gives you a small note to carry, and your browser shows that note every time it comes back. The clerk with amnesia hands you a numbered ticket; you flash the ticket on your next visit, and they look up everything tied to that number.
The two headers that make cookies work
Cookies live entirely inside HTTP headers. There are only two you need to know, and they form a pair.
The server sets a cookie using a Set-Cookie header in its response. The browser sends cookies back using a Cookie header in its request. That is the whole mechanism.
| Header | Direction | Who sends it | Purpose |
|---|---|---|---|
Set-Cookie |
Response | Server → browser | “Please store this value for me.” |
Cookie |
Request | Browser → server | “Here is the value you asked me to keep.” |
Let’s see it in motion. Imagine you visit acy-partner.com for the first time. The server’s response includes a header telling your browser to remember a value:
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: session_id=abc123
Your browser reads that Set-Cookie line, stores the cookie (the name session_id with the value abc123), and from that point on, every request your browser makes to acy-partner.com automatically carries it back:
GET /account HTTP/1.1
Host: acy-partner.com
Cookie: session_id=abc123
Notice that you never wrote any of this. The browser handles it all. The page’s JavaScript does not have to attach anything, and you certainly do not type it. Once a cookie is stored, resending it is automatic.
┌──────────┐ ┌──────────┐
│ Browser │ ── GET / ──────────────────────▶ │ Server │
│ │ ◀── 200 OK Set-Cookie: id=abc ─ │ │
│ (stores │ │ │
│ the │ ── GET /account ──────────────▶ │ │
│ cookie) │ Cookie: id=abc │ (knows │
│ │ ◀── 200 OK (personalised) ────── │ it's you)│
└──────────┘ └──────────┘
A cookie is just text
A cookie is nothing magical. It is a name=value pair of plain text, usually small (kilobytes at most). The server decides what to put in it, and the browser’s only job is to store it and send it back to the right site.
What goes inside a cookie
The value of a cookie can be almost anything the server wants, as long as it fits the size limit and avoids certain characters. In practice, sites use one of two styles.
The first style stores the actual information in the cookie. For example, a preference like theme=dark or lang=en can sit right there in the cookie value, and the server reads it on each request.
The second style, which is far more common for anything sensitive, stores only an opaque identifier. The cookie holds something meaningless on its own, like session_id=abc123, and the real data lives safely on the server, looked up by that ID. This is the pattern behind logged-in sessions: the cookie is just a claim ticket, and the valuables stay behind the counter.
How a server uses a session cookie to keep you logged in is a topic in its own right, and it belongs to the authentication world rather than the plain HTTP mechanism. Here we are focused on the transport: how the value travels between browser and server, not what the server does with it once it arrives.
Cookie attributes: the fine print
A Set-Cookie header can carry more than just a name and value. After the value, the server can add attributes, separated by semicolons, that control how long the cookie lives and where the browser is allowed to send it. These attributes are where cookies get interesting, and where a lot of security depends.
Set-Cookie: session_id=abc123; Expires=Wed, 30 Sep 2026 10:00:00 GMT; Secure; HttpOnly; SameSite=Lax
Let’s unpack the ones that matter most.
Expires (and Max-Age): how long it lasts
By default, a cookie with no expiry is a session cookie — it lives only until you close the browser, then it disappears. To make a cookie stick around longer, the server adds an expiry date.
The Expires attribute gives an exact date and time after which the browser should throw the cookie away. A close relative, Max-Age, does the same thing but counts in seconds from now (for example, Max-Age=3600 means one hour). When both are present, Max-Age wins.
Set-Cookie: lang=en; Max-Age=31536000
That cookie says “remember the language for a year.” This is how a site can recognise you days or weeks later without making you log in every single visit.
Secure: only over HTTPS
The Secure attribute tells the browser to send the cookie only over encrypted HTTPS connections, never over plain HTTP. This matters because a cookie sent over an unencrypted connection could be read by anyone watching the network. For any cookie that identifies a user, Secure should be considered essential.
HttpOnly: hidden from JavaScript
Normally, JavaScript running on a page can read cookies through document.cookie. The HttpOnly attribute switches that off: a cookie marked HttpOnly is invisible to JavaScript and is sent only inside HTTP headers.
Why would you want that? Because if an attacker manages to inject a malicious script into a page, the first thing it often tries to do is steal session cookies. Marking the session cookie HttpOnly means that even a successful script injection cannot read it. The cookie still works perfectly for the server, it just stays out of reach of page code.
HttpOnly is not a typo
The name is confusing for beginners. HttpOnly does not mean “only over HTTP” (that would be the opposite of secure). It means “only accessible via HTTP headers, not via JavaScript.” Pair it with Secure for sensitive cookies.
SameSite: controlling cross-site sending
The SameSite attribute decides whether a cookie should be sent when the request comes from a different site. Suppose you are logged in to acy-partner.com and you click a link on some other website that points back to it. Should your cookie ride along on that request? SameSite answers that question.
| Value | Behaviour |
|---|---|
Strict |
The cookie is sent only when the request originates from the same site. Maximum protection, but it can log you out when arriving from external links. |
Lax |
The cookie is sent for same-site requests and for top-level navigations (like clicking a link), but not for most cross-site background requests. A sensible default. |
None |
The cookie is sent on all requests, including cross-site ones. Must be combined with Secure. |
SameSite exists largely to reduce a class of attack where a malicious site tricks your browser into making a request to a site you are logged in to, riding on your cookie. By limiting cross-site sending, the browser closes much of that door. Modern browsers treat Lax as the default when no SameSite is specified.
Domain and Path: where a cookie applies
Two more attributes decide the scope of a cookie. Domain controls which hosts the cookie is sent to, and Path controls which URLs within that host.
By default, a cookie belongs to the exact host that set it. If acy-partner.com sets a cookie without a Domain attribute, the browser sends it back only to acy-partner.com, not to blog.acy-partner.com. Setting Domain=acy-partner.com explicitly widens it to cover subdomains too.
The Path attribute narrows things in a different direction. Path=/account means the cookie is attached only to requests whose URL begins with /account. Left unset, it usually defaults to the path of the page that set it, and most sites simply use Path=/ to cover the whole site.
One Set-Cookie per cookie
If a server wants to set several cookies in one response, it sends a separate Set-Cookie header for each one. It does not cram them all into a single line. The browser, however, bundles everything it sends back into one Cookie header, separated by semicolons.
Seeing cookies for yourself
You don’t need any special tools to watch this happen. Open your browser’s developer tools (often by pressing F12), go to the Network tab, and reload a page. Click any request and you can read its request and response headers — including Cookie going out and Set-Cookie coming in. There is usually an Application or Storage tab too, which lists every cookie a site has stored, along with each one’s expiry, Secure flag, HttpOnly flag, and SameSite value. It is the clearest way to turn this article from words into something you can poke at.
Recap
Cookies are the standard answer to a genuine gap in HTTP: the protocol forgets you between requests, and cookies give it a way to remember. The mechanism is small and elegant. The server sends a Set-Cookie header in a response, the browser stores the value, and from then on the browser automatically returns it in a Cookie header on every matching request.
Around that core sit the attributes that make cookies safe and practical: Expires and Max-Age decide how long a cookie lives, Secure keeps it on HTTPS, HttpOnly hides it from JavaScript, SameSite limits cross-site sending, and Domain and Path define its scope. Knowing what each one does turns a vague idea (“the site remembers me”) into a concrete picture of headers travelling back and forth.
From here, a natural next step is to look at how sessions and authentication build on top of this foundation — how that little session_id cookie becomes a secure login. But that is the server’s side of the story. The HTTP part, the part you have just learned, is the same simple handshake underneath all of it.