What Is a Cookie on a Website? How Browser Cookies Actually Work
HTTP has no memory. Every request a browser sends to a server is treated as a brand new conversation, with no built-in way for the server to know it already talked to you five seconds ago. That’s fine for reading a static article. It falls apart the moment you log in, because a login only means something if the next request can prove it came from the same person.
Cookies are the fix. A cookie is a small piece of text that a website asks your browser to store, and that your browser then sends back with every later request to that same site. The server gives you a name tag; you show it each time you walk through the door. Lou Montulli, working at Netscape in 1994, came up with the idea to build shopping carts that didn’t forget what you’d added, and the mechanism he designed is still, in its essentials, what browsers use today.
How a Cookie Travels Between Browser and Server
There are only two HTTP headers involved. The server sets a cookie by including Set-Cookie in a response:
HTTP/1.1 200 OK
Set-Cookie: sessionId=abc123; Max-Age=604800; Path=/; HttpOnly; Secure; SameSite=Lax
The browser stores it. From then on, every request to a matching site carries the cookie back in a Cookie header, with no extra code on your part:
GET /dashboard HTTP/1.1
Host: example.com
Cookie: sessionId=abc123
That’s the entire mechanism. The server reads the value, looks up who abc123 belongs to, and responds accordingly. A cookie is a name and a value, plus a few attributes that control where and for how long it’s sent. Browsers limit a single cookie to roughly 4 KB, which tells you something about intent: cookies are for small identifiers, not for storing data.
What a Login Actually Looks Like
The most common use is session management. You submit a username and password. The server checks them, creates a session record on its side, and sends back a cookie holding a random session ID. Your browser stores that ID and attaches it to every request after that. When the server sees sessionId=abc123, it finds the matching session and knows you’re logged in as you.
Notice what’s in the cookie and what isn’t. The cookie holds an opaque ID, not your name, not your email, and certainly not your password. All the real information lives on the server. This is the model to follow in your own projects, and it’s why a stolen session ID is a serious problem: whoever holds it can act as you until the session expires or the server invalidates it.
It’s also why 401 Unauthorized responses so often trace back to cookies. If the cookie expired, never got sent, or was blocked by a cross-site rule, the server sees an anonymous request and refuses it. Our HTTP status codes guide covers the 401 versus 403 distinction in more detail.
Session Cookies and Persistent Cookies
A cookie with no expiry is a session cookie, and the browser discards it when the session ends. In practice that usually means when you close the browser, though some browsers restore sessions on restart and keep these cookies alive longer than you’d expect. A cookie with an Expires date or a Max-Age value is persistent, and it stays on disk until that time passes.
Persistent cookies power “remember me” checkboxes and saved preferences like dark mode. Browsers also cap how long they’ll honor one. Chrome, for example, limits cookie lifetimes to 400 days, regardless of what the server asks for.
First-Party and Third-Party Cookies
The distinction depends on whose domain the cookie belongs to compared with the page you’re on. A first-party cookie comes from the site in your address bar. A third-party cookie comes from a different domain, usually because the page embeds something from elsewhere, like an ad, a video player, or a social media widget.
Third-party cookies are what made cross-site tracking possible. An ad network embedded on a thousand different sites could recognize the same browser on all of them. Safari, Firefox, and Brave block third-party cookies by default. Chrome has been the holdout: after years of announcing and delaying a phase-out, Google said in April 2025 that it would keep third-party cookies available by default and leave the choice in Chrome’s settings. So for most Chrome users they still work, and you shouldn’t assume they’re gone. You also shouldn’t build anything important on them, since a large share of your visitors are on browsers that already block them.
The Attributes That Matter
A bare cookie works, but a cookie without attributes is a cookie with default behavior, and the defaults are rarely what you want. These are the ones worth knowing.
| Attribute | What It Does |
|---|---|
| Max-Age / Expires | Sets how long the cookie lives. Max-Age is in seconds and wins if both are present. |
| Path | Limits the cookie to URLs under a given path. Path=/ means the whole site. |
| Domain | Controls which hosts receive it. Omit it and the cookie goes only to the exact host that set it; set it and subdomains get it too. |
| Secure | Only send the cookie over HTTPS. |
| HttpOnly | Hide the cookie from JavaScript. The browser still sends it with requests. |
| SameSite | Controls whether the cookie is sent on cross-site requests. Values are Strict, Lax, and None. |
Why HttpOnly, Secure, and SameSite Are Not Optional
HttpOnly protects against a specific attack. If an attacker manages to inject a script into your page, a cross-site scripting bug, that script can read document.cookie and send your session ID to their own server. With HttpOnly set, the script can’t see the cookie at all. It won’t stop the injection, but it stops the most damaging thing the injection could do.
Secure keeps the cookie off unencrypted connections, where anyone on the same network could read it. SameSite is the defense against cross-site request forgery, where a malicious page tricks your browser into sending a request to a site you’re logged into. Strict never sends the cookie on cross-site requests, Lax allows it only for top-level navigations like clicking a link, and None sends it everywhere but requires Secure. Chrome and other Chromium browsers treat a cookie with no SameSite attribute as Lax, but other browsers differ, so set it explicitly rather than relying on a default.
Setting and Reading Cookies in Code
On the server, most frameworks give you a helper. In Express, setting a session cookie with all the flags looks like this:
res.cookie("sessionId", sessionId, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days, in milliseconds
});
Note that Express takes maxAge in milliseconds, while the raw Max-Age header uses seconds. It’s an easy conversion bug to introduce.
In the browser, you can work with cookies through document.cookie, which is awkward by design. Writing one looks like an assignment but adds a cookie rather than replacing the whole string:
document.cookie = "theme=dark; max-age=31536000; path=/; samesite=lax";
console.log(document.cookie); // "theme=dark; other=value"
Reading returns every readable cookie jammed into one string, which you have to split yourself. And anything marked HttpOnly won’t appear in it. To delete a cookie, set it again with the same name and path and a max-age=0.
Cookies, localStorage, and sessionStorage
Developers often reach for localStorage when they want to remember something in the browser, and for many cases that’s the right call. The difference is that cookies are sent to the server automatically, and web storage never is.
| Feature | Cookies | localStorage | sessionStorage |
|---|---|---|---|
| Sent to server automatically | Yes, with every matching request | No | No |
| Capacity | About 4 KB each | Around 5 MB per origin | Around 5 MB per origin |
| Lifetime | Session or until expiry | Until cleared | Until the tab closes |
| Can be hidden from JavaScript | Yes, with HttpOnly | No | No |
| Best use | Authentication and server-side state | Client-only preferences and cached data | Temporary per-tab state |
That last row of the table is the real guidance. Anything the server needs to see on every request, like a session ID, belongs in a cookie. Anything only your frontend cares about, like a theme choice or a cached list, can live in localStorage. Putting an authentication token in localStorage is a common pattern, and it works, but it leaves the token readable by any script on the page, which is exactly the exposure HttpOnly was built to remove.
The Cost Nobody Mentions
Because cookies ride along on every request to their domain, they add weight everywhere. A few large cookies on your main domain get sent with every image, stylesheet, and script request too, if those are served from the same host. That’s wasted bandwidth on every page load. It’s one reason sites often serve static assets from a separate cookieless domain or CDN hostname, and one more reason to keep cookie values short.
Privacy, Consent, and the Banner You’ve Clicked Past
Those consent banners exist because of laws, mainly the EU’s GDPR and ePrivacy rules, along with similar regulations elsewhere. The general principle is that you need a visitor’s consent before setting cookies that aren’t strictly necessary, which in practice means analytics and advertising cookies. A session cookie that keeps someone logged in, or a cart cookie that remembers what they’re buying, is generally treated as strictly necessary and exempt.
If you run a site with visitors in regulated regions, this affects what you can set and when, and the details depend on your jurisdiction and your setup. This post isn’t legal advice, and for anything beyond a personal blog it’s worth reading the guidance from your relevant regulator or asking someone qualified.
Looking at Cookies Yourself
The fastest way to make this concrete is to open DevTools on any site you’re logged into. In Chrome, go to the Application tab and expand Cookies in the sidebar. You’ll see each cookie’s name, value, domain, expiry, and which flags are set. The Network tab shows the same story from the request side: click any request and check the Request Headers for Cookie and the Response Headers for Set-Cookie.
A good exercise is to log into a site, find the session cookie, and delete it by hand. Refresh the page and you’re logged out, which makes the connection between that one small value and your entire logged-in state immediately obvious. When something about authentication breaks in your own app, this is also where to start: is the cookie being set, does it have the attributes you expect, and is it being sent on the failing request. Most login bugs turn out to be one of those three, and DevTools answers all of them in under a minute.
Discover more from Developer Hint
Subscribe to get the latest posts sent to your email.
