Skip to content
intermediate Phase 11 · Caching and Scalability Architecture

Vertical vs Horizontal Scaling

Grow capacity by enlarging nodes or by adding more nodes to a fleet.

45m
0 problems
Topic Progress 0%

When the Biggest Machine Stops Helping: The Ceiling Behind Vertical Scaling

Your primary PostgreSQL node sits at 82% CPU through the evening peak. You resize from db.m6g.2xlarge (8 vCPU, 32 GiB) to db.m6g.8xlarge (32 vCPU, 256 GiB), latency recovers, and the team stops thinking about capacity. Three months later the same graph points upward, and this time the largest instance your region sells is within spitting distance of what you think you need. Vertical scaling — making the single node bigger — ran out of road.

Vertical scaling is the default first move for stateful systems for a good reason: it needs no code changes, no data redistribution, no coordination between replicas. One parameter flips, one failover later, you have 4× the CPU. But every rung on the instance ladder costs more per unit of capacity than the one below it, and the top of the ladder is a hard wall.

Instance          vCPU   RAM (GiB)   On-demand $/hr (us-east-1, approx)
m6i.large            2          4          0.096
m6i.xlarge           4          8          0.192
m6i.4xlarge         16         64          0.768
m6i.16xlarge        64        256          6.144
m6i.metal          128        512         12.288

The dollar cost is the least interesting part. Three other costs bite harder:

  • You hit a hardware ceiling you cannot buy past. Largest general-purpose offerings top out around 448–512 vCPU and 3–8 TiB of RAM. If your working set or request rate outgrows that, there is no rung eight.
  • Software scales sublinearly with the box. A JVM with a 64 GiB heap pays multi-second GC pauses you never saw at 8 GiB. PostgreSQL's shared buffers, single-writer WAL, and max connection limits do not care that you doubled the cores. Lock contention inside a process gets worse with more cores chasing the same mutex — the classic scaling wall shows up as CPU parked in kernel futex waits while the box looks idle.
  • The node becomes a single point of failure with a maintenance window. Resizing a database means a failover or a restart; a restart is an outage for anyone with open transactions. And with one node, there is no partial degradation — a lost box is a lost service. Capacity is also bought for peak and paid for 24/7: a diurnal workload at 30% average utilization still keeps the 3 AM-sized machine running.

Amdahl's law puts a number on the first software limit. If 5% of your request path is serialized (a global lock, a single-threaded serializer), the theoretical maximum speedup from infinite cores is 1 / 0.05 = 20× and long before that, contention dominates. Doubling cores rarely doubles throughput on a lock-heavy service. The cheap pre-purchase test is empirical: run your load test at 2× the cores and check whether throughput moved. If it improved by 15% instead of 90%, you have found your serialized fraction, and the next three sizes will disappoint you the same way.

So vertical scaling is not wrong — it is the right tool until the ceiling, and the wrong tool as a strategy. It wins for single-writer datastores (a relational primary is a single writer no matter how many machines you own), for software licensed per core where doubling cores doubles the license bill, and for small fleets where operating four boxes costs more engineer time than one box costs in dollars.

The interview version of this question is rarely "which is better." It is: where does scale-up stop working, and what do you do next? The answer is the subject of the next chapter — Load Balancing and Autoscaling is only possible once the tier you are scaling is built from interchangeable, stateless nodes. The state question comes first.

Externalize the State, Then Add Nodes: What "Stateless" Actually Requires

You double your web pods from 4 to 8 behind the load balancer. Half of your users are now logged out at random, checkout carts empty on refresh, and file uploads fail intermittently. Nothing is broken in the traditional sense — every pod is healthy. The state simply lives in the wrong place: inside one process, reachable only by the requests that happened to hit it.

Horizontal scaling adds identical nodes instead of enlarging one. It only works under a strict precondition: any node must be able to serve any request. That single sentence is what "stateless tier" means, and it is the prerequisite for cheap scale-out called out in every architecture review.

Inventory what typically hides in process memory, then move it:

State Wrong place (in-process) Right place
Login session In-memory map keyed by session id Redis/DynamoDB with TTL, or a signed JWT
Uploaded files Local disk /tmp Object storage (S3) with presigned URLs
Background work In-process queue Dedicated queue (SQS, Kafka, RabbitMQ)
Rate-limit counters Local counter per pod Shared store, or accept per-pod budgets
Hot reference data Local cache only Shared cache (Redis) + local L1 with short TTL
WebSocket registrations Pod-local registry External pub/sub for cross-pod fan-out

Sticky sessions look like a fix and are usually a trap: they pin a user to a pod so in-memory sessions keep working, but now scaling out redistributes only new users, a dead pod drops its whole shard of sessions, and any rebalancing requires session migration. Keep affinity only where physics demands it (long-lived WebSocket connections), and use consistent hashing so that removing one node relocates about 1/N of connections instead of all of them — you will see that algorithm properly in Load Balancing and Autoscaling.

The subtler cost of scale-out is that every node carries the full fixed overhead, and shared backends feel the sum of all pools. PostgreSQL's default max_connections is 100. One node with a 50-connection pool was comfortable; ten pods with the same pool want 500 connections and will exhaust the primary unless you introduce a pooler (PgBouncer in transaction mode) or shrink per-pod pools. Budget the arithmetic before you autoscale:

per-node pool 20 connections × 20 pods  = 400 backend connections
PostgreSQL default max_connections      = 100   ← you are already broken at 5 pods
fix: PgBouncer (transaction pooling) or pool = floor(100 / (pods × safety))

The payoff is real, though. Capacity becomes additive: four nodes at 500 requests/second gives ~2000 req/s, and failure becomes partial — lose one node of ten and the load balancer routes around it while you lose 10% of capacity, instead of 100%. That is the difference between a scaling model and a disaster-recovery story.

Two practical consequences follow. First, boot is now a capacity event: a fresh node starts with cold caches and empty JIT profiles, so expect several minutes of degraded latency after scale-out — warm pools and gradual traffic ramps exist for exactly this reason. Second, memory is no longer shared for free: ten JVMs with 2 GiB heaps cost 20 GiB where one 8 GiB heap did it, because each node caches its own copy of hot data and holds its own runtime overhead.

If you take one thing from this chapter: horizontal scaling is not "add machines," it is make the node disposable. Anything that survives only inside one process is a blocker to scale-out and should be treated as a design defect, not an implementation detail.

Which Axis First: A Capacity Decision Table for Scale-Up vs Scale-Out

"Scale up or scale out?" is a standard senior-level interview question, and the weak answer picks one and defends it. The strong answer starts from the workload: what kind of state does it hold, how does it fail, and how does demand curve over time. After API Gateways and Backend-for-Frontend taught you to peel cross-cutting concerns off the app tier, this is the decision you apply to what is left.

Workload First move Why Escape hatch when it hurts
Stateless web/API tier Horizontal, immediately Requests are interchangeable; demand is bursty Never needed � this axis always scales further
Relational OLTP primary Vertical Single writer; sharding is a rewrite Read replicas ? partition by tenant ? Citus/Vitess
Lock-heavy in-memory index Vertical Contention is per-process; splitting needs a protocol Shard the key space by range/hash
CPU-bound batch jobs Horizontal Jobs partition cleanly; embarrassingly parallel Reduce per-job overhead (startup, model load)
Per-core licensed software Vertical, reluctantly Cost tracks core count exactly Replace the license model before adding cores
Diurnal consumer traffic Horizontal Elasticity beats a permanently oversized box Pre-warm with scheduled scaling for known peaks

The economics deserve concrete numbers. Two capacity plans can cost the same and behave nothing alike:

Plan A (vertical): 1 � m6i.4xlarge  16 vCPU, 64 GiB   $0.768/hr
Plan B (horizontal): 4 � m6i.xlarge  4 vCPU, 16 GiB   4 � $0.192 = $0.768/hr

Same price. Plan A: one failure domain, capacity fixed at 16 vCPU, resize = restart.
Plan B: 4 failure domains, scale 4?8 for the peak and back, deploy = rolling, lose a node = lose 25%.

For a workload with a 5� peak-to-trough ratio, Plan B also wins on utilization: at trough you can run two nodes and scale to ten during the peak, while Plan A must be sized for the peak permanently � a 16-vCPU box idling at 20% for eighteen hours a day. That elasticity argument is the reason almost all application tiers go horizontal on sight, and it is what the autoscaling policies in Load Balancing and Autoscaling exploit.

The honest counterweight is everything scale-out makes you pay. Each node duplicates its runtime overhead: a 1 GiB heap plus a sidecar at 100 MB per pod means 5 replicas burn ~5.5 GiB doing what one node did with 1.2. Serialization replaces function calls � remote state lookups cost 0.5�2 ms where an in-process map costs microseconds. Cache hit ratios can fall when a shared cache's keyspace is partitioned across nodes and hot keys overload their shard. Distributed correctness (exactly-once delivery, cross-node locks) is now your problem. If a system is small, single-digit RPS, and boring, one well-chosen box and a backup plan is genuinely the better engineering answer.

A decision procedure you can state in an interview:

  1. Classify the state. None (or externalizable) ? horizontal, full stop. Single-writer, strongly consistent ? vertical first.
  2. Plot the demand curve. Spiky or diurnal ? horizontal, because you pay for elasticity. Flat and modest ? vertical may be cheaper in total cost of ownership.
  3. Find the ceiling. Largest instance you can buy � current size = your runway in months. When runway drops under a quarter, start the sharding or replica design.
  4. Price the failure. What does an unplanned restart cost in money and SLO? If the answer is "an outage," that is a strike against a single node regardless of size.

The synthesis to carry into the room: scale vertically where the data model forces a single writer, scale horizontally everywhere else, and treat statelessness as the enabling condition � not a stylistic preference � for everything that follows in this phase.

Quiz

1. Your 8-vCPU database primary hits 90% CPU at peak every quarter. Resizing to 64 vCPU fixes it, the problem returns, and the region sells only a 128-vCPU instance. Which statement best explains why vertical scaling keeps coming back as a fix?

Question 1 options

2. You scale your web tier from 4 to 8 pods behind the load balancer. Half of your users are randomly logged out and carts empty on refresh, while every pod reports healthy. What is the root cause?

Question 2 options

3. PostgreSQL defaults to max_connections = 100. Each pod configures a pool of 20 connections. At roughly what fleet size do you break the database?

Question 3 options

4. Which workload most clearly justifies vertical scaling as the first move rather than scale-out?

Question 4 options

Flashcards

Question

What precondition makes horizontal scaling possible, and what breaks when it is violated?

Answer

Any node must be able to serve any request — sessions, uploads, queues, counters, and hot data live outside the process (stateless tier). Violate it and scaling out redistributes traffic without redistributing state: users get logged out, carts vanish, and work queued on one pod never runs elsewhere.

Question

Why does vertical scaling create a capacity cliff for a diurnal workload?

Answer

The single box must be sized for peak and stays running (and paid for) at trough — a service averaging 30% utilization still keeps the peak-sized machine alive at 3 AM. There is no way to shed capacity, so you either overpay all day or fail at peak; scale-out converts this into 4 nodes at trough and 10 at peak.

Question

Name three software-level limits that appear as one machine grows, before any hardware limit.

Answer

GC/allocator pauses grow with heap size (multi-second JVM GC at 64 GiB); lock contention and Amdahl serialization cap speedup (5% serialized work = 20x theoretical maximum, practically far less); and single-writer designs like PostgreSQL WAL plus max_connections plus per-core licensing make extra cores progressively less useful or more expensive.

Question

What happens to backend connection budgets when you scale pods out, and how do you prevent an outage?

Answer

Pools multiply with node count: 20 pods x 20 connections = 400 against PostgreSQL max_connections = 100, so the fifth pod breaks a database that was fine at four. Prevent it by sizing pools from a formula (floor(limit / pods / safety)), using transaction-mode PgBouncer, or moving pool management to a proxy — decided before autoscaling, not after.

Question

Compare failure domains: one 16-vCPU instance vs four 4-vCPU instances at identical hourly cost.

Answer

One node: an unplanned restart or a resize is a 100% outage and deploys are not rolling. Four nodes: losing one costs 25% of capacity while the balancer routes around it, deploys are gradual, and capacity tracks demand. You pay with duplicated per-node overhead (heaps, sidecars), multiplied connection pools, and distributed-correctness complexity.

Revision Notes

Key Takeaways

  • 1. Vertical scaling is the right first move for single-writer stateful systems and the wrong long-term strategy: you hit the largest SKU, sublinear software scaling, and a single failure domain with a restart window
  • 2. Horizontal scaling is not "add machines" — it is "make the node disposable"; anything living in process memory (sessions, uploads, queues, counters) is a blocker, not a detail
  • 3. Every node repeats the full fixed overhead: heaps, sidecars, and pool sizes multiply — budget backend limits like max_connections from pods x pool before you autoscale
  • 4. Scale-out turns failure into partial loss (1 of 10 nodes = 10%) and capacity into an elastic variable, which is why bursty or diurnal traffic always justifies horizontal first
  • 5. Cheap pre-purchase test: load-test at 2x the cores; if throughput moves less than ~1.5x, you found the serialized fraction and buying a bigger box will not help

Interview Tips

  • • "Scale up or scale out?" — refuse the binary: classify the state first (externalizable → horizontal; single-writer → vertical), then cite failure domain, demand curve, and the largest-SKU ceiling as your three arguments
  • • "You doubled the box and throughput barely moved. Why?" — invoke Amdahl and lock contention: name the serialized fraction, mention GC/allocator growth with heap, and propose measuring contention (futex waits, lock wait time) before buying again
  • • "What does ‘stateless’ actually require?" — enumerate the five categories (sessions, uploads, in-process queues, counters, hot data), give the external store for each, and note sticky sessions as a trap that only relocates the problem
  • • "You added pods and the database started refusing connections." — show the pool multiplication arithmetic against max_connections and answer with PgBouncer transaction pooling or a formula-driven pool cap, decided before scale-out

Cheat Sheet

Vertical vs Horizontal Scaling

Decision rule: stateless tier → horizontal on sight; single-writer datastore → vertical until the ceiling, then read replicas, then sharding.

Question Scale up (vertical) Scale out (horizontal)
What changes Bigger instance More identical instances
Failure domain 1 node = 100% of capacity lose 1 of N = 1/N capacity
Elasticity None — sized for peak, paid 24/7 Autoscale with demand
Ceiling Largest SKU (~448–512 vCPU, 3–8 TiB) Effectively none
Code impact None State must leave the process
Hidden cost Superlinear software limits, per-core licenses Duplicated overhead, pools x N, warmup

Stateless checklist: sessions · uploads · queues · rate counters · hot reference data — every item out of process memory, or scale-out is theater.

Connection math before adding pods:

pods x pool <= max_connections x safety
20 pods x 20 = 400 > PostgreSQL default 100  →  PgBouncer (transaction) or smaller pools

Amdahl check: serialized fraction 5% → max speedup 1/0.05 = 20x. If doubling cores buys <1.5x throughput, the next rung of the ladder is wasted money.

Continue Learning