Cleaning Up Unused AWS Resources
A quarterly cleanup audit of a single AWS account belonging to a 10-person engineering team found $800/month in resources nobody knew were running. This is not unusual. Cloud environments accumulate waste through normal development activity — the difference is whether anyone looks for it.
The anatomy of cloud resource waste
Orphaned resources accumulate in predictable ways:
- A developer launches an EC2 instance for testing, terminates the instance but not the EBS volume
- An old application is decommissioned, but the NAT Gateway in its VPC keeps running
- A load balancer is created, the application behind it is removed, but the load balancer stays
- Elastic IPs are allocated for instances that are later terminated
- A Lambda function gets new versions deployed daily, and old versions with provisioned concurrency accumulate
- An RDS instance is restored from a snapshot for investigation, then terminated without deleting the snapshot
- CloudWatch log groups from defunct services fill with retained logs
None of this is negligence — it’s just the natural consequence of a fast-moving engineering team deploying and changing things without a systematic decommissioning process.
Unattached EBS volumes
EBS volumes exist independent of EC2 instances. When an instance is terminated, the root volume is deleted (if it was configured with DeleteOnTermination=true), but data volumes and any manually created volumes remain.
Cost: gp3 volumes at $0.08/GB/month, gp2 at $0.10/GB/month. A 200GB volume nobody uses costs $16/month.
Find all unattached EBS volumes:
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query 'Volumes[*].{
ID: VolumeId,
SizeGB: Size,
Type: VolumeType,
Created: CreateTime,
Name: Tags[?Key==`Name`]|[0].Value
}' \
--output tableBefore deleting, check:
- When was the volume last modified? (very recent = possibly in use by a process)
- Does it have a
NameorProjecttag indicating ownership? - Does it have any recent snapshots? (suggests someone is actively using it)
To create a final snapshot before deleting:
aws ec2 create-snapshot \
--volume-id vol-1234567890abcdef0 \
--description "Final snapshot before deletion 2026-03-19"
# After snapshot completes, delete the volume
aws ec2 delete-volume --volume-id vol-1234567890abcdef0Unassociated Elastic IP addresses
Elastic IPs are free when attached to a running instance, but cost $0.005/hour (~$3.65/month) when idle. This seems small, but a team that allocates and forgets 20 Elastic IPs pays $73/month for nothing.
Find all unassociated Elastic IPs:
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==null].{
IP: PublicIp,
AllocationId: AllocationId,
Domain: Domain
}' \
--output tableIf the IP has no association ID, it’s costing money and serving no purpose. Release it:
aws ec2 release-address --allocation-id eipalloc-1234567890abcdef0Note: releasing an Elastic IP is irreversible. You will get a different IP if you allocate a new one. Make sure nobody is using it as a known IP in firewall rules or DNS before releasing.
Old EBS snapshots
EBS snapshots are incremental and stored in S3 at $0.05/GB/month. They accumulate from:
- Manual backups taken during incident investigation
- Automated backup tools that don’t have retention policies
- AMI creation (every AMI has backing snapshots)
- Copying snapshots between regions for DR that was later abandoned
Find all snapshots owned by your account:
aws ec2 describe-snapshots \
--owner-ids self \
--query 'Snapshots[*].{
ID: SnapshotId,
SizeGB: VolumeSize,
Created: StartTime,
Description: Description
}' \
--output table \
--query 'sort_by(Snapshots, &StartTime)[*].{
ID: SnapshotId,
SizeGB: VolumeSize,
Created: StartTime
}'Identify snapshots older than 90 days with no associated AMI. Check if the originating volume or instance still exists before deleting — if the instance is gone, the snapshot may be the only copy of the data.
Prevent future accumulation: Use Amazon Data Lifecycle Manager to enforce snapshot retention policies. A 14-day retention policy keeps the last 14 daily snapshots and automatically deletes older ones.
aws dlm create-lifecycle-policy \
--description "14-day EBS snapshot retention" \
--state ENABLED \
--execution-role-arn arn:aws:iam::123456789012:role/AWSDataLifecycleManagerDefaultRole \
--policy-details '{
"PolicyType": "EBS_SNAPSHOT_MANAGEMENT",
"ResourceTypes": ["VOLUME"],
"TargetTags": [{"Key": "Backup", "Value": "true"}],
"Schedules": [{
"Name": "Daily",
"CreateRule": {"Interval": 24, "IntervalUnit": "HOURS", "Times": ["03:00"]},
"RetainRule": {"Count": 14},
"CopyTags": true
}]
}'Idle load balancers
Application Load Balancers cost $0.008/LCU-hour plus $0.0225/hour just for running. A load balancer with no registered healthy targets costs at minimum $16.43/month for doing nothing.
Find load balancers and check their targets:
# List all load balancers
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[*].{
Name: LoadBalancerName,
State: State.Code,
ARN: LoadBalancerArn
}' \
--output table
# For each load balancer, check target group health
aws elbv2 describe-target-groups \
--load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-lb/...
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:...A load balancer with 0 healthy targets and no recent traffic in CloudWatch is a deletion candidate. Check the ALB access logs or CloudWatch RequestCount metric for the past 30 days to confirm it’s inactive.
Unused NAT Gateways
NAT Gateways cost $0.045/hour ($32.85/month) plus $0.045/GB processed. A NAT Gateway in a VPC that no longer has any running EC2 instances is pure waste.
# List all NAT Gateways
aws ec2 describe-nat-gateways \
--filter Name=state,Values=available \
--query 'NatGateways[*].{
ID: NatGatewayId,
VpcId: VpcId,
Created: CreateTime,
State: State
}' \
--output tableFor each NAT Gateway, check its VPC. If the VPC has no running EC2 instances or ECS tasks that need outbound internet access, the NAT Gateway can be deleted.
Stale CloudWatch log groups
CloudWatch Logs charges $0.03/GB/month for stored logs and $0.50/GB for ingestion. Log groups from services that were decommissioned months ago accumulate stored log data indefinitely unless a retention policy is set.
Log groups have no default retention. Without an explicit retention policy, logs are kept forever.
# Find log groups with no retention policy (retention = null means forever)
aws logs describe-log-groups \
--query 'logGroups[?retentionInDays==`null`].{
Name: logGroupName,
StoredBytes: storedBytes,
Created: creationTime
}' \
--output tableFor active log groups, set a retention policy appropriate to your needs (30, 60, or 90 days for most application logs):
aws logs put-retention-policy \
--log-group-name /aws/lambda/my-function \
--retention-in-days 30For log groups belonging to decommissioned services, delete them after verifying no compliance requirement mandates retention:
aws logs delete-log-group --log-group-name /aws/lambda/old-functionAutomating cleanup detection with Trusted Advisor and Config
Manual CLI queries find what’s there now. For ongoing detection, two AWS services help:
AWS Trusted Advisor (Business/Enterprise support) runs automated checks for:
- Idle EC2 instances
- Unassociated Elastic IPs
- Idle load balancers
- Underutilised EBS volumes
Trusted Advisor findings appear in the console and can be exported. Set up a weekly review of the cost optimisation section.
AWS Config can detect non-compliant resources with managed rules:
ec2-volume-inuse-check— flags unattached EBS volumeseip-attached— flags unassociated Elastic IPsrequired-tags— flags resources without required tags (which makes cleanup attribution possible)
Config rules can trigger Lambda remediation automatically (e.g., send an SNS notification when an unattached EBS volume is detected) or through Systems Manager Automation.
See resource tags for how proper tagging makes cleanup decisions much easier — tagged resources tell you who owns them and whether they’re still needed.
Note: Before running any automated cleanup scripts, always do a dry run first. Print the list of resources that would be deleted and verify manually before executing deletions. Accidental deletion of the wrong resource — especially snapshots or volumes containing unique data — cannot be undone. The extra step is worth it.
Common mistakes
- Deleting resources without checking dependencies — A snapshot might be the only copy of a decommissioned database’s data that someone will want to restore in 6 months. An EBS volume might be shared between instances or contain data a compliance requirement mandates keeping. Verify before deleting.
- Doing cleanup once and not again — New orphaned resources appear every sprint. Cleanup is not a one-time event — it needs to be a recurring process. Schedule a monthly or quarterly cleanup review.
- Not setting CloudWatch log retention policies on new resources — It’s easier to set retention at creation time than to hunt down log groups without retention later. Add retention policy configuration to your IaC templates so every new log group has it from day one.
- Cleaning up dev accounts but not staging — Staging environments often mirror production architectures and accumulate the same waste. Apply the same cleanup practices to all non-production environments.
- Not checking old AMIs — AMIs created from instances that no longer exist still have backing EBS snapshots charged at $0.05/GB/month. Deregistering the AMI does NOT automatically delete its snapshots — you must delete them separately after deregistering.
Summary
- Terminating an EC2 instance does not delete its EBS snapshots — delete them separately
- Unassociated Elastic IPs cost $0.005/hr each — audit and release any not in active use
- Load balancers with no healthy targets cost $16+/month — check and delete idle ones
- CloudWatch log groups have no default retention — set retention policies on every log group
- NAT Gateways in empty VPCs cost $32.85/month just for existing — delete them if nothing needs outbound access
- AWS Config rules can continuously detect orphaned resources; Trusted Advisor provides a consolidated cost review
- Always do a dry run before automated deletion — verify the resource list manually first
Frequently asked questions
Does terminating an EC2 instance delete its EBS snapshots?
No. EBS snapshots are independent resources. Terminating an EC2 instance deletes the root EBS volume (if configured with DeleteOnTermination=true) but does not delete any manually created snapshots. Snapshots must be deleted explicitly. Check for old snapshots using `aws ec2 describe-snapshots --owner-ids self`.
How much does an unattached Elastic IP address cost?
Elastic IP addresses are free when attached to a running EC2 instance. When not attached (or attached to a stopped instance), they cost $0.005/hour — approximately $3.65/month per idle Elastic IP.
What is the easiest way to find orphaned resources across an entire AWS account?
AWS Trusted Advisor (requires Business or Enterprise support plan) scans for idle EC2 instances, unattached EBS volumes, unassociated Elastic IPs, and idle load balancers. For free tier accounts, use AWS CLI commands to query each resource type individually.