sehaj/learn
System design/02 From one server to millions

02.03 · concept · 11 min

Stateless servers behind a load balancer

Run several copies of the app behind a load balancer. It only works once no copy keeps anything the others need.

The problem: one app server

Snip's app server is still alone. When it crashes, every redirect fails, and every deploy means a few seconds of errors while it restarts. Snip promised redirects would almost never fail, so the fix is the same as for the database: more than one copy, so any one can fail or be restarted while the others answer.

Two copies need something in front of them to decide which one gets each request. That's a load balancer.

The load balancer

A load balancer is the one address the outside world talks to. It receives each request and passes it to one of the app servers behind it:

Snip: from one app server to several

ChangeAdd a load balancer and run the app three times

BrowserSnip appPostgresprimary + standby
Any app server can answer any request. One can crash, or be taken out for a deploy, and the other two carry on.
Text version

Change: Add a load balancer and run the app three times

Added: Load balancer, Snip app 2, Snip app 3.

New connections: Browser → Load balancer; Load balancer → Snip app 1; Load balancer → Snip app 2; Load balancer → Snip app 3; Snip app 2 → Postgres; Snip app 3 → Postgres.

It does three jobs:

  • Spreads requests. The simplest rule is round robin: first request to server 1, next to server 2, and so on. "Fewest open connections" is a common alternative when requests vary in cost. 03.06 Load balancers covers the options.
  • Checks health. Every few seconds it asks each server a simple question, usually an HTTP request to a path like /health. A server that fails a few checks in a row stops getting traffic until it recovers. This is what turns "one server crashed" from an outage into a non-event.
  • Hides the servers. Users only know the load balancer's address, so you can add, remove and replace servers behind it without anyone noticing.

That last point also gives Snip deploys without downtime: take one server out of rotation, update it, check its health, put it back, and repeat for the next. At every moment at least two servers are answering.

The catch: state

This all works only if any server can handle any request. Here's what goes wrong if one doesn't. Say the app keeps logged-in sessions in its own memory: after you log in, server 1 remembers "this cookie belongs to Ana". Now step through Ana's next two requests:

Sessions kept in one server's memory
Ana's browserLoad balancerApp server 1App server 21 POST /login2 POST /login3 Set-Cookie: session=abc4 GET /links5 GET /links (abc)6 302 → /login

6 messages. Use Next and Back to follow them in order.

Text version
  1. Ana's browser → Load balancer: POST /login — Ana logs in. The load balancer sends this request to server 1.
  2. Load balancer → App server 1: POST /login — Server 1 checks her password and stores her session in its own memory.
  3. App server 1 → Ana's browser: Set-Cookie: session=abc (reply) — Ana's browser now holds a cookie that only server 1 understands.
  4. Ana's browser → Load balancer: GET /links — She opens her dashboard. This time round robin picks server 2.
  5. Load balancer → App server 2: GET /links (abc) — Server 2 has never seen session abc.
  6. App server 2 → Ana's browser: 302 → /login (reply) (fails) — Ana is logged out for no reason she can see. One click later she might land on server 1 and be logged in again.

Anything a server keeps in its own memory or on its own disk between requests is state: sessions, uploaded files waiting to be processed, counters, an in-memory list of rate limits. With state on the servers, they're no longer interchangeable, and losing one loses its state.

There are two ways out.

Sticky sessions. Tell the load balancer to always send the same user to the same server, usually with a cookie of its own. It's quick to set up, but it treats the symptom: load spreads unevenly, and when a server dies, everyone stuck to it still loses their session.

Stateless servers. Move the state out of the app servers into something they all share:

StateWhere it goes instead
SessionsA shared store like Redis or the database, or a signed token the browser holds itself
Uploaded filesObject storage (S3 or similar), not the server's disk
Counters, rate limitsRedis or the database
Cached dataA shared cache (02.04 Read replicas and a cache)

Now each server is stateless: it keeps nothing between requests that another server would need. Any server can answer any request, a crash loses nothing, and adding capacity means starting another copy. This is the version Snip uses. A Snip redirect was stateless from the start (look up the code, answer); only the logged-in dashboard needed fixing.

The load balancer is a single point of failure too

Putting everything behind one load balancer just moves the problem. In practice you either run a pair of them, where a standby takes over the shared address if the active one fails, or use a cloud provider's managed load balancer, which is already run as many machines behind the scenes. Either way, it's worth saying in an interview.

Seen in production

Heroku

2011

The Twelve-Factor App

The Twelve-Factor App, written at Heroku from what its team saw running apps on the platform, says app processes should be "stateless and share-nothing": anything that needs to persist goes in a backing service such as a database. It says sticky sessions "should never be used or relied upon", and suggests a store with expiry, such as Memcached or Redis, for session data.

What we foundA widely used guide to building web apps makes statelessness a rule, and calls sticky sessions a violation.

The Twelve-Factor App: VI. Processes12factor.net ↗
Predict first

After Snip adds a second app server, users say they're logged out at random, then logged in again on refresh. What's the most likely cause?

Commit to an answer before you read on.

Recap

Interview questions

JuniorfreqMust-knowConcept

What does it mean for a web server to be stateless, and why does it matter for scaling?

MidfreqGood to knowConcept

How does a load balancer know a server is unhealthy, and what happens next?

Report a mistakeEdit this page