Auto-scaling › Implementation

Michael BrenndoerferFebruary 10, 20268 min read
Language AI HandbookAll chapters
Part 65 of 67Production Systems
Chapter 494Auto-scaling › Implementation
Reading section 7 of 12 · Implementation · 8 minSection 7 of 12 · 8 min

Book contents

Section 7 of 12 · 8 min read

Auto-scaling › ImplementationOne section · Sections in this chapter7 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let us build a simulation of a reactive auto-scaling controller and visualize how different policies behave under a realistic LLM traffic pattern.

In[4]:
Code
import numpy as np

# Simulate 24 hours of per-minute request arrivals
# Pattern: low overnight, ramp in morning, two midday peaks, evening decline
minutes = np.arange(0, 24 * 60)
hours = minutes / 60.0

base_rate = 20  # requests per minute at minimum
daily_pattern = (
    base_rate
    + 80 * np.maximum(0, np.sin(np.pi * (hours - 7) / 12)) ** 2
    + 40 * np.exp(-0.5 * ((hours - 12.5) / 0.8) ** 2)
    + 25 * np.exp(-0.5 * ((hours - 15.0) / 1.0) ** 2)
)

# Add Poisson noise to simulate request arrival randomness
arrivals = np.random.poisson(daily_pattern)
In[5]:
Code
def simulate_autoscaler(
    arrivals,
    policy,
    capacity_per_instance=40,
    min_instances=2,
    max_instances=16,
    startup_minutes=3,
    cooldown_minutes=5,
):
    """
    Simulate auto-scaling under a given policy.

    policy: callable(metric_value, target) -> desired_instance_delta
    capacity_per_instance: max requests/min each instance can handle
    """
    T = len(arrivals)
    instances = np.zeros(T, dtype=int)
    utilization = np.zeros(T)
    queued = np.zeros(T)
    served = np.zeros(T)

    # State
    current_instances = min_instances
    pending = 0  # instances booting up
    pending_ready_at = []
    cooldown_remaining = 0

    for t in range(T):
        # Check if any pending instances are ready
        newly_ready = sum(1 for r in pending_ready_at if r <= t)
        current_instances += newly_ready
        current_instances = min(current_instances, max_instances)
        pending_ready_at = [r for r in pending_ready_at if r > t]
        pending = len(pending_ready_at)

        # Capacity and utilization
        capacity = current_instances * capacity_per_instance
        served_t = min(arrivals[t], capacity)
        queue_t = max(0, arrivals[t] - capacity)
        util_t = served_t / max(capacity, 1)

        instances[t] = current_instances
        utilization[t] = util_t
        queued[t] = queue_t
        served[t] = served_t

        # Apply policy
        if cooldown_remaining > 0:
            cooldown_remaining -= 1
            continue

        desired = policy(
            util_t, current_instances, min_instances, max_instances
        )
        delta = desired - current_instances

        if delta > 0 and current_instances < max_instances:
            # Scale out: schedule new instances
            for _ in range(min(delta, max_instances - current_instances)):
                pending_ready_at.append(t + startup_minutes)
            cooldown_remaining = cooldown_minutes
        elif delta < 0 and current_instances > min_instances:
            current_instances = max(min_instances, current_instances + delta)
            cooldown_remaining = cooldown_minutes

    return instances, utilization, queued, served


# Define scaling policies
def threshold_policy(util, current, min_inst, max_inst, upper=0.99, lower=0.40):
    if util > upper:
        return current + 1
    elif util < lower and current > min_inst:
        return current - 1
    return current


def target_tracking_policy(util, current, min_inst, max_inst, target=0.70):
    desired = int(np.ceil(current * util / target))
    return max(min_inst, min(max_inst, desired))


def step_policy(util, current, min_inst, max_inst):
    if util > 0.85:
        return min(max_inst, current + 4)
    elif util > 0.75:
        return min(max_inst, current + 2)
    elif util > 0.65:
        return min(max_inst, current + 1)
    elif util < 0.30:
        return max(min_inst, current - 1)
    return current
In[6]:
Code
# Run simulations for all three policies
inst_thresh, util_thresh, queue_thresh, served_thresh = simulate_autoscaler(
    arrivals, threshold_policy
)
inst_track, util_track, queue_track, served_track = simulate_autoscaler(
    arrivals, target_tracking_policy
)
inst_step, util_step, queue_step, served_step = simulate_autoscaler(
    arrivals, step_policy
)

hours_axis = minutes / 60.0


# Compute summary statistics
def dropped_requests(arrivals, served):
    return arrivals - served


dropped_thresh = dropped_requests(arrivals, served_thresh).sum()
dropped_track = dropped_requests(arrivals, served_track).sum()
dropped_step = dropped_requests(arrivals, served_step).sum()

avg_inst_thresh = inst_thresh.mean()
avg_inst_track = inst_track.mean()
avg_inst_step = inst_step.mean()
Out[7]:
Console
Policy Comparison Summary
Policy                 Avg Instances   Dropped Requests
-------------------------------------------------------
Threshold                       2.57                 27
Target Tracking                 2.57                  5
Step Scaling                    3.11                  0

Each policy responds to the midday peak, but they differ in how aggressively they scale out. The conservative threshold policy waits until utilization is effectively saturated, keeping average capacity low but dropping the most requests during the ramp. Target tracking reacts earlier and drops fewer requests at similar average capacity. Step scaling is the most aggressive, provisioning extra headroom to eliminate almost all drops at a higher cost.

Out[8]:
Visualization
Two-panel chart showing request arrivals and instance counts under three scaling policies over 24 hours.
Simulated LLM request arrivals over 24 hours (top panel) and the resulting instance count from each scaling policy (bottom panel). Target tracking closely follows demand throughout the day, while threshold-based scaling exhibits more stepwise behavior. Step scaling responds most aggressively to high utilization, rapidly adding multiple instances during midday peaks.
Out[9]:
Visualization
Two-panel chart showing GPU utilization and unserved arrivals per minute over 24 hours for three scaling policies.
GPU utilization (top panel) and unserved arrivals per minute (bottom panel) across the same 24-hour window. Target tracking stays near the 70% target while substantially reducing the capacity shortfalls seen under conservative threshold scaling. Aggressive step scaling adds enough headroom to avoid drops in this simulated workload.
Out[10]:
Visualization
Line chart comparing unserved requests across a three-stage midday traffic ramp for startup times of 1, 2, and 5 minutes.
Effect of instance startup latency on unserved requests during a three-stage synthetic midday ramp from 11:00 to 14:00 under the same conservative threshold policy. Each capacity step creates a shortfall whose duration and peak grow as startup increases from 1 to 2 to 5 minutes.

The startup latency chart illustrates why fast instance initialization matters so much for LLM auto-scaling. Under the same conservative threshold policy and three-stage synthetic ramp, each traffic step exposes the startup lag: the 1-minute case produces a brief, modest shortfall, the 2-minute case creates a taller and longer one, and the 5-minute case falls behind most severely. This motivates the engineering investment in pre-warmed instance pools and snapshot-based loading strategies, both of which are discussed in the next section.

Out[11]:
Visualization
Scatter plot comparing average instance count and total dropped requests across three scaling policies.
Cost proxy (average instance count over 24 hours) versus service quality (total dropped requests) for each scaling policy. Target tracking drops substantially fewer requests than conservative threshold scaling at similar average cost. Step scaling eliminates drops by provisioning more capacity.

The cost-quality scatter reveals the fundamental tradeoff for every scaling policy. In this run, target tracking cuts dropped requests sharply without materially increasing average capacity relative to conservative threshold scaling. Step scaling eliminates drops entirely, but only by maintaining noticeably more capacity. The right operating point depends on the value of each request to your business: a service where each request represents a paid API call has very different economics than a service where requests are from free-tier users.