What each holds
A cookie is a small key-value pair in the browser, sent automatically on each request as the carrier of identity. A session is server-side user state; usually only a random session id goes into the cookie while the real data stays on the server.
Login flow
- The user logs in; the server verifies the password, creates a session, and stores
user_id; - It writes the session id into a cookie and returns it;
- On later requests the browser sends the cookie, and the server looks up the user by id.
JWT is not a session
A JWT encodes state into the token itself (with a signature), so the server may skip the lookup. Good for stateless services, but hard to revoke -- the token stays valid until expiry, unlike a session you can delete at will.
Cookie security flags
- HttpOnly: blocks JS reads, defends against XSS token theft;
- Secure: HTTPS only;
- SameSite: limits cross-site sending, easing CSRF.
Real-world cases: three "logged out for no reason" bugs
- Lost login after a redirect: the cookie vanishes when returning from a third-party site, usually because
SameSite=Laxblocked the cross-site request. If cross-site sending is genuinely required, setSameSite=None; Secureexplicitly and serve over HTTPS. - Same-named cookies overwriting each other: the apex domain and a subdomain each set a cookie with the same name; the browser sends both and the server reads the stale one. Set
DomainandPathdeliberately, or rename one. - Logged out mid-session: sessions kept in process memory are invisible to other instances after scaling out. Move them to shared storage such as Redis, or switch to a stateless token.
FAQ
How long should a session id be? At least 128 random bits from a cryptographically secure source — never a sequence number or timestamp. Why rotate the session id on login? To stop session fixation: if the id is unchanged across login, an attacker can plant a known id and wait. Cookie or localStorage for tokens? A cookie with HttpOnly cannot be read by XSS and suits session credentials; localStorage is readable by any script. How do I refresh an expired JWT? Commonly a short-lived access token plus a longer refresh token, where the refresh token can be revoked and stored separately.
Try it
Generate a strong session secret: Password generator.
Choosing and scaling session storage
- In-process memory: simplest on one node, but flaky across instances and needs migrating as soon as you scale out;
- Shared store such as Redis: the common choice — set expiries, pick a serialisation format, pool connections, and plan degradation (reject new logins rather than letting everything through);
- Database: suits auditing and long retention, but a query per request becomes a bottleneck, so add caching;
- Encrypted cookie: keeps the server stateless at the cost of size limits and hard-to-revoke sessions;
- Capacity planning: estimate active users times average session size, add peak headroom, and avoid saturating the store during a sale.
Expiry and renewal
Set both idle and absolute timeouts: the first expires after inactivity, the second caps total lifetime. Sliding renewal should extend the idle timeout but never the absolute one; for sensitive actions (password change, payment) re-authenticate rather than trusting the session.
Sessions across platforms and services
- Decide multi-device policy first: whether one account may be signed in on several devices and whether a new login evicts the old session shapes storage design — settle it before implementation.
- Pass identity explicitly between services: internal calls should carry the caller's identity rather than relying on shared session storage, or internal requests bypass authorisation.
- Keep sessions lean: store identifiers and necessary state, fetching details on demand; oversized sessions cost storage and raise per-request transfer and parsing.
- Tolerate stateless components: gateways and edge functions that cannot reach shared storage should receive limited information in a token instead of requiring session access.
- Monitor anomalies: one session appearing in two locations in a short window, or a spike in sessions per account, should alert and support fast invalidation.
The key is deciding up front who stores state and who verifies it — separating those makes scaling and hardening much easier.