backend / rest apis / 09_stateful_vs_stateless.md

Stateful vs Stateless (REST and beyond)

7 interview angles 9 min read source

Stateful vs Stateless (REST and beyond)

One of REST’s six original constraints (Fielding 2000) and the one most often violated in production. Interviewers ask “is REST stateful or stateless?” expecting “stateless” — then probe whether you understand what that actually means and where teams cheat.

What “stateless” means in REST

From Fielding’s dissertation:

Each request from client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server. Session state is therefore kept entirely on the client.

Two halves:

  1. Each request is self-contained. The server can serve it without knowing what came before.
  2. No server-side session state. “User A is mid-checkout step 3” is not in server memory; it’s in the request (cookies, body, headers) or in a durable store the client identifies.

Crucially, statelessness is about per-client session state, NOT about resource state. The database obviously has state — that’s the resource state, not session state.

Why stateless

Stateless servers have nice properties:

Property Why
Horizontal scaling any server can handle any request — no affinity needed
Resilience if a server dies, the next request just goes elsewhere; no session lost
Caching identical requests produce identical responses; cacheable by URL
Simplicity at the server no in-memory session table, no cleanup, no eviction policy
Visibility a single request fully describes the operation (easier debugging / monitoring)

The cost is on the client: every request carries all the identifying info (auth token, context). Bandwidth and request size grow.

What “stateful” looks like

Request 1: POST /login           → server stores session in memory
                                  → returns session-id cookie
Request 2: POST /cart/add        → server looks up session-id → "user 42, in checkout"
Request 3: GET /cart             → server uses session for context

The server’s session table contains:

sessions = {
    "abc123": {"user_id": 42, "step": "checkout", "cart": [...], "preferences": {...}},
    "def456": {"user_id": 99, "step": "browsing", ...},
}

Now server A holds Alice’s session; server B doesn’t. If Alice’s next request hits server B, B has no idea who Alice is.

Workarounds:

  1. Sticky sessions — load balancer pins Alice to server A.
  2. Shared session store — Redis / Memcached / DB; all servers read from it.
  3. Stateless auth — JWT in the request; no server-side session.

Each workaround has trade-offs (see below).

Sticky sessions / session affinity

Alice → LB → Server A (forever)
Bob   → LB → Server B (forever)
Carol → LB → Server A (forever)

LB hashes by IP or session cookie. Alice keeps hitting server A as long as it’s alive.

Pros:

  • No shared session store needed.
  • Simple to set up.

Cons:

  • Hot/cold imbalance: if Alice is heavy and Bob is light, A is overloaded.
  • Failover loses sessions: if A dies, Alice’s session is gone — she logs in again.
  • Deploys are bumpy: rolling restart drops sessions.
  • Scale-out doesn’t redistribute: existing users stay pinned; new servers only get new connections.

Sticky sessions are a hack for inherently-stateful systems. Better to make the app stateless.

Shared session store

Alice → LB → any server → reads session from Redis
Bob   → LB → any server → reads session from Redis

Pros:

  • Any server can handle any request.
  • Failover is transparent — new server picks up the same session.
  • Scale-out works.

Cons:

  • Per-request store lookup — adds latency (usually <1ms with Redis).
  • The store becomes a SPOF — if Redis is down, all sessions are unreachable.
  • Operational overhead — running and monitoring the session store.

This is the dominant pattern. Django + Redis sessions, Flask-Session, Express-session, Rails — all use this model.

Important: the server still has per-client session state; it’s just stored externally. Strictly, this isn’t “stateless” in Fielding’s sense — but operationally it solves the horizontal-scaling problem.

Stateless auth — JWT and friends

POST /login → server returns signed JWT
GET /cart   Authorization: Bearer <jwt>
            → server verifies signature, reads user from claims, processes

The token IS the session state. Server doesn’t store anything.

Pros:

  • True stateless servers — any can handle any request.
  • No session store.
  • Scales horizontally without coordination.

Cons:

  • Revocation is hard — JWT is valid until exp. Can’t force logout without a blocklist (which defeats statelessness).
  • Token bloat — claims grow over time; tokens get large; bandwidth and parse cost grow.
  • Refresh complexity — short access tokens + refresh tokens add moving parts.
  • PII in tokens — claims are visible (base64); careful what goes in.

JWT vs session-store debate is essentially “true stateless complexity costs” vs “shared session store operational cost.” See ../11_authentication/sso/04_session_management.md.

Where state actually lives — taxonomy

State type Where it should live
Resource state the database (users, orders, products) — always stateful, that’s the point
Session state (“who is this user, mid-flow context”) client cookies / tokens; or shared store keyed by session id
Application state (“which page am I on”) client (URL, browser history)
Transient state (“temporary upload in progress”) object store with TTL; not in app server memory
Cache external (Redis, Memcached); ephemeral; reconstructible

Application servers should hold none of these in their own memory across requests. Every request reconstructs context from the request itself + external stores.

“Stateless” doesn’t mean “no cookies”

A common confusion. Cookies make a request self-contained: they ARE the per-request state carrier. A request with a session-id cookie is self-contained — the server can look up the session (in Redis) without knowing what came before.

Stateless = no server-side memory of which client this is. Cookies (or headers) tell the server who is asking; the server then queries whatever it needs.

JWT and cookie-with-session-id both satisfy this, just differently. JWT: claims travel in the cookie/header. Session-id: an opaque identifier travels; claims live in Redis.

Auth patterns in stateful vs stateless

Pattern State location Revocation Scaling
Server session table server memory easy (delete from table) needs sticky sessions or shared store
Cookie + Redis session Redis easy (delete key) scales horizontally
JWT in cookie / header client hard (need blocklist) trivial horizontal scaling
API key DB lookup per request revocable (mark inactive) DB is the bottleneck
OAuth opaque + introspection OP database easy via OP adds OP round trip

Pragmatic recommendation for most apps: Redis-backed session with HttpOnly cookie. Best of stateless-app-server + easy revocation + simple model.

JWT shines for: ephemeral systems (Lambda), short-lived clients, scenarios where Redis is impractical. Worth the revocation pain only in specific contexts.

The Richardson Maturity context

Fielding’s REST is “Level 3” Richardson Maturity (07_richardson_maturity_hateoas.md) — full constraints including HATEOAS and statelessness. Almost no production REST API is Level 3.

Most real “REST APIs” are Level 2: resources + HTTP verbs + status codes, often with cookie sessions (i.e., technically stateful per Fielding’s strict definition). Nobody calls those “not REST” in practice; the term has shifted.

When an interviewer asks “is REST stateless?” the right answers:

  • By Fielding’s definition: yes, strictly.
  • In practice: it depends on your auth model. Cookie + Redis session is operationally stateless (any server handles any request) but technically holds per-client state.
  • Why the constraint exists: horizontal scaling, caching, simpler servers. The benefits are what matter, not the dogma.

Long-lived connections — WebSocket and SSE

Client opens WebSocket → connects to Server A → conversation

WebSocket connections are inherently stateful — they live on one server until closed. Two implications:

  1. Sticky sessions are required for the WebSocket itself (the connection can’t move servers).
  2. Application state still goes to external storage — the connection is on A, but A reads/writes Redis/DB; if A dies the client reconnects and lands on B, which has the same external state.

This is consistent with REST statelessness for the underlying app state — only the connection is pinned, not the data.

Same for SSE (Server-Sent Events) and long-polling: the transport is sticky; the app data isn’t.

When is true statelessness worth it?

Scenarios where statelessness pays off most:

  • Serverless / Lambda: instances die between requests; you literally can’t hold session memory.
  • Massive scale: per-server session tables would consume too much RAM; shared store latency matters.
  • Multi-region active-active: writing session state to a regional store and reading from another is painful; stateless tokens transit unchanged.
  • Edge compute (Cloudflare Workers): short-lived requests across edge nodes; no session continuity assumed.

Scenarios where stateful (Redis-backed) is fine:

  • Single-region web app: Redis is fast; sessions are cheap.
  • B2B SaaS: tens of thousands of sessions, not millions. Redis handles it.
  • Apps needing easy revocation: logout, force-logout, session-listing for users.

Common pitfalls

  • In-memory sessions on a multi-server deploy without sticky sessions — random logouts as requests hit different servers.
  • Sticky sessions + rolling deploy — every restart drops sessions; users get logged out.
  • JWT in localStorage — XSS-vulnerable; refresh tokens leaked. Use HttpOnly cookies.
  • Long-lived JWTs without revocation plan — stolen token works for hours/days.
  • Session-id cookie without HttpOnly — JS reads it; XSS leaks the session.
  • Stateful counters in app memory (“user has tried 3 times”) — lost on restart; doesn’t work across servers. Use Redis.
  • Calling app “stateless” while using sticky sessions — those are contradictory.

Common interview confusions

  • “REST is stateless because it uses HTTP.” — HTTP is stateless; REST inherits that constraint. But cookies, sessions, and JWTs are application-layer state; both can exist on top of HTTP.
  • “Stateless means no auth state.” — auth state can be in a token (stateless) or in a server-side session (stateful). Either is compatible with HTTP.
  • “Stateless apps are always better.” — not always. Easy revocation and small token sizes are real wins for stateful sessions; JWT brings complexity.
  • “Stateful = monolithic, stateless = microservices.” — both can be either. The relationship is incidental, not causal.

Interview angle

  • “Is REST stateful or stateless?” — stateless by Fielding’s definition. Each request contains everything the server needs; no server-side per-client session state. In practice, most “REST APIs” use cookies + server-side sessions (technically stateful) but with shared session stores so any app server can handle any request.
  • “What does ‘stateless’ actually mean?” — the server doesn’t keep per-client state in its own memory between requests. Each request is self-contained (carries auth, context, identifiers). Resource state in the DB is fine — that’s not session state.
  • “How do you scale a stateful application?” — options: sticky sessions (LB pins client to server — hot/cold imbalance, deploys drop sessions), shared session store (Redis/Memcached — adds latency + SPOF), stateless auth (JWT — hard revocation). Shared store is the common compromise.
  • “Cookie session vs JWT — which?” — cookie + Redis session for most apps (easy revocation, simple model, scales fine). JWT for serverless / edge / massive scale where shared store is impractical. Both put the session ID in a cookie; the difference is where the claims live.
  • “Why are sticky sessions a hack?” — they couple a user to a specific server. Hot spots, lost sessions on failover, bumpy deploys. Cure: externalize the session state.
  • “Are WebSockets compatible with stateless architecture?” — the connection itself is stateful (lives on one server). Application state should still be external. Only the transport is pinned; the data isn’t.
  • “True statelessness — when is it worth the cost?” — serverless / Lambda (no choice), massive scale (per-server session tables too large), edge compute, multi-region active-active. For typical web apps, Redis-backed sessions are simpler and fine.