Your developers work about 40 hours a week. If your staging servers and test databases run around the clock, AWS bills you for 168. That leaves roughly 128 hours a week of machines nobody is using.

The second leak is quieter. When someone deletes an EC2 instance by hand, the disk, the Elastic IP or the snapshot attached to it often stays behind. AWS keeps charging for them every month. I find some version of both on almost every bill review I do.

Why it happens

Nobody decided to waste the money. Staging was set up once and left on because switching it off felt risky. Instances get terminated in a hurry, and the console does not make you clean up what was attached. The person who created the resource has moved on. The bill is one line per service, so a forgotten 500 GB volume never looks suspicious.

Each item is small. It adds up because nothing ever expires.

How to check your own account

Start in Cost Explorer. Filter by service “EC2-Other” and group by Usage Type. EBS volumes and snapshots show up here, separate from your instance costs. Public IP address charges sit under the “Virtual Private Cloud” service, so check that one too.

Then list the leftovers. Run these per region, and check every region you have ever used, not just the main one.

Volumes attached to nothing:

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

Elastic IPs not associated with anything:

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

Snapshots you own, oldest first:

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

Old snapshots are not always waste. Some are your only backups. Check the description and whether the source volume still exists before you delete anything.

Load balancers are the other usual suspect. In the EC2 console, open Load Balancers, then Target groups, and look for balancers whose target groups have no registered targets. An Application Load Balancer with nothing behind it still bills by the hour.

For idle environments, list what is running:

aws ec2 describe-instances \
  --filters Name=instance-state-name,Values=running \
  --query 'Reservations[].Instances[].[InstanceId,InstanceType,Tags[?Key==`Name`]|[0].Value]' \
  --output table

Anything named staging, test or dev that is running at 3 a.m. on a Sunday is a candidate.

Fixing it and keeping it fixed

For ghost resources, snapshot a volume you are unsure about, then delete it. Release unused Elastic IPs. AWS charges for public IPv4 addresses whether or not they are attached, so an address you do not need is pure cost.

For staging, schedule it. Tag non-production instances, for example Environment=staging, then use EventBridge Scheduler or Instance Scheduler on AWS to stop them in the evening and start them in the morning. I usually go further than nights: stop on Friday evening, start on Monday morning. A small Lambda function does the same job.

A few things to know before you do:

  • Stopped EC2 instances still pay for their EBS volumes. You save the compute, not the storage.
  • RDS instances can be stopped too, but AWS starts them again automatically after seven days. The schedule has to stop them every week.
  • Test the morning start-up once with the team watching. A staging box that does not come back is how people lose faith in the schedule.

To stop the ghosts returning, tag everything with an owner and an environment, and review the EC2-Other line in Cost Explorer monthly. Add a Cost Anomaly Detection monitor so a jump gets flagged without anyone having to look.

Infrastructure should follow your business hours, not the server’s uptime. If you would rather have someone run this sweep every month, that is part of ongoing AWS management.