Stateless vs Stateful Servers: Sessions, Sticky Sessions and Shared State

Why scalable web servers are stateless: the lost shopping cart, sticky sessions, client-side storage options and shared session stores like Redis.

Beginner 5 min read · Updated

A server is stateful when it keeps data about a user (a login session, a shopping cart) in its own memory between requests, so that user's next request must come back to the same server. A server is stateless when it keeps nothing between requests: every request carries or points to everything needed, so any server can handle any request. Stateless servers are what make horizontal scaling and load balancing work, which is why almost every modern web tier is stateless.

The problem: the disappearing shopping cart

Take an online store that has grown to several web servers behind a load balancer. Shopping carts are usually built on user sessions, and a simple session implementation keeps each session in the memory of the server that created it.

A user adds a bag of dog food to their cart. Server 1 handles that request, creates a session and sets a cookie for it. The user's next request goes to server 2, which has never heard of that session. The cart is empty. The user is (rightly) frustrated.

UserLoad balancerServer 1Server 2add dog food to cartforwardserver 1 stores the cart in its memorySet-Cookie: session=abcview cart (session=abc)forwardcart is empty ✗
Session state kept in one server's memory: the second request lands on a different server and the cart has vanished.

The root cause: session data (the state) is stored inside each server, so the servers are stateful. There are two ways out: make the load balancer always send a user to the same server, or remove the state from the servers.

Option 1: sticky sessions (stay stateful)

Configure the load balancer for session affinity ("sticky sessions"): it remembers which server each user started on and sends all of that user's requests there for the rest of the session. The server keeps the session state and keeps working. It works, but it has real costs:

  • Server failure loses sessions. If the server dies, every session stored on it is gone, and those users lose their carts or get logged out.
  • Harder to scale. The load balancer has to track a mapping from each user to a server, which is itself state it must maintain.
  • Uneven load. Traffic can't be spread freely: some servers end up overloaded with long sessions while others sit idle, and new servers only receive new users.

Option 2: make the servers stateless

Move the state out of the web servers. There are two places to put it: on the client, or in a shared store that every server can reach.

Keep state on the client

Store the cart in the user's browser or app, for example in Local Storage or IndexedDB in a browser, or Core Data on iOS:

  1. When the user adds an item, the client saves the cart in its own storage.
  2. Every later request sends the cart data along, in the body or a custom header.
  3. The server uses the cart it was sent (to compute the total, apply discounts) and responds. It remembers nothing between requests.
request + cartBrowsercart in Local StorageLoad balancerServer 1statelessServer 2statelessServer 3stateless
Client-side state: the browser keeps the cart and sends it with each request, so any server can answer.

There are several places on the client to keep state:

Client storageGood forProsConsUse when
CookiesSessions, login, preferences such as language or a small cartSupported everywhere; sent automatically with every HTTP requestSmall (about 4 KB); security risks such as CSRF if mishandledSmall data that the server needs on every request and that must persist across visits
Local Storage / Session StorageSettings (theme, layout); session storage for temporary data like a multi-step formSimple API; larger (roughly 5–10 MB)Local storage never expires on its own; session storage ends with the tabLong-lived client data (local) or one-session data (session)
IndexedDBLarge datasets, offline-first apps (a notes app storing notes for offline use)Large, structured, asynchronousMore complex APIComplex apps that store and query lots of data on the client. See offline-first sync
Client-side JavaScript state libraries (Redux for React, Vuex/Pinia for Vue)Global UI state in single-page apps: login status, shared user data across componentsCentralized and easy to debugExtra complexity; overkill for simple appsSPAs with complex state. See frontend state management
JWT (JSON Web Tokens)Carrying the user's identity and claims to APIs after loginStateless authentication; self-contained; works across domainsWhere to store it is tricky; exposed to XSS if stored carelesslyRESTful APIs and SPAs that need security, scale and cross-domain access. See authentication

Never trust state that comes from the client. A cart sent by the browser can be edited, so the server must re-check prices, stock and discounts every time; signed tokens (JWTs) can be trusted only after verifying the signature.

Keep state in a shared store

Put an in-memory store such as Redis or Memcached behind the web servers, shared by all of them:

  1. When the user adds an item, the server creates a session and saves the cart in the shared store.
  2. It sends the browser a session identifier, typically in a session cookie.
  3. The browser sends that identifier with every request.
  4. Whichever server receives the request uses the identifier to load the session (the cart) from the shared store, makes the change and saves it back.
Browsercookie: session=abcLoad balancerServer 1Server 2Server 3Redis / Memcachedsession abc → cart
Shared session store: any server can load any user's session from Redis using the session ID in the cookie.

Because every server can reach the store, sessions survive any one server failing, and the load balancer can send each request anywhere. Redis and Memcached keep data in memory, so lookups are much faster than a database. They're the standard choice for session storage, and clouds offer them as managed services (for example AWS ElastiCache for Redis and Memcached). Make the store itself highly available, since it is now a critical dependency; see cache high availability.

Which to use

Sticky sessionsClient-side stateShared session store
Where state livesOne server's memoryBrowser or appRedis / Memcached
Server failureSessions on it are lostNo effectNo effect (if the store is replicated)
Load balancingUneven, user pinnedFreeFree
LimitsHard to scaleSize limits; must not trust client dataAn extra dependency and network hop
Good forLegacy apps you can't changePreferences, small carts, JWT identityLogins, carts, anything sensitive or large

Most modern production systems run stateless web servers: compute is the cheap, easy-to-add resource and can scale almost without limit, and autoscaling can add servers when demand rises and remove them when it falls. State goes to the client, to a shared cache, or to the database; never into one server's memory.

The same rule covers other kinds of state: put uploaded files in object storage rather than on local disk, and put background work on a queue rather than in in-process threads that die with the server.

Where it’s used

E-commerce carts

Logged-out carts often live in a cookie or local storage; logged-in carts in a shared store or database, so they follow the user across devices.

Login sessions

Session IDs in cookies backed by Redis (Reddit moved its sessions from server memory to Redis to scale out), or self-contained JWTs.

Autoscaled web fleets

Kubernetes and cloud autoscaling groups add and remove identical stateless instances freely because no instance holds user data.

Serverless functions

Functions are stateless by design; anything that must persist goes to a database, cache or object store.

In an interview

  • Say your app servers are stateless and name where state lives (Redis sessions, JWT, database, object storage).
  • If sticky sessions come up, explain why they hurt failover and load balancing.
  • Re-validate anything stored on the client before trusting it.
  • Make the shared session store highly available, since every request now depends on it.