Module 5 · System architecture

5.1 Scalability: what actually breaks when ten thousand people arrive

Lesson 5.1 · 11 min read

Your app works. You've clicked through it, a friend has tried it, everything responds instantly. Then something good happens, it gets shared somewhere, and a few thousand people show up at once. And it falls over.

Here is the thing nobody warns you about: it doesn't fail gracefully. It doesn't get gently slower in proportion to the load. It works fine, and works fine, and works fine, and then at some threshold it collapses, and every user gets nothing. Systems under load have a cliff, not a slope, and the cliff arrives without warning if you don't know where it is.

This module is about understanding that cliff. Not because you'll have ten thousand simultaneous users next week, but because the concepts that explain the cliff are the same concepts that explain why real systems are built the way they are. Everything from here, caching, queues, containers, distributed data, is a response to the pressures this post describes. Understand the pressures and the rest of the module becomes obvious rather than arbitrary.


What "load" actually means

Start with a mental picture that is more accurate than "lots of users."

Your server is a single computer running a program. That program can handle some number of requests at the same moment. When a request arrives, it occupies some of the machine's attention, uses some memory, perhaps waits on the database, and then finishes and releases everything it held.

Load is not how many users you have. It's how many requests are being handled simultaneously. Ten thousand users who each load a page once an hour is nothing. A hundred users who each hold an open connection streaming updates is more demanding than it sounds. What matters is concurrency: how many things are in flight at once.

And this reveals the first genuinely important idea. How many requests are in flight at once depends on two things: how many arrive per second, and how long each one takes. If requests arrive ten per second and each takes one second, roughly ten are in flight at any moment. If each one suddenly takes five seconds, fifty are in flight. Same traffic, five times the load.

That relationship is why slow things are dangerous in a way that has nothing to do with user patience. Slowness multiplies into load. A single query that starts taking three seconds instead of thirty milliseconds doesn't just annoy people; it causes requests to pile up, each holding resources while it waits. The server accumulates half-finished work it cannot let go of. Memory fills. New requests queue behind old ones, so they take longer too, so more pile up. This is a feedback loop, and it is the cliff.


Why the failure is sudden

Let's be precise about the cliff, because understanding it is what makes the rest of this module make sense.

Picture a coffee shop with one barista who can make a drink a minute. Customers arrive every ninety seconds. She keeps up easily, there's no line, and she even has idle moments. Now customers start arriving every fifty seconds. She still keeps up, mostly, but the queue never fully clears now, and there's always someone waiting. Traffic increases slightly again, to one customer every fifty-five seconds... and the queue grows, forever, without bound.

Nothing about her changed. She makes drinks at exactly the same speed she always did. But she crossed the point where arrivals exceed capacity, and past that point the line doesn't grow proportionally, it grows without limit. Wait times go from thirty seconds to infinity, and the transition happens in the space of one small increase in traffic.

That is the cliff, and it is the fundamental behavior of every queueing system, which is what every server is. As long as you can serve requests faster than they arrive, you're fine and everything feels instant. The moment you can't, work accumulates, waits grow, and the system does not degrade gracefully. It goes from healthy to unusable across a narrow band of traffic.

Two consequences follow, and both matter enormously.

Being at eighty percent capacity is not safe. It feels safe, because everything is fast and there's headroom. But you're one traffic spike, one slow query, one dependency getting sluggish away from the cliff. Real systems run with far more headroom than seems necessary, precisely because the failure mode isn't gentle.

The cure for load and the cure for slowness are the same medicine. Making each request faster doesn't merely improve the experience of one user. It reduces how many requests are in flight simultaneously, which moves the entire system further from the cliff. Every optimization is a capacity increase. This is why the latency post from Module 0 was not a UX post. It was a scalability post wearing a UX costume.

Interactive: load cliff visualization

A hands-on visual for this idea is in the works. The text around it carries everything you need for now.


The two ways to add capacity

So you need to handle more. There are exactly two directions, and understanding why one of them wins in the end is the crux of this post.

Vertical scaling means making the machine bigger. More CPU, more memory, faster disk. Your program doesn't change at all. You move it onto a beefier computer and it handles more.

The appeal is that it requires nothing of you. No architectural change, no new concepts, no distributed anything. For most applications, most of the time, this is genuinely the right first answer, and people underrate it badly. Modern servers are enormous. A single well-configured machine can serve a startling amount of traffic, and the complexity you avoid by not distributing is real, valuable complexity to avoid.

The limits are two, and they're both hard. There is a biggest machine that exists, and you can buy it, and then you are finished. And the cost curve is unkind: doubling a machine's power costs considerably more than double, because you're buying increasingly exotic hardware. Somewhere on that curve, adding a second ordinary machine becomes obviously cheaper than doubling one extraordinary machine.

Horizontal scaling means running many machines, each with a copy of your program, and spreading the requests among them. Ten servers, each handling a tenth of the traffic.

This has no ceiling. It's how every large system on earth works. Need more capacity? Add more machines. The individual machines can be ordinary and cheap. And you get something you didn't have before, which turns out to be as valuable as the capacity: if one machine dies, the other nine keep serving, and your users never notice. A single vertically-scaled server, however enormous, is a single point of failure. Ten servers are a system that survives losing one.

But horizontal scaling asks something of you, and this is the part that actually matters.


The price of horizontal scaling: your app must not remember

To spread requests across ten identical servers, something has to be true: any server must be able to handle any request. A user's first request goes to server three, their next to server seven, and both must work identically.

That requirement has a sharp consequence. If server three remembered something about that user, in its own memory, server seven doesn't have it. The user uploaded a file that got saved to server three's disk? Server seven can't find it. The user logged in and server three stored the session in memory? Server seven has never heard of them, and they're logged out.

So the rule for horizontally scalable applications is: your servers must not hold state. They must be interchangeable, disposable, ignorant. Every request must carry, or be able to fetch, everything needed to handle it. A server should be able to die mid-afternoon and be replaced by a fresh copy that knows nothing, and nobody should notice.

This is called being stateless, and it is the single most important architectural property of a scalable system. Notice how many things you already learned are quietly consequences of it.

Tokens, from the authentication post, are stateless credentials: the request carries proof of identity that any server can verify independently, without looking anything up. That's why they scale better than server-stored sessions, and now you understand why that trade exists rather than just that it does.

Files go in object storage, from the database module, not on the server's disk. Now you can see the deeper reason: the server's disk is not a real place, because the server is disposable.

The database is the shared truth precisely because it's the one thing every server can reach. State didn't disappear. It moved somewhere shared, and that's the whole trick.

Where a stateless design does need shared memory, session data, temporary values, caches, it goes into a shared store that all servers can reach, which is exactly the key-value store from the storage post. Every piece of the picture you've been assembling was, secretly, about this.


The load balancer, and what it enables

If you have ten servers, something must decide which one gets each request. That something is a load balancer, and it sits in front of your servers as the single address the world talks to.

Its job sounds trivial and isn't. It receives every request and forwards it to one of the servers, spreading work among them. But it also watches them: it periodically checks whether each server is healthy, and when one stops responding, it quietly stops sending traffic there. Your users experience nothing.

This is what turns ten machines from a pile of computers into a system. And it's what makes deployment safe at scale: you can update one server at a time, taking each out of the rotation, updating it, checking it's healthy, putting it back. At no point are your users without a working server. That's how large systems ship changes without downtime, and it's only possible because the servers are interchangeable, which is only possible because they're stateless. The concepts stack.


Where the bottleneck actually goes

Here's the punchline of the entire post, and it reframes everything.

You scaled horizontally. Ten servers, load balanced, stateless, ready for anything. And they all talk to one database.

The bottleneck moved. It didn't vanish. Your application servers are now cheap and infinitely multipliable, so they stopped being the constraint, and now every one of them is sending queries to a single database, which is a single machine, with all the limits a single machine has. You have ten times the request capacity pointed at exactly the same data layer as before.

This is nearly always where real systems actually struggle. Application servers are easy to scale, precisely because we made them stateless and disposable. Databases are hard to scale, precisely because their entire job is to hold state, and state is the thing that cannot be casually copied without deciding what happens when two copies disagree.

So the honest picture of scaling a real system looks like this. First you make the app faster, because speed is capacity. Then you scale the servers horizontally, which is comparatively easy. And then you spend the rest of your time on the data layer, and every one of the remaining posts in this module is, in one way or another, about that: putting a cache in front of the database so most requests never reach it, moving slow work out of the request path entirely with queues, and finally splitting the data itself, which forces you to confront the hardest tradeoffs in the field.

You now know the shape of the problem, and it's the shape of the module.


What this means for how you build

You are unlikely to have ten thousand concurrent users soon, and building as though you do is a genuine mistake. Premature scaling is a real and costly error, and a startup that distributes its data layer before it has customers has spent its most valuable resource on a problem it doesn't have.

But there's a difference between building for scale and building things that can scale, and it costs almost nothing to be on the right side of it.

Keep your servers stateless. Don't write files to the server's disk. Don't hold user sessions in a variable in memory. These decisions are free when you make them at the start and expensive to retrofit, because retrofitting means finding every place you assumed a server remembered something.

Care about the speed of your queries, from the very beginning, because speed is capacity and because a query that scans an entire table is fine with a thousand rows and lethal with a million. The point at which it becomes lethal will arrive without warning, and it will arrive on the day something good happens to you.

And understand where your bottleneck will be, so that when the moment comes, you don't waste a week scaling the thing that wasn't the problem. It will be the database. It's almost always the database.

That's not preparing for scale. It's declining to build in a way that forecloses it, which is what a design engineer does instead of either ignoring the future or building for a future that hasn't arrived.


Do this before the next post

Find the slowest thing in something you've built, and understand why it's slow rather than fixing it.

Open your app, open the network tab, and look at the timing column. Find the request that takes the longest. Now ask: what is it doing during that time? Is it waiting on a database query? Is it fetching from an external API? Is it doing work that could have been done earlier, or not at all?

Then do the arithmetic that makes this real. Suppose that request takes two seconds. Suppose your server can handle fifty simultaneous requests before it starts to struggle. How many people per second can be hitting that endpoint before you're at capacity? Twenty-five. That's it. That's the number, and it's much smaller than you expected, and it fell directly out of one slow query.

Now suppose you got that request down to two hundred milliseconds, ten times faster. Redo the arithmetic. You can now serve two hundred and fifty people per second, on exactly the same hardware. You did not add a single machine. You made one thing faster, and your capacity grew tenfold.

Sit with that, because it's the entire lesson of this post in one calculation. Speed is capacity. The optimization you thought was about user experience was quietly about how far you are from the cliff, and now you can see both at once, which is what it means to think about a system rather than a feature.