Last month a client's AWS bill jumped by $3,200 with zero new features shipped and zero traffic growth. Three engineers spent two days convinced someone had left a crypto miner running on a forgotten EC2 instance. The real culprit was way more boring: 47 unattached EBS volumes going back 14 months, a stack of manual snapshots nobody ever cleaned up, and two NAT gateways sitting idle in a VPC nobody used anymore.
Nobody deletes these on purpose. Someone terminates an EC2 instance during a "quick test" and the volume survives because "Delete on Termination" wasn't checked. Someone takes a manual snapshot before a risky deploy and forgets about it. Multiply that by every engineer, every sprint, for two years, and you've got a graveyard quietly billing you by the GB-month.
The Fix #
Start with what's actually costing you money, not the general "let's tag everything" idea people give up on in week two.
Find unattached EBS volumes right now:
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query 'Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,AZ:AvailabilityZone,Created:CreateTime}' \
--output table
status=available means the volume isn't attached to anything. That's your hit list.
Turn on AWS Config with the right managed rules. Enable ec2-volume-inuse-check for unattached volumes and ec2-stopped-instance for zombie instances nobody restarted. Add a custom rule or scheduled Lambda for unattached Elastic IPs — these carry an hourly charge whether or not they're attached, and they're the line item almost nobody checks during an audit.
Automate the sweep. Schedule a Lambda via EventBridge weekly:
- List volumes with
status=availableolder than N days (useCreateTime). - Snapshot first, tag
pending-deletion, and notify the owner via Slack or email. - Auto-delete after a grace period if nobody objects.
Enforce tagging at creation, not after the fact. Require Owner and Team tags via a Service Control Policy or Config rule that denies untagged RunInstances / CreateVolume calls. Retroactive tagging on a two-year-old account never actually happens.
Turn on Cost Anomaly Detection. Point it at your top cost categories (EC2, EBS, RDS) so a sudden spend spike pages someone within a day, not at month-end when finance forwards you the invoice.
The Gotchas #
- Never auto-delete without a snapshot first. The one time you skip this is the one time that "unused" volume had the only copy of a database restore someone was mid-testing.
- Old snapshots backing already-deleted volumes still cost money. Nobody thinks to check snapshots separately — they assume deleting the volume deletes its history. It doesn't.
- Cross-region blindness is the biggest one. Everyone audits their primary region and forgets the DR region, or the region a replication bucket quietly lives in.
- gp2 vs gp3 is free money on the table. Migrating volumes you're keeping from gp2 to gp3 typically saves around 20%, with better baseline IOPS included.
- NAT Gateways bill hourly whether or not traffic flows through them. A NAT gateway in a VPC nobody's deployed into in six months is pure waste — check CloudWatch's
BytesOutToDestinationbefore assuming it's dead, then kill it.
TL;DR #
- Unattached EBS volumes, orphaned snapshots, idle NAT gateways, and unattached Elastic IPs are the most common silent cost leaks — check them this week, not next quarter.
- AWS Config managed rules plus a scheduled Lambda sweep turn this from a manual quarterly panic into a boring automated process.
- Snapshot before you delete, tag at creation not after, and don't forget the regions you don't normally look at.