The Runtime Theory
Cloud & Infrastructure

Autoscaling Is a Latency Decision

Reaction time versus provisioning time, HPA sync intervals, cooldowns, and hysteresis — why autoscaling is a latency engineering problem before it is a capacity problem.

The Runtime Theory Team4 min read#autoscaling#kubernetes#hpa#latency#capacity
On this page

Autoscaling is not a capacity-management feature. It is a latency feature with capacity side effects. Every autoscaler, from Kubernetes HPA to AWS target-tracking to a hand-rolled fleet manager, is making one decision: can the existing fleet absorb this load before the new capacity exists? If the answer is no, the autoscaler has already failed — no matter how many instances it eventually adds.

Reaction time vs. provisioning time

Every scale-out has two clocks, and they run simultaneously:

text
reaction time      — metric latency + collection interval + decision interval
                     (often 1–5 minutes of pure delay)
 
provisioning time  — instance/pod creation + image pull + init + readiness
                     (anywhere from 30 seconds to 5+ minutes)

The total time from "traffic spikes" to "new capacity serving traffic" is the sum. For a typical Kubernetes workload that's 1 minute of metric lag + 15 seconds of HPA sync + 2 minutes of pod scheduling and readiness = roughly 3 minutes of exposure. During those 3 minutes, the existing pods take the full spike — which means the metric that triggered the scale-out (CPU, latency) is the exact thing that degrades while you wait for the fix.

That is the entire argument: autoscaling is a latency decision because the decision is made on the metric you can least afford to lose.

How HPA actually decides

The Kubernetes HorizontalPodAutoscaler works on a fixed control loop, every 15 seconds by default:

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 3
  maxReplicas: 30
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
    scaleDown:
      stabilizationWindowSeconds: 300

The decision is pure arithmetic. Each control loop it computes desiredReplicas = ceil(currentReplicas * currentUtilization / targetUtilization). At 140% CPU with a 70% target, it wants 2x the replicas — and it scales up in one step (the stabilization window for scale-up is effectively zero), but by default tolerates 5 minutes of low utilization before scaling down.

The asymmetry is deliberate and it is the whole point:

  • Scale-up must be aggressive because you're racing a latency cliff.
  • Scale-down must be lazy because a downscale that flips into an upscale the next minute is thrash — pods torn down and re-created, request queues drained, connection pools reset. Hysteresis (the gap between the threshold that triggers up and the threshold that triggers down) is what turns a controller into a stable system instead of an oscillator.

The cooldown trap

"Cooldown" sounds like protection; it is mostly delay. AWS Application Auto Scaling's default cooldown (300 seconds after each scaling activity) is there to prevent flapping, but on a fast-growing workload it means: scale up, wait 5 minutes, measure again, scale up again. A 10x spike needs 4 scale events at 5 minutes apart before the fleet catches up — by which point the p99 has been red for 20 minutes.

The practical fix hierarchy:

  1. Scale out on leading indicators. Queue depth, request latency, or synthetic probe results react before CPU does. CPU is a lagging indicator — it shows the pressure after the pods are already hot.
  2. Proactively oversize the floor. If your traffic has a diurnal pattern, the cheapest autoscaling is the floor you set in advance. minReplicas is not a safety net; it is a precomputed latency budget.
  3. Shrink provisioning time. Pre-warmed images, readiness gates that check real dependencies, fast-start frameworks (GraalVM, statically linked binaries) convert minutes of provisioning into seconds. Every second shaved off provisioning is a second of latency saved during every future spike.
  4. Use predictive autoscaling where the load is schedulable (batch windows, market opens, cron-driven jobs) — the scheduler can start capacity before the metric moves.

The scale-down cliff

Downscaling is where autoscalers cause outages. The failure mode:

text
traffic drops -> utilization drops -> HPA scales down -> 300s stabilization
-> a straggler request (queue drain, long tail latency) arrives at a pod being
terminated -> connection refused -> retry storm -> utilization spikes -> scale up
-> stabilization window -> thrash

The mitigation is to make termination graceful and slow: terminationGracePeriodSeconds, preStop hooks that drain in-flight requests, and readiness probes that remove pods from the service before the kubelet kills them. Kubernetes already implements this on every pod delete — the autoscaler is the trigger that abuses it 30 times a day.

What autoscaling can't fix

Autoscaling cannot fix a request that arrives before the fleet has capacity — it can only shrink how long that window is. If a single pod boots in 4 minutes, no target utilization setting changes that. If your queues back up faster than new workers can join, autoscaling just adds more workers to a growing backlog.

The real design question for any workload is: what is the worst-case gap between the metric moving and new capacity serving traffic, and is the old fleet able to survive that gap? If the answer is "no," the fix is not autoscaling tuning — it is request shedding, queue depth limits, and latency-aware retry budgets so the system fails somewhat gracefully instead of degrading linearly.

The runtime view

  • Autoscaling is reaction time + provisioning time; both are latency before they are capacity.
  • Scale-up aggressive, scale-down lazy: hysteresis is the only thing standing between a stable controller and an oscillator.
  • Cooldowns delay capacity; the fix is leading indicators, faster provisioning, and a higher floor, not shorter cooldowns.
  • Measure the autoscaler by the worst-case time-to-serve-new-capacity, not by its average replica count.