You’re staring at the AWS Cost Explorer, and something doesn’t add up. Your EC2 instances are reserved, your RDS instances are right-sized, and your S3 lifecycle policies are humming along perfectly. Yet, there it is: CloudWatch. It’s sitting comfortably as the third or fourth largest line item on your bill, sometimes rivaling the cost of the actual compute it’s supposed to be monitoring. It feels like a betrayal. You’re paying a premium for the “privilege” of seeing why your application is expensive.
I’ve sat across the desk from dozens of CTOs who feel this exact same anxiety. They want observability, but they don’t want to pay a “visibility tax” that scales linearly—or worse, exponentially—with their traffic. The frustration stems from the fact that CloudWatch pricing is intentionally granular and deceptively low-stakes at a small scale. But when you hit production at scale, those $0.50 per GB ingestion fees and $0.30 per custom metric charges turn into a runaway freight train. We’re going to stop that train today. We aren’t just going to “monitor” costs; we are going to perform a forensic teardown of where your money is leaking and implement a hard-stop strategy to reclaim your budget.
To slash CloudWatch costs immediately, set a 14-day retention policy on all log groups to stop paying for “zombie” data, replace high-cardinality PutMetricData calls with CloudWatch Metric Filters, and offload high-volume telemetry to an OpenTelemetry-based stack like Grafana Loki or VictoriaMetrics to bypass the $0.50/GB ingestion tax.
Why is my AWS CloudWatch bill so high every month?
The primary reason CloudWatch becomes a budget black hole is that it is the only service in the AWS ecosystem where the default settings are actively designed to maximize long-term storage costs. When you create a Log Group, the default retention period is “Never Expire.” Think about that for a second. Every debug log, every stdout from a container that crashed three years ago, and every health check ping from a deprecated load balancer is still sitting there, and you are paying for it every single month.
But storage is only the tip of the iceberg. The real “silent killers” are Ingestion and Custom Metrics. AWS charges roughly $0.50 per GB for data ingested into CloudWatch Logs. In a microservices architecture, where every service is chatting away, it’s remarkably easy to ingest several terabytes of logs a month without realizing it. If your developers left “DEBUG” mode on in production, you aren’t just troubleshooting code; you’re writing a blank check to Seattle.
Then we have the Custom Metrics trap. At $0.30 per metric per month, it sounds cheap. But in the world of Kubernetes and modern ephemeral infrastructure, “cardinality” is the enemy. If you track a metric like request_latency and include a pod_id or a user_id as a dimension, every single unique ID creates a new metric. If you have 1,000 pods that rotate daily, you aren’t paying for one metric; you’re paying for thousands. This is how a simple dashboard ends up costing $5,000 a month.
How do I identify the biggest CloudWatch cost drivers?
Before you start deleting things, you need to know where the blood is coming from. You cannot manage what you cannot measure, and AWS doesn’t make it particularly easy to see which specific Log Group is the most expensive in a single glance. You need to dive into the CloudWatch Usage metrics under the AWS/Logs and AWS/CloudWatch namespaces.
Look specifically for IncomingBytes. This will tell you which Log Groups are the hungriest. Often, you’ll find that 80% of your costs are driven by 20% of your logs—usually VPC Flow Logs, CloudTrail, or a particularly chatty application framework. Once you identify these, you can apply the “Value vs. Cost” framework. Does a VPC Flow Log providing “ACCEPT” data for every internal packet really need to be ingested into CloudWatch Logs at $0.50/GB? Probably not. It belongs in S3 via Kinesis Data Firehose, where ingestion is significantly cheaper and you can query it with Athena only when you actually need to perform a forensic investigation.
| Cost Component | Standard Rate (Approx) | The “Gotcha” | Optimization Strategy |
|---|---|---|---|
| Log Ingestion | $0.50 per GB | Linear scaling with traffic; DEBUG logs in prod. | Use Log Levels; move high-volume logs to S3. |
| Log Storage | $0.03 per GB/mo | “Never Expire” default setting. | Set 14-30 day retention policies globally. |
| Custom Metrics | $0.30 per metric | High cardinality (Pod IDs, User IDs). | Use Metric Filters or OpenTelemetry. |
| GetMetricData API | $0.01 per 1k metrics | Aggressive dashboard refreshing. | Increase dashboard refresh intervals. |
| VPC Flow Logs | Varies | Ingesting all traffic metadata by default. | Send to S3 directly, bypass CloudWatch. |
What is the hidden cost of high-cardinality metrics?
In the modern observability stack, we love dimensions. We want to know the latency of a request, but we also want to know it by region, by availability zone, by instance ID, and by the specific version of the software. Each unique combination of these dimensions is a “unique metric” in CloudWatch’s eyes.
If you are using the PutMetricData API call, you are paying for each of those combinations. I recently audited a client who was tracking “API Response Time.” They included customer_id as a dimension. They had 5,000 customers. That single metric was costing them $1,500 a month ($0.30 * 5,000). The kicker? They only looked at the aggregate average across all customers. They were paying for granular data they never queried.
The solution here is twofold. First, stop using PutMetricData for high-cardinality data. Instead, use CloudWatch Metric Filters. Metric Filters allow you to ingest a log (which you’re already paying for) and extract a metric from it using a pattern. For example, you can look for the string “ResponseTime: 120ms” in your logs and tell CloudWatch to turn that into a metric. The beauty? Metric filters don’t charge you per dimension in the same way. You pay for the ingestion of the log, and the metric extraction is essentially a bonus (up to certain limits).
Can I use OpenTelemetry to bypass CloudWatch pricing?
If you are serious about infrastructure forensics and cost containment, you eventually have to outgrow the “native-only” mindset. OpenTelemetry (OTel) is the industry standard for a reason. By deploying an OTel Collector on your EKS or EC2 clusters, you gain control over your data before it hits the AWS billing engine.
The OTel Collector can act as a “traffic cop.” It can sample your logs (e.g., only send 10% of successful 200 OK logs but 100% of 500 Error logs), it can strip out unnecessary metadata, and most importantly, it can route data to different destinations. You can send your “mission-critical” metrics to CloudWatch for alerting and send your “high-volume/low-value” telemetry to a self-hosted Grafana Loki instance or an S3 bucket. This architectural shift moves you from a “pay-per-byte” model to a “pay-for-compute” model, which is almost always cheaper at scale.
The 4:00 PM CloudWatch Cost Reduction Playbook
You can execute these steps before you log off today and see the impact on your bill within 24 hours.
1. The Retention Sweep: Run a script (using the AWS CLI or SDK) to find every Log Group with “Never Expire” retention. Update them to 14 or 30 days. Unless you are in a highly regulated industry like FinTech or Healthcare that requires 7-year cold storage, you do not need 3-year-old application logs in a “hot” storage tier like CloudWatch.
* *Command:* aws logs put-retention-policy --log-group-name /aws/lambda/my-func --retention-in-days 14
2. The VPC Flow Log Pivot: Check if you are sending VPC Flow Logs to CloudWatch. If you are, and you aren’t actively using them for real-time CloudWatch Alarms, delete the subscription and recreate it with S3 as the destination. You’ll save the $0.50/GB ingestion fee immediately.
3. The Cardinality Audit: Go to the CloudWatch Metrics console and sort by “Metric Name.” Look for metrics with thousands of dimensions. If you see RemoteAddr or SessionID as a dimension, kill it. Move that data to “Embedded Metric Format” (EMF) or use Metric Filters to extract only the aggregates you need.
4. Implement Log Levels: Audit your application code. Ensure that “DEBUG” and “TRACE” logs are disabled in production. If you need them for a specific incident, enable them temporarily using environment variables, but never leave them on by default.
5. The “Infrequent Access” Tier: For logs that you need to keep but rarely query, use the CloudWatch Logs Infrequent Access (IA) log class. It reduces ingestion costs by about 50% ($0.25/GB vs $0.50/GB) by stripping out features like Metric Filters and subscription filters that you likely don’t need for archival data.
Frequently Asked Questions
How much does CloudWatch cost per GB?
CloudWatch Logs costs approximately $0.50 per GB for data ingestion and $0.03 per GB per month for data storage in most standard regions (like us-east-1). There are additional costs for data scanned during CloudWatch Logs Insights queries ($0.005 per GB scanned).
What is the difference between CloudWatch Standard and Infrequent Access?
Standard is designed for real-time monitoring and supports features like Metric Filters, Alarms, and Subscription Filters. Infrequent Access (IA) is 50% cheaper for ingestion but lacks these real-time features, making it ideal for ad-hoc forensic analysis and compliance logs.
How do I stop CloudWatch from charging for custom metrics?
To reduce custom metric costs, minimize the number of unique dimension combinations (cardinality). Use CloudWatch Metric Filters to extract metrics from existing logs instead of using the PutMetricData API, or aggregate metrics at the application level before sending them to AWS.
Is CloudWatch Logs Insights free?
No. While there is no upfront cost to use the Insights interface, you are charged $0.005 for every GB of data scanned during a query. Large-scale queries over long time ranges can become expensive quickly if logs are not properly filtered by time.
