Part 01 · The Backend Mindset

Stateless vs Stateful Thinking

StatelessStateful

Why remembering nothing between requests makes a server easier to scale, and what it costs.

⏱ 1 hours · Station 5 of 153

The cart that kept forgetting

Your shop is growing, so you start a second copy of the server and put a load balancer in front to share the traffic. Everything still works in testing.

Then the complaints start. "I added tea, then cake, and my cart only shows cake." "I refreshed and my cart was empty." "It shows tea, then cake, then tea again, every time I reload."

The code didn't change. The only thing that changed is that there are now two copies of it. This lesson is about why that's enough to break the cart, and about the one question that decides whether a backend can grow: where does the state live?

The problem: memory belongs to one process

Last lesson sorted values into data, state, logic and computation. State is "where things stand right now": a cart, a login, a half-finished form.

The easiest place to keep state is in the server's own memory, a map inside the running program. It's fast, and it's one line of code. But that memory has three properties people forget:

Property of process memoryWhat it means for state
It belongs to one processA second copy of the server can't see it
It disappears when the process stopsEvery deploy, crash or restart wipes it
It lives on one machineWhen that machine fails, the state goes with it

A server that keeps state in its own memory between requests is called stateful. A server that keeps nothing between requests, and gets everything it needs from the request itself or from a shared store, is called stateless.

Think of a doctor's clinic

Picture a clinic with two doctors working the same shift.

In the stateful clinic, each doctor keeps patient notes in their own head. You see Dr. A on Monday. On Wednesday Dr. A is busy, so you see Dr. B, who has never heard of you. If Dr. A goes on holiday, your history goes with them.

In the stateless clinic, the doctors remember nothing between patients. Your file sits in a shared records room, and whichever doctor is free pulls it out, reads it, adds a note and puts it back. Any doctor can see you. A doctor can leave and a new one can start, and no history is lost.

The stateless doctors aren't less capable. They just don't own anything. That's what makes them interchangeable.

where does the cart live?browserload balancercopy Acopy Bshared storetea, caketeacakeown memory: the cart is splitteacakestateless copies: any one can answer
The load balancer alternates between two copies. When each copy keeps carts in its own memory, tea lands in A and cake lands in B, and the cart is split. When both copies use one shared store, either one can answer and the cart stays whole.

Where the analogy stops working. Fetching a paper file takes minutes; reading from a shared store takes about a millisecond, but that millisecond is paid on every request, and the shared store becomes a single important machine that everything depends on. Stateless servers don't make state disappear. They move it somewhere that has to be built carefully. (Module 6.)

The building blocks

  1. Every useful system has state. Logins, carts, game scores, chat history. "Stateless" never means "no state". It means the server process doesn't keep it between requests.
  2. State can live in three places:
    • In the client. A cookie, local storage, a token, or a hidden form field. The client sends it with each request. Cheap, but the client can read and change it (lesson 2), and it's limited in size.
    • In the server's memory. Fast and simple. Lost on restart, and invisible to other copies.
    • In a shared store. A database, or a fast key-value store like Redis. Every copy reads and writes the same place. Survives restarts. Costs a network hop per request.
  3. A stateless server treats every request as if it's the first. Everything it needs comes from the request (a token, the item id) or is looked up in the shared store. Nothing is carried over in the process.
  4. Why stateless wins for most web servers:
    • Scale out by adding copies. Any copy can serve any request.
    • Restart freely. Deploys, crashes and autoscaling lose nothing.
    • Balance load simply. The load balancer doesn't have to remember who went where.
  5. Where stateful is the right answer: things that are naturally tied to one live connection or are too hot to fetch every time: a WebSocket chat connection, a multiplayer game room, a database itself, an in-memory cache. These are designed stateful on purpose, with a plan for failure.
Predict first

Your stateful server keeps carts in memory. You add a second copy and the load balancer alternates requests: A, B, A, B. A user adds tea (goes to A), then adds cake (goes to B), then opens the cart (goes to A). What do they see?

Behind the scenes: one cart, two copies

The same user, the same four clicks, with state in process memory.

Step through it

A stateful cart behind a load balancer

BrowserLoad balancerCopy ACopy B

Step 1/6 Add tea

The browser sends POST /cart {item: tea} with Alice's session cookie. It has no idea there are two servers.

request
add tea

The picture to keep in your head

Draw your servers as identical, empty boxes, and draw the state outside them:

  • client-side state on the left (cookies, tokens),
  • server copies in the middle, with nothing inside them,
  • shared state on the right (database, Redis).

If you can delete any middle box at any time and a user wouldn't notice, your servers are stateless. If deleting a box loses something a user cares about, that box is stateful, and you need to know exactly what it holds and what happens when it dies.

Try it: stateful or stateless?

For each design, decide whether the server process is stateful, and whether that's a problem.

Try each case

Where does this state live?

sessions = {abc123: alice}
in process memory
breaks: Stateful. Every restart logs everyone out, and with two copies, users are randomly logged out depending on which copy they hit. Move sessions to Redis or a database, or use a signed token the client carries.

Code from first principles

Two copies of the same server, A and B, with a load balancer that sends each request to one of them. The same six steps run twice side by side:

  • stateful: each copy keeps carts in its own memory.
  • stateless: both copies keep nothing and use one shared store (standing in for a database).

Restarting a copy is modelled as replacing it with a fresh one, which is exactly what happens to a real process.

package main

import (
	"fmt"
	"strings"
)

// A store maps a user to their cart. Each server copy can have its own, or share one.
type store map[string][]string

// server is one running copy of the backend. It only knows the store it was given.
type server struct{ carts store }

func (s *server) add(user, item string) { s.carts[user] = append(s.carts[user], item) }

func (s *server) view(user string) string {
	if len(s.carts[user]) == 0 {
		return "(empty)"
	}
	return strings.Join(s.carts[user], ", ")
}

func main() {
	// Stateful: each copy keeps carts in its own memory.
	stateful := []*server{{store{}}, {store{}}}
	// Stateless: the copies keep nothing; both use one shared store (think: a database).
	shared := store{}
	stateless := []*server{{shared}, {shared}}

	steps := []struct{ do, item, copy string }{
		{"add", "tea", "A"},
		{"add", "cake", "B"},
		{"view", "", "A"},
		{"view", "", "B"},
		{"restart", "", "A"},
		{"view", "", "A"},
	}
	fmt.Printf("%-10s %-4s %-26s %s\n", "request", "copy", "stateful (own memory)", "stateless (shared store)")
	for _, st := range steps {
		i := strings.Index("AB", st.copy) // the load balancer sent this request to copy A or B
		switch st.do {
		case "add":
			stateful[i].add("alice", st.item)
			stateless[i].add("alice", st.item)
		case "restart":
			stateful[i] = &server{store{}} // a fresh process starts with empty memory
			stateless[i] = &server{shared} // ...but it reconnects to the same shared store
			fmt.Printf("%-10s %-4s %-26s %s\n", "restart", st.copy, "memory wiped", "nothing lost")
			continue
		}
		label := strings.TrimSpace(st.do + " " + st.item)
		fmt.Printf("%-10s %-4s %-26s %s\n", label, st.copy, stateful[i].view("alice"), stateless[i].view("alice"))
	}
}

Output (the same in all five languages):

request    copy stateful (own memory)      stateless (shared store)
add tea    A    tea                        tea
add cake   B    cake                       tea, cake
view       A    tea                        tea, cake
view       B    cake                       tea, cake
restart    A    memory wiped               nothing lost
view       A    (empty)                    tea, cake

Reading the output:

  • add cake on B: the stateful column shows only cake, because B started a brand-new cart. The stateless column shows tea, cake: B wrote to the same store A did.
  • view on A, view on B: the stateful answers disagree (tea vs cake) for the same user at the same moment. That's the "my cart keeps changing" complaint.
  • restart A: the stateful copy loses everything it held; the stateless copy loses nothing, because it held nothing.
  • view on A after the restart: (empty) vs tea, cake. With stateless copies, you can restart, replace or add servers at any time.

Notice that the server code itself is identical in both columns. The only difference is which store it was given. Stateless is a decision about where state lives, not a different kind of code.

Real engineering

  • Make web servers stateless by default. It's what lets platforms autoscale, deploy without downtime and replace failed machines automatically. Twelve-factor apps call this "processes are stateless and share nothing".
  • Sticky sessions are a workaround, not a fix. A load balancer can pin a user to one copy (by cookie or IP). The cart stops splitting, but load spreads unevenly, and when that copy restarts the user's state is still lost.
  • Choose the state's home by how it's used. Sessions and carts: Redis or a database. Files: object storage. Long-lived connections (WebSockets): stateful servers with a plan for reconnecting. Short-lived, safe-to-lose copies: in-memory caches.
  • Client-side state must be verified. Tokens and cookies the server didn't sign can't be trusted (lesson 2). Signed tokens remove the lookup, but make logout and revocation harder.
  • The shared store becomes critical. Stateless servers are easy to scale; the database behind them isn't. That's why Module 6 spends so long on databases, and Module 8 on what happens when even one database isn't enough.

When it breaks

Try each case

Pick a failure

2 copies → 50% of requests say 'please log in'
breaks: Sessions were stored in each process's memory. Diagnose by checking which copy served each failed request (add the instance id to logs). Fix by moving sessions to a shared store.

Think before you read on

A team says: "Our API is stateless. We use JWT tokens, so we don't store sessions anywhere." A user reports that after changing their password, their stolen phone is still logged in. Is the API really stateless, and what went wrong?

Reveal

The servers are stateless, but the system still has state: it's inside the token on the stolen phone, and the server agreed to trust it until it expires. Changing the password didn't cancel it, because there's nowhere the server checks. The usual fixes put a little state back: short token lifetimes plus refresh tokens stored server-side, or a revocation list checked on each request. Stateless is a trade-off, not a free lunch.

Check yourself

1. What does "stateless server" actually mean?

Answer

The server process keeps nothing between requests. Each request carries what it needs or the server looks it up in a shared store, so any copy can handle any request.

2. Name the three places state can live, with one weakness of each.

Answer

The client (can be read and changed by the user, size-limited), the server's memory (lost on restart, invisible to other copies), a shared store (an extra network hop on each request, and a critical dependency).

3. Why does adding a second copy break a stateful server even though no code changed?

Answer

Each copy has its own memory. Requests from one user land on different copies, so their state is split between them and each copy gives a different answer.

4. Give one example where a stateful server is the right design.

Answer

A multiplayer game room, a WebSocket chat server, a database or an in-memory cache: cases where the state is too hot or too tied to a live connection to fetch on every request.

Your challenge: survive a restart

Take the program and add a copy C that the load balancer can also send requests to. Then change the steps so that copy B is restarted in the middle, and a final view goes to C.

  • Predict both columns before running it.
  • Then add a third column, sticky: each copy has its own memory, but the load balancer always sends alice to the copy she first used.

You've succeeded when your output shows the stateless column always printing the full cart, the stateful column printing different carts per copy, and the sticky column working until the copy Alice is pinned to restarts, then showing (empty).

The mental model

Every system has state; the question is where it lives. Keep it in a server's memory and that server becomes special: it can't be copied, restarted or replaced without losing something. Keep the server empty and put state in the client or a shared store, and every copy becomes interchangeable. Make web servers stateless by default, and make anything stateful on purpose, with a plan for when it dies.

Station stamp

Finish the chapter

Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.

Go Pro to earn XP →