DEV Community

Cover image for Find the AWS waste in one afternoon: 7 read-only CLI checks
Neeraj Sharma
Neeraj Sharma

Posted on AI-assisted

Find the AWS waste in one afternoon: 7 read-only CLI checks

If you're at a seed or Series A company, nobody owns the AWS bill until it gets big enough to scare someone. By then the waste has been sitting there for months. It's rarely one big mistake. It's a pile of small things: a volume nobody deleted, a load balancer from a demo, logs kept forever.

This is the afternoon audit I run first on a startup's AWS account. Every check is a read-only CLI command, and each one comes with what it costs if you find something. Prices below are us-east-1 list prices. Check your own region before quoting numbers to anyone.

The seven checks, what each one finds and its us-east-1 price

Before you start

Use a role with read-only access (the ReadOnlyAccess managed policy covers everything below; if the Cost Explorer call returns AccessDenied, add ce:GetCostAndUsage). Most resources are regional, so run each check in every region you use:

for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  echo "== $r"
  # paste any check below here, with --region "$r"
done
Enter fullscreen mode Exit fullscreen mode

0. Where the money actually goes

Start with last month's bill by service so you know which checks matter most:

aws ce get-cost-and-usage \
  --time-period Start=2026-09-01,End=2026-10-01 \
  --granularity MONTHLY --metrics UnblendedCost \
  --group-by Type=DIMENSION,Key=SERVICE \
  --query 'ResultsByTime[0].Groups[].[Keys[0],Metrics.UnblendedCost.Amount]' \
  --output table
Enter fullscreen mode Exit fullscreen mode

Cost Explorer API calls cost $0.01 each, so run this once, not in a loop.

1. Volumes attached to nothing

An EBS volume in the available state isn't attached to any instance, but you still pay for every gigabyte.

aws ec2 describe-volumes --filters Name=status,Values=available \
  --query 'Volumes[].[VolumeId,Size,VolumeType,CreateTime]' --output table
Enter fullscreen mode Exit fullscreen mode

At $0.08 per GB-month for gp3, 500 GB of forgotten volumes is $40 a month. Snapshot anything you're unsure about, then delete the volume.

2. gp2 volumes that should be gp3

gp3 costs $0.08 per GB-month against $0.10 for gp2, which is 20% cheaper, and its baseline performance is at least as good for most workloads. The change happens online, without detaching anything.

aws ec2 describe-volumes --filters Name=volume-type,Values=gp2 \
  --query 'Volumes[].[VolumeId,Size,Iops]' --output table

# per volume, after checking its IOPS needs:
aws ec2 modify-volume --volume-id vol-0123456789abcdef0 --volume-type gp3
Enter fullscreen mode Exit fullscreen mode

2,000 GB moved from gp2 to gp3 saves 2,000 × 0.02 = $40 a month. Before you convert a large gp2 volume, look at its IOPS: gp2 gets 3 IOPS per GB, and gp3 starts at a flat 3,000.

3. Snapshots nobody will restore

aws ec2 describe-snapshots --owner-ids self \
  --query "Snapshots[?StartTime<='2026-04-01'].[SnapshotId,VolumeSize,StartTime,Description]" \
  --output table
Enter fullscreen mode Exit fullscreen mode

Standard snapshot storage is $0.05 per GB-month. VolumeSize overstates the real cost because snapshots are incremental, so read the "EBS:SnapshotUsage" line in Cost Explorer for the true figure. For snapshots you must keep but won't touch, the archive tier is $0.0125 per GB-month, with a 90-day minimum and a fee to restore.

4. Public IPv4 addresses

Since February 2024 every public IPv4 address costs $0.005 an hour, whether it's in use or idle. That's 0.005 × 730 = $3.65 a month each. Start with Elastic IPs that aren't associated with anything:

aws ec2 describe-addresses \
  --query 'Addresses[?AssociationId==null].[PublicIp,AllocationId]' --output table
Enter fullscreen mode Exit fullscreen mode

Then list every network interface with a public IP. You'll often find instances in public subnets that don't need one:

aws ec2 describe-network-interfaces \
  --query 'NetworkInterfaces[?Association.PublicIp].[NetworkInterfaceId,Association.PublicIp,Description]' \
  --output table
Enter fullscreen mode Exit fullscreen mode

5. Load balancers with no traffic

An Application Load Balancer costs $0.0225 an hour before any traffic, so 0.0225 × 730 = $16.43 a month just for existing. Find the ones that served no requests in the last 14 days:

for arn in $(aws elbv2 describe-load-balancers --query 'LoadBalancers[?Type==`application`].LoadBalancerArn' --output text); do
  name=$(echo "$arn" | cut -d/ -f2-4)
  sum=$(aws cloudwatch get-metric-statistics --namespace AWS/ApplicationELB \
    --metric-name RequestCount --dimensions Name=LoadBalancer,Value="$name" \
    --start-time "$(date -u -d '14 days ago' +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
    --period 1209600 --statistics Sum --query 'Datapoints[0].Sum' --output text)
  echo "$name $sum"
done
Enter fullscreen mode Exit fullscreen mode

None or 0 means nothing has hit it in two weeks. On macOS, use date -u -v-14d instead of -d '14 days ago'.

6. Log groups that keep everything forever

New CloudWatch log groups default to "never expire". Storage is about $0.03 per GB-month, but ingestion at $0.50 per GB is usually the bigger number. This lists groups with no retention and their stored size; for ingestion, check each large group's IncomingBytes metric:

aws logs describe-log-groups \
  --query 'logGroups[?!retentionInDays].[logGroupName,storedBytes]' --output table

# set a retention period per group:
aws logs put-retention-policy --log-group-name /aws/lambda/my-fn --retention-in-days 30
Enter fullscreen mode Exit fullscreen mode

If one group is ingesting tens of GB a day, the fix is less logging, not shorter retention.

7. NAT gateways doing more work than they should

A NAT gateway costs $0.045 an hour (0.045 × 730 = $32.85 a month) plus $0.045 for every GB it processes. The usual culprit is S3 traffic (ECR image layers included, since they're pulled from S3) that should go through a free gateway endpoint. I wrote up that fix separately: why your NAT gateway bill keeps growing.

aws ec2 describe-nat-gateways --query 'NatGateways[].[NatGatewayId,VpcId,State]' --output table
Enter fullscreen mode Exit fullscreen mode

What a typical first pass adds up to

Here's an illustrative example of findings, not a specific account:

Example findings from one afternoon audit, monthly cost by item

Finding Monthly saving (USD)
500 GB of unattached gp3 volumes 40.00
2,000 GB of gp2 moved to gp3 40.00
8 idle public IPv4 addresses 29.20
2 load balancers with no traffic 32.85
3 TB of old standard snapshots 150.00
Total 292.05 USD a month

That's about 292.05 × 12 = 3,505 dollars a year from an afternoon of reading. None of this needs a reserved instance or a Savings Plan. Those are bigger levers, but they lock you in, so do this cleanup first and commit to the smaller bill afterwards.

Make it stick

  1. Tag everything with an owner and a purpose. Untagged resources are the ones nobody deletes.
  2. Turn on AWS Cost Anomaly Detection. It's free, and it emails you when a service jumps.
  3. Put a monthly calendar reminder on someone's name to run this list again.

If you want a rough number before you start, our cloud cost calculator estimates how much of a bill is usually recoverable. For a full review across AWS, GCP and Azure, that's the cloud cost optimization work I do at Axionry.

Prices: AWS EBS, VPC, Elastic Load Balancing and CloudWatch pricing pages (us-east-1), checked October 2026. I used an AI assistant while drafting this.

Top comments (0)