Set a policy
- Dashboard
- CLI
- app.yaml
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 isceil(current * value / target), the same proportional rule Kubernetes uses.
- Dead band. Nothing moves while the reading is within
scale_up_toleranceabove the target (default 10 percent) andscale_down_tolerancebelow 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 mostmax_step_upreplicas (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 waitsscale_down_cooldown(default 10 minutes) after any change, and removes at mostmax_step_downreplicas (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
minandmax(for example after you lowermax), 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:
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
replicason 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.