Fundamentals
Stateless services
A stateless service can handle any request on any instance. Learn where the state goes instead, and the login bug that appears when a service keeps sessions in memory.
A service is stateless when any instance can handle any request. Nothing it needs lives only in one instance's memory or local disk. Stateless services are easy to scale out. You add instances, register them with the load balancer, and you are done. If one dies, the others carry on.
State still has to live somewhere. The work is moving it into components built to hold it.
The big idea
Every piece of state has a better home than a process's memory. A process can be restarted, redeployed or replaced at any moment, and its memory disappears with it. Shared components keep state across those events. The table below shows where each kind goes.
| State | Where it goes instead |
|---|---|
| User sessions | A shared store such as Redis, or a signed token the client carries, such as a JWT |
| Uploaded files | Object storage such as S3, never local disk |
| Background work | A queue, not an in-memory list that vanishes on deploy |
| Shared caches | A distributed cache such as Redis or Memcached, covered in caching |
| Business data | The database |
| Local caches | Can stay in-process, if a miss is slower but not wrong |
Walking through it
Here is the classic bug. A server stores login sessions in a local map:
// Sketch: an Express-style app. Breaks as soon as there are two instances.
const sessions = new Map<string, Session>()
app.post("/login", (req, res) => {
const id = crypto.randomUUID()
sessions.set(id, { userId: req.body.userId })
res.cookie("sid", id).send("ok")
})
app.get("/me", (req, res) => {
const s = sessions.get(req.cookies.sid) // undefined on the other instance
if (!s) return res.status(401).send("log in again")
res.json({ userId: s.userId })
})Run two instances behind a load balancer and trace one user:
POST /login → instance A stores session abc
GET /me → instance B: no session abc → 401 "log in again"Sticky sessions at the load balancer hide the bug by sending each user to the same instance. That creates a new problem. If instance A dies, everyone pinned to it is logged out, and the load balancer cannot rebalance freely. Moving sessions into Redis, or into a signed cookie, fixes the root cause.
Note
Stateful tiers, such as databases, caches, brokers and connection gateways, can also scale out. That takes real machinery: replication to copy data, sharding to split it, and consensus to agree on who is in charge.
Common mistakes
- Storing uploads on local disk. A second instance cannot see the file.
- Using sticky sessions as the fix. They hide the state and make failover messy.
- Keeping correctness-critical state in memory. A local cache is fine if a miss only costs time.
Check yourself
-
Why do sticky sessions only half-solve the session problem?
Answer: losing the pinned instance logs out its users, and the load balancer cannot rebalance freely.
-
Which kind of state can safely stay in a process?
Answer: a local cache, as long as a cache miss only makes things slower and never wrong.
Key takeaways
- Stateless services scale out by adding instances and can lose any instance safely.
- Move sessions to a shared store or signed tokens, files to object storage, and background work to queues.
- Sticky sessions hide the problem and add failover risk.
- Local caches are fine when a miss is only slower.
Further learning
Next, see the order in which a growing web app adds these pieces, in the canonical scaling path.
Related
Vertical versus horizontal scaling
Scaling up with a bigger machine versus scaling out with more machines, what each costs, and when scaling up stops being the right answer.
Caching
Where to put a cache, how to read and write through it, how to size it from hit-rate math, and how to keep it from serving stale data or melting down.
Object storage
Store files and media in S3-style object stores. Keep bytes out of your app servers and databases, and design uploads, metadata and costs around that split.
Message queues
A durable buffer between the code that creates work and the code that does it. Start here for the map of queue concepts.