Skip to content

Target tracking vs step scaling — picking the right ASG policy for the workload

Target tracking is the default for a reason — but it flaps on bursty workloads. Step scaling is more code, more right for cron-driven batch. Here is how to decide and how to spot a target-tracking policy that is fighting itself.

The ASG was set up two years ago with a single target-tracking policy: maintain average CPU at 50%. It worked fine for a year. Then the workload changed — a new batch job ships logs every fifteen minutes, the spike runs 90 seconds, the box hits 95% CPU during it. Target tracking sees the spike, scales out, adds two instances. Ninety seconds later the spike is gone and the new instances are idle. Five minutes after that, the cooldown clears and target tracking sees average CPU at 28%, scales in, removes two instances. Then the next batch arrives. The ASG is now flapping on a 15-minute cycle, costing extra instance-hours and triggering a small notification storm every time. The right answer is not “tune the target” — it is to pick the right policy shape for the workload’s behavior.

What’s happening

ASG has three policy types. Simple scaling is the original, single-threshold model: when an alarm fires, change capacity by N. It is legacy. AWS keeps it for compatibility. Do not use it for anything new. The other two are the real choice.

Target tracking is the modern default. You pick one metric (CPU, network in/out, request count per target, or a custom metric) and a target value. AWS creates two CloudWatch alarms for you — one for “above target,” one for “below target” — and the policy adds or removes capacity to keep the metric near the target. The math AWS does behind the scenes is conservative: it estimates how many instances would have been needed to keep the metric at target during the alarm period, then takes a fraction of that delta to avoid overshoot.

Target tracking shines when:

  • The metric scales roughly linearly with capacity. CPU and request count per target are the canonical examples.
  • The workload is steady-state-with-trend — diurnal traffic curves, slow weekly growth, predictable peaks. The kind of thing a SaaS web tier produces.
  • You do not want to maintain alarm definitions yourself. Target tracking owns the alarms it created.

Step scaling is the explicit model. You define a CloudWatch alarm yourself, attach a step scaling policy to it, and specify scaling steps based on how far the metric is from the threshold. For example: alarm when CPU > 70% for 2 of 3 periods. Steps: +1 instance when 70–80%, +2 instances when 80–90%, +4 instances when above 90%.

Step scaling shines when:

  • The workload is event-driven or cron-driven — batches, ETL runs, scheduled load tests. The metric does not move smoothly.
  • The right response to a 10-point overage is different from the right response to a 30-point overage. Target tracking treats both the same way relative to error.
  • You need to scale on a non-linear metric — queue depth where adding one consumer halves the depth, not adjusts it by some smooth fraction.
  • You want explicit, auditable alarm definitions in IaC. Step scaling’s alarms are your own; target tracking’s are AWS-managed and harder to reason about in Terraform diffs.

The flapping pattern in the opener is the most common failure mode of target tracking. It happens when the metric spikes faster than the scaling action can take effect. Target tracking sees the spike, fires the high alarm, requests N more instances. By the time the instances are warm and pass health checks, the spike is over. Now the metric is below target, the low alarm fires, the policy scales in. The pattern repeats on the next batch.

The other failure mode: target tracking on a metric that is not linear in capacity. If you scale on “memory available” and the workload is bursty, doubling the fleet does not halve the memory pressure during the burst — the burst is over before the new instances accept traffic. The target is never reached, the policy keeps adding capacity, the cap saves you.

How to verify

To see whether target tracking is flapping, pull the scaling activity log for the past 24 hours:

aws autoscaling describe-scaling-activities \
  --auto-scaling-group-name my-app-prod-asg \
  --max-items 100 \
  --query 'Activities[].[StartTime,StatusCode,Description]' \
  --output table

Healthy steady-state target tracking shows a handful of activities per day, mostly during diurnal transitions. Flapping shows pairs of scale-out / scale-in activities close together — often the same hour. If you see “Launching a new EC2 instance” followed within ten minutes by “Terminating EC2 instance”, target tracking is fighting itself.

Look at the metric the policy is tracking, with the policy’s evaluation window overlaid:

aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 \
  --metric-name CPUUtilization \
  --dimensions Name=AutoScalingGroupName,Value=my-app-prod-asg \
  --start-time $(date -u -v-1d +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 60 \
  --statistics Average \
  --output text

A flapping workload’s metric chart shows sharp spikes returning to baseline within minutes. A smooth workload shows a slow curve. Target tracking fits the second, fights the first.

To find the target tracking policy’s auto-managed alarms:

aws cloudwatch describe-alarms \
  --query 'MetricAlarms[?starts_with(AlarmName, `TargetTracking-`)].[AlarmName,Threshold,EvaluationPeriods]' \
  --output table

If you see an alarm with a 1-period evaluation and a 60-second period, the policy is reacting to every minute. That is fast for a flapping workload — and it amplifies the flap. Target tracking’s hard-coded 3-minute scale-in cooldown and 3-out-of-3-period requirement help, but on a 15-minute cycle they are not enough.

The fix

Decision tree, in order:

Is the metric linear in capacity? If yes, target tracking is on the table. If no — for example queue depth where one extra worker halves the depth, or memory pressure where bursts complete before the new instance is in service — go straight to step scaling.

Is the workload smooth or spiky? Smooth (diurnal curves, slow ramps, gradual load increase) → target tracking. Spiky (cron batches, scheduled tests, event-driven surges) → step scaling.

Does the right response vary with the size of the overage? A 5-point CPU overage on a single-tier web app warrants a small change; a 30-point overage warrants a bigger one. Target tracking will treat the two proportionally but with the same time constant. If you need explicit non-linear response, step scaling.

For the flapping scenario in the opener, the fix is to replace the single target-tracking policy with a layered approach:

  1. Smooth-curve coverage with target tracking on ASGAverageCPUUtilization, but with a longer scaling cooldown and a target that leaves more headroom (40% rather than 50%).

  2. Batch coverage with a step-scaling policy on an alarm that fires only when CPU is above 85% for 3 of 5 one-minute periods — which the brief batch spike never sustains. The step policy handles real load excursions, not 90-second blips.

  3. Predictive scaling if the batch is scheduled and predictable. aws autoscaling put-scheduled-action lets you bump min-size before a known window and drop it after.

Concrete step-scaling policy for the batch coverage:

aws autoscaling put-scaling-policy \
  --auto-scaling-group-name my-app-prod-asg \
  --policy-name scale-out-on-sustained-cpu \
  --policy-type StepScaling \
  --adjustment-type ChangeInCapacity \
  --metric-aggregation-type Average \
  --step-adjustments '[
    {"MetricIntervalLowerBound": 0, "MetricIntervalUpperBound": 10, "ScalingAdjustment": 1},
    {"MetricIntervalLowerBound": 10, "MetricIntervalUpperBound": 20, "ScalingAdjustment": 2},
    {"MetricIntervalLowerBound": 20, "ScalingAdjustment": 4}
  ]'

The step bounds are relative to the alarm threshold. With an alarm at CPU > 70%, the policy adds 1 instance when CPU is 70–80%, 2 when 80–90%, 4 when above 90%. The alarm needs to be configured for sustained breach (3 of 5 periods) so the policy does not fire on the same 90-second spike that target tracking was chasing.

Three rules that hold across every ASG policy review:

One policy per behavior. Target tracking for the smooth curve. Step scaling for the sustained excursions. Scheduled action for the known windows. Do not pile a target on top of a step on the same metric without thinking about which wins when both alarm at the same time.

Look at the activity log before tuning. Target tracking that is “scaling too much” is usually a workload-shape mismatch, not a target-value problem. Lowering the target makes a flapping ASG flap harder.

Set realistic warmup. The default_instance_warmup on the ASG (and EstimatedInstanceWarmup on the policy) tells target tracking how long a new instance takes to actually carry load. If you set it to 60 seconds but the AMI bootstraps for 4 minutes, the policy will see the metric still high and double-scale. For most application servers it is 300–600 seconds, not 60.

Target tracking is the right default for a SaaS web tier. Step scaling is the right default for batch and event-driven work. The mistake is reaching for target tracking everywhere because it is shorter to write, then debugging flaps when the workload does not match the model.


Stack Harbor operates ASG-backed application tiers and cost-capped auto-scaling policies as part of Managed AWS and the Auto-Scaling operating layer. If your scale events are still surprises, start with an infrastructure consult.