Logs are the CloudWatch line everyone notices, and we’ve covered them in CloudWatch Logs to S3: where the savings are. This article is about the rest of the CloudWatch bill: metrics, dashboards, alarms and the API calls that read them.
Each charge is small on its own: thirty cents for a metric, ten cents for an alarm, three dollars for a dashboard. Nobody decides to spend $4,000 a month on metrics, but one team adds a dimension, another installs an agent with the default config, a third points a SaaS monitoring tool at the account, and none of it gets removed because there’s no resource to delete.
Prices below are us-east-1 list prices at the time of writing. Check the CloudWatch pricing page for your region.
What CloudWatch charges for, apart from logs
| Charge | us-east-1 list price |
|---|---|
| Custom metrics, first 10,000 | $0.30 per metric-month |
| Custom metrics, next 240,000 | $0.10 per metric-month |
| Custom metrics, next 750,000 | $0.05 per metric-month |
| Custom metrics, over 1,000,000 | $0.02 per metric-month |
| Standard-resolution alarm | $0.10 per alarm metric per month |
| High-resolution alarm (10 or 30 seconds) | $0.30 per alarm metric per month |
| Composite alarm | $0.50 per alarm per month |
| Dashboard | $3.00 per dashboard per month |
| GetMetricData and GetMetricWidgetImage | $0.01 per 1,000 metrics requested |
| Other API requests | $0.01 per 1,000 requests |
| Metric streams | $0.003 per 1,000 metric updates |
| Contributor Insights | $0.50 per rule per month, plus $0.02 per million matched log events |
The free tier, which doesn’t expire, covers 10 custom metrics, 10 alarm metrics, 3 dashboards of up to 50 metrics each, and one million API requests a month. The million requests do not include GetMetricData or GetMetricWidgetImage.
The metrics AWS services publish on their own, at basic resolution, are free however many resources you have. Every charge in the table is something somebody turned on. Custom metric charges are prorated by the hour and metered only in hours when data arrives; that detail matters later.
Find out which line is yours
Cost Explorer breaks the bill down by usage type, and the CloudWatch names are blunt enough to read. Each call costs $0.01.
# Read-only: last full month of CloudWatch spend by usage type
START=$(date -u -d "$(date -u +%Y-%m-01) -1 month" +%F 2>/dev/null || date -u -v1d -v-1m +%F)
END=$(date -u +%Y-%m-01)
aws ce get-cost-and-usage \
--time-period Start="$START",End="$END" \
--granularity MONTHLY --metrics UnblendedCost \
--filter '{"Dimensions":{"Key":"SERVICE","Values":["AmazonCloudWatch"]}}' \
--group-by Type=DIMENSION,Key=USAGE_TYPE \
--query 'ResultsByTime[0].Groups[].[Keys[0],Metrics.UnblendedCost.Amount]' \
--output text | sort -t$'\t' -k2 -rn
Usage types containing MetricMonitorUsage are custom metrics, AlarmMonitorUsage is alarms, DashboardsUsageHour is dashboards, Requests and GMD-Metrics are the API, and anything with DataProcessing or TimedStorage belongs to the logs article. Then count the metrics that are costing money, namespace by namespace. --recently-active PT3H limits the list to metrics that received data in the last three hours, close to what the hourly metering bills:
# Read-only: active custom metrics per namespace (AWS/ namespaces are free)
aws cloudwatch list-metrics --recently-active PT3H \
--query 'Metrics[?!starts_with(Namespace, `AWS/`)].Namespace' --output text |
tr '\t' '\n' | sort | uniq -c | sort -rn
Whichever namespace sits at the top is where the next section applies.
A metric is a name plus its dimensions
CloudWatch counts a metric as one unique combination of namespace, metric name and dimension values. Latency with Endpoint=/orders is one metric; Latency with Endpoint=/orders, StatusCode=200 is another. Dimensions multiply. Twenty endpoints, six status codes, three regions and forty instance IDs is 14,400 metrics for a single Latency measurement, which at the tier prices above is $3,000 for the first 10,000 and $440 for the rest, every month, for one metric name.
It’s worse than it looks because CloudWatch does not aggregate across dimensions for you. Storing Latency per instance does not let you graph Latency for the whole service; you publish the total separately, as its own metric with fewer dimensions, and pay for both.
The expensive patterns are the ones where a dimension value is an identifier rather than a category: request ID, user ID, session, container ID, build number, Git SHA. Every new value is a new metric. Hourly metering softens the blow for short-lived values, since a metric that exists for one hour costs about 1/720 of a metric-month, but a thousand different request IDs every hour still costs the same as a thousand metrics that live all month.
Two rules we apply before anything gets published:
- A dimension should have a small, bounded set of values you would put in a dropdown. Service, environment, endpoint, queue name. Not an ID.
- Publish the aggregate you alarm on, and only the breakdowns someone has looked at this quarter. Everything else goes into a log line, where Logs Insights can group by any field on demand with no per-dimension charge.
High-resolution custom metrics (one-second) cost the same per metric but 60 times the PutMetricData requests. Batch data points into each call; a single request can carry up to 1,000 of them.
The embedded metric format
The embedded metric format (EMF) is a JSON log line with an _aws block that tells CloudWatch which fields are metrics and which are dimensions. You write the line to a log group, or to stdout in Lambda, and CloudWatch extracts the metrics asynchronously, with no PutMetricData calls in the request path.
It moves the money rather than removing it. Each line is billed as log ingestion, and each extracted metric is a normal custom metric at the normal price. Only the dimension sets you declare in the _aws block are billed, and that is the useful part: fields that are not declared as dimensions stay in the log event, searchable, and cost nothing as metrics. Put RequestId, UserId and TraceId in the event as plain properties, keep the declared dimensions down to Service and Operation, and you get per-request detail in Logs Insights and a handful of cheap metrics to alarm on.
Watch the Dimensions array, which is a list of lists. Each inner list is a separate dimension set with its own metrics, so [["Service"], ["Service","Operation"], ["Service","Operation","Region"]] publishes every metric three times over.
Detailed monitoring on EC2 and the agent
EC2 publishes its basic metrics every five minutes for free. Detailed monitoring switches the instance to one-minute metrics and is charged as seven custom metrics per instance, which at the first tier is $2.10 per instance-month. Five hundred instances is $1,050 a month. Stopped instances don’t publish and so don’t bill.
One-minute metrics earn their keep when a scaling policy keys off instance CPU and needs to react inside five minutes. They do little for a dev box, a bastion, or a fleet that scales on ALB request count or SQS queue depth, which are one-minute metrics for free. Launch templates carry the setting, so one default copied between templates can turn it on for every instance in the account:
# Read-only: running instances with detailed monitoring, and their launch template
aws ec2 describe-instances \
--filters Name=monitoring-state,Values=enabled Name=instance-state-name,Values=running \
--query 'Reservations[].Instances[].[InstanceId,InstanceType,Tags[?Key==`aws:ec2launchtemplate:id`].Value|[0],Tags[?Key==`Name`].Value|[0]]' \
--output table
The CloudWatch agent is a separate bill, often a bigger one, because it reports what EC2 can’t see from outside: memory, disk usage, processes, per-core CPU. The configuration choices that multiply metrics:
append_dimensionswithInstanceIdmakes every metric unique per instance, so in an Auto Scaling group the metric count tracks instance churn, not fleet size. Useaggregation_dimensionswithAutoScalingGroupNameand drop the per-instance dimension where you don’t alarm on it.resources: ["*"]oncpureports every core, and ondiskevery mount point, including the ones Docker creates. Picktotalcpuand name the mount points you care about.- The agent adds a
hostdimension by default. Setomit_hostnameto true unless you query by hostname.
Changing metrics_collection_interval from 60 to 300 seconds does not reduce the metric count, because a standard-resolution metric costs the same at one data point a minute or one every five. It only reduces PutMetricData calls, which matter once you’re past the free million requests.
Container Insights and Lambda Insights
Both products are bundles of custom metrics plus a log group, so both bill on the metrics table above and on log ingestion.
Container Insights for ECS publishes metrics at the cluster, service and task-definition family level, which keeps the count modest. The original Container Insights for EKS collected metrics per cluster, node, namespace, service and pod, so the count scales with pod count, and a 2,000-pod cluster produces a large bill through the ContainerInsights namespace. Container Insights with enhanced observability for EKS is priced per observation rather than per metric; the pricing page has the formula, and the result depends heavily on container count, so work it out for your cluster before turning it on everywhere. The namespace count from the second section shows what you pay now; ContainerInsights and LambdaInsights appear there like any other custom namespace.
Lambda Insights is a layer or extension that emits a set of metrics per function, with the function name as a dimension, and writes performance events to /aws/lambda-insights. The per-function cost is small; the surprise is enabling it across 400 functions through a Terraform module default. We turn it on for the functions being tuned and off afterwards. The rest of the Lambda bill is in our Lambda cost guide.
Dashboards above the free three
Three dashboards are free if each has at most 50 metrics. After that it’s $3.00 per dashboard per month, and a dashboard with more than 50 metrics is billable even if it’s one of your first three. Nobody notices $3. Twenty dashboards, one per microservice, is $51 a month. A CDK construct that creates a dashboard per stack, deployed to twelve ephemeral dev stacks, is $36 a month for dashboards nobody has opened.
aws cloudwatch list-dashboards returns each dashboard’s name, size and last-modified time. Dashboards don’t record who viewed them, so last-modified is the best signal you have. Consolidate: one dashboard with a variable for service beats twenty copies, and cross-account observability lets a single dashboard in the monitoring account show metrics from the others.
Alarms: standard, high-resolution, composite and anomaly detection
An alarm is billed per metric it watches. A plain alarm on one metric at standard resolution is $0.10 a month. An alarm on a metric math expression pays for each metric the expression references, so errors / requests is $0.20. High-resolution alarms, with a period of 10 or 30 seconds, cost three times as much. An anomaly detection alarm is billed as three alarm metrics, so $0.30 a month at standard resolution, because the band is two more metrics. A composite alarm is $0.50 a month however many children it has; it doesn’t reduce the cost of the children, it reduces the noise.
At these prices, alarm cost is rarely about the type. It’s about the count. An alarm on a deleted instance, a renamed Lambda function or a removed queue goes to INSUFFICIENT_DATA and stays there, billing $0.10 a month forever; a thousand of them is $100 a month for alarms that can never fire. Per-instance alarms in autoscaled fleets, created by a script on every launch and never removed on termination, are the usual source. Find the stale ones:
# Read-only: alarms in INSUFFICIENT_DATA for more than 30 days, plus counts by type
CUTOFF=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)
aws cloudwatch describe-alarms --state-value INSUFFICIENT_DATA \
--query "MetricAlarms[?StateUpdatedTimestamp<'$CUTOFF'].[AlarmName,Namespace,MetricName,StateUpdatedTimestamp]" \
--output table
echo "high-resolution: $(aws cloudwatch describe-alarms --query 'length(MetricAlarms[?Period<`60`])')"
echo "anomaly detection: $(aws cloudwatch describe-alarms --query 'length(MetricAlarms[?ThresholdMetricId])')"
echo "composite: $(aws cloudwatch describe-alarms --alarm-types CompositeAlarm --query 'length(CompositeAlarms)')"
Review the first list with the alarm owners before deleting anything; an alarm with no data for a month is usually orphaned, but occasionally it watches a quarterly batch job. The wider sweep for leftovers is in idle and orphaned AWS resources.
GetMetricData, third-party agents and metric streams
If you use Datadog, New Relic, Grafana Cloud or similar, their AWS integration pulls your CloudWatch metrics by calling GetMetricData on a schedule. That call is billed at $0.01 per 1,000 metrics requested, it isn’t in the free million, and the integration asks for every metric in every namespace you enabled, in every region. The arithmetic: 20,000 metrics polled every 10 minutes is 2.88 million metrics requested a day, about $29 a day and $860 a month, before the vendor’s own bill. Poll every minute and it’s ten times that.
CloudWatch publishes usage metrics about itself, so you can see the call volume without CloudTrail:
# Read-only: GetMetricData requests per day, last 14 days (each request can carry up to 500 metrics)
END=$(date -u +%Y-%m-%dT00:00:00Z)
START=$(date -u -d '14 days ago' +%Y-%m-%dT00:00:00Z 2>/dev/null || date -u -v-14d +%Y-%m-%dT00:00:00Z)
aws cloudwatch get-metric-statistics --namespace AWS/Usage --metric-name CallCount \
--dimensions Name=Service,Value=CloudWatch Name=Type,Value=API Name=Resource,Value=GetMetricData Name=Class,Value=None \
--start-time "$START" --end-time "$END" --period 86400 --statistics Sum \
--query 'sort_by(Datapoints,&Timestamp)[].[Timestamp,Sum]' --output table
Three levers, in order. First, restrict the integration to the namespaces and regions you look at; every vendor has an allowlist and the default is everything. Second, lengthen the poll interval; anything that needs faster alerting should alarm in CloudWatch and notify outward. Third, consider metric streams.
A metric stream pushes every update of the selected metrics through Kinesis Data Firehose to S3 or a partner endpoint, at $0.003 per 1,000 metric updates plus Firehose and destination charges. It’s cheaper than polling only when polling is frequent. A 1-minute metric produces 1,440 updates a day, so 20,000 streamed metrics are 28.8 million updates, about $86 a day: a third of polling every minute, but three times polling every 10 minutes. Either way, filter the stream to specific namespaces and metric names rather than streaming everything.
Contributor Insights
Contributor Insights answers “which ten things are generating this” from log data: top IPs by request count, top customers by error, top keys by write. A rule is $0.50 a month plus $0.02 per million log events it processes, and the first rule and first million events are free. For the high-cardinality questions from earlier, it’s the right tool: a rule reporting the top 100 user IDs by latency costs a few dollars a month, while a UserId dimension on a Latency metric costs $0.30 per user.
The cost to watch is rule count. Rules deployed per service from a shared template keep billing their flat fee whether the log group is busy or not; aws cloudwatch describe-insight-rules lists them, and disabled rules aren’t charged.
FAQ
How is a CloudWatch custom metric counted?
As each unique combination of namespace, metric name and dimension values that receives data in a given hour. Charges are prorated hourly and tiered from $0.30 down to $0.02 per metric-month, us-east-1 list price at the time of writing.
Is EC2 detailed monitoring worth the cost?
For instances whose scaling policy depends on instance-level CPU or network metrics, often yes. For everything else, the free five-minute metrics are usually enough, and $2.10 per instance-month at the first tier adds up across a fleet.
How much does a CloudWatch alarm cost?
$0.10 a month per metric at standard resolution, $0.30 for high-resolution or anomaly detection, and $0.50 for a composite alarm, with the first 10 alarm metrics free.
Are CloudWatch dashboards free?
The first three are, as long as each has 50 metrics or fewer. Beyond that every dashboard is $3.00 a month, including any of the first three that grows past 50 metrics.
Does GetMetricData count toward the free one million API requests?
No. GetMetricData and GetMetricWidgetImage are billed separately at $0.01 per 1,000 metrics requested from the first call, which is why third-party monitoring integrations show up as a CloudWatch charge.
Start with the biggest namespace
Run the namespace count from the second section, open the namespace at the top of the list, and look at its dimensions. Usually there is an identifier in there that should have been a log field, and removing one dimension does more than every alarm and dashboard cleanup combined. Then run the stale-alarm command monthly, because the list refills.
Kostrix runs that kind of check read-only on a weekly schedule: its idle check lists alarms that have sat in INSUFFICIENT_DATA for weeks and dashboards past the free three, priced from the bill so you know which account to open first.
