back to blog
// post

How to find idle AWS resources without living in the CLI

Victor Grenu·2024-09-24·updated 2026-09-23·3 min read

I used to send clients a shell history. CPU near zero. RDS with no connections. Volumes in available. Snapshots older than anyone on the team. It works. It also takes a day, and it is stale the next morning.

If you want the DIY version for one account and one region:

EC2. CloudWatch CPUUtilization and network packets, not a single 5-minute dip. An instance can look idle at noon and busy at 03:00.

RDS. DatabaseConnections at zero for a week is a smell. Confirm it is not a replica you still need.

EBS. Unattached volumes are the easy win:

aws ec2 describe-volumes \
  --query "Volumes[?State=='available'].[VolumeId,Size]" \
  --output table

Elastic IPs. An address that is not attached to anything still bills by the hour:

aws ec2 describe-addresses \
  --query "Addresses[?AssociationId==null].[PublicIp,AllocationId]" \
  --output table

Snapshots. Sort yours by age and read the top of the list. The oldest ones usually belong to a migration that ended:

aws ec2 describe-snapshots --owner-ids self \
  --query "sort_by(Snapshots,&StartTime)[].[SnapshotId,VolumeSize,StartTime,Description]" \
  --output table

NAT gateways. About $32 a month each before a single byte. List them, then check a week of outbound traffic per gateway. Near zero means nothing uses it:

aws ec2 describe-nat-gateways \
  --filter Name=state,Values=available \
  --query "NatGateways[].[NatGatewayId,VpcId]" \
  --output table

aws cloudwatch get-metric-statistics \
  --namespace AWS/NATGateway --metric-name BytesOutToDestination \
  --dimensions Name=NatGatewayId,Value=nat-0123456789abcdef0 \
  --start-time 2026-09-16T00:00:00Z --end-time 2026-09-23T00:00:00Z \
  --period 604800 --statistics Sum

Logs. Log groups with no retention fill quietly:

aws logs describe-log-groups \
  --query "logGroups[?retentionInDays==null].[logGroupName,storedBytes]" \
  --output table

Every command above covers one region. Loop over the regions your account has enabled:

for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do
  echo "== $region"
  aws ec2 describe-volumes --region "$region" \
    --query "Volumes[?State=='available'].[VolumeId,Size]" \
    --output text
done

Then do the same for load balancers with no targets, SageMaker endpoints, ElastiCache, and RDS snapshots. Then do it for every account. This is why the audit became a product.

Why this is a bad weekly ritual

A FinOps engineer can run the CLI. A CTO will not. A platform manager with 40 accounts will not do it every Monday. The miss is always the same: us-west-2 was in the script, eu-central-1 was not, and that is where the demo VPC still lives.

unusd.cloud runs 55+ checks like these on a schedule, with a read-only role, in every region you enabled. You still choose what to delete.

Idle resources are also attack surface. An open sandbox with an old key is not only a cost problem.

Connect one account if you are done maintaining the script.

// try unusd

Stop paying for resources nobody is using.

Connect a read-only role. Digest by email, full web report in the app - minutes to first scan.

Start free scan→