Scaling a system up and out
Open in the interactive app: practice, quiz, mock interview →In one minute
- A system is scalable when adding resources raises capacity roughly in proportion, at a cost per request that stays flat or falls. "It handles a lot of traffic" is not scalability; "doubling nodes gives 1.9x throughput at the same p99" is.
- Vertical scaling (a bigger machine) is simple and has a ceiling. Horizontal scaling (more machines) has no hard ceiling but forces you to split state, coordinate, and route.
- Two forces bend the curve: contention (work that must be done one at a time) and coherency (nodes talking to each other to agree). The Universal Scalability Law puts numbers on both.
- Stateless tiers scale almost for free. The data tier is where scalability is won or lost: replicas scale reads, partitioning scales writes, and hot keys defeat both.
- Measure the curve at 1, 2, 4 and 8 nodes before you trust it at 64.
How it works
Load, capacity and the scaling curve
Start by naming the load parameter that matters. A chat server breaks on open connections, a feed service on fan-out per post, a ledger on writes to one account. Capacity is the highest load served within the latency objective; past it, queues form and latency climbs sharply. Werner Vogels' definition is the one most curricula reuse: a service is scalable if adding resources raises performance in proportion (Vogels). Scalability is the shape of the capacity curve as you add resources: linear is the ideal, sublinear is normal, and retrograde (more nodes, less throughput) is the failure to design against.
Vertical scaling
A bigger machine is often the best first move. The largest cloud instances now reach 32 TiB of memory and up to 1,920 vCPUs (AWS EC2 U7i), and many companies never outgrow a well-tuned Postgres primary. It keeps transactions local and needs no code changes. Its limits are a hard ceiling, price that rises faster than capacity at the top end, and a single failure domain.
Horizontal scaling
Horizontal scaling adds machines behind a router. It is cheap for stateless services: if any instance can serve any request, you add instances until the next tier saturates. Making a service stateless means moving sessions to a shared store or signed token, uploads to object storage, and treating local caches as optional accelerators.
State is the hard part. Three moves cover most designs:
- Replicate read-mostly data so reads spread over copies (see the Database Replication lesson). Writes still funnel through one leader.
- Partition (shard) data by key so each node owns a slice of writes. Cross-partition queries and transactions become expensive.
- Cache hot reads in front of the store, trading freshness for capacity.
A related map is the scale cube from The Art of Scalability: clone instances (X axis), split by function into services (Y axis), and split by data (Z axis) (microservices.io). Large systems use all three.
Why the curve bends
Amdahl's law caps speedup when part of the work is serial: with 5% serial work the limit is 20x. Distributed systems also pay a coherency cost for messages that agree on membership, invalidate caches or take locks, and it grows with the number of node pairs, roughly N squared. Neil Gunther's Universal Scalability Law combines both:
X(N) = λN / (1 + σ(N − 1) + κN(N − 1))
Here λ is single-node throughput, σ is the contention fraction and κ is the coherency cost. Gunther's own pages write the same law with α for contention and β for coherency (Gunther, USL); Azure's scale-out guidance uses the same two words, contention and coherency (Azure Architecture Center). With κ above zero, throughput peaks at about √((1 − σ)/κ) nodes and then falls. That is why a cluster can get slower when you add nodes: a global lock, a gossip protocol that sends N squared messages, or a cache invalidation that hits every peer.
The problem shapes behind scalability
Two shapes recur. Hot spots: sharding by user spreads load until one celebrity account takes 30% of writes. Overload: autoscaled web servers exhaust the database's connection limit. An AI agent platform hits the same two, when one popular prompt template hammers a single cache key and parallel tool calls exceed the model provider's rate limit. The fixes carry across: better partition keys, key splitting, request coalescing, queues and backpressure.
Down the stack
Layer 1The contract
A scalability claim must be testable: "From N to 2N nodes, throughput at p99 under 200 ms rises at least 1.8x, and cost per million requests does not rise." State the data shape too (key distribution, payload size, read/write mix), because a system that scales on uniform keys can collapse on skewed ones. Say what happens past the breaking point: shed load with 429s, queue, or fall over.
Layer 2The mechanism
Stateless tiers use a load balancer plus an autoscaler. Kubernetes' Horizontal Pod Autoscaler runs every 15 seconds, computes desiredReplicas = ceil(currentReplicas × currentMetric / targetMetric), ignores changes within a 10% tolerance, and waits out a 300-second stabilization window before scaling down. Scale-up has no window by default: it may add 100% of current pods or 4 pods, whichever is more, every 15 seconds. Older guides say the tolerance is cluster-wide only; since Kubernetes 1.33 each HPA can set its own under behavior.scaleUp.tolerance and behavior.scaleDown.tolerance, and the field is stable as of 1.37 (Kubernetes docs). Data tiers use a partition map: key ranges or hash slots assigned to nodes, held in etcd or ZooKeeper or gossiped (Cassandra's token ring), with consistent hashing or many virtual partitions so a new node takes about 1/N of the data. Little's law sizes everything: 20,000 requests per second at 50 ms means 1,000 requests in flight, which sets thread, connection and queue limits.
Layer 3The machine
Scaling limits are usually physical and boring. A Postgres connection is its own backend process, spawned by the postmaster for each client (Postgres docs) and using several MB, so 5,000 connections from autoscaled pods can exhaust RAM before CPU; PgBouncer multiplexes them onto a few hundred. Linux's default ephemeral port range, 32768 to 60999 or about 28,000 ports (kernel docs), caps outbound connections from one IP to one destination address and port, and TIME_WAIT holds each port for 60 seconds. Inside a box, a contended lock bounces its cache line between cores at roughly 50 to 100 ns per transfer across sockets, so a "fast" global counter incremented by 64 cores serializes the whole machine. A 25 Gbps NIC moves about 3 GB per second, which is only 300,000 requests per second at 10 KB each.
Why it's built this way
- One machine has finite cores, memory and NIC bandwidth, so large systems must use many machines.
- Many machines cannot share memory cheaply, so state must be split or copied, and every split adds routing and every copy adds coordination.
- Coordination costs grow with the number of participants, so designs keep the coordinating group small (a Raft group of 3 or 5) and partition everything else.
- Load is skewed and bursty, so partition keys, caches and admission control matter more than raw node count.
Diagram
flowchart LR
C[Clients] --> LB[Load balancer]
LB --> A1[App 1]
LB --> A2[App 2]
LB --> A3["App N (stateless)"]
A1 --> CA[Cache]
A2 --> CA
A3 --> CA
CA -->|miss| R{Partition map}
R --> S1["Shard 1 leader"]
R --> S2["Shard 2 leader"]
S1 --> S1R[Shard 1 replicas]
S2 --> S2R[Shard 2 replicas]Worked example
A checkout API runs on one 16-core node at 1,000 requests per second with p99 of 120 ms. Black Friday needs 8,000. The team load-tests at 1, 2, 4, 8 and 16 nodes and gets 1,000, 1,900, 3,440, 5,690 and 8,040 requests per second. Fitting the USL gives σ ≈ 0.05 and κ ≈ 0.001. The model predicts a peak of about 9,040 requests per second at 31 nodes and a decline after that, so 16 nodes only just meets the target with no headroom, and no amount of autoscaling will ever reach 10,000.
The traces explain both coefficients. σ comes from a single inventory row per SKU locked with SELECT ... FOR UPDATE; a popular item serializes all its buyers. κ comes from a cache invalidation that each node broadcasts to every other node. The fixes target the coefficients, not the node count: split hot inventory into 10 sub-counters that are reserved independently (σ falls toward 0.01), and replace peer broadcast with a single pub/sub channel (κ falls toward 0.0001). The refitted model predicts a peak near 99 nodes and about 13,600 requests per second at 16 nodes, so the same hardware now has 70% headroom over the target.
In the wild
- AWS Kinesis, 2020: adding servers broke every server. Each Kinesis front-end server kept OS threads for every other server in the fleet, so threads per server grew with fleet size, the coherency term in its purest form. A capacity add on 25 November pushed every front-end server past the OS thread limit, shard-map caches failed to build, and requests could not be routed. Kinesis was impaired for about 17 hours, along with Cognito, CloudWatch and other dependents. AWS moved to fewer, larger servers, added thread alarms and sped up splitting the front end into cells (AWS).
- Roblox, 2021: one shared coordination cluster. Roblox enabled a new Consul streaming feature, which under heavy load contended on a single Go channel, while BoltDB freelist bloat slowed Raft writes: the log was 4.2 GB with 489 MB live. Many backend services, and the telemetry needed to diagnose the fault, depended on that one Consul cluster, so the platform was down for 73 hours for about 50 million daily players. Roblox moved some critical services onto their own dedicated Consul clusters (Roblox).
- Slack, 2021: autoscaling into limits. A post-holiday surge with cold client caches overloaded an AWS Transit Gateway. Web tiers saturated and autoscaling tried to add about 1,200 servers, most of which failed on open-file and quota limits. Slack planned to request gateway upscaling before holidays and to load-test provisioning (Slack).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Vertical scaling | No code changes, local transactions, low latency | Hard ceiling, steep price at the top, one failure domain | Early, or the workload fits one large box with headroom |
| Stateless horizontal (X axis) | Near-linear, easy autoscaling | Pushes load onto shared state tiers | Web and API tiers |
| Functional split (Y axis) | Teams and tiers scale independently | Network hops, distributed failures | Distinct workloads with different load shapes |
| Data partitioning (Z axis) | Scales writes and storage | Cross-shard queries, rebalancing, hot keys | Write volume or data size exceeds one node |
| Read replicas and caches | Cheap read capacity | Staleness, invalidation | Read-heavy workloads tolerant of lag |
Pitfalls in production
- Autoscaling into the database. The app tier triples and opens three times as many connections, and the database falls over. Put a connection pooler in front, cap pool size per pod, and scale on the bottleneck metric, not CPU.
- Linear extrapolation. A test at 2 nodes "proves" 50 nodes will work. Test at several points, fit a curve, and find where it turns over.
- Hidden global state. A shared sequence, a global lock, or a leader that every request touches caps the whole fleet. Find it in traces by looking for one resource on every request's path.
- Scaling the wrong tier. Adding web servers when the queue consumer is the bottleneck just grows the backlog. Graph saturation for every tier (utilization, queue depth, errors) side by side.
- Ignoring scale-in. The autoscaler removes an instance halfway through a job or with requests still open, and users see errors every evening as traffic falls. Handle the shutdown signal, drain in-flight work, keep jobs on a queue so another instance can pick them up, and make clients retry (Azure Architecture Center).
- Cold scale-out. New instances take two minutes to warm caches and JIT, so autoscaling reacts too late to a spike. Keep headroom, pre-scale for known events, and warm instances before adding them to the pool.
Leadership lens
- Ask for the scaling curve, not a single number: "What is throughput at 1, 2, 4 and 8 nodes, and where does it peak?"
- Defer sharding until a measured limit forces it; a large vertical box plus replicas is cheaper to operate than a sharded fleet for most products.
- Make cost per request a tracked metric, so scale-out that costs more per unit of work is visible before the bill arrives.
- Partition keys are close to permanent. Review them as carefully as a public API, with real traffic data.
Check yourself
Why can adding nodes reduce total throughput?
Show answer
Coherency costs such as cache invalidation, membership gossip or distributed locks grow roughly with the square of node count, so beyond the peak each new node adds more coordination work than useful capacity.
Your API tier is stateless and CPU usage is 30%, yet throughput stopped rising. Where do you look?
Show answer
At shared tiers and serial points: database connections and locks, a single queue partition, a rate-limited dependency, or a hot key. Idle CPU with flat throughput means requests are waiting, not working.
Why does sharding by user ID not guarantee even load?
Show answer
Hashing spreads keys evenly, not traffic. A few users can generate a large share of requests, so a single shard can still saturate.
What does Little's law tell you about a service doing 5,000 requests per second at 40 ms?
Show answer
About 200 requests are in flight at any moment, which sets the minimum concurrency (threads, connections, pool slots) the service needs to sustain that rate.
Sources and further study
Primary
- Gunther, A General Theory of Computational Scalability Based on Rational Functions (2008): the Universal Scalability Law derived from a queueing model, with Amdahl's law as a special case.
- Gunther: How to quantify scalability: the law in practical form, what each coefficient means and how to fit it to load-test data.
Systems docs
- Kubernetes: Horizontal Pod Autoscaling: the replica formula, tolerance, stabilization windows and scaling policies.
- PostgreSQL: How connections are established: the process-per-connection model behind every connection-pooler recommendation.
Books
- Designing Data-Intensive Applications (2nd ed., 2026), Kleppmann and Riccomini, Ch 2 "Defining Nonfunctional Requirements" (the Scalability section) and Ch 7 "Sharding": load parameters, then how to split data.
- Systems Performance (2nd ed., 2020), Brendan Gregg, Ch 2 "Methodologies": the models and concepts used to analyse utilisation, saturation and capacity.
- System Design Interview, Volume 1, Alex Xu, Ch 1 "Scale From Zero To Millions Of Users": the standard step-by-step growth story from one server to sharded data.
Other curricula
- Azure Architecture Center: Design to scale out: frames scalability as throughput gain over resource increase, limited by bottlenecks and synchronization points, and stresses designing for scale-in.
- microservices.io: The scale cube: frames scaling as three axes and places microservices on the Y axis.
- donnemartin/system-design-primer: separates a performance problem (slow for one user) from a scalability problem (fast for one, slow under load).
Watch
- CS75 Lecture 9: Scalability (Harvard, David Malan): a long, accessible lecture walking from vertical scaling through load balancing, caching, replication and partitioning.
- Building Software Systems at Google and Lessons Learned (Jeff Dean, Stanford): how Google's systems evolved under several orders of magnitude of growth, with real latency and capacity numbers.
Free resources
- Werner Vogels: A Word on Scalability: a short, precise definition of scalability from Amazon's CTO.
- Amdahl's law (Wikipedia): the serial-fraction limit on speedup, with the derivation.
- Little's law (Wikipedia): the relation between concurrency, throughput and latency used in every capacity plan.
- AWS Builders' Library: Workload isolation using shuffle sharding: how partitioning customers limits the blast radius of hot or abusive tenants.
- James Hamilton: On Designing and Deploying Internet-Scale Services: a classic checklist of practices for services that must grow without heroics.