Cookies
Learn how browser cookies store small values, travel with requests, and use attributes like Expires, Max-Age, SameSite, Secure, and HttpOnly.
- 01Read and write cookies safelyWrite, change, and delete a cookie with
document.cookie, encode its value, and read it back without mixing up the value and its rules. - 02Choose the right rulesSay what
Max-Age,Expires,Path,Domain,SameSite,Secure, andHttpOnlyeach do, and when you need them. - 03Pick the right place for dataDecide whether a piece of data belongs in a cookie, in localStorage, or nowhere in the browser.
Cookies in one minute
A cookie is a small piece of text that the browser saves for one website. It has a name and a value, written like theme=dark.
A few words first. A server is the computer that sends you a website. A request is a message the browser sends to a server, for example to get a page or an image. A header is a short line of extra information on a request or on the server's answer.
Here is what makes cookies special. The browser sends a saved cookie back to its website by itself, on every request that matches the cookie's rules. You don't write any code for that. Sites use this for:
- remembering that you are signed in,
- remembering your language,
- remembering whether you said yes to tracking.
Because cookies travel with requests, keep them small. Browsers allow about 4 KB (about 4,000 characters) per cookie, and the exact limit changes from browser to browser. Cookies are for short values, not for big data.
At an event, staff stamp your hand once. When you come back in, they look at the stamp. A cookie works the same way. The server gives the browser a value once. The browser shows that value again on later requests.
- In real life: Staff stamp your hand at the door
- In JavaScript: The server sends
Set-Cookie: session=abc - In real life: The stamp stays on your hand
- In JavaScript: The browser saves the cookie
- In real life: You show the stamp when you come back in
- In JavaScript: The browser sends
Cookie: session=abcwith matching requests - In real life: Staff see the stamp and let you in
- In JavaScript: The server knows your browser again
Set-Cookie header. 2. The browser stores it and sends it back in a Cookie header on later requests to that site.The table below lists four words you will meet in every section. Each row names one idea, says what it is, and says why it matters.
| Idea | What it is | Why it matters |
|---|---|---|
| Value | The name=value text, like theme=dark. | This is the part JavaScript can read, unless the cookie is HttpOnly. |
| Attributes | Rules written after the value, like Max-Age, Path, SameSite, and Secure. | They decide how long the cookie lasts and when it is sent. You can't read them back with document.cookie. |
| Sent by itself | The browser adds matching cookies to requests without your code. | Good for staying signed in. Bad for big data, which would travel every time. |
| Who can read it | Page JavaScript can read normal cookies. It can't read HttpOnly cookies. | Never put a secret in a cookie that JavaScript can read. |
A cookie's lifetime is one of its rules. This code writes the same cookie two ways: one with an end date, one with a number of seconds.
const expires = new Date("2030-01-01T00:00:00Z").toUTCString();
const byDate = "session=short; Expires=" + expires + "; Path=/";
const bySeconds = "session=short; Max-Age=3600; Path=/";
console.log(byDate.includes("Expires="));
console.log(bySeconds.includes("Max-Age=3600"));Line 2 uses Expires= with a date. Line 3 uses Max-Age=3600, which means 3,600 seconds (one hour) from now. Lines 5 and 6 both print true, because each string contains the rule we checked for. You will learn both rules in the lifetime section.
In the rest of this lesson you will write real test cookies on this site, read them back, try the SameSite rules, and learn when a cookie is the wrong place for your data.
Expires and Max-Age: how long a cookie lasts
LIFETIMEA cookie with no Expires and no Max-Age is a session cookie. The browser may throw it away when you close the browser. To choose the lifetime yourself, add one of these rules. Each row of the table is one rule, with an example and what it does.
| Rule | Example | What it does |
|---|---|---|
Max-Age | Max-Age=3600 | The cookie lasts 3,600 seconds from now. If a cookie has both rules, this one wins. |
Expires | Expires=Wed, 21 Oct 2030 07:28:00 GMT | The cookie lasts until that exact date and time. |
| Delete | Max-Age=0 | Write the same cookie again with zero seconds left. The browser removes it. |
For Expires, the date must be in the format the web uses. date.toUTCString() gives you that format. If dates feel shaky, look back at the Date and time lesson. In most app code, Max-Age is easier, because it is just a number of seconds.
Path says which pages of the site get the cookie, like /app. Domain says which web addresses get it. Two cookies can share a name if their Path or Domain is different. So a cookie for /app is not the same cookie as one for /. To delete one, write the same name, the same Path and Domain, and Max-Age=0.
const appCookie = "theme=dark; Path=/app";
const deleteRoot = "theme=; Max-Age=0; Path=/";
const deleteApp = "theme=; Max-Age=0; Path=/app";
console.log(appCookie.includes("Path=/app"));
console.log(deleteRoot === deleteApp);Line 1 is a cookie made for Path=/app. Lines 2 and 3 are two delete writes. They differ only in the path, but they point at different cookies. Line 6 prints false: the two writes are not the same. Only line 3 removes the /app cookie. In a real app, keep the code that creates a cookie next to the code that deletes it, so both use the same path.
A hand stamp can say "today only" or "main hall only". Cookie rules do the same job. They decide when and where the browser may use the value.
- In real life: Good for today only
- In JavaScript:
ExpiresorMax-Age - In real life: Good only in one hall
- In JavaScript:
PathandDomain - In real life: Checked only at the guarded door
- In JavaScript:
Secure - In real life: Visitors can't see it, only staff
- In JavaScript:
HttpOnly
SameSite: when another site starts the request
SIMULATORTwo pages are on the same site when they share the main web address, like www.example.com and shop.example.com. A request is cross-site when a page on a different site starts it, for example a link or a form on other.com that goes to example.com.
SameSite is a rule that says whether the browser sends the cookie on cross-site requests. It helps stop CSRF (cross-site request forgery). In CSRF, a bad site tries to make your browser send a request to a site where you are signed in. If your sign-in cookie goes along, the request looks like it came from you.
SameSite has three values:
Strict: send the cookie only on same-site requests.Lax: send it on same-site requests, and also when you follow a normal link from another site. A normal link is aGETrequest that opens a whole new page. If a cookie has noSameSiterule, most browsers today treat it likeLax.None: send it on cross-site requests too. Browsers requireSecurewith it. Browsers may still block it in some cases, as you'll see below.
SameSite=Lax, the browser sends the cookie when you follow a normal link, but not with a POST form.const requestCookie = "session=abc123";
const requestType = "cross-site image";
console.log(requestType + ": " + requestCookie);
console.log("Browser policy still decides whether it is sent");This code only prints two lines. It shows the situation: an image from another site, and a cookie that could go with it. Whether the cookie is really sent is up to the browser, not your code.
A third-party cookie is a cookie for a site other than the one in the address bar. For example, a page on news.com shows an image from ads.com, and the request for the image carries an ads.com cookie. Browsers block or limit these more and more, mostly to stop tracking. SameSite=None; Secure is needed to allow them, but it does not promise that every browser will send the cookie.
LaxBlocked. Lax keeps the cookie off POST forms, iframes, images, and fetch calls from another site. This helps stop CSRF.
Secure and HttpOnly: how a cookie travels and who can read it
SAFETYSecure and HttpOnly do two different jobs:
Secure: the browser sends the cookie only over HTTPS, the locked, encrypted kind of web address. Some browsers make an exception forlocalhostwhile you test, so in real apps think ofSecureas "HTTPS only".HttpOnly: page JavaScript cannot read the cookie at all. It is still sent with requests.
A server sets these rules with a Set-Cookie header, one header per cookie. Here are two:
Set-Cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=LaxSet-Cookie: widget_id=public; Path=/; Secure; SameSite=NoneLine 1 is a sign-in cookie, session. It has HttpOnly, so scripts can't read it. Line 2, widget_id, has no HttpOnly, so scripts can read it. It also has SameSite=None, so it can go with cross-site requests.
widget_id. The HttpOnly session cookie is hidden from it.Only a server can set HttpOnly. If you write document.cookie = "session=abc; HttpOnly", you don't get a hidden cookie. Send sign-in tokens from the server with a Set-Cookie header. Never put a secret in a cookie that JavaScript can read.
That is why sign-in cookies are usually set by the server, not by page code. With HttpOnly; Secure; SameSite=Lax, the browser keeps sending the sign-in cookie, and page scripts can't read it.
Where you'll use cookies
SORTERTo decide, ask one question: does the server need this small value with each request?
- If yes, a cookie may fit.
- If only your JavaScript needs it,
localStorageorsessionStorage(the next lesson) is often better. - If the value is a real secret, keep it out of the browser.
const themeCookie = "theme=" + encodeURIComponent("dark")
+ "; Max-Age=31536000; Path=/; SameSite=Lax";
console.log(themeCookie);
// In a browser page: document.cookie = themeCookie;A theme (light or dark colors) is a good cookie when the server must pick the colors for the very first page, before your JavaScript runs. The rules in this cookie:
Max-Age=31536000keeps it for one year, written in seconds.Path=/sends it to every page of the site.SameSite=Laxkeeps it off most cross-site requests.
If only your JavaScript reads the theme after the page loads, use localStorage instead. Then the theme is not sent with every request.
Matching cookies go with every request, not just page requests. Images, scripts, and style files carry them too. So keep a cookie to a short ID or a small yes/no flag. About 4 KB per cookie is a common limit, but it changes from browser to browser, and all the headers of one request together have a limit too.
- A session ID that shows who is signed in
- A dark mode choice used only after the page loads
- A secret API key for a paid service
- The user's yes or no to tracking, which the server must follow
- A large shopping cart with many product details
- A secret key the server uses to sign CSRF tokens
Drag each piece of data to the safest place to keep it.
Common misconceptions
- "Writing to
document.cookiereplaces all cookies." It writes one cookie. The others stay. - "I can read the rules later." A read shows only
name=valuepairs, never rules likePath. - "Setting an empty value deletes a cookie." An empty value is still a cookie. Delete it with
Max-Age=0or anExpiresdate in the past. - "
Securehides a cookie from JavaScript."HttpOnlyhides it.Secureonly means "send over HTTPS". - "
SameSiteis all the CSRF protection I need." It helps a lot, but real apps also check requests on the server, for example with special tokens. - "Cookies are a good place for secrets." Only a server-set
HttpOnlycookie should hold a sign-in token. Never keep secret API keys anywhere the browser can read.
Practice exercises
5 EXERCISESRead the starter code. Object.fromEntries turns a list of [name, value] pairs into an object. What exact text does the code print?
const raw = "cf_demo_theme=dark; cf_demo_name=Ada%20Lovelace";
const cookies = Object.fromEntries(
raw.split("; ").map((pair) => pair.split("=").map(decodeURIComponent))
);
console.log(cookies.cf_demo_name);const raw = "cf_demo_theme=dark; cf_demo_name=Ada%20Lovelace";
const cookies = Object.fromEntries(
raw.split("; ").map((pair) => pair.split("=").map(decodeURIComponent))
);
console.log(cookies.cf_demo_name);The code turns the string into an object. The value of cf_demo_name is Ada%20Lovelace. Decoding it gives Ada Lovelace, and that is what prints.
Which rule, written as name=value, goes into the write that deletes this cookie?
document.cookie = "cf_demo_theme=dark; Path=/";
// Now delete the same cookie.document.cookie = "cf_demo_theme=; Max-Age=0; Path=/";Writing the same cookie again with Max-Age=0 tells the browser to remove it. Keep the same path, Path=/.
This app saves a sign-in token from JavaScript. Explain what is wrong. Then write the safer Set-Cookie header the server should send.
document.cookie = "token=secret.jwt.value; Max-Age=3600; Path=/; Secure";Set-Cookie: token=secret.jwt.value; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=LaxA sign-in token should be set by the server with HttpOnly, so page JavaScript can't read it. Secure alone is not enough: it only means the cookie travels over HTTPS.
A different site sends a POST form to your app. Your sign-in cookie has SameSite=Lax. Is the cookie sent or blocked?
// Cross-site POST form with SameSite=Lax
console.log("blocked");// Cross-site POST form with SameSite=Lax
console.log("blocked");A POST form from another site does not get a SameSite=Lax cookie. So the cookie is blocked.
Your server shows the first page in dark colors before any of your JavaScript runs. Pick a cookie or localStorage for the theme, and say why.
document.cookie = "theme=dark; Max-Age=31536000; Path=/; SameSite=Lax";The server builds the first page in dark colors, so it needs the theme with the request. A tiny cookie fits. If only your JavaScript needed it after loading, localStorage would also be fine.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does writing to
document.cookiedo?Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconst raw = "theme=dark; name=Ada%20Lovelace"; const pairs = raw.split("; ").map((pair) => pair.split("=")); console.log(decodeURIComponent(pairs[1][1]));Choose an answer to see the explanation.
Question 3 of 7A cookie has both
ExpiresandMax-Age. Which one wins?Choose an answer to see the explanation.
Question 4 of 7A POST form on another site sends a request to your site. Which SameSite value lets the cookie go along?
Choose an answer to see the explanation.
Question 5 of 7Which cookie rule stops JavaScript from reading the cookie?
Choose an answer to see the explanation.
Question 6 of 7This code acts out a cookie read: it keeps only the text before the first semicolon. What does it print?
Read the code, then predictconst write = "theme=dark; Max-Age=3600; Path=/; SameSite=Lax"; const readable = write.split(";")[0]; console.log(readable);Choose an answer to see the explanation.
Question 7 of 7When is the Cookie Store API a better fit than
document.cookie?Choose an answer to see the explanation.
Key takeaways
- A cookie is a small
name=valuetext that the browser saves and sends back to its site. - Reading
document.cookiegives all readable cookies as one string. Writing to it sets one cookie. Max-Agewins overExpires. To delete, write the cookie again withMax-Age=0and the same name, path, and domain.SameSitedecides whether the cookie goes with cross-site requests.NoneneedsSecure.Securemeans "send over HTTPS only".HttpOnlymeans "JavaScript can't read it".- Check that the Cookie Store API exists before you use it, and keep a backup.
Final definition: a cookie is a small piece of text the browser saves for a site and sends back with matching requests. Its rules decide how long it lasts, where it goes, and who can read it. JavaScript can set some rules; only the server can set all of them.
Up next: localStorage and sessionStorage.