How to Audit Your AWS Costs with Native Tools (Cost Explorer, Trusted Advisor, Compute Optimizer)
In part one of this series we covered the seven places AWS bills leak: unattached EBS volumes and orphaned snapshots, idle or oversized EC2 instances, RI/Savings Plans coverage gaps, unused Elastic IPs and idle load balancers, over-provisioned RDS instances, data transfer costs, and non-production environments running 24/7.
This article is the audit itself: a complete AWS cost audit of all seven sources using only tools you already have — Cost Explorer, Trusted Advisor, Compute Optimizer, CloudWatch, and the AWS CLI. No third-party tooling, no agents, nothing to install. Budget 3–5 hours for a first pass on a multi-account setup, and expect the findings document you produce to pay for that time many times over.
We'll work tool by tool, because that's how you'll actually do it — each tool covers several waste sources at once.
Tool 1: Cost Explorer — the map
Cost Explorer answers "where is the money going" and covers waste sources #3 (coverage gaps), #6 (data transfer), and #7 (non-prod). If you've never opened it, enable it first (Billing and Cost Management → Cost Explorer); data appears within 24 hours (AWS docs).
Step 1: Establish the shape of the bill
Console: Cost Explorer → New report → Cost and usage. Set the date range to the last 3 months, granularity Monthly, and Group by: Service.
You're looking for two things: which services dominate (EC2, RDS, and data transfer usually cover 60–80% at SaaS companies), and which services are growing faster than your traffic. Write the top five down — they set your audit priorities.
Step 2: Check Savings Plans / RI coverage (waste source #3)
Console: Billing and Cost Management → Savings Plans → Coverage report, and if you hold RIs, Reservations → Coverage.
CLI equivalent:
aws ce get-savings-plans-coverage \
--time-period Start=2026-06-01,End=2026-07-01 \
--granularity MONTHLY
How to read it. The number that matters is coverage of your stable baseline — compute that runs 24/7 regardless of load. Healthy targets: 70–85% coverage for steady workloads, with the remainder on-demand for elasticity. Below 60%, you're overpaying materially. Also open Savings Plans → Recommendations: AWS computes a suggested hourly commitment from your own last 30–60 days of usage. Treat it as a floor estimate — it's deliberately conservative.
While you're here, check utilization too (the neighboring report). Coverage above 85% with utilization below roughly 95% means you over-committed; that's a different problem, but you want to know before buying more.
Step 3: Isolate data transfer (waste source #6)
Console: Cost Explorer → Group by: Usage Type, then filter Usage Type Group to EC2: Data Transfer - Inter AZ. Repeat for usage types containing NatGateway.
CLI:
aws ce get-cost-and-usage \
--time-period Start=2026-06-01,End=2026-07-01 \
--granularity MONTHLY --metrics UnblendedCost \
--group-by Type=DIMENSION,Key=USAGE_TYPE \
--filter '{"Dimensions":{"Key":"USAGE_TYPE_GROUP","Values":["EC2: Data Transfer - Inter AZ"]}}'
How to read it. There's no universal "healthy" number, so use a ratio: if inter-AZ transfer plus NAT processing exceeds 3–5% of your total bill, something architectural is worth investigating. The two usual suspects: replicated data stores chatting across AZs (often unavoidable) and S3/ECR/DynamoDB traffic routed through a NAT gateway (almost always avoidable with gateway VPC endpoints, which are free).
Step 4: Measure non-prod spend (waste source #7)
If your resources carry an environment tag (activate it first under Billing → Cost allocation tags):
aws ce get-cost-and-usage \
--time-period Start=2026-06-01,End=2026-07-01 \
--granularity MONTHLY --metrics UnblendedCost \
--group-by Type=TAG,Key=environment
How to read it. Take every non-prod environment's compute cost and multiply by 0.36 (60 business hours out of 168 weekly hours). The difference between that number and what you're paying is your scheduling opportunity. Also look at the "no tag" bucket in the output — if it's more than about 10% of spend, tagging discipline is itself an audit finding.
Tool 2: Trusted Advisor — the pre-built checks
Trusted Advisor covers waste sources #1 (EBS), #2 (idle EC2), #4 (EIPs and idle load balancers), and parts of #5 (idle RDS) with zero setup. One catch: the full cost optimization checks require a Business, Enterprise On-Ramp, or Enterprise support plan (AWS docs). On Basic/Developer support you only get a small subset — skip to the CLI section and do it manually.
Console: Trusted Advisor → Recommendations → Cost optimization. The checks to open, mapped to our waste sources:
| Trusted Advisor check | Waste source |
|---|---|
| Low Utilization Amazon EC2 Instances | #2 idle/oversized EC2 |
| Underutilized Amazon EBS Volumes | #1 unattached EBS |
| Unassociated Elastic IP Addresses | #4 unused EIPs |
| Idle Load Balancers | #4 idle LBs |
| Amazon RDS Idle DB Instances | #5 RDS |
| Savings Plan / RI recommendations | #3 coverage |
How to read it. Trusted Advisor's thresholds are generous — "Low Utilization" means CPU at or below 10% and network I/O at or below 5 MB on 4 or more of the last 14 days. Anything it flags is a strong candidate; plenty of waste passes beneath its thresholds, which is why we still do the CloudWatch pass below. Each check exports to CSV — download them all; they're the backbone of your findings doc.
You can also pull results programmatically (Business+ support, us-east-1 endpoint):
# List all cost checks and their IDs
aws support describe-trusted-advisor-checks --language en \
--query 'checks[?category==`cost_optimizing`].{id:id,name:name}' --output table
# Pull one check's results, e.g. Low Utilization EC2
aws support describe-trusted-advisor-check-result \
--check-id eW7HH0l7J9 --language en
Tool 3: Compute Optimizer — the rightsizing engine
Compute Optimizer covers waste sources #2 (EC2 rightsizing), #1 (EBS volume types), and #5 (RDS). It's free, but you must opt in (Compute Optimizer console → Get started) and wait about 24 hours; it then analyzes 14 days of CloudWatch metrics by default (AWS docs).
Pull every over-provisioned instance:
aws compute-optimizer get-ec2-instance-recommendations \
--query 'instanceRecommendations[?finding==`OVER_PROVISIONED`].{Arn:instanceArn,Current:currentInstanceType,Recommended:recommendationOptions[0].instanceType}' \
--output table
Repeat with get-ebs-volume-recommendations (flags gp2-to-gp3 conversions and oversized IOPS) and get-rds-database-recommendations.
How to read it. Findings come back as OPTIMIZED, OVER_PROVISIONED, or UNDER_PROVISIONED. For each over-provisioned finding, look at the recommendation options — each includes projected utilization and a performance-risk score. A recommendation with low performance risk and one full size-step down is usually safe to act on; treat multi-step downsizes (2xlarge to medium) with more care and verify against 30 days of data, not 14. Enable enhanced infrastructure metrics if you want the lookback extended to 93 days — that catches monthly batch spikes the default window misses.
The one thing Compute Optimizer won't tell you: whether an instance should exist at all. It rightsizes; it doesn't question. That judgment call is yours.
Tool 4: CloudWatch — the verification layer
Before you act on any finding from the tools above, verify against raw metrics. This is also your primary tool for waste source #5 (RDS), where a CPU-only view misleads.
For a suspect RDS instance, pull CPU, connections, and memory together:
for metric in CPUUtilization DatabaseConnections FreeableMemory; do
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS --metric-name $metric \
--dimensions Name=DBInstanceIdentifier,Value=your-db-name \
--start-time $(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 86400 --statistics Average Maximum
done
(macOS: use date -u -v-30d +%Y-%m-%dT%H:%M:%SZ for the start time.)
How to read it. A downsize candidate looks like: average CPU under 15%, maximum under 50%, connections well below the class limit, and FreeableMemory consistently above roughly 40% of instance RAM. If memory is tight but CPU is idle, move within the memory-optimized family rather than down a size. For NAT gateway analysis (waste source #6), the same pattern applies with --namespace AWS/NATGateway --metric-name BytesOutToDestination — sum it monthly and multiply by $0.045/GB to price the traffic, then go find out what it is.
Tool 5: AWS CLI sweeps — the orphan hunt
Finally, the direct inventory checks for waste sources #1 and #4 — these catch everything regardless of support plan. Run them in every active region:
# Unattached EBS volumes
aws ec2 describe-volumes --filters Name=status,Values=available \
--query 'Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,Created:CreateTime}' --output table
# Old snapshots you own (adjust date to your retention policy)
aws ec2 describe-snapshots --owner-ids self \
--query 'Snapshots[?StartTime<=`2026-01-01`].{ID:SnapshotId,Vol:VolumeId,Size:VolumeSize}' --output table
# Unassociated Elastic IPs
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].{IP:PublicIp,AllocId:AllocationId}' --output table
# All load balancers (then check RequestCount for each in CloudWatch)
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[*].{Name:LoadBalancerName,Type:Type,Created:CreatedTime}' --output table
To loop across regions:
for r in $(aws ec2 describe-regions --query 'Regions[*].RegionName' --output text); do
echo "== $r =="
aws ec2 describe-volumes --region $r --filters Name=status,Values=available \
--query 'Volumes[*].VolumeId' --output text
done
How to read it. Every available volume gets a snapshot-then-delete ticket. Every EIP with a null association gets released. Every load balancer with near-zero RequestCount over 14 days gets an owner asked "can this go?" — and if no owner answers in a week, it goes.
At the end of these five passes you'll have a findings document covering all seven waste sources, with dollar estimates. On a typical first audit of a mid-market account, that document is worth 15–30% of the monthly bill.
The honest part: where the DIY audit falls short
You should absolutely run this audit. You should also know its four structural limits before you rely on it as a strategy:
It's a point-in-time snapshot. The audit describes your account on audit day. Next sprint's deploys, terminations, and scale-ups start regenerating waste immediately — every one of the seven sources is a process, not a one-off mess.
It costs real engineer time, every time. First pass: 3–5 hours. Repeat passes are faster but never free, and they compete with feature work. In practice, "monthly audit" becomes "quarterly audit" by Q3.
Findings decay. A rightsizing recommendation based on last month's metrics is stale after the next traffic shift. Trusted Advisor's 14-day windows and Compute Optimizer's default lookback both trail reality. A finding you act on 45 days after the data was collected may simply be wrong.
Detection isn't remediation. Nothing above deletes a volume, resizes an instance, or buys a Savings Plan. Every finding becomes a ticket, and tickets without owners are where cost savings go to die. Most teams recover well under half of the audited savings for exactly this reason.
None of this makes the audit worthless — it makes it a baseline. The question it always leads to: how do you make these checks run continuously, with remediation attached?
What comes next
Everything you did manually in this guide can run continuously. CloudThinker's CostOps agent performs these same checks — all seven waste sources — through read-only AWS access, every day instead of every quarter, with remediation gated behind your approval. That's part three of this series.
Or skip ahead and see your own account's findings: the free tier includes 100 premium credits, no card required.
