Photo by Luke Chesser on Unsplash
To get your AWS CloudWatch cost this month, call Cost Explorer’s GetCostAndUsage with a TimePeriod from the 1st of the month to today, a Filter on the SERVICE dimension set to AmazonCloudWatch, and GroupBy on USAGE_TYPE. The groups show which part of CloudWatch you’re paying for: logs, metrics, alarms or API calls.
CloudWatch is the AWS line item that grows quietly: a chatty Lambda function, a debug log level left on, or a dashboard polling thousands of metrics. This example is for engineers who want to check the AWS CloudWatch cost this month before the invoice arrives. You’ll get a TypeScript script for the AWS SDK for JavaScript v3 that prints the month-to-date total, splits it by usage type and projects where the month will end.
It’s one of our AWS practical examples with full TypeScript scripts. To see CloudWatch in the context of every other service, start with the script to break last month’s AWS bill down by service.
What does the script do?
- Sets the month-to-date windowFrom the 1st of the current month (UTC) to today, with today as the exclusive end date. That covers every complete day so far; today’s charges are still arriving and are left out.
- Looks up the CloudWatch service name
GetDimensionValuessearches theSERVICEdimension for “CloudWatch” in that window. Cost Explorer reports CloudWatch asAmazonCloudWatch, but looking it up means the filter matches exactly what your account’s data contains, and the script stops early if there were no CloudWatch charges. - Gets the cost by usage type
GetCostAndUsagewithFilter: { Dimensions: { Key: "SERVICE", Values: [...] } }andGroupByonUSAGE_TYPE, followingNextPageTokenif the response is paginated. - Projects the month-end totalIt divides the month-to-date cost by the number of complete days and multiplies by the days in the month. That’s a straight-line estimate, not an AWS forecast.
Prerequisites
- Node.js 18 or later, npm and
tsx. - The
@aws-sdk/client-cost-explorerpackage. - Cost Explorer enabled for the account.
- A profile allowed to call
ce:GetDimensionValuesandce:GetCostAndUsage.
Which IAM permissions does it need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadCloudWatchCostMonthToDate",
"Effect": "Allow",
"Action": [
"ce:GetDimensionValues",
"ce:GetCostAndUsage"
],
"Resource": "*"
}
]
}
Note that no cloudwatch: permissions are needed: the script reads billing data, not metrics. The free IAM policy generator for TypeScript will confirm the two actions if you paste the script in, and the guide on reviewing IAM policies for least privilege explains what to check before you attach any generated policy.
The full script: AWS CloudWatch cost this month by usage type
// cloudwatch-cost-this-month.ts
// Month-to-date Amazon CloudWatch cost, split by usage type, from Cost Explorer.
// Makes 2 Cost Explorer API requests (more if results are paginated); each costs $0.01.
// Usage: npx tsx cloudwatch-cost-this-month.ts
import {
CostExplorerClient,
GetCostAndUsageCommand,
GetDimensionValuesCommand,
type GetCostAndUsageCommandOutput,
} from "@aws-sdk/client-cost-explorer";
const iso = (d: Date) => d.toISOString().slice(0, 10);
async function main(): Promise<void> {
// Cost Explorer uses UTC dates and an exclusive End date, so this covers
// the 1st of the month through yesterday (today's data is still arriving).
const now = new Date();
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
if (today.getTime() === start.getTime()) {
console.log("It is the 1st of the month (UTC): there is no complete day to report yet.");
return;
}
const period = { Start: iso(start), End: iso(today) };
const ce = new CostExplorerClient({ region: "us-east-1" });
// 1. Find the exact SERVICE value(s) Cost Explorer uses for CloudWatch in this period.
const dims = await ce.send(
new GetDimensionValuesCommand({
TimePeriod: period,
Dimension: "SERVICE",
SearchString: "CloudWatch",
Context: "COST_AND_USAGE",
}),
);
let requests = 1;
const services = (dims.DimensionValues ?? [])
.map((d) => d.Value)
.filter((v): v is string => typeof v === "string" && v.length > 0);
if (services.length === 0) {
console.log(`No CloudWatch charges between ${period.Start} and ${period.End}.`);
return;
}
// 2. Month-to-date cost for those services, grouped by usage type.
const byUsageType = new Map<string, number>();
let nextPageToken: string | undefined;
do {
const res: GetCostAndUsageCommandOutput = await ce.send(
new GetCostAndUsageCommand({
TimePeriod: period,
Granularity: "MONTHLY",
Metrics: ["UnblendedCost"],
Filter: { Dimensions: { Key: "SERVICE", Values: services } },
GroupBy: [{ Type: "DIMENSION", Key: "USAGE_TYPE" }],
NextPageToken: nextPageToken,
}),
);
requests++;
for (const result of res.ResultsByTime ?? []) {
for (const group of result.Groups ?? []) {
const usageType = group.Keys?.[0] ?? "(unknown)";
const amount = Number(group.Metrics?.UnblendedCost?.Amount ?? 0);
byUsageType.set(usageType, (byUsageType.get(usageType) ?? 0) + amount);
}
}
nextPageToken = res.NextPageToken;
} while (nextPageToken);
const rows = [...byUsageType.entries()].sort((a, b) => b[1] - a[1]);
const total = rows.reduce((s, [, amount]) => s + amount, 0);
// A straight-line projection: average daily cost so far x days in the month.
const daysElapsed = Math.round((today.getTime() - start.getTime()) / 86_400_000);
const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
const projected = (total / daysElapsed) * daysInMonth;
console.log(`CloudWatch cost (${services.join(", ")}), ${period.Start} to ${period.End} (exclusive)`);
console.table(
rows
.filter(([, amount]) => Math.abs(amount) >= 0.005)
.map(([usageType, amount]) => ({ "Usage type": usageType, Cost: amount.toFixed(2) })),
);
console.log(`Month to date: ${total.toFixed(2)} USD over ${daysElapsed} days`);
console.log(`Straight-line projection for the month: ${projected.toFixed(2)} USD`);
console.log(`Cost Explorer API requests made: ${requests} (about $${(requests * 0.01).toFixed(2)})`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
A normal run makes two Cost Explorer requests, so it costs about $0.02. To filter a different service, change the SearchString; to see which regions the CloudWatch cost comes from, change the GroupBy key to REGION.
How do you run it?
npm install @aws-sdk/client-cost-explorer
npm install --save-dev tsx typescript
AWS_PROFILE=billing-readonly npx tsx cloudwatch-cost-this-month.ts
Sample output
CloudWatch cost (AmazonCloudWatch), 2026-09-01 to 2026-09-27 (exclusive)
┌─────────┬──────────────────────────────┬─────────┐
│ (index) │ Usage type │ Cost │
├─────────┼──────────────────────────────┼─────────┤
│ 0 │ 'DataProcessing-Bytes' │ '38.61' │
│ 1 │ 'CW:MetricMonitorUsage' │ '14.40' │
│ 2 │ 'TimedStorage-ByteHrs' │ '6.92' │
│ 3 │ 'CW:GMD-Metrics' │ '3.18' │
│ 4 │ 'CW:AlarmMonitorUsage' │ '2.50' │
│ 5 │ 'EU-DataProcessing-Bytes' │ '1.87' │
│ 6 │ 'CW:Requests' │ '0.42' │
└─────────┴──────────────────────────────┴─────────┘
Month to date: 67.90 USD over 26 days
Straight-line projection for the month: 78.35 USD
Cost Explorer API requests made: 2 (about $0.02)
Figures are illustrative. Usage types in regions other than US East (N. Virginia) carry a region prefix, such as EU- for Europe (Ireland), which is how you can tell where a cost comes from.
What drives CloudWatch cost, and how do you read the usage types?
| Usage type | What it usually means | First thing to check |
|---|---|---|
DataProcessing-Bytes |
CloudWatch Logs ingestion | Log levels and verbose functions or containers |
TimedStorage-ByteHrs |
Log storage over time | Log groups set to never expire |
CW:MetricMonitorUsage |
Custom metrics | High-cardinality dimensions creating many metrics |
CW:AlarmMonitorUsage |
Alarms | Alarms left over from deleted resources |
CW:GMD-Metrics, CW:Requests |
API calls such as GetMetricData |
Dashboards and third-party tools that poll often |
Prices for each of these vary by region and by tier. The official Amazon CloudWatch pricing page lists the current rates for logs, metrics, alarms and API requests; compare them with the usage types in your output before deciding what to cut. If CW:AlarmMonitorUsage stands out, the script to find CloudWatch alarms stuck in INSUFFICIENT_DATA finds alarms left over from deleted resources. Dashboards beyond the free tier are billed per dashboard each month; the script to find and delete unused CloudWatch dashboards shows which ones nobody opens. If log storage (TimedStorage-ByteHrs) leads, part of it may sit in groups nothing writes to anymore, which the script to find and delete abandoned CloudWatch log groups lists by last event time.
Log ingestion is most often the biggest line. When it’s Lambda logs, the guide on investigating Lambda errors with CloudWatch helps you find the noisy functions, and the Lambda invocation count example in the related guides shows which ones run most. GetMetricData calls are billed too, so scheduled scripts that read metrics, including our own examples, add to CW:GMD-Metrics.
Troubleshooting
- “No CloudWatch charges” but you know there are some. Early in the month there may be no complete day yet, and Cost Explorer data can lag by up to a day. Try again tomorrow, or check that the profile belongs to the account that pays the bill.
AccessDeniedExceptiononGetDimensionValues. Many policies grant onlyce:GetCostAndUsage. Addce:GetDimensionValues, or follow the steps to troubleshoot AWS IAM access denied errors if an organization policy is involved.- The projection looks too high. A one-off event, like a backfill that pushed a lot of logs on one day, gets multiplied across the month. Switch
GranularitytoDAILYto spot spikes. - Vended logs appear as separate usage types. Logs from services such as VPC Flow Logs are billed under CloudWatch with their own usage types, so they show up as extra rows.
Ask ChatWithCloud instead
Instead of running the script, you can ask ChatWithCloud “How much has CloudWatch cost this month, and what’s driving it?” It writes AWS SDK for JavaScript v2 code for Cost Explorer, runs it on your machine with your AWS profile and explains the answer. You can then follow up with “which log groups have no retention set?”, the kind of question covered in the guide to troubleshoot AWS infrastructure with an AI CLI. The profile needs ce: permissions for the cost part. Because generated code runs without a confirmation step, use a read-only profile when you connect ChatWithCloud to AWS, and see how ChatWithCloud turns questions into SDK calls for the details.
Frequently asked questions
Why is my CloudWatch bill so high?
Most often it’s log ingestion (DataProcessing-Bytes), followed by custom metrics and log storage with no retention limit. Run the script to see which usage type leads for your account, and set CloudWatch log retention for all log groups if storage keeps growing.
Does CloudWatch Logs cost show up under CloudWatch in Cost Explorer?
Yes. Log ingestion and storage are billed under the AmazonCloudWatch service, with usage types such as DataProcessing-Bytes and TimedStorage-ByteHrs.
Can I get an official forecast instead of a straight-line projection?
Yes, Cost Explorer’s GetCostForecast API returns a forecast with a filter on the same service. It needs ce:GetCostForecast and is one more $0.01 request.
How current is the month-to-date number?
Cost Explorer data refreshes at least once every 24 hours, so the total can trail real usage by up to a day.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud
