Cookies vs tokens for systems analyst interviews

Train for your next tech interview
1,500+ real interview questions across engineering, product, design, and data — with worked solutions.
Join the waitlist

Why this question never goes away

You walk into a systems analyst loop at Stripe, Notion, or Airbnb and the interviewer draws a login box on the whiteboard. "User clicks sign in. Walk me through what happens between their browser and your backend." Within ninety seconds you will be asked the question every SA candidate dreads in different costumes — cookies versus tokens, sessions versus JWT, server state versus stateless auth. It is the auth equivalent of "SQL vs NoSQL" and most candidates answer in slogans instead of trade-offs.

The reason this keeps showing up is that the choice leaks into every other design decision. It changes how you scale horizontally, how you log out a compromised account, how you build a mobile app against the same backend, and how you reason about CSRF, XSS, and token theft. A senior SA is expected to hold both models in their head, sketch the request flow on demand, and name the one failure mode that would make them pick the other approach.

This post is the version that does not get you eliminated. It walks through both mechanisms, the trade-offs to name out loud, the hybrid pattern most production systems use, and the pitfalls interviewers love to probe.

A cookie session is the older, server-stateful approach. The server creates a record in its own session store — Redis, Memcached, or a dedicated table — and hands the browser an opaque session ID in a cookie. The cookie itself carries no meaning; it is a lookup key. Every subsequent request, the browser attaches the cookie automatically and the server resolves it back to the user.

The flow looks like this:

1. POST /login  { email, password }
2. Server validates credentials
3. Server creates session in Redis: session:abc123 -> { user_id: 42, roles: [...] }
4. Server responds: Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax
5. Next request: GET /dashboard with Cookie: session_id=abc123 (browser auto-attaches)
6. Server: GET session:abc123 from Redis -> resolve user -> serve response

The defaults you should name in the interview are HttpOnly, Secure, and SameSite. HttpOnly hides the cookie from JavaScript, which kills the most common XSS-to-session-theft chain. Secure forces HTTPS, so the cookie is never sent over plaintext. SameSite=Lax (or Strict) restricts cross-origin sending, which is your primary CSRF mitigation. These three flags do most of the heavy lifting — and forgetting any one of them is a classic interview gotcha.

The advantages are real. Revocation is trivial — delete the row in Redis and the session is dead on the next request. Session payload lives server-side, so you can stuff arbitrary state (cart, feature flags, impersonation chain) without bloating the cookie. And browsers handle the storage and attachment for you, which means zero client code.

The cost is server-side state. Every node that handles a request needs to reach the session store, which forces you to either share Redis or rely on sticky sessions at the load balancer. At small scale this is invisible. At Uber-scale it becomes a hot path you tune for years.

JWT tokens in practice

A JWT (JSON Web Token) is a self-contained, signed token. The server signs a payload with a secret or private key and hands the whole thing to the client. There is no server-side record. On every request the client sends the token back, the server verifies the signature, and the embedded claims (user_id, roles, expiry) tell the server who is calling.

1. POST /login  { email, password }
2. Server validates credentials
3. Server signs JWT:
   header.payload.signature
   payload = { sub: 42, roles: ["admin"], iat: ..., exp: ... (15 min) }
4. Server responds: { access_token: "eyJ...", refresh_token: "..." }
5. Client stores tokens (memory, secure storage, etc.)
6. Next request: GET /dashboard with Authorization: Bearer eyJ...
7. Server verifies signature -> reads claims -> serves response (no DB lookup)

The headline win is statelessness. Any node can validate any token without talking to a shared store — you just need the signing key. This makes horizontal scaling trivial and works beautifully across services, regions, and microservice meshes. JWTs are also the natural fit for mobile, native, and machine-to-machine clients, where cookies are awkward and CORS gets messy.

The headline pain is revocation. A signed token is valid until it expires. If a user changes their password, gets fired, or you discover a token leak, you cannot un-sign the bytes already in the wild. Teams patch this with short access-token lifetimes (5-15 minutes), longer-lived refresh tokens, and a denylist of revoked token IDs — which, ironically, reintroduces the server-side state you were trying to avoid.

Load-bearing trick: if your answer to "how do you revoke a JWT?" is "rotate the signing key," you have just logged out every user. The honest answer is: short expiry + refresh tokens + a small denylist for emergencies.

JWTs also tend to be larger than session cookies (1-4 KB versus a 40-byte session ID), and the client now has the storage problem — localStorage is XSS-readable, in-memory is lost on refresh, and httpOnly cookies for JWTs work but defeat half the point.

Side-by-side comparison

The table you want to be able to draw from memory at the whiteboard:

Dimension Cookie + session JWT
Server state Required (Redis / DB) None for verification
Revocation Easy — delete server record Hard — needs denylist or short TTL
Horizontal scale Sticky sessions or shared Redis Trivial — any node verifies
Cross-domain Awkward (CORS + cookie rules) Native — header-based
Mobile / native clients Awkward — no cookie jar Native — just an Authorization header
Payload size ~40 bytes (opaque ID) 1-4 KB (signed claims)
Where state lives Server Client (in the token)
Primary attack to defend CSRF XSS / token theft
Default browser behavior Auto-attach Manual attach (client code)

Notice the symmetry: every advantage of one is a cost of the other. Cookies trade scalability for revocation; JWTs trade revocation for scalability. There is no free lunch, and saying so out loud is what gets you past the question.

A small interview tell — strong candidates always pair the dimension with the failure mode it implies. "Cookies make revocation easy" lands flat. "Cookies make revocation easy, which is why finance and healthcare teams default to them — they need to kill a compromised session within seconds, not minutes" lands.

Train for your next tech interview
1,500+ real interview questions across engineering, product, design, and data — with worked solutions.
Join the waitlist

Hybrid patterns most teams actually ship

Real production systems rarely pick one and stop. The pattern you should reach for in a system design round looks like this:

Sanity check: the web app uses cookies; the mobile app and partner APIs use tokens; the auth server is the one place that issues both.

Concretely, most modern stacks layer it as follows. The first-party web app authenticates with a short-lived access token in an HttpOnly cookie, plus a refresh-token cookie scoped to /auth/refresh. CSRF protection comes from SameSite=Lax plus a double-submit token. The mobile app logs in via OAuth and stores JWT access + refresh tokens in platform secure storage (Keychain on iOS, Keystore on Android). Third-party API clients use OAuth 2.1 with short-lived bearer tokens. The internal service mesh uses mTLS or signed service-to-service JWTs with very short expiry (under 5 minutes).

This is the answer that signals you have shipped auth and not just read about it. It also opens the door to discussing token rotation (refresh tokens rotate on each use to detect replay), session binding (tokens bound to a device fingerprint so a stolen token cannot be used elsewhere), and step-up auth (sensitive actions require fresh authentication regardless of token validity).

Common pitfalls

The most common stumble is treating this as a religious war instead of a trade-off. Candidates who answer "JWT is always better because stateless" reveal they have not run the on-call rotation when a token leak happens at 2 AM. The fix is to always name the constraint that drove the choice — "we picked sessions because compliance required revocation within 60 seconds" sounds like an engineer; "JWT is the modern way" sounds like a tutorial.

A second trap is forgetting CSRF when defending cookie sessions. Candidates often list HttpOnly and Secure, then stop. Without SameSite or a CSRF token, a malicious site can forge a state-changing request and the browser will attach your session cookie. Name all three defenses — SameSite=Lax, an anti-CSRF token for state-changing endpoints, and Origin/Referer checks as a second layer.

The third pitfall is storing JWTs in localStorage and calling it secure. localStorage is readable from any script on the page, so a single XSS — a compromised npm dependency, a sloppy dangerouslySetInnerHTML — leaks every active token. The fix is either HttpOnly cookies for JWTs (giving up most of the cross-domain benefit) or in-memory storage with short access-token lifetimes.

The fourth pitfall is misunderstanding "stateless". JWTs are not stateless in the system sense — your auth server still has a user table, a refresh-token store, and usually a denylist. They are stateless only at the request-verification layer. Telling a senior interviewer "JWTs are fully stateless" triggers the follow-up "great, so how do you log out?" — and the conversation goes downhill. Be precise: JWTs let verification skip the database, not the whole auth system.

The final trap is picking JWT for an internal monolith because the diagram looks cooler. If your app is one process behind a single load balancer, you already have a session store — your database. Switching to JWT buys nothing and costs revocation. Pick the boring option until scale forces your hand.

If you want to drill auth and system design questions like this one daily, NAILDD is launching with 500+ systems analyst interview problems across exactly this pattern.

FAQ

Default to cookie sessions when your product is a browser-first web app with a single domain, when revocation latency matters (compliance, financial services, healthcare), and when you do not yet have a mobile or third-party API surface. You get strong browser defaults for free, trivial logout, and a smaller cookie payload. The day you add a mobile app or open the API to partners is the day you start layering tokens on top — not before.

Are JWTs ever actually stateless in production?

In the strict sense, no. The signing key lives somewhere, the user table lives somewhere, refresh tokens are usually stored server-side, and any team that takes security seriously runs a denylist for compromised tokens. What JWTs give you is stateless request verification — any service can validate a token using only the public key, without a round trip to a central session store. That is a real and valuable property at scale, but it is not the same as "no server state anywhere."

How do I revoke a JWT before it expires?

You have three honest options. First, keep access-token lifetimes short (5-15 minutes) so the worst-case exposure window is small, and rely on the refresh-token flow to enforce policy changes. Second, maintain a denylist of revoked token IDs (the jti claim) and check it on every request — yes, this reintroduces a database lookup, and yes, it is still cheaper than full session lookups because most tokens are not on the list. Third, in emergencies, rotate the signing key, which invalidates every token at once. That last option is the nuclear button — it logs out all users — so reserve it for breach-response, not routine revocation.

Where should the client store a JWT?

There is no perfect answer, only trade-offs. localStorage is convenient but XSS-readable. In-memory survives XSS reasonably well but is lost on refresh, forcing a silent refresh call on every page load. HttpOnly cookies are safe from JavaScript but reintroduce CSRF concerns. The pragmatic combo for first-party web apps is a short-lived access token in memory plus a refresh-token cookie scoped to the auth endpoint with SameSite=Strict.

Do mobile apps need cookies at all?

Almost never. Native mobile clients do not have the browser's cookie machinery, do not benefit from same-origin policy, and have a real secure-storage primitive (Keychain on iOS, Keystore on Android). Stick to JWTs (or OAuth tokens) over an Authorization: Bearer header, store the refresh token in secure storage, and keep access tokens in memory. The only time cookies show up in mobile is when the app embeds a WebView for OAuth flows — and even there the cookies belong to the auth provider, not your backend.

How does this come up in a systems analyst loop specifically?

SA loops ask the question in three costumes. First, the explicit trade-off question — name dimensions and defaults. Second, a system design prompt ("design login for a new app") where the trade-off is implicit. Third, a debugging prompt ("users report being logged out randomly") where they want you to reason about session expiry, refresh-token rotation, and clock skew. Prepare all three forms.