Concepts

Measuring systems

Foundations

Stateless vs Stateful

A stateless server remembers nothing between requests, so any copy can serve anyone and copies can come and go. State has to live somewhere — the question is where.

Keeping state on the server is fast and simple until there is more than one server. Moving it out costs a network hop on every request and buys the ability to scale, deploy and fail without anyone noticing.

sessionssticky sessionsshared statescale out

Try it

Move the dials — the sentence under the picture changes.
Session lives
Server 125% of sessionsServer 225% of sessionsServer 325% of sessionsServer 425% of sessionslogged out: 0%extra latency: +0 msusers pinned to servers
Sessions in each server’s memory. With a normal load balancer a user’s second request lands on a different server that has never heard of them — this does not work at all past one server. Kill one anyway.

In plain words

A stateless server keeps nothing between requests. Everything it needs to answer arrives with the request or is fetched from somewhere shared; when the request is done, the server forgets it. A stateful server remembers — who you are, what is in your cart, which page you were on — in its own memory. Stateful is simpler and faster for one server. Stateless is what lets you have more than one.

Where state can live

In the server's memory
stateful

Fastest possible. Works for exactly one server. A restart or crash loses it; a second server cannot see it.

In memory, with sticky routing
stateful, patched

The load balancer hashes the cookie so a user always lands on the same server. Works — until that server dies (its users are logged out), deploys (same), or gets more than its share of heavy users (cannot rebalance).

In a shared store
stateless server

Redis, a database, a cookie the client carries. Every request costs a lookup (~0.5 ms). Any server can serve anyone; servers can be added, removed, restarted, and nobody notices.

Press "kill a server" in each mode of the widget. The middle option is the one teams reach for when they discover the problem, and the third is the one they end up with.

Making a server stateless

  1. Find what it remembers

    Sessions, shopping carts, upload progress, WebSocket connections, in-process caches, local files, rate-limit counters. Anything a user would miss if this process vanished.

  2. Move each thing to something shared

    Sessions and carts → Redis or a signed cookie. Uploads → object storage. Counters → Redis. Files → object storage. Caches → a shared cache (or accept that a local one is only a hint).

  3. Let the client carry what it can

    A signed token (JWT) holds the user id and expiry; the server verifies the signature and needs no lookup at all. Great for identity; wrong for anything that changes often, because a token cannot be edited once issued.

BrowserServer 1Server 2RedisPOST /loginSET session:a1b2 {user: 42} EX 86400Set-Cookie: sid=a1b2GET /cart Cookie: sid=a1b2balancer picked a different serverGET session:a1b2{user: 42}cart for user 42server 2 never met this user before — it did not need to
With the session in a shared store, the second server is as good as the first.
Session middleware: memory on one server, Redis on manyTypeScript
// The only line that changes between "works on one box" and "works on fifty".
const store = new RedisStore({ client: redis, prefix: "session:", ttl:  });

app.use(session({
  store,                          // was: the default in-memory store
  secret: process.env.SESSION_SECRET!,
  cookie: { httpOnly: true, secure: true, sameSite: "lax" },
}));

Things that are stateful on purpose

Not everything can or should be stateless. Some parts are the state:

  • Databases and caches — their whole job is remembering. They scale by replication and sharding, not by being interchangeable.
  • WebSocket gateways — a held connection is state you cannot move. The chat problem keeps them stateless in every other way so a reconnect can land anywhere.
  • Stream processors — a running count over the last five minutes lives somewhere. Frameworks checkpoint it to shared storage so a worker can be replaced.

The rule is not "no state". It is: know exactly which parts hold state, keep them few, and make everything else interchangeable.

Where it goes wrong

  • Hidden state. An in-process cache that "just" speeds things up — until two servers cache different values and users see the price flip between page loads. Local caches are fine as long as nothing depends on them.
  • Sticky sessions as a permanent fix. They make the pool impossible to drain for a deploy and turn one server's death into a mass logout. A stepping stone, not a destination.
  • Tokens for mutable things. A JWT that says "role: admin" is admin until it expires, even after you revoke it. Keep short-lived tokens or a revocation check for anything security-relevant.
  • The shared store as a single point of failure. Moving sessions to one Redis moves the outage. Replicate it — and decide what a Redis outage does (everyone logged out, or degraded mode).

Take this with you

  • The one idea: state has to live somewhere. Keep it out of the servers you want to be interchangeable, and know exactly where it went.
  • In an interview, say the servers are stateless and where each kind of state lives; name the stateful parts on purpose.
  • At work, kill one server in staging with real users on it. Whoever gets logged out tells you where the hidden state is.