Sticky sessions, the vertical ceiling, and why state — not traffic — is what actually makes scaling hard.
Module F-9 — Scaling Up vs Scaling Out; Stateless vs Stateful
What this module covers: What actually stops you from adding a second server. This module covers vertical vs horizontal scaling and why "scale up first" is better advice than it sounds, then the real subject: state. Every kind of state that hides in an application — sessions, in-memory caches, local files, open sockets, in-process schedulers, rate-limit counters — and where each one has to move before a second instance is safe. Plus sticky sessions and the four things they cost, the services that are irreducibly stateful, and the reason scaling out the app tier so often just relocates the bottleneck.
Two Ways to Get Bigger
Scaling up (vertically) means a bigger machine: more cores, more RAM, faster disks. Scaling out (horizontally) means more machines.
| Scale up | Scale out | |
|---|---|---|
| Code changes | Usually none | Statelessness required |
| Ceiling | Hard — the largest instance available | Effectively none |
| Cost curve | Super-linear at the top end | Roughly linear |
| Failure domain | One machine — a single point of failure | Many; one loss is survivable |
| Resize | Often needs a restart | Add or remove instances live |
| Operational surface | One thing to run | Load balancing, discovery, config, deploys across N |
The conventional advice is "scale out, it's the modern way." The better advice is: scale up first, but design so you can scale out.
Why scaling up first is usually right:
- It's astonishingly effective. Cloud instances go to hundreds of vCPUs and terabytes of RAM. A dataset that fits in RAM does not need a distributed cache, and a Postgres with 512 GB of
shared_buffershandles workloads people assume require sharding. - It's a card you can play in an afternoon. Resizing an instance is minutes of work. Making a stateful application horizontally scalable is weeks, and introduces new failure modes you'll be debugging for months.
- The complexity you avoid is the expensive part. Every module in Phase 3 exists because distribution creates problems that a single machine doesn't have.
Why designing for scale-out anyway matters:
- The ceiling is real and it arrives suddenly. When you're on the largest available instance, there is no next step — and by then the migration is urgent, which is the worst time to attempt it.
- One machine is one failure domain. Vertical scaling gives you no availability story. A single instance cannot be patched, redeployed, or rebooted without downtime.
- Cost turns super-linear. The top-end instance is disproportionately expensive per unit of capacity, so at some point three medium instances genuinely beat one large one on price (Module O-8).
So: run on one box for as long as it's honestly working, and keep the application able to run as many. That second clause is the entire content of the rest of this module.
Analogy: scaling up is hiring a stronger single worker; scaling out is hiring a team. The team is unboundedly more capable, but only if the work can be divided — and work can only be divided if nobody is holding something the others need. State is that something.
State Is the Only Thing That Makes This Hard
An instance is stateless if any request can be served by any instance, and losing an instance loses nothing but the requests currently in flight. Statelessness isn't "no data" — it's "no data here." The data lives in a shared store; the instance is a replaceable worker.
Here is every place state hides in a typical application, and where it has to go:
In-memory sessions. The default in many frameworks. Instance A holds the session; the load balancer sends the next request to instance B; the user is logged out. → Move to Redis or a signed stateless token (Module A-13 covers the trade: revocation is easy with server-side sessions and awkward with JWTs).
In-process caches. A Map of computed results. With N instances you get N independent caches: N× the memory, a fraction of the hit rate, and N different answers to the same question after an update. → Shared cache (Module P-12). Worth noting the honest exception: a small, short-TTL in-process cache in front of a shared cache is a legitimate optimisation, as long as you've accepted that instances can briefly disagree.
Local file storage. Uploads written to ./uploads. Instance B can't serve a file instance A received, and a container restart deletes everything. → Object storage with pre-signed uploads, which also keeps the bytes out of your app tier entirely (Module A-11).
Long-lived connections. WebSockets and SSE bind a client to a specific instance for the connection's life. Publishing an event on instance A doesn't reach a subscriber on instance B. → A pub/sub backplane so any instance can deliver to any connection (Module A-10).
In-process schedulers. setInterval or an in-app cron. With N instances the job runs N times — which for a billing or email job is a customer-visible incident. → An external scheduler, or a distributed lock/leader election so exactly one instance acts (Modules A-2, P-19).
In-memory counters. Rate limiters, feature-flag caches, circuit-breaker state. A per-instance limit of 100 req/min becomes an effective limit of N × 100, and the client hitting a different instance each time sees no limit at all. → Redis-backed counters (Module P-18).
Uploaded-file temp state and multi-step wizards. Anything held between two requests belonging to the same user, in memory. → Externalise or make it client-carried.
Why this matters in production: these fail intermittently, which is what makes them expensive. With two instances, half the requests work. Users report "sometimes it logs me out," QA can't reproduce it, and the bug survives for weeks because the failure is a coin flip rather than a crash.
Sticky Sessions: The Tempting Shortcut
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion