Amazon ECS Service Auto Scaling, Explained: Metrics, Policies, and Real-World Patterns
A breakdown of how ECS service auto scaling actually works - target tracking vs. step scaling, the metrics ECS gives you for free, and how to wire in custom signals like SQS queue depth - with patterns you can apply to real workloads.
Why auto scaling still trips people up
Amazon ECS makes "add more tasks when busy" feel like a checkbox in the console. But the moment your scaling need goes beyond watching CPU, most people hit the same wall: what's the difference between target tracking and step scaling, which metric should actually drive scaling for this workload, and how do you get something like queue depth into the mix? This guide walks through the building blocks and a few patterns that hold up in production.
The building blocks
ECS itself doesn't do the scaling math - it hands that job to Application Auto Scaling, which watches a metric in CloudWatch and adjusts your service's desired task count according to a policy you define. Three pieces make up any scaling setup:
A metric - what you're watching (CPU, memory, request count, queue depth, or something custom)
A scaling policy - the logic that turns that metric into a task-count decision
Boundaries - your min/max task count, so scaling can't run away in either direction
Four policy types are worth knowing:
Target tracking - pick a target value (e.g., "keep average CPU at 50%") and Application Auto Scaling handles the math, like a thermostat. The right default for most services.
Step scaling - define explicit steps ("if CPU > 70%, add 2 tasks; if CPU > 90%, add 5"), giving you control over the size of the reaction.
Scheduled scaling - fixed capacity changes at known times, for predictable daily or weekly patterns.
Predictive scaling - ECS now supports ML-based predictive scaling that pre-scales ahead of anticipated demand, layered on top of a reactive target-tracking policy.
What you get for free
Every ECS service automatically publishes two CloudWatch metrics: ECSServiceAverageCPUUtilization and ECSServiceAverageMemoryUtilization. Pair either with a target tracking policy and you have working auto scaling in a few clicks. If your service sits behind an Application Load Balancer, you also get ALBRequestCountPerTarget - often a better proxy for "busy" than CPU, since a task can hold open a lot of slow connections while barely touching the CPU.
As of a recent update, you can also opt into 20-second-resolution metrics (instead of the default 60-second interval) in the service's monitoring configuration, so target tracking reacts faster to sudden spikes - at a small extra CloudWatch cost.
When you need a custom metric
CPU, memory, and request count don't tell the whole story for every workload. The classic case is a queue-processing service: task count should track queue depth, not CPU, since a task can sit at 100% CPU while barely denting the backlog - or idle at 5% while messages pile up.
The common pattern here is backlog-per-task:
Publish a custom CloudWatch metric - typically SQS's ApproximateNumberOfMessagesVisible, or a derived BacklogPerTask value (queue depth ÷ current task count) - often via a small scheduled Lambda function.
Create a target tracking policy against that custom metric, with a target like "keep backlog per task under 10."
Attach the policy to your service's scalable target.
It works well once it's in place, but notice how many moving parts that is compared to checking the CPU box: a publisher function, a metric, and a policy, before ECS makes a single scaling decision. That gap between "built-in metric" and "everything else" is worth keeping in mind as you design a service.
Picking a pattern for your workload
Stateless web service behind an ALB → target tracking on ALBRequestCountPerTarget, with CPU as a secondary policy for safety.
Queue worker → backlog-per-task, as above.
Predictable spiky traffic (daily batch jobs, known marketing pushes) → scheduled scaling as a floor, layered under a reactive policy.
Latency-sensitive with unpredictable spikes → predictive scaling combined with high-resolution metrics, so the reactive policy isn't your only line of defense.
Gotchas worth knowing up front
Cooldowns matter. Target tracking has built-in scale-in cooldowns to prevent flapping - if traffic is genuinely bursty both ways, tune this rather than fighting it with a tighter target.
Min/max bounds are a safety net, not a formality. Set them deliberately; an uncapped policy is a surprise bill waiting to happen.
EC2 launch type adds a layer. On EC2 (rather than Fargate), service-level scaling (task count) and cluster-level scaling (EC2 instance count via a Capacity Provider) are separate mechanisms that need to agree, or tasks will scale up with nowhere to run.
Custom metrics cost money and add a dependency. A publisher Lambda is cheap, but it can fail silently - alarm on the metric's own delivery, not just on the scaling outcome.
Wrapping up
Target tracking with the built-in CPU, memory, or request-count metrics covers most services well, and it's worth defaulting to before reaching for anything custom. When a workload's real signal isn't one of those three - queue depth being the most common case - backlog-per-task is the well-worn path, even if it takes more setup than flipping on the built-in option. If that gap between "built-in" and "custom" metrics is something you run into often on your own services, you're not alone - it's a common enough friction point to flag as feedback next time you're in Builder Center.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article