# Designing for Scale (Load Balancing & Statelessness)

> How to take on more load without falling over: scale out instead of just up, make your servers stateless so any box can handle any request, put a load balancer in front, and push the parts that can't be cloned - sessions, the database, the cache - out to the edges.


---

# Designing for Scale (Load Balancing & Statelessness)

Your app works. Then it works *too well* - traffic climbs, the one server you've been running starts to sweat, response times creep up, and you get the message every engineer eventually gets: "it needs to handle more load." The panic move is to buy a bigger box and hope. That buys you a little time and teaches you nothing, and one day there is no bigger box to buy.

This guide is about the calm alternative: designing a system that grows by *adding* machines instead of *replacing* them - and the one property that makes that possible. Almost all of scaling comes down to a single idea, and once you see it, the architecture diagrams stop looking like magic. The idea is this: **if any server can handle any request, you can add servers freely.** Everything else here - load balancers, stateless services, shared session stores - is in service of that one sentence.

## How to read this
- **Need the mental model fast?** Read [Phase 1: Scale Up vs Scale Out](01-scale-up-vs-scale-out.md) - statelessness is the whole game, and it's explained there first.
- **Want it to finally make sense?** Read in order. The statelessness idea makes load balancing make sense, and load balancing makes the "what about the stateful bits?" question make sense.

## The phases
1. **[Scale Up vs Scale Out, and Why Statelessness Matters](01-scale-up-vs-scale-out.md)** - bigger box (simple, capped) vs more boxes (the real answer for big scale), and the property that unlocks the second one: statelessness.
2. **[Load Balancing](02-load-balancing.md)** - spreading requests across many identical servers: what a load balancer actually does, health checks, and the sticky-session trap.
3. **[Scaling the Stateful Bits](03-scaling-the-stateful-bits.md)** - the parts you can't just clone: sessions (move them to a shared store), the database (the usual bottleneck), and caching to shed load.

> Deliberately deferred to follow-up guides: scaling the database itself (replication and sharding) lives in [Scaling a Database](/guides/scaling-a-database); the mechanics of caching live in [Caching, Explained](/guides/caching-explained); and what to do when a machine *dies* rather than just gets busy is [Designing for Failure](/guides/designing-for-failure). This guide is about handling more load. Those are about handling everything around it.


---

# Scale Up vs Scale Out, and Why Statelessness Matters

You have one server. It's getting busy. There are exactly two directions you can go: make *that machine*
more powerful, or run *more machines*. That's it. Everything in this guide is a consequence of which one
you pick, and the second one - the one you'll need for real scale - only works if your servers have a
specific property, and one of the two has a hidden requirement that trips up everyone the first time.
We'll build to that property, because it's the single most important idea here.

## Scale up: a bigger box

📝 **Terminology.** *Scaling up*, also called *vertical scaling*, means giving your existing machine more resources - more CPU cores, more RAM, faster disks. Same machine, same setup, just beefier.

**What it actually is.** Scaling up is replacing your server with a bigger server (or, in the cloud, resizing it to a larger instance type). Nothing about your application changes. It's still one process on one machine; that machine just has more room to breathe.

**Why people reach for it first.** It's wonderfully simple. There's nothing to redesign, no new moving parts, no new failure modes. Your code doesn't know or care that it's running on a bigger box. For a long time, for a lot of applications, this is genuinely the right call - a single modern server is enormous, and resizing an instance is a five-minute job.

**Why it runs out.** Two walls, and you'll hit both eventually:

- **There's a biggest box.** Hardware has a ceiling. You can rent machines with hundreds of gigabytes of RAM, but you cannot rent an infinitely large one, and the price climbs much faster than the power as you approach the top of the range.
- **It's still one machine.** This is the quieter, more dangerous limit. A single box is a single point of failure. When it reboots, restarts, or dies, your *entire* application is down - there is nowhere else for requests to go. No amount of "bigger" fixes "only one."

💡 **Key point.** Scaling up buys time and simplicity, not headroom for the long run. It's the move you make until you can't - and "you can't" usually arrives as either a price you won't pay or an outage you can't afford.

## Scale out: more boxes

📝 **Terminology.** *Scaling out*, also called *horizontal scaling*, means running more machines and spreading the work across them. Instead of one server doing all the work, you have a *pool* of identical servers sharing it.

**What it actually is.** Scaling out is turning "my app server" into "my app servers" - several identical copies of the same application, each on its own machine, each capable of handling requests. When load grows, you add another copy. When it shrinks, you remove one. Capacity becomes a dial you can turn, not a box you have to replace.

**Why this is the real answer for big scale.** It removes both walls at once. There's no single biggest box to worry about, because you're adding boxes, not growing one. And there's no single point of failure, because if one server dies, the others keep serving - the pool absorbs it. This is how every system that handles serious traffic is built. Not one heroic machine; a herd of ordinary ones.

Here's the shape of the two approaches, side by side:

```text
   SCALE UP (vertical)                    SCALE OUT (horizontal)
   ─────────────────────────────         ─────────────────────────────
   one machine, made bigger        │     many identical machines
   nothing to redesign             │     needs stateless servers (below)
   simple, one mental model        │     a pool you add to / remove from
   capped - there's a biggest box  │     scales far past any single box
   one machine = one failure point │     one dies, the rest carry on
   resize and you're done          │     add a load balancer in front (Phase 2)
```

**The catch nobody mentions up front.** Scaling out *sounds* like the obvious win, so why doesn't everyone start here? Because it has a requirement scaling up doesn't. If five identical servers can each get any request, every server has to be able to handle *any* request, with no special knowledge only it possesses. The moment one server knows something the others don't, your pool of interchangeable machines stops being interchangeable - and the whole model breaks. That property has a name, and it's the heart of this guide.

## Statelessness: the property that makes scale-out work

📝 **Terminology.** A server is *stateless* when it keeps no per-user, per-conversation information in its own memory between requests. Each request carries (or can look up) everything needed to handle it, so the server can treat every request as if it had never seen this user before. The opposite - *stateful* - means the server remembers things about you locally, and your next request has to come back to *that same server* to find them.

**What it actually is.** A stateless server is one where it genuinely does not matter which machine in the pool handles your request. Request one goes to server A, request two goes to server C, request three goes to server A again - and everything works identically, because none of them is hoarding anything about you in local memory. They're interchangeable. That interchangeability is the entire point: **if any server can handle any request, you can add servers freely.**

**Why this is the unlock.** Go back to the dial. To turn capacity up, you add a server - but a freshly booted server knows *nothing* about any user. If your application requires that the new server somehow already remembers your logged-in session, your shopping cart, or where you are in a multi-step form, then a new server is useless until it magically acquires that knowledge. Statelessness sidesteps the whole problem: there's nothing to remember locally, so a brand-new server is immediately as useful as an old one. Adding capacity becomes trivial precisely *because* no server holds anything special.

**A real example.** The client sends a token with the request; any server can verify it and respond. Here the same authenticated call hits two different machines and behaves identically:

```console
$ curl -s -H "Authorization: Bearer eyJhbGci..." https://api.example.com/me
{"served_by":"app-server-A","user":"ada","plan":"pro"}

$ curl -s -H "Authorization: Bearer eyJhbGci..." https://api.example.com/me
{"served_by":"app-server-C","user":"ada","plan":"pro"}
```

*What just happened:* the first request was routed to `app-server-A`, the second to `app-server-C` - same answer both times, no error, no re-login. Each server figured out who you were *from the request itself* (the `Authorization` token), not from anything stashed in its own memory. Neither server needed to have met you before. That's statelessness in one transcript: the server identity changed and nothing else did.

⚠️ **Gotcha - in-memory session state quietly breaks all of this.** The classic mistake is storing per-user data in the server's own memory: the logged-in user object, a shopping cart, the step of a checkout wizard. On *one* server this works perfectly and sails through development. Add a second server, requests start landing on whichever machine is free, and users get randomly logged out or watch a form forget step two on step three. The data is sitting in server A's memory, but the request went to server B, which never heard of it - nothing crashed, it just *forgets* intermittently, which is maddening to debug. The fix is [Phase 3](03-scaling-the-stateful-bits.md): take state *out* of local memory and put it somewhere all servers can reach.

🪖 **War story.** A team scaled from one app server to three for a launch, deployed on a Friday, and spent the weekend chasing a "random logout" bug they could never reproduce locally - because locally they only ran one server, so every request hit the same memory. In production, the load balancer was doing exactly its job, fanning requests across all three, and one in three landed on a server that didn't hold the session. The bug wasn't random. It was the architecture telling them their servers were secretly stateful.

**Why this saves you later.** Design for statelessness from the start - every request self-describing, nothing important kept in local memory - and scaling out becomes almost boring: add a machine, point the load balancer at it, done. No migration, no "what about the sessions on the old box?" Your servers become *cattle, not pets* - identical, disposable, replaceable. That's the foundation the next two phases stand on.

## Recap

1. **Scale up** = a bigger box. Simple and requires no redesign, but it's capped (there's a biggest box) and it's a single point of failure (one machine, one outage).
2. **Scale out** = more identical boxes sharing the work. It's the real answer for serious scale: no single biggest box, and one failure doesn't take everything down.
3. **Scaling out has a requirement: statelessness.** If any server can handle any request, you can add servers freely. That only holds when no server keeps per-user state in its own local memory.
4. **In-memory session state is the silent killer** - it works on one box, then causes intermittent "forgot who you are" bugs the moment you add a second. The fix is to externalize state (Phase 3).

Next, the piece that actually sends each request to one of your interchangeable servers - and the trap that tries to make them stateful again.


---

# Load Balancing

You've got a pool of identical, stateless servers from Phase 1. Now there's an obvious question: when a request arrives, *who decides* which server handles it? Your users only know one address - `api.example.com` - they don't know or care that there are five machines behind it. Something has to stand at the front door and direct traffic. That something is a load balancer, the piece that turns "a bunch of servers" into "a service" - and it also offers one feature that can quietly drag you back into the stateful world you worked so hard to leave.

## What a load balancer actually is

📝 **Terminology.** A *load balancer* (often shortened to *LB*) is a server whose entire job is to receive incoming requests and forward each one to one of several backend servers. *Backend* here means your actual application servers - the pool. The load balancer is the public-facing front; the backends do the real work.

**What it actually is.** A load balancer is a traffic director that sits between your users and your server pool. Every request hits the load balancer first; it picks one healthy backend and forwards the request there, then passes the response back to the user. The user never sees the backend's address - as far as they're concerned, the load balancer *is* the service.

Here's the whole arrangement in one picture:

```mermaid
flowchart TD
  Req([requests]) --> LB["LOAD BALANCER<br/>health-checks + distributes<br/>(the only address users know)"]
  LB -->|traffic| A["server A (healthy)"]
  LB -. "no traffic - failed health check" .-> B["server B (DOWN)"]
  LB -->|traffic| C["server C (healthy)"]
```

**Why people get this wrong.** Newcomers picture a load balancer as something exotic and heavyweight. It isn't. Conceptually it's a smart receptionist: it knows which desks are staffed, and it hands each visitor to a free, working one - two jobs, done relentlessly: *distribution* (spreading the requests) and *health checking* (knowing which backends are actually up). Get those two ideas and you understand a load balancer.

## Job one: distribution

The load balancer has to decide which backend gets each request. The common strategies are simpler than their names suggest:

- **Round robin** - hand requests out in rotation: A, B, C, A, B, C… Dead simple, and surprisingly effective when your servers are equal and your requests are roughly equal in cost. This is the default in many setups.
- **Least connections** - send the next request to whichever backend currently has the fewest in-flight requests. Better when some requests take much longer than others, so a slow request doesn't pile more onto an already-busy server.
- **Weighted** - give some backends a bigger share, useful when machines aren't identical (a beefier box can take more).

💡 **Key point.** Notice *why* round robin is even allowed to be this dumb: it works precisely because your servers are stateless and interchangeable (Phase 1). If it didn't matter which server handled a request, then a blind rotation is perfectly safe. The simplicity of the load balancer is a *reward* for the statelessness you built. Break statelessness and even the cleverest distribution strategy can send a user to a server that doesn't have their data.

## Job two: health checks

📝 **Terminology.** A *health check* is a small request the load balancer sends to each backend on a schedule - often a hit to a dedicated endpoint like `/health` - to ask "are you alive and able to serve?" A backend that responds correctly is *healthy* and stays in rotation; one that fails (errors, times out, or returns the wrong thing) is marked *unhealthy* and pulled out.

**What it does in real life.** This is the feature that makes a pool resilient instead of just big. The load balancer is constantly, quietly polling every backend. The instant one stops answering - it crashed, it's overloaded, someone's deploying to it - the load balancer notices and *stops sending it traffic*. Users never see the dead server, because their requests are routed only to the ones that passed the most recent check.

**A real example - watching the load balancer pull a sick backend.** Here's what a health-check log looks like when a backend goes bad and comes back:

```console
$ tail -f /var/log/lb/health.log
10:42:01  check server-A /health -> 200 OK (12ms)   [healthy]
10:42:01  check server-B /health -> 200 OK (15ms)   [healthy]
10:42:01  check server-C /health -> 200 OK (11ms)   [healthy]
10:42:06  check server-B /health -> timeout (5000ms) [UNHEALTHY] removed from pool
10:42:11  check server-B /health -> timeout (5000ms) [UNHEALTHY]
10:42:16  check server-B /health -> 200 OK (18ms)   [healthy] returned to pool
```

*What just happened:* at `10:42:06` server B stopped answering its health check in time, so the load balancer marked it unhealthy and **removed it from the pool** - from that moment, no requests were sent to B. The two healthy servers absorbed its share. Ten seconds later B recovered, passed a check, and was put back into rotation automatically. No human touched anything, and no user got an error from the broken box.

⚠️ **Gotcha - your health endpoint should mean what it says.** A naive `/health` that just returns `200 OK` proves the web process is running, not that the server can do real work - it might be unable to reach the database, and happily pass health checks while failing every real request. Make the check verify what actually matters, but don't over-couple it either: if `/health` checks the database and the database blips, *every* backend fails at once and the load balancer pulls the *entire* pool, turning a small wobble into a total outage.

## The sticky-session trap

Now the part this phase exists to warn you about. Most load balancers offer a feature called **sticky sessions** (also *session affinity*), and it is a tempting, plausible-sounding mistake.

📝 **Terminology.** *Sticky sessions* mean the load balancer remembers which backend it first sent a given user to, and then keeps routing that same user to that same backend for the rest of their session - usually by tagging them with a cookie.

**Why it's tempting.** Picture the Phase 1 bug: you stored a user's session in server A's local memory, so it only works if they keep coming back to A. Sticky sessions make that bug *go away* - the load balancer faithfully sends each user back to "their" server, so the in-memory session is always there. It looks like a fix. It's actually a way to *hide* the statefulness instead of removing it.

**What it does in real life, and why it bites.** You've now pinned each user to one specific server, which quietly undoes much of why you scaled out in the first place:

- **A dead backend takes its users down with it.** When server A fails its health check and gets pulled, every user stuck to A loses their session - logged out, cart emptied - even though the pool is otherwise fine. The whole point of a pool was that one death is survivable; stickiness makes it personal.
- **Load gets lumpy.** New users spread evenly, but long-lived sessions accumulate on whichever servers have been up longest. Add a fresh server and it sits nearly idle, because no existing sessions are stuck to it - exactly when you need it to help.
- **Deploys hurt.** Restart a backend to ship code and you've just kicked every user stuck to it.

```text
   STICKY SESSIONS                        STATELESS + SHARED STORE
   ─────────────────────────────         ─────────────────────────────
   user pinned to "their" server   │     user can go to any server
   that server dies -> session lost│     a server dies -> no one notices
   load piles up unevenly          │     load spreads evenly
   new server starts out idle      │     new server is useful immediately
   a workaround for local state    │     a real fix: no local state at all
```

💡 **Key point - externalizing state is the cleaner fix.** Sticky sessions treat the symptom (state is on one server, so keep going back to it). The real cure treats the cause: *don't keep the state on a server at all.* Move it to a shared store every backend can read, and any server can serve any user again - no stickiness, no pinning, no lost sessions when a box dies. The load balancer goes back to being a simple, dumb, beautiful round-robin director. That move - pulling state out to a place all servers share - is exactly what Phase 3 is about.

🪖 **War story.** A team turned on sticky sessions to fix their random-logout bug (the Phase 1 one) and declared victory - until their busiest day, when one backend got overwhelmed by the long-running sessions piled onto it, fell over, and took a chunk of active users down with it while the other servers sat half-idle, unable to help, because those users were *stuck* to the dead box. The "fix" had converted a graceful, survivable architecture back into a fragile one. They ripped it out and moved sessions to a shared store the next week.

## Recap

1. A **load balancer** sits in front of your server pool and is the only address users know; it forwards each request to one backend.
2. It does two jobs: **distribution** (round robin, least connections, weighted) and **health checks** (pulling unhealthy backends out of rotation automatically, so users never hit a dead box).
3. Distribution can be dumb-simple *because* your servers are stateless - simplicity at the front is the reward for statelessness at the back.
4. **Sticky sessions are a trap.** They hide local state instead of removing it, and they cost you resilience (a dead box loses its users), even load, and painless deploys.
5. The clean fix is to **externalize state** so any server can serve any user - which is Phase 3.

Next, the parts of your system you *can't* clone freely - sessions, the database, the cache - and how to handle each one.


---

# Scaling the Stateful Bits

By now you have the comfortable part of the picture: a load balancer in front of a pool of identical, stateless app servers you can add and remove at will. That works because those servers remember *nothing*. But a real application clearly *does* remember things - who you're logged in as, what's in your cart, every row of data it has ever stored. So where did all that state go?

It didn't disappear. It moved. The whole trick of Phase 1 was to take state *out* of the app servers, but it has to live *somewhere*, and that somewhere is the subject of this phase - the bits you can't clone freely, the parts that resist horizontal scaling, where the hard problems concentrate. Good scaling architecture isn't about making everything stateless; it's about being deliberate about *where the unavoidable state lives* and shrinking how much of it there is.

## The shape of the problem

Here's the mental model for the whole phase. You've pushed state off the app servers - now it sits in a small number of shared places that *all* the app servers talk to:

```mermaid
flowchart TD
  Users([users]) --> LB[load balancer]
  LB --> App["app servers ×N<br/>(stateless - clone freely)"]
  App --> SS[(session store)]
  App --> DB[(database)]
  App -.-> Cache[(cache)]
  Cache -.->|sheds load| DB
```

The app servers are STATELESS - clone freely. The session store and database are STATEFUL - the hard parts, shared by every app server.

The app servers in the middle are easy - clone them all day. The shared stores at the bottom are where scaling gets real. Let's take them one at a time.

## Sessions: move them to a shared store

This is the direct fix for the bug that's haunted the last two phases - the in-memory session that breaks the moment you add a second server, and that sticky sessions only papered over.

**What it actually is.** Instead of keeping a user's session in whichever app server happened to handle their login, you keep it in a single **shared session store** that every app server can read and write. The classic choice is **Redis** - an in-memory data store that's extremely fast and lives on its own, separate from your app servers.

📝 **Terminology.** *Redis* is an in-memory key-value store (think: a giant, very fast hash map that lives on the network) commonly used for sessions, caching, and other small fast-access data. *Session store* just means "the shared place sessions live"; Redis is the usual one, but a database table works too.

**What it does in real life.** When a user logs in, their session is written to Redis under a key (often carried in a cookie). On their next request - whichever app server it lands on - that server reads the session straight out of Redis. Now it genuinely doesn't matter which server handles the request: they all look in the same place.

**A real example.** A session written once and read back from a different app server, both talking to the same Redis:

```console
# app-server-A handles the login and writes the session to shared Redis
$ redis-cli SET session:ada42 '{"user":"ada","cart":["sku-7","sku-9"]}'
OK

# later, app-server-C handles ada's next request and reads it right back
$ redis-cli GET session:ada42
"{\"user\":\"ada\",\"cart\":[\"sku-7\",\"sku-9\"]}"
```

*What just happened:* Server A wrote the session to Redis, not to its own memory. When server C later needed it, it read the exact same value out of the shared store - cart and all. Neither server kept anything locally, so the load balancer is free to send Ada wherever it likes. This is the clean fix that makes sticky sessions unnecessary.

**Why this saves you later.** The random-logout bug is gone *by construction* - there's no local session to miss, so there's nothing to be inconsistent about. You can add servers, remove servers, deploy, and reboot, and no user notices, because no user's state was ever tied to a particular box.

⚠️ **Gotcha - you just moved the single point of failure, you didn't delete it.** Your app servers are now happily disposable, but if that one Redis goes down, *every* session is gone and *every* server is stuck at once. That's a fair trade - one well-run, dedicated session store is far easier to make reliable than state smeared across a dozen app servers - but it does mean the session store now needs its *own* redundancy (a replica, failover). Keeping that store alive when machines die is the heart of [Designing for Failure](/guides/designing-for-failure).

## The database: the usual bottleneck

Here's the uncomfortable truth of scaling: you can clone app servers until you're blue in the face, and pretty soon they'll all be waiting on the *same database*. The database is where the real, permanent state lives - every user, every order, every row - and it's almost always the part that can't be casually cloned, because all those copies would have to agree with each other. It's far more often than not the actual bottleneck the whole system runs into.

**Why it's the hard part.** App servers are interchangeable because they hold nothing. The database is the opposite: it holds *everything*, and it has to be *correct*. You can't run five databases the way you run five app servers, because then a row written to one wouldn't exist on the others. Making a database take more load means special, careful techniques - read-only copies, or splitting data across machines - each with real trade-offs around consistency and complexity.

**What to reach for, in order.** The good news is the cheap fixes come first, and most applications never need the dramatic ones:

- **Optimize before you scale.** A slow database is usually a missing index or a bad query, not a hardware shortage - fixing the query is free and often a bigger win than any new machine.
- **Scale up the database** before you scale it out. A bigger single database is vastly simpler than several coordinating ones, and one beefy box carries most applications a long way.
- **Then scale out** - read replicas for read-heavy load, sharding for write-heavy load - only when you genuinely must.

This guide isn't going to re-teach all of that, because it has a proper home. The full treatment - scale up vs out for databases, replication, sharding, read-heavy vs write-heavy, and the order to try things in - is [Scaling a Database](/guides/scaling-a-database). The thing to carry from *here* is just the architectural fact: **the database is the state you can't clone, so it's where scaling gets genuinely hard, and it's usually the wall you hit first.**

## Caching: shed load instead of adding capacity

Before you take on the hard work of scaling the database, there's a move that often removes the need: stop asking the database the same questions over and over.

**What it actually is.** A **cache** is a fast, temporary store (Redis again, or an in-memory layer) that holds the answers to expensive or frequently-repeated queries, so your app servers can get them without touching the database at all. The common pattern is *cache-aside*: check the cache first; on a miss, query the database and stash the result for next time.

**What it does in real life.** If your homepage runs the same "top 10 articles" query for every visitor, you're asking the database the identical question thousands of times for an answer that changes maybe once an hour. A cache turns thousands of database hits into one, plus thousands of cheap cache reads. Crucially, caching doesn't *add capacity*, it *removes load*: every request the cache answers is a request the database never sees - frequently the highest-leverage change available for read-heavy systems.

⚠️ **Gotcha - caching trades freshness for speed, on purpose.** A cached answer can be stale until it expires or you invalidate it. The genuinely hard part isn't filling the cache, it's deciding when to throw entries away so users don't see old data - and like the session store, a cache is more shared state with its own failure modes. Full mechanics in [Caching, Explained](/guides/caching-explained). The role it plays here: shedding load off the stateful bottleneck, often buying you out of a harder scaling project entirely.

## The big picture: push state to the edges

Step back and look at what every move in this guide had in common. Statelessness (Phase 1) was about getting state *out* of the app servers. The load balancer (Phase 2) only worked *because* the servers held no state. And this phase has been about taking the state that's left - sessions, the database, the cache - and giving each piece a deliberate, shared home, then shrinking how much load actually reaches it.

💡 **Key point - the one idea to keep.** Scaling is mostly the art of **pushing state to the edges so the middle can be cloned.** The stateless middle - your app servers - is the easy, infinitely-cloneable part. The stateful edges - sessions, database, cache - are the hard parts, so you make them as few and as small as you can, give each one a single shared home, and protect that home.

One question this guide has deliberately left open: everything here assumed servers getting *busy*, not servers *dying*. The load balancer pulling a dead backend, the session store and database becoming single points of failure once centralized - surviving a machine *failing*, not just being overwhelmed, is its own discipline of redundancy and failover. That's the natural next step: [Designing for Failure](/guides/designing-for-failure).

## Recap

1. State doesn't vanish when you make app servers stateless - it **moves to a few shared stores** that every server talks to.
2. **Sessions** go in a shared store (commonly Redis) so any server can serve any user - the real fix that makes sticky sessions unnecessary.
3. The **database** is the state you can't clone, so it's the hard part and usually the first wall you hit. Optimize, then scale up, then scale out - full details in [Scaling a Database](/guides/scaling-a-database).
4. **Caching** sheds load off that bottleneck by not asking the same question twice - often buying you out of a harder scaling project. Details in [Caching, Explained](/guides/caching-explained).
5. Externalizing state **concentrates the single point of failure** into those shared stores, which then need their own redundancy - the subject of [Designing for Failure](/guides/designing-for-failure).
6. The whole game: **push state to the edges so the middle can be cloned.**
