Most EC2 Auto Scaling setups start with CloudWatch metrics.
CPU utilization is the obvious example. If CPU goes above 70 percent, add an instance. If it drops below 30 percent, remove one.
That works well for many applications.
The problem appears when CPU is no longer a good representation of the workload.
A web application may have low CPU usage while the request queue is growing.
A background worker may have moderate CPU usage while thousands of jobs are waiting.
A custom application may expose a metric such as requests per second, active jobs, queue depth, or processing latency that tells you much more about when another EC2 instance is needed.
If the application already exposes those metrics through Prometheus, creating another copy of the same metric in CloudWatch just for Auto Scaling can add unnecessary configuration.
AWS now provides a way to use Prometheus-compatible metrics with CloudWatch and EC2 Auto Scaling. Amazon Managed Service for Prometheus can collect application metrics, while Amazon CloudWatch can use metrics from Amazon Managed Service for Prometheus through the CloudWatch agent and Metric Insights. EC2 Auto Scaling can then use the resulting CloudWatch metric for scaling decisions.
The important idea is simple:
Application
|
| Prometheus metrics
v
Amazon Managed Service for Prometheus
|
| CloudWatch integration
v
Amazon CloudWatch
|
| Scaling metric
v
EC2 Auto ScalingThis lets the scaling signal come from the application workload rather than forcing every scaling decision to be based on infrastructure metrics.
Why CPU Is Not Always the Right Scaling Signal
Imagine an EC2-based order-processing service.
Each instance has four worker processes.
The application exposes:
orders_waiting
orders_processed_total
processing_latency
active_workersNow imagine the queue grows like this:
10 jobs
25 jobs
80 jobs
200 jobs
500 jobsBut CPU remains around 45 percent.
If your Auto Scaling policy only watches CPU, it may decide that nothing needs to change.
The application sees a completely different situation.
The workers are falling behind.
A better scaling signal could be:
orders_waitingFor example:
0 to 20 jobs 1 instance
20 to 100 jobs 2 instances
100 to 250 jobs 3 instances
250+ jobs 4+ instancesThe exact thresholds depend on the application.
The important point is that the metric represents the thing you are trying to control.
If your goal is to keep queue latency low, scaling on CPU alone may not be enough.
A Typical Architecture
A production architecture might look like this:
EC2 Application
|
|
Prometheus metrics
|
v
Amazon Managed Service
for Prometheus
|
|
CloudWatch agent
|
v
Amazon CloudWatch
|
|
Scaling metric
|
v
EC2 Auto Scaling
|
+----------+----------+
| | |
v v v
EC2 EC2 EC2There are several separate responsibilities here.
The application is responsible for producing meaningful metrics.
Prometheus is responsible for collecting and storing those metrics.
The CloudWatch integration makes the required metric available to CloudWatch.
CloudWatch provides the metric that the Auto Scaling policy evaluates.
EC2 Auto Scaling changes the number of instances based on that metric.
Keeping those responsibilities separate makes the system easier to troubleshoot.
Start With a Metric That Represents Real Work
Before configuring Auto Scaling, choose the metric carefully.
This is probably the most important part of the entire design.
A bad metric produces bad scaling.
For a web application, useful metrics might include:
Requests per second
Active requests
Request latency
Requests waiting
Queue depth
Error rate
For background workers:
Jobs waiting
Jobs currently processing
Processing latency
Jobs processed per minute
For API services:
Requests per second
Concurrent requests
p95 latency
Work waiting in an internal queue
For image or video processing:
Files waiting
Processing duration
Active workers
Items completed per minute
Try to choose a metric that has a direct relationship with the amount of capacity your application needs.
For example, this is often more meaningful:
jobs_waitingthan:
cpu_utilizationwhen the workload is a queue processor.
Expose the Metric From the Application
A Prometheus metric should be generated by the application or its supporting infrastructure.
For a .NET application, a Prometheus-compatible metrics library can expose application measurements.
A simplified example might look like this:
public static readonly Gauge JobsWaiting =
Metrics.CreateGauge(
"orders_jobs_waiting",
"Number of orders waiting to be processed");When the application checks the queue:
JobsWaiting.Set(queueDepth);The application can expose the metric through its Prometheus endpoint.
For example, the endpoint may return:
orders_jobs_waiting 42The actual metric naming and instrumentation depend on the library and application architecture.
The important thing is that the metric represents the current workload.
Avoid Metrics That Only Look Useful
Not every application metric is suitable for Auto Scaling.
Consider a metric such as:
total_orders_processedThis is a counter.
It tells you how many orders have been processed since the process started.
It does not directly tell you how much work is waiting right now.
A better scaling signal may be:
orders_waitingor a rate derived from the counter.
The difference matters because Auto Scaling needs a metric that helps it decide whether additional capacity is required.
Think about the question the metric answers.
Bad question:
How many jobs have we processed since startup?
Better question:
How much work is waiting right now?Prometheus Metric Types Matter
Prometheus commonly uses metrics such as counters, gauges, histograms, and summaries.
For scaling, a gauge is often convenient for values such as:
queue_depth
active_jobs
connected_usersA counter is useful for values such as:
requests_total
jobs_processed_total
errors_totalA counter can become a useful scaling signal when you calculate a rate.
For example, you might use a PromQL expression that calculates requests per second.
Conceptually:
rate(http_requests_total[5m])This turns a monotonically increasing counter into a rate.
That can be much more useful for capacity decisions.
Why Use Prometheus Instead of Creating a New CloudWatch Metric?
If your application already uses Prometheus, you may already have the metrics you need.
Without an integration path, teams often create another publishing mechanism:
Application
|
+-- Prometheus metric
|
+-- CloudWatch metricNow the application has two metric pipelines.
That can create problems.
The metric names may drift.
The labels may be different.
The aggregation rules may differ.
One system may update faster than the other.
Developers may fix one metric and forget the other.
Using Prometheus as the application metric source reduces some of this duplication.
The overall model becomes:
Application
|
v
Prometheus instrumentation
|
+----------------+
| |
v v
Prometheus CloudWatch
analysis scalingPrometheus remains useful for observability.
CloudWatch provides the AWS-native integration needed by Auto Scaling.
Use Labels Carefully
Prometheus labels are powerful, but they can create problems if you use too many unique values.
For example:
orders_waiting{instance="i-123456"}is reasonable.
But a metric such as:
request_count{user_id="123456"}can produce extremely high cardinality if the application has millions of users.
This is especially important when metrics are being transferred between monitoring systems.
For scaling, keep the metric focused.
A good metric might be:
orders_waitingwith a small number of dimensions such as:
environment
service
queueAvoid putting request IDs, user IDs, transaction IDs, or other highly unique values into scaling metrics.
Create a PromQL Expression for Scaling
The next step is deciding what Prometheus should calculate.
Suppose your application exposes:
orders_waitingYou might not want to scale directly from the raw value.
Instead, you could aggregate across instances.
For example:
sum(orders_waiting)Now the metric represents the total number of waiting jobs across the service.
This is often better than scaling independently from each EC2 instance.
Another example is request rate:
sum(rate(http_requests_total[5m]))This gives you the total request rate across the service.
The exact PromQL expression depends on your application.
The key question is:
What single value should represent the workload that the Auto Scaling group needs to handle?Be Careful With Per-Instance Metrics
Suppose you have three EC2 instances.
Their queue depths are:
Instance A 100
Instance B 20
Instance C 0If you scale using an average, the result is around:
40 jobs per instanceBut that can hide uneven distribution.
If the queue is shared, total queue depth may be a better metric:
sum(orders_waiting)If each instance has an independent queue, the scaling design may need to consider the maximum or another workload-specific aggregation.
The correct aggregation depends on the architecture.
Do not blindly use average, sum, or maximum just because it is available.
Connect the Metric to CloudWatch
EC2 Auto Scaling policies operate using CloudWatch metrics.
Join the conversation! Your thoughts help the community grow.