Skip to content

Horizontal vs Vertical Scaling

Core Concept LearningJuly 4, 20264 min readUpdated July 21, 2026

Traffic climbs and something saturates — CPU, memory, disk, or connection count. Vertical scaling gives one machine more resources. Horizontal scaling adds machines and spreads work. The wrong move either burns budget on a larger box that remains a single point of failure, or fans out a stateful app that breaks sessions. Layer 4 vs Layer 7 load balancing explains how traffic reaches those added instances.

This article compares both, traces a request across a scaled-out API, and gives a progression you can justify with load-test evidence — not guesswork. When the bottleneck is stateful storage, use the separate database scaling techniques decision path.

Vertical vs horizontal scaling
Vertical vs horizontal scaling

Vertical Scaling (Scale Up)

Vertical scaling upgrades one host: more CPU, RAM, or faster disk. No load balancer, no distributed session redesign. For a monolithic API or a database with moderate traffic, resizing the instance is often the fastest path to relieve a measured bottleneck.

The ceiling is real: cloud instance families top out, and one machine remains one failure domain. Many resizes still need a maintenance window. Cost often rises faster than linear with size. When not to stop here: you need multi-instance availability, or profiling shows you are already on a large SKU with no headroom.

Skimmer: scale the resource or the number of workers.
MoveUse whenTrade-off
Scale upOne host is limited by CPU, memory, or I/O and can be resizedSimple change; finite ceiling and one host failure domain
Scale outWork partitions across stateless workers or HA needs replicasMore capacity domains; routing and shared-state complexity
Scale up: one server gets more resources
Scale up: one server gets more resources

Quick reference

  • Best for: early growth, hard-to-shard workloads, single-primary databases.
  • Strengths: minimal code change; direct relief for CPU/RAM/I/O bottlenecks.
  • Weaknesses: hard ceiling, single failure domain, possible resize downtime.
  • Database path: bigger primary first → read replicas → sharding only when needed.
  • Resize from evidence: CPU steal, memory pressure, disk queue — not vibes.
  • When not to: HA requires more than one healthy instance.
Capture vertical-scaling evidence
1# Observe the same container during a representative load test.2docker stats api --no-stream3docker inspect api --format   'memory={{.HostConfig.Memory}} nano_cpus={{.HostConfig.NanoCpus}}'4 5# Expected before resize: CPU is pinned or memory approaches its6# configured limit while request latency rises. If neither happens,7# buying a larger limit does not test the observed bottleneck.
Raise one measured resource, then repeat
1# compose.yaml — the host must have this capacity available2services:3  api:4    image: example/orders-api:local5    mem_limit: 2g6    cpus: 2.07    restart: on-failure8 9# Apply and verify the effective limits:10# docker compose up -d && docker inspect api --format #   'memory={{.HostConfig.Memory}} nano_cpus={{.HostConfig.NanoCpus}}'11#12# Repeat identical traffic; pass only if the saturated metric and13# latency improve. Set mem_limit below the working set to rehearse14# OOM/restart; the single container is unavailable while restarting.

Remember this

Scale up first when a single measured resource is the bottleneck and HA is not yet the requirement.

Horizontal Scaling (Scale Out)

Horizontal scaling adds instances behind a load balancer. Autoscaling groups add capacity on CPU, request rate, or custom metrics, and remove it when load falls. That only works if the app is stateless on the node: sessions in Redis (or signed cookies), uploads in object storage, no reliance on local disk for durable data.

Request trace: Client → LB → instance A handles GET /orders/42 → Redis session lookup → Postgres → response. Failure path: instance A dies mid-request → LB stops routing new traffic there (health check fail) → retry or next request lands on instance B with the same session store. If sessions lived only in A’s memory, B cannot continue the user’s flow.

Scale out: add more servers behind a load balancer
Scale out: add more servers behind a load balancer

Quick reference

  • Best for: web APIs, workers, HA, elastic traffic.
  • Strengths: add/remove capacity without a single-box ceiling.
  • Weaknesses: requires stateless design + LB + shared data stores.
  • Stateless checklist: no local sessions, no local durable uploads, shared cache carefully keyed.
  • Autoscale on evidence (CPU, RPS, queue depth); scale out fast, scale in slowly to avoid thrash.
  • When not to: the workload cannot be partitioned and a single writer is mandatory — fix the data model first.
Stateful — horizontal scaling breaks
1// In-memory sessions stick the user to one process2const sessions = new Map<string, Session>();3 4app.post("/login", (req, res) => {5  const sid = crypto.randomUUID();6  sessions.set(sid, { userId: req.body.userId });7  res.cookie("sid", sid);8});9// Next request on another instance → sid unknown → forced re-login
Stateless node — shared session store
1app.post("/login", async (req, res) => {2  const sid = crypto.randomUUID();3  await redis.set(`sess:${sid}`, JSON.stringify({ userId: req.body.userId }), "EX", 3600);4  res.cookie("sid", sid, { httpOnly: true, sameSite: "lax" });5});6 7app.use(async (req, res, next) => {8  const sid = req.cookies.sid;9  if (!sid) return next();10  const raw = await redis.get(`sess:${sid}`);11  if (!raw) return res.status(401).json({ error: "session_expired" });12  req.session = JSON.parse(raw);13  next();14});

Remember this

Scale out only after the node is stateless — otherwise the load balancer multiplies pain.

Combining Both Strategies

Production systems mix strategies by tier. App servers usually go horizontal early. Databases often go vertical first, then read replicas for read scale, then partitioning when writes demand it. Caches and CDNs are horizontal by design.

Evidence-based progression: (1) load-test and find the bottleneck, (2) vertical if one resource is cheap to buy, (3) horizontal app tier + shared session/cache, (4) DB replicas, (5) shard or split only with a measured write ceiling. Skipping steps without metrics is how teams invent distributed complexity before they need it.

Progression per tier: app horizontal early, database vertical then replicas then shard
Progression per tier: app horizontal early, database vertical then replicas then shard

Quick reference

  • App tier: prefer horizontal once traffic or HA needs more than one node.
  • Database: vertical → replicas → shard/partition with a plan for cross-partition queries.
  • Cache/CDN: horizontal; watch stampede and key design.
  • Load-test before and after each change at the same percentile.
  • Document the next bottleneck you expect — so the next change is deliberate.

Remember this

Use vertical and horizontal as a progression per tier, driven by measured bottlenecks.

Recover One Instance Failure Behind the Load Balancer

Trigger. Instance A, one of four behind the load balancer, crashes mid-request — an OOM kill, a bad deploy, a hardware fault. Symptom. The in-flight request to A fails; the next request from the same user, routed to instance B, either continues seamlessly or forces a re-login, depending entirely on where that user's session actually lived.

Root mechanism. The load balancer's health check needs one or more failed probes before it stops routing new traffic to A — a brief window where new connections can still land on a dead instance, the same detection-delay trade-off covered in Load Balancing: Layer 4 vs Layer 7. Separately, whether B can pick up the user's session depends on whether that session was ever node-local. Recovery: once the health check fails A, all new traffic routes to B, C, and D only; if sessions live in shared Redis, B reads the same session state A would have and the user never notices. Verification: kill one instance mid-session in a local two-process test and confirm the next request still authenticates on the survivor — the practice exercise below is exactly this test. Prevention: treat any node-local state (in-memory sessions, local file uploads, in-process caches assumed durable) as a horizontal-scaling blocker to fix before adding instances, not an acceptable trade-off to discover during an actual instance failure.

Instance A dies mid-request — the next request lands on B
Instance A dies mid-request — the next request lands on B

Quick reference

  • Trigger: one instance behind the load balancer crashes mid-request.
  • Symptom: whether the next request continues seamlessly depends entirely on where session state actually lived.
  • Root mechanism: health-check detection delay is a separate, expected gap from the session-locality question.
  • Recovery: shared session storage (Redis, signed cookies) lets any surviving instance continue the user's flow.
  • Prevention: audit for node-local state before scaling out — an instance failure is the wrong time to discover it.

Remember this

An instance crash behind a load balancer is only invisible to the user if session state was never node-local to begin with — verify this with an actual kill-and-retry test, not an assumption.

Key takeaway

Vertical scaling is the simplest relief for a single-box bottleneck. Horizontal scaling is the path to elastic capacity and instance-level HA — after you remove node-local state. Defend each step with load-test numbers and a clear failure domain story.

Practice (20 min): Take one authenticated endpoint. Confirm where the session lives today. If it is in process memory, move it to Redis (or signed cookies), run two local app processes behind a simple LB/proxy, kill one process mid-session, and verify the next request still authenticates on the survivor.

Share:

Related Articles

Checkout slows while product pages remain healthy. CPU is moderate, but the orders table shows rising lock waits and wri

Read

At the heart of every database system lies a Storage Engine that determines how data is written to disk, indexed, and re

Read

Rate Limiting is a critical defense mechanism for production APIs, protecting downstream microservices from traffic spik

Read

Keep learning

Follow a structured path or browse all courses to go deeper.