System designCore11 min

Autoscaling

Traffic rises and falls through the day, and a fleet sized for the peak sits half idle the rest of the time. An autoscaler adds and removes servers to follow the load. It works well for slow curves and badly for sudden spikes, because new servers always arrive late.

1 · The problem

A fixed fleet is either wasted or overwhelmed

An online tax-filing site serves 2,000 requests a second overnight and 10,000 at its afternoon peak. Each web server handles 200 requests a second comfortably, so the site needs 10 servers at 4 a.m. and 50 at 3 p.m. A typical weekday, modeled as a smooth curve:

  • Servers needed
  • Fixed fleet sized for the peak
0102030405005101520Hour of dayServers

Demand = 2,000 + 8,000 × sin(π × (hour − 8) ÷ 14) requests a second between 8:00 and 22:00, and 2,000 otherwise; servers needed = demand ÷ 200. The fixed fleet of 50 is more than 90% used for only about 4.5 hours a day.

A fleet of 50 all day costs 50 × 24 = 1,200 server-hours and averages about 50% busy. Following the curve with whole servers, in 5-minute steps, needs about 603 server-hours: roughly half. Size the fleet for the average instead, and the site falls over every afternoon.

Autoscaling resizes the fleet automatically from measurements. Adding servers is scaling out; removing them is scaling in. (Moving to bigger machines is scaling up, a different tool covered on Scalability.) It assumes servers are interchangeable, so any of them can take any request (Stateless services & sessions).

2 · The idea

An autoscaler is a loop that keeps one number near a target

Most autoscalers work like a thermostat. You pick a metric and a target, say average CPU at 60%, and the autoscaler repeats four steps:

  1. Measure

    Read the metric across the fleet. Kubernetes' Horizontal Pod Autoscaler checks every 15 seconds by default. Amazon EC2's CPU metrics arrive every 5 minutes by default, or every minute with detailed monitoring.

  2. Compute

    Work out how many servers would bring the metric back to the target.

  3. Resize

    Start or stop servers to reach that number, within a minimum and a maximum you set.

  4. Wait

    Let new servers start and warm up before measuring them, so their empty first minutes don't mislead the next decision.

The computation is a proportion. If the load is spread evenly, doubling the servers halves the load on each:

servers wanted = ceil(servers now × metric now ÷ target)

At 3 p.m. the site has 20 servers running at 90% CPU against a 60% target: ceil(20 × 90 ÷ 60) = 30 servers. Kubernetes uses exactly this formula, and ignores changes within 10% of the target so that small wobbles don't resize the fleet.

Why aim for 60% and not 95%? The gap is headroom. It absorbs a rise in traffic during the minutes before new servers arrive. A high target is cheaper and a low one is safer; the right value depends on how fast your traffic can rise compared with how fast servers start (see the next sections).

3 · The signal

Scale on a number that falls when you add servers

The formula works only if the metric drops in proportion as servers are added. CPU per server does, when requests are spread evenly. Many tempting metrics don't:

Metric

Good for scaling?

Why

Average CPU per server

Yes, for CPU-bound servers

Twice the servers, half the CPU each

Requests per server

Yes

Twice the servers, half the requests each

Total requests at the load balancer

No

Doesn't change when you add servers

Response time

No

Rises with load, but not in proportion, and also when a database is slow

Messages waiting in the queue

Not on its own

Doesn't say how many workers are needed

Messages waiting per worker

Yes, for queue workers

Twice the workers, half the backlog each

Response time deserves a warning. If the database is the slow part, every web server waits on it, response time climbs, and the autoscaler adds servers. More servers send more queries to the struggling database and make things worse.

Queue workers need a different target. The site turns each filed return into a PDF, which takes a worker 3 seconds, and users accept up to a 60-second wait. That sets the target backlog:

acceptable backlog per worker = acceptable wait ÷ time per job = 60 ÷ 3 = 20 jobs

At 9 p.m. 1,200 PDFs are waiting. 1,200 ÷ 20 = 60 workers clears them within the minute; with 30 workers, each faces 40 jobs, a 2-minute wait, so the autoscaler doubles them (Message queues & pub/sub).

4 · The catch

New capacity arrives minutes after it was needed

On deadline day, at 11 p.m., traffic climbs from 6,000 to 18,000 requests a second in 10 minutes. The autoscaler reacts, but each decision takes time to become a working server: the metric is collected and averaged, the decision is made, a machine boots, the application starts, caches warm up, and health checks pass. Suppose all of that takes 5 minutes. The fleet is always sized for the demand of 5 minutes ago, at 60% utilization (capacity = demand ÷ 0.6):

  • Demand
  • What the fleet can serve
010,00020,00030,000051015Minutes after the surge startsRequests per second

Fleet capacity = demand 5 minutes earlier ÷ 0.6. Headroom covers the first 3 minutes. From about minute 3.3 to minute 7.5 demand runs ahead, up to 2,000 requests a second short at minute 5, until the new servers catch up.

Two things decide the gap: how fast traffic can rise, and how long a new server takes to be useful. Headroom buys time against the first. Faster starts shrink the second: a container on an already-running host usually starts faster than a new virtual machine, and some platforms keep a warm pool of instances that have already been through their slow setup and wait, stopped, to be switched on. During the gap itself, the servers that exist should refuse what they can't handle rather than slow down for everyone (Load shedding & backpressure).

When the surge is predictable, don't wait to react to it:

  • Scheduled scaling: the deadline is known, so raise the fleet's minimum to the deadline-night size from 10:30 p.m. on deadline day.
  • Predictive scaling: a forecast built from past days and weeks adds servers before the usual afternoon rise. It suits daily and weekly cycles and applications that are slow to start.

Reactive scaling stays on underneath, for the traffic the forecast missed.

5 · Scaling in

Remove servers slowly, and let them finish their work

Scaling in is where autoscalers cause their own trouble. Traffic is noisy: if the fleet shrinks the moment load dips, it grows again a minute later, then shrinks, then grows. This flapping costs boot time on every cycle and leaves the fleet short whenever the dip ends.

So autoscalers add quickly and remove slowly. Kubernetes looks back over a stabilization window, 5 minutes by default, and scales in only to the highest size recommended during that window. AWS target tracking likewise scales in gradually and won't remove a server if doing so would push the metric back over the target.

A server being removed may still be in the middle of work. Removing it safely takes a few steps:

No new requests reach the server, and the ones it has can finish. In Kubernetes the shutdown request is a SIGTERM, and the grace period before the kill is 30 seconds by default.

Queue workers have it harder: a worker halfway through a 3-second PDF is fine, but one halfway through a 20-minute job loses 20 minutes of work. Either let it finish (some platforms can protect a busy instance from scale-in), or rely on the queue: an unfinished message becomes visible again and another worker processes it. That second run is one reason consumers must handle repeats (Delivery semantics).

6 · The limits

Autoscaling moves the bottleneck; it doesn't remove it

Every web server opens a pool of connections to the database. At 20 connections each, the afternoon fleet of 50 opens 1,000. PostgreSQL's default limit is typically 100 connections. The web tier scaled out perfectly, and the database refuses most of it. The things autoscaling quietly depends on:

Shared resource

What happens as servers are added

The usual answer

Database connections

Each server adds its pool; the database's limit doesn't move

A connection pooler between them, and a maximum fleet size

The database itself

More servers send it more queries

Caching, read replicas, sharding; the database rarely autoscales in minutes

Other services and APIs

Your fleet doubles; their rate limits don't

Know their quotas; queue and retry with backoff (Timeouts, retries, backoff & jitter)

Your budget

A bug or an attack scales the fleet up too

A maximum size and a cost alert

The last row hides a trap. A retry storm or a flood of bot traffic looks exactly like popularity, and the autoscaler answers it with more servers. Limits and rate limiting decide what deserves capacity (Rate limiting algorithms); autoscaling only supplies it.

Autoscaling also needs servers that can appear and vanish without notice. A server that keeps users' sessions in memory, or files on its own disk, loses them when it is scaled in. Keep that state in a shared store, so any server can take any request (Stateless services & sessions).

7 · In practice

React for the unexpected, schedule for the known

Autoscalers offer a few kinds of policy. Most real fleets combine them:

Policy

How it decides

Best for

Target tracking

Keeps a metric near a target, proportionally

The default for web servers and workers

Step scaling

Adds or removes a set amount when an alarm crosses a threshold

When you need explicit control of each step

Scheduled

Sets the size at given times

Known events: a deadline, a launch, business hours

Predictive

Forecasts from past days and weeks, and scales ahead

Regular daily or weekly cycles, slow-starting apps

When setting one up:

  • Scale on a metric that falls as servers are added: CPU or requests per server, backlog per worker.
  • Set a minimum that survives the loss of a server or a zone (Redundancy & failure domains), and a maximum your database, quotas and budget can take.
  • Leave headroom that covers the traffic rise during one server start.
  • Measure how long a new server takes to be useful, and work to shorten it.
  • Scale in slowly, and drain servers so in-flight work finishes.
  • Schedule the peaks you already know about, and keep load shedding for the minutes when capacity is behind.

Check yourself

5 questions

1. 20 servers run at 90% average CPU with a 60% target. How many servers does a target-tracking autoscaler aim for?
2. Why is total request count at the load balancer a poor scaling metric?
3. Each PDF takes 3 seconds, users accept a 60-second wait, and 1,200 jobs are queued. How many workers are needed?
4. Traffic triples in 10 minutes, and a new server takes 5 minutes to be useful. Which change does NOT help during the surge?
5. The web tier scales from 10 to 50 servers, each with a 20-connection database pool, and the database allows 100 connections. What happens?

Takeaways

Remember this

  • Autoscaling resizes a fleet of interchangeable servers to follow load, roughly halving cost for a daily curve compared with sizing for the peak.
  • Target tracking computes servers wanted = ceil(servers now × metric now ÷ target), so the metric must fall as servers are added.
  • New servers arrive minutes late; headroom, faster starts, and scheduled or predictive scaling cover the gap, and load shedding covers what's left.
  • Scale out quickly and in slowly, and drain servers so in-flight work finishes.
  • Autoscaling moves the bottleneck to shared resources such as the database, so set a maximum and protect what's downstream.