lumo

Module 01 · Lesson 9 of 14Free

Vertical and horizontal scaling: when to use each

Level
Beginner
Topic
System design fundamentals
Reading time
14 min

When you finish this page you can

  1. Identify the resource that limits capacity before choosing a scaling direction.
  2. Compare a larger server with several smaller servers using explicit assumptions.
  3. Calculate capacity remaining after one application server fails.
  4. Explain why adding application servers can overload a shared database.

Builds on 3 lessons

Start with lesson 315 min
  1. Server concurrency: handling many requests at once15 min
  2. Latency and throughput: measuring speed and capacity14 min
  3. Design tradeoffs: making and explaining decisions18 min

Video lesson · coming soon

At the festival opening, receives 1,000 requests a second. Its application server has 8 CPU cores. Someone proposes replacing it with a larger machine; someone else proposes adding another server.

Which change would help, and what evidence would justify it?

Check yourselfAnswer in your head first, then reveal
Each request needs 5 ms of CPU work. At 1,000 requests a second, must Kartly replace its 8-core server?Show answerHide answer
Not from that number alone. The requests demand 5 CPU-seconds each second, or about 62.5% of eight cores under an idealized model. You still need to check waiting, memory and dependencies.

Bigger and more solve different problems

changes one machine’s resources: more CPU cores, more memory, or a different machine size. changes the number of application instances running the workload. An instance is one running copy of the application.1

Picture a checkout area. A larger counter gives one worker more space. Opening another counter lets another worker serve a different customer. Neither change fixes a payment terminal that every counter must share.

More resources in one server, or more servers sharing the work. Neither option automatically enlarges the database.

Kartly currently has one application server and one PostgreSQL database. The multi-server designs below are possible next steps, not changes already made to the running example.

As Server concurrency explains, requests can wait without keeping a CPU core busy. A high number of requests in progress therefore does not prove that Kartly needs more cores.

The first decision is not “bigger or more”. It is “what resource is preventing useful work?”

Find the limit before adding capacity

A is the resource currently limiting useful . Google’s overload guidance warns that requests per second alone can hide large differences in request cost.2

Use the measurements from Latency and throughput, then connect the delay to a resource.

What you observe What to investigate
CPU stays busy as response times rise CPU work per request and whether all cores are used
Requests wait for database access Query duration and available database connections
Memory keeps growing Live application data and unfinished work
Only one request type slows down Its code path and dependencies

For this lesson’s next scenario, assume a new page renderer doubles CPU work to 10 ms per request. Database and pricing waits remain unchanged.

Assume load measurements now show CPU saturation under the sale workload. The team has ruled out an easy code improvement. It wants a planning limit of 70% CPU usage.

That percentage is a scenario choice, not a universal safe threshold. It leaves room for uneven work and unexpected demand.

Calculate CPU demand

1,000 requests/s × 0.010 CPU-s/request = 10 CPU-s/s

The workload asks for more CPU time each second than eight cores can supply.

Estimate the existing server’s planning capacity

8 cores × 0.70 ÷ 0.010 CPU-s/request = 560 requests/s

This is a CPU-only estimate, assuming work spreads across all cores.

Estimate a larger server

16 cores × 0.70 ÷ 0.010 CPU-s/request = 1,120 requests/s

A larger server appears sufficient for the target traffic. Dependencies must still support that rate.

These estimates assume comparable cores and unchanged request cost. They are a reason to test a candidate configuration, not a performance guarantee.

A larger server preserves simplicity

Vertical scaling keeps the familiar: the application receives a request, reads PostgreSQL, calls pricing and returns the page.

For this scenario, assume the replacement server has comparable per-core performance. Also assume Kartly’s application can use its additional cores.

Should Kartly first replace its CPU-limited application server or introduce multiple servers?

Run two application servers.

Two 8-core servers offer the same total core count. They also require traffic routing and an application that works when requests reach different copies.

Why this choice

Choose the larger machine if Kartly can accept a planned interruption and does not yet require application-server failure tolerance. Vertical resizing often involves temporary unavailability.1

Before switching, compare measured performance and quoted cost. Do not assume the larger machine costs exactly twice as much.

The choice changes if a suitable machine is unavailable, too expensive, or still too small. It also changes if Kartly must continue serving during an application-server failure.

More cores do not automatically shorten one request. Under our assumption, each request still needs 10 ms of CPU work. The extra cores let independent requests run at the same time.

The upgrade solves the estimated CPU shortage. It does not remove the application server as a failure point.

More servers divide independent work

Now add a product requirement: browsing must continue if one application server fails.

Horizontal scaling needs a , a component that selects a server for incoming traffic. Kartly’s proposed balancer sends each new request to a healthy application instance. Instances must be ready before receiving traffic. Departing instances should stop receiving new work and finish existing requests where possible.1

Diagram not drawn yet: scaling-kartly-surviving-one-failure.svg

Customers send 1,000 requests per second through a load balancer to three application servers with 8 cores each. Server C is marked failed. Servers A and B each receive about 500 requests per second. Both surviving servers use the same PostgreSQL database and pricing service.

Three smaller servers leave enough estimated CPU capacity after one fails. The shared dependencies still need separate capacity checks.

For this proposed version, assume all durable customer data lives in PostgreSQL. No request needs data held only in another application server’s memory.

The read path becomes:

  1. The customer requests a product page.
  2. The balancer selects a healthy application server.
  3. That server reads PostgreSQL and asks pricing for the price.
  4. It builds the response and sends it back through the balancer.

A cart write follows the same route, but the selected application server saves the change in PostgreSQL. Adding application instances does not copy or divide the database.

Component Responsibility and state Failure consequence
Load balancer Routes requests; keeps the available-server list Routing can stop; its own redundancy must be checked
Application servers Execute requests; keep temporary request data Affected requests can fail; healthy servers receive later traffic
PostgreSQL Stores durable customer data Database-dependent operations cannot complete
Pricing service Answers price requests Product pages need an explicit failure response

The balancer can stop routing to a failed server, but it cannot recover a response already lost during that failure. Several servers therefore require both spare capacity and failure handling.

Spare capacity makes failover useful

Two servers sound redundant. But redundancy is not enough if the survivor cannot handle the traffic.

Under our CPU model, two 8-core servers provide a combined planning capacity of 1,120 requests a second. Losing either leaves only 560.

The customer still sees slow pages, even though a server remains alive.

How many smaller application servers meet both traffic and one-server-failure requirements?

Two 8-core servers.

Normal operation fits the CPU estimate. After one server fails, the survivor cannot serve the target traffic within the chosen CPU budget.

Why this choice

For this scenario, three servers satisfy the extra failure requirement. This assumes balanced work and sufficient dependency capacity.

Two could be enough at lower traffic. Larger instances are another option. Choose using measured capacity after failure, not server count alone.

Remove one server from the plan

3 servers − 1 failed server = 2 surviving servers

Count only the capacity available during the failure.

Calculate surviving planning capacity

2 × 560 requests/s = 1,120 requests/s

The estimated surviving capacity covers the sale traffic.

Check CPU usage under balanced routing

500 requests/s × 0.010 CPU-s/request ÷ 8 cores = 62.5%

Each survivor stays below the chosen planning threshold.

take time to detect failure. During that interval, some customers can still receive errors. This design targets enough surviving capacity; it does not promise uninterrupted requests.

Shared limits move the bottleneck

A tempting belief is “twice as many servers means twice the capacity”. That only holds if other limits do not intervene. Microsoft’s guidance explicitly calls for checking databases and downstream APIs before adding compute.1

Consider a separate scenario assumption: PostgreSQL can complete 900 representative queries a second within Kartly’s response-time target. Each page request requires one query.

Even if the application tier supports more traffic, the database limits these pages to at most 900 a second under those conditions.

Adding application servers may move waiting into a database . Customers still wait for the same scarce resource. Overload handling must therefore remain part of the design, even with traffic distribution.2

Connections create another shared limit. Suppose every application instance allows 40 open database connections. Three instances could request 120 connections.

PostgreSQL’s typical default connection limit is 100. Some slots are reserved. Raising the limit also increases certain resource allocations; it does not establish that queries will execute faster.3

Set a fleet-wide connection budget, then divide it across instances. Investigate expensive queries before introducing more database infrastructure. Scaling a relational database covers the later choices.

The useful question becomes “what limits completed requests now?”, not “how many machines can we add?”

Add capacity before customers need it

For Kartly’s fixed-time sale, prepare capacity before opening. Scheduled scaling can account for startup time; reactive scaling must first detect demand and then start usable instances.1

Use this release checklist:

  • Confirm the expected request mix, not only total traffic.
  • Measure response times and errors at the target load.
  • Remove one application instance and repeat the measurement.
  • Check database connections and dependency waiting.
  • Confirm new instances are ready before receiving requests.

Treat the estimates as hypotheses. Keep the change only if completed requests improve without moving unacceptable delays elsewhere.

Where this shows up

Before Kartly adds its second application server, it must answer another question: what happens when that server cannot see a customer’s ?

Common mistakes

  • Scaling from CPU usage alone. Check whether customers are waiting on another resource before buying compute.
  • Calling two servers failure-ready. Calculate whether the survivors can handle the required traffic.
  • Assuming scaling out fixes every layer. Application instances still share database and pricing limits.
  • Expecting an immediate traffic response. Allow time for detection, startup and readiness.

Check your understanding

Check yourselfAnswer in your head first, then reveal
Kartly’s application CPU is lightly used, but database queries are becoming slower. What should you investigate before adding application servers?Show answerHide answer
Investigate database work and waiting first. More application servers do not add database capacity and may send it more concurrent work.
Check yourselfAnswer in your head first, then reveal
Two application servers each support 600 requests a second within the target. Traffic is 1,000 a second. Does this configuration meet a one-server-failure requirement?Show answerHide answer
No. Normal combined capacity is sufficient, but one failure leaves only 600 requests a second. Surviving capacity is the relevant number.
Check yourselfAnswer in your head first, then reveal
A request uses one core for its CPU work. You replace an 8-core machine with a comparable 16-core machine. Must that individual request finish twice as fast?Show answerHide answer
No. More cores can increase throughput across independent requests, but they do not necessarily shorten this request’s CPU work.
Check yourselfAnswer in your head first, then reveal
Three application instances can each open 40 database connections. The fleet budget is 90. What configuration change is needed?Show answerHide answer
Reduce per-instance limits or otherwise enforce the fleet budget. Three instances configured for 40 could request 120 connections, exceeding the budget.

Summary

  1. Choose bigger or more servers by identifying the limiting resource and the required failure behavior.
  2. Vertical scaling adds resources to one machine and preserves a simpler application topology.
  3. Horizontal scaling divides independent work across instances, but needs routing and shared-data planning.
  4. Size for surviving capacity when server failure is part of the requirement.
  5. Recheck shared dependencies after every scaling change; estimates guide measurements, not guarantees.

Report a mistake

Tell us what’s wrong in this lesson and we’ll fix it.