To find the most expensive AWS service in your account, query Cost Explorer’s GetCostAndUsage API for the last few complete months, grouped by the SERVICE dimension, then add each service’s monthly amounts and sort by total. Looking at three months instead of one smooths out one-off spikes. Each API request costs $0.01.
“Where does our AWS money actually go?” is the first question in any cost review, and the answer should come from billing data, not a guess based on which service you use most. This example gives you a TypeScript script for the AWS SDK for JavaScript v3 to find the most expensive AWS service over a window you choose. It ranks every service, shows its share of total spend and the change between the last two months, and can exclude credits that would otherwise hide the real cost.
It’s part of our library of AWS SDK v3 practical examples in TypeScript. If you only need a single month’s breakdown, the sibling script to get last month’s AWS bill broken down by service is shorter and supports CSV export.
Most used or most expensive: which question are you asking?
Cost Explorer can return both cost metrics and UsageQuantity. Usage isn’t a good way to rank services, because every service counts usage in its own unit: instance hours, GB-months, requests, log bytes. Adding hours to gigabytes gives a meaningless number. Cost is the one unit every service shares, so “most used” in practice means “most expensive”, and that’s what this script ranks.
This fits the way the FinOps Foundation frames cost visibility: shared, timely reporting of spend by the things teams recognize. Its Reporting and Analytics capability in the FinOps Framework describes the practice this script is a small, scriptable part of.
What does the script do?
- Picks complete monthsWith the default
--months 3, a run in October covers July, August and September. The current, partial month is left out so services aren’t compared on uneven periods. - Requests monthly cost by serviceOne
GetCostAndUsagecall withGranularity: "MONTHLY", theUnblendedCostmetric andGroupByonSERVICE. The response has oneResultsByTimeentry per month, and the script followsNextPageTokenif the result is paginated. - Optionally removes credits and refunds
--exclude-creditsadds a filter that excludes theCreditandRefundrecord types. Promotional credits are applied per service as negative amounts, so without the filter a heavily credited service can look cheap. - Ranks and comparesServices are sorted by total across the window. The table shows each one’s share and how much last month moved compared with the month before, then names the most expensive service.
Prerequisites
- Node.js 18 or later, npm and
tsx. - The
@aws-sdk/client-cost-explorerpackage. - Cost Explorer enabled in the account’s Billing and Cost Management settings.
- A profile that can call
ce:GetCostAndUsage. For organization-wide rankings, use a profile in the management account; a member account only sees its own spend.
Which IAM permissions does it need?
Cost Explorer permissions aren’t part of the ReadOnlyAccess managed policy, so grant the single action explicitly:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RankServicesBySpend",
"Effect": "Allow",
"Action": "ce:GetCostAndUsage",
"Resource": "*"
}
]
}
If you add calls such as GetCostForecast, the TypeScript IAM policy generator drafts the extra actions from your code. Treat its output as a starting point and check every action before you attach it.
The full script to find the most expensive AWS service
// most-expensive-service.ts
// Ranks AWS services by spend over the last N complete months with Cost Explorer
// and names the most expensive one. Each Cost Explorer API request costs $0.01.
// Usage: npx tsx most-expensive-service.ts [--months 3] [--top 10] [--exclude-credits]
import {
CostExplorerClient,
GetCostAndUsageCommand,
type Expression,
type GetCostAndUsageCommandOutput,
} from "@aws-sdk/client-cost-explorer";
function numberArg(flag: string, fallback: number): number {
const i = process.argv.indexOf(flag);
const value = i === -1 ? fallback : Number(process.argv[i + 1]);
if (!Number.isInteger(value) || value < 1) throw new Error(`${flag} must be a positive whole number`);
return value;
}
const iso = (d: Date) => d.toISOString().slice(0, 10);
async function main(): Promise<void> {
const months = numberArg("--months", 3);
const top = numberArg("--top", 10);
const excludeCredits = process.argv.includes("--exclude-credits");
// Complete months only: from the 1st of (this month - N) to the 1st of this month (exclusive).
const now = new Date();
const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - months, 1));
// Credits and refunds appear as negative amounts and can hide what a service really costs.
const filter: Expression | undefined = excludeCredits
? { Not: { Dimensions: { Key: "RECORD_TYPE", Values: ["Credit", "Refund"] } } }
: undefined;
const ce = new CostExplorerClient({ region: "us-east-1" });
const byService = new Map<string, number[]>(); // service -> amount per month
const monthLabels: string[] = [];
let requests = 0;
let nextPageToken: string | undefined;
do {
const res: GetCostAndUsageCommandOutput = await ce.send(
new GetCostAndUsageCommand({
TimePeriod: { Start: iso(start), End: iso(end) },
Granularity: "MONTHLY",
Metrics: ["UnblendedCost"],
GroupBy: [{ Type: "DIMENSION", Key: "SERVICE" }],
Filter: filter,
NextPageToken: nextPageToken,
}),
);
requests++;
for (const result of res.ResultsByTime ?? []) {
const label = result.TimePeriod?.Start?.slice(0, 7) ?? "?";
if (!monthLabels.includes(label)) monthLabels.push(label);
const m = monthLabels.indexOf(label);
for (const group of result.Groups ?? []) {
const service = group.Keys?.[0] ?? "(unknown)";
const amounts = byService.get(service) ?? new Array<number>(months).fill(0);
amounts[m] = (amounts[m] ?? 0) + Number(group.Metrics?.UnblendedCost?.Amount ?? 0);
byService.set(service, amounts);
}
}
nextPageToken = res.NextPageToken;
} while (nextPageToken);
const ranked = [...byService.entries()]
.map(([service, amounts]) => ({ service, amounts, total: amounts.reduce((s, a) => s + a, 0) }))
.sort((a, b) => b.total - a.total);
if (ranked.length === 0) {
console.log("No cost data returned for this period.");
return;
}
const grandTotal = ranked.reduce((s, r) => s + r.total, 0);
const last = monthLabels.length - 1;
console.log(`Top ${top} services by UnblendedCost, ${monthLabels[0]} to ${monthLabels[last]}`);
console.table(
ranked.slice(0, top).map((r) => ({
Service: r.service,
Total: r.total.toFixed(2),
Share: grandTotal > 0 ? `${((r.total / grandTotal) * 100).toFixed(1)}%` : "-",
"Last month": (r.amounts[last] ?? 0).toFixed(2),
"vs previous": last > 0 ? ((r.amounts[last] ?? 0) - (r.amounts[last - 1] ?? 0)).toFixed(2) : "-",
})),
);
const winner = ranked[0];
console.log(`Most expensive service: ${winner.service} (${winner.total.toFixed(2)} USD over ${months} months)`);
console.log(`Cost Explorer API requests made: ${requests} (about $${(requests * 0.01).toFixed(2)})`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
One request usually covers the whole window, because a monthly result grouped by service is small. The vs previous column is last month minus the month before, so a positive number is growth. A service that keeps growing is a good candidate for its own budget; the script to create an AWS budget alert with AWS SDK v3 adds alerts at the thresholds you choose.
How do you run it?
npm install @aws-sdk/client-cost-explorer
npm install --save-dev tsx typescript
# Rank services over the last 3 complete months
AWS_PROFILE=billing-readonly npx tsx most-expensive-service.ts
# Last 6 months, top 5, ignoring credits and refunds
AWS_PROFILE=billing-readonly npx tsx most-expensive-service.ts --months 6 --top 5 --exclude-credits
Sample output
Top 5 services by UnblendedCost, 2026-06 to 2026-08
┌─────────┬──────────────────────────────────────────┬───────────┬─────────┬────────────┬─────────────┐
│ (index) │ Service │ Total │ Share │ Last month │ vs previous │
├─────────┼──────────────────────────────────────────┼───────────┼─────────┼────────────┼─────────────┤
│ 0 │ 'Amazon Elastic Compute Cloud - Compute' │ '1802.16' │ '40.7%' │ '612.48' │ '14.30' │
│ 1 │ 'Amazon Relational Database Service' │ '956.70' │ '21.6%' │ '318.90' │ '0.00' │
│ 2 │ 'EC2 - Other' │ '498.35' │ '11.3%' │ '184.12' │ '41.87' │
│ 3 │ 'Amazon Simple Storage Service' │ '281.02' │ '6.3%' │ '96.37' │ '3.10' │
│ 4 │ 'AmazonCloudWatch' │ '176.44' │ '4.0%' │ '71.05' │ '18.92' │
└─────────┴──────────────────────────────────────────┴───────────┴─────────┴────────────┴─────────────┘
Most expensive service: Amazon Elastic Compute Cloud - Compute (1802.16 USD over 3 months)
Cost Explorer API requests made: 1 (about $0.01)
Figures are illustrative. Share is calculated against all services, including ones outside the top 5, so the shares shown don’t add up to 100%.
What should you do after you find the most expensive service?
The ranking tells you where to look, not what to change. Typical next steps depend on the service at the top:
- EC2 compute. Look for instances that run all day with almost no load. The companion script to detect and stop underutilized EC2 instances by CPU lists them and only stops anything when you pass a flag. If reservations cover much of that compute, also find EC2 Reserved Instances about to expire.
- EC2 – Other. This line holds EBS volumes, snapshots and NAT gateway hours. Unattached volumes are a common cause; the example to find unattached EBS volumes and tag them for review covers it. For volumes still in use, converting EBS gp2 volumes to gp3 lowers the storage price by about 20%.
- CloudWatch rising month over month. Usually log ingestion. Break it down with the script to check this month’s CloudWatch cost by usage type.
- S3. Rank buckets by stored bytes with the S3 bucket size example in the related guides below, then compare storage classes before moving data.
- DynamoDB. For tables in provisioned mode, the example to find overprovisioned DynamoDB read and write capacity compares each table and GSI with last month’s consumed units.
Troubleshooting
AccessDeniedException. The profile is missingce:GetCostAndUsage, or an organization policy denies Cost Explorer. Check identity policies, permission boundaries and service control policies, in that order, to find the blocking statement.- A service shows a negative or near-zero total. Credits cover it. Re-run with
--exclude-creditsto see the underlying cost. ValidationExceptionabout the time period. Cost Explorer keeps 13 months of history by default, so very large--monthsvalues reach past the data you have.- The top service differs from last month’s invoice. The ranking covers several months. Use
--months 1for last month alone.
Ask ChatWithCloud instead
You can ask the same question in plain English: run ChatWithCloud and type “Which AWS service cost the most over the last three months, and how did it change?” It writes AWS SDK for JavaScript v2 code, runs it locally with your profile, and explains the result, and you can keep drilling down in the same session, as the guide to ask AI why your AWS bill went up shows. It needs ce: permissions on the profile, and each Cost Explorer call it makes is billed by AWS like any other. Changes run without a confirmation step, so set up a read-only AWS profile for ChatWithCloud first. How ChatWithCloud runs generated SDK code on your machine explains each step between your question and the answer.
Frequently asked questions
How do I find which AWS service costs the most?
In the console, open Cost Explorer and group by Service. From code, call GetCostAndUsage grouped by SERVICE and sort the groups by amount, as the script above does.
How much does it cost to query Cost Explorer from code?
$0.01 per API request (as of September 2026). This script normally makes one request per run.
Why is EC2 – Other one of my most expensive services?
EC2 - Other includes EBS volumes and snapshots, NAT gateways and some data transfer. Group it by USAGE_TYPE to see which of those is growing.
Should I rank services by unblended or amortized cost?
Unblended matches the invoice. If you buy Reserved Instances or Savings Plans with upfront payments, amortized cost spreads those fees over the term and gives a fairer month-to-month ranking.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud
