Skip to main content
Autoscaling keeps an app’s replica count in step with its load. You give an app a minimum, a maximum, a metric and a target. Every 30 seconds the control plane compares the metric with the target and, when it is clearly off, writes a new desired replica count. The normal reconciler then starts or stops containers to match. The decision is plain arithmetic on measured numbers. The same inputs always give the same answer. No model, forecast or AI is involved.

Set a policy

Open the app, choose Deploys, and find the Autoscaling card. Set a minimum, a maximum, the metric and the target, then save. The card shows the replica count now and the count the autoscaler wants, the latest metric reading, the reason for the last decision, and a timeline of recent decisions.

Metrics

CPU and memory are sampled from one container per collection tick, so they read the load of a typical replica, which is what a balanced app needs.

How a decision is made

For a reading outside the dead band, the wanted count is ceil(current * value / target), the same proportional rule Kubernetes uses.
  • Dead band. Nothing moves while the reading is within scale_up_tolerance above the target (default 10 percent) and scale_down_tolerance below it (default 30 percent). The band is wide below on purpose: removing capacity should need clear evidence.
  • Scale up is fast. A scale up waits only for scale_up_cooldown (default 1 minute) since the last scale up, and adds at most max_step_up replicas (default 4) at a time.
  • Scale down is slow. A lower count must be recommended continuously for stabilization (default 5 minutes), and the app lands on the highest count recommended during that window. It also waits scale_down_cooldown (default 10 minutes) after any change, and removes at most max_step_down replicas (default 1) at a time.
  • No oscillation by construction. A scale down never goes below the count that would put the metric back at the target, so the new reading cannot cross the scale up threshold on its own. Tolerances, cooldowns and steps cannot be switched off: an unset value takes its default.
  • Bounds always hold. If the running count is outside min and max (for example after you lower max), it is corrected at once, without waiting for a cooldown.

When it holds, and why

Every tick records one of these reasons. The dashboard, apps autoscale show and the history all use them. A decision that was cut short also records what limited it in blocked_by: cooldown, stabilization, spend_cap, no_capacity, max_replicas, global_ceiling, step_limit or no_metrics. Silence is not zero. For requests_per_replica, a window with no requests counts as idle only while the container’s own samples are fresh; otherwise the reading is unknown and the count is held.

Manual replicas while a policy is set

The autoscaler owns the replica count. A manual edit that would change it (dashboard, apps scale, PUT /api/v1/apps/{name}) is refused with 409, and a deploy whose app.yaml sets replicas fails with the same explanation. To set replicas by hand, clear the policy first: apps autoscale clear leaves the count where it is. A deploy never resets the count the autoscaler chose.

Limits you control

Alerts

The autoscaler records four gauges per app each tick, so an ordinary threshold alert rule can watch them: For example, alert when autoscale_no_capacity is above 0 for 5 minutes, or when autoscale_flaps_1h reaches 3:
The status condition also reads Flapping once the threshold is reached, and the dashboard card says so.

API

Every change is audited, and the history records who changed the policy.

Design notes

  • A controller that writes desired state. It never touches containers. It writes replicas on the app, and the existing reconciler converges. A crash between the two is safe: intent and cooldown are saved first, and a failed write gives the cooldown back.
  • Level-triggered. Each tick re-derives the decision from stored state and current signals. There is no event queue to lose.
  • Hold on the unknown. Missing data, an unreadable spend cap state and a failed capacity check all hold the count rather than guess. Capacity that cannot be told at all (single node with no reported capacity) does not block.
  • Why proportional with a dead band, not a PID loop. It is predictable, explainable in one line per decision, and replayable from a recorded metric trace, which the decision tests do.
  • Not in scope. Predictive or learned scaling, scaling to zero (see sleep when idle), custom metrics, and scheduling replicas across several nodes.
Last modified on October 17, 2026