Table of Contents
Imagine a waiter with no memory at all. You order a drink. He brings it. You ask for the bill, and he has no idea who you are, what you ordered, or whether you have paid. That is HTTP. Every single request arrives as a complete stranger.
Cookies and sessions are how the web gives that waiter a notebook. Get them right and people stay logged in safely. Get them wrong and you have an account-takeover bug. This guide shows you exactly how they work, with a step-by-step login you can click through, real code for PHP and Node, and the security flags that actually matter.
What you will learn (11 minute read):
✓ Why HTTP forgets you, and what that forces developers to build
✓ A clickable walkthrough of a real login, showing both the browser and the server
✓ Cookies vs sessions vs tokens, with a clear decision guide
✓ Working code: PHP, Node.js and plain JavaScript
✓ The four cookie flags that stop most attacks
✓ How to read cookies in DevTools and check your own site
The Problem: HTTP Has No Memory
HTTP is stateless. Each request stands completely alone. The server answers it and immediately forgets everything. Without help, this happens:
Without cookies you would have to log in again on every single page. Cookies are the fix.
Cookies and Sessions in One Picture
People mix these up constantly, so here is the distinction in one line: a cookie is a note the browser keeps; a session is a file the server keeps. They work together, because the cookie is what carries the key to the server’s file.
The cloakroom analogy. The cookie is the numbered ticket in your pocket. The session is your coat in the back room. The ticket is worthless to a thief who cannot reach the cloakroom, and losing the ticket does not give anyone your coat unless they also walk in with it. That is why we protect the ticket so carefully.
Watch a Real Login Happen
This is the part that makes everything else obvious. Press Start and step through a login. Watch the cookie appear in the browser, the session appear on the server, and the exact HTTP headers travelling between them.
Six steps, from typing a password to logging out.
# Press "Start the login" aboveThe key insight from step 3: the cookie only ever holds sid=7f3a9b2c. The user’s name, role and permissions never leave the server. Even if someone reads the cookie, they learn nothing about the account, which is why this pattern has lasted thirty years.
Seeing Cookies on Any Website
You do not need to take my word for it. On any site, press F12, open the Application tab (Chrome and Edge) or Storage (Firefox), and click Cookies. You will see something like this:
| Name | Value | Expires | HttpOnly | Secure | SameSite |
|---|---|---|---|---|---|
| sid | 7f3a9b2c4e… | Session | ✓ | ✓ | Lax |
| theme | dark | 2027-10-17 | ✗ | ✓ | Lax |
| lang | en | 2027-10-17 | ✗ | ✓ | Lax |
| _ga | GA1.2.118… | 2028-04-17 | ✗ | ✓ | Lax |
An illustration of the DevTools cookie panel. Note that the login cookie sid has HttpOnly ticked, while the harmless preference cookies do not need it.
A two-minute exercise. Open DevTools on a site where you are logged in, find the session cookie, delete it, and refresh. You are logged out instantly. That one cookie was your login. Now you understand why protecting it matters so much.
The Code
PHP sessions
PHP does nearly all of this for you. session_start() creates the session, sends the cookie, and loads the data back on later requests.
<?php // Set the cookie rules BEFORE starting the session session_set_cookie_params([ 'lifetime' => 0, // 0 = until the browser closes 'path' => '/', 'secure' => true, // HTTPS only 'httponly' => true, // JavaScript cannot read it 'samesite' => 'Lax', // blocks most CSRF attacks ]); session_start(); if (password_verify($_POST['password'], $user['password_hash'])) { // Critical: new ID on login, to stop session fixation session_regenerate_id(true); $_SESSION['user_id'] = $user['id']; $_SESSION['role'] = $user['role']; $_SESSION['last_seen'] = time(); header('Location: /dashboard.php'); exit; }
<?php session_start(); // Not logged in? Send them away. if (empty($_SESSION['user_id'])) { header('Location: /login.php'); exit; } // Log out after 30 minutes of inactivity if (time() - $_SESSION['last_seen'] > 1800) { session_unset(); session_destroy(); header('Location: /login.php?timeout=1'); exit; } $_SESSION['last_seen'] = time(); echo 'Welcome back, user #' . $_SESSION['user_id'];
Logging out properly
<?php session_start(); $_SESSION = []; // 1. empty the data setcookie(session_name(), '', time() - 3600, '/'); // 2. kill the cookie session_destroy(); // 3. delete it on the server header('Location: /');
Deleting the cookie alone is not logging out. If the session still exists on the server, anyone holding a copy of that old session ID can keep using it. Always destroy the server side too, in that order.
Node.js with Express
const express = require('express'); const session = require('express-session'); const app = express(); app.use(session({ secret: process.env.SESSION_SECRET, // never hard-code this name: 'sid', // avoid the default name resave: false, saveUninitialized: false, // no cookie until it is needed cookie: { httpOnly: true, secure: true, // HTTPS only sameSite: 'lax', maxAge: 1000 * 60 * 30 // 30 minutes } // In production use a real store (Redis, a database), // not the default in-memory one. })); app.post('/login', async (req, res) => { const user = await checkPassword(req.body); if (!user) return res.status(401).json({ error: 'Invalid login' }); req.session.regenerate(() => { // new ID on login req.session.userId = user.id; res.json({ ok: true }); }); }); app.post('/logout', (req, res) => { req.session.destroy(() => { res.clearCookie('sid'); res.json({ ok: true }); }); });
Plain cookies in JavaScript
For harmless things like a theme preference, you can set a cookie in the browser. Remember that JavaScript can never read an HttpOnly cookie, which is exactly the point.
// Write a cookie that lasts a year document.cookie = 'theme=dark; Max-Age=31536000; Path=/; Secure; SameSite=Lax'; // Read one back function getCookie(name) { return document.cookie .split('; ') .find(row => row.startsWith(name + '=')) ?.split('=')[1]; } // Delete one: set it to a date in the past document.cookie = 'theme=; Max-Age=0; Path=/'; // For data the server never needs, localStorage is simpler localStorage.setItem('theme', 'dark');
The Four Flags That Stop Most Attacks
A cookie without these is a cookie waiting to be stolen. This is the single most valuable table in this article.
| Flag | What it does | What it prevents |
|---|---|---|
HttpOnly | JavaScript cannot read the cookie at all | A script injected into your page stealing the login cookie (XSS) |
Secure | Only sent over HTTPS | Someone on the same Wi-Fi reading it in plain text |
SameSite | Controls whether it is sent from other sites | Another site making requests as the logged-in user (CSRF) |
Max-Ageor Expires | Sets when the cookie dies | A login that stays valid forever on a shared computer |
Set-Cookie: sid=7f3a9b2c4e8d1f60; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800
Choosing a SameSite value
| Value | Behaviour | Use for |
|---|---|---|
| Lax | Sent when someone clicks a link to your site, not on background cross-site requests | The sensible default for login cookies |
| Strict | Never sent from another site at all, even a normal link | Banking and admin panels, where the user arriving logged-out once is acceptable |
| None | Always sent, and requires Secure | Only when your cookie genuinely must work inside another site, such as an embedded widget |
Cookies, Sessions and Tokens Compared
| Cookie | Session | JWT token | |
|---|---|---|---|
| Stored where | The user’s browser | Your server (memory, Redis, database) | The browser, often in a cookie |
| Size limit | About 4 KB | Whatever your server allows | Small, but bigger than a session ID |
| Can the user read it | Yes, unless HttpOnly | No | Yes, it is only encoded, not encrypted |
| Safe for secrets | No | Yes | No, never put private data in the payload |
| Instant logout | Delete the cookie | Yes, delete it server-side | Hard: a token stays valid until it expires |
| Scales across servers | n/a | Needs a shared store such as Redis | Easy, nothing to look up |
| Best for | Preferences, the session ID itself | Normal website logins | APIs, mobile apps, services talking to services |
Which should you pick? For an ordinary website or a WordPress project, use server-side sessions with a secure cookie. It is simpler, you can log people out instantly, and nothing sensitive leaves your server. Reach for JWTs when a mobile app or another service needs to authenticate, not because they sound more modern.
Where to Store What
Never store these in a cookie or in localStorage: passwords, password hashes, credit card numbers, API secrets, or a plain flag like isAdmin=true. Anyone can edit those in DevTools in five seconds. Authorisation decisions belong on the server, every time.
Cookies and the Law
Rules such as the GDPR in Europe treat non-essential cookies as personal data. The practical version for a developer:
A compliant banner: rejecting is one click, just like accepting (illustration).
Mistakes That Cause Real Security Bugs
| Mistake | Why it is dangerous | Fix |
|---|---|---|
| No HttpOnly on the session cookie | Any injected script can read the login cookie and impersonate the user | Set HttpOnly on every authentication cookie |
| Not regenerating the session ID at login | An attacker who planted a known session ID beforehand is logged in as the victim (session fixation) | session_regenerate_id(true), or req.session.regenerate() |
| Storing roles in a cookie | role=admin can simply be edited by the user | Keep the role in the server session and check it there |
| Sessions that never expire | A stolen cookie works forever, and shared computers stay logged in | Set an idle timeout, and an absolute maximum lifetime |
| Logout that only clears the cookie | The session survives on the server and can be reused | Destroy the server session as well |
| Cookies without Secure on an HTTPS site | The cookie can leak over an accidental HTTP request | Always add Secure, and redirect HTTP to HTTPS |
| In-memory sessions in production | Everyone is logged out on every restart, and nothing works behind a load balancer | Use Redis, a database, or your framework’s recommended store |
Common Questions
Are cookies dangerous or spyware?
A cookie is just a small piece of text your browser stores and sends back. It cannot run code or read your files. The privacy concern is not the technology but what some companies store in it: identifiers used to follow people across many websites. Your own login cookie is simply a ticket number.
What is the difference between a session cookie and a persistent cookie?
A session cookie has no expiry date and disappears when the browser closes. A persistent cookie has Max-Age or Expires set and survives restarts. Login cookies are usually session cookies unless the user ticks “remember me”.
Why do I get logged out even though I did nothing?
Usually an idle timeout, the server restarting with in-memory sessions, or the cookie being blocked. In development, a common cause is Secure: true on a cookie while testing over plain HTTP, so the browser silently refuses to store it.
Should I use localStorage for my login token?
It is not recommended. Anything in localStorage is readable by any JavaScript on the page, so one injected script exposes the token. An HttpOnly cookie cannot be read by scripts at all, which is why it remains the safer default for browser logins.
How big can a cookie be, and how many can I set?
Roughly 4 KB per cookie, and browsers typically allow somewhere around 50 per domain, with limits varying. Remember that every cookie is sent with every request to that domain, so a pile of large cookies slows your whole site down.
What are third-party cookies and should I worry about them?
They are cookies set by a different domain from the one in the address bar, mostly used for advertising. Browsers have been restricting them for years, so do not build anything important on top of them. First-party cookies, the kind your own site sets, continue to work normally.
Read next: The Role of APIs in Modern Web Development covers how tokens and sessions work when the client is not a browser.
Final Thoughts
Cookies and sessions exist because HTTP forgets you the moment it answers. Remember the cloakroom: the cookie is the ticket, the session is the coat. Keep the ticket small, mark it HttpOnly, Secure and SameSite, give it an expiry, and keep everything that matters on the server.
Do this now: open DevTools on your own project, look at your session cookie, and check those three flags. Most sites are missing at least one. Which one were you missing? Tell me in the comments. I read every one.
If this made cookies finally click for you, share it with someone still guessing.



