Stopped EC2 instance still charging? Here's what you're paying for
Last reviewed
You stopped the instance. The bill didn't go down. That's not a mistake on AWS's
side — stopping an instance only stops the compute charge. Everything attached
to it keeps billing.
What a stopped instance still costs
When an EC2 instance is stopped, AWS stops charging you for the instance hours.
It does not stop charging for:
- EBS volumes — the root disk and any data disks. You pay for the provisioned size every month, whether the machine is running or not.
- Elastic IP addresses — an Elastic IP stays attached to a stopped instance, and AWS charges for every public IPv4 address.
- Snapshots of those volumes, if you have any.
A typical example, in us-east-1:
| Monthly | |
|---|---|
| 100 GB gp3 root volume ($0.08/GB-month) | $8.00 |
| One Elastic IP ($0.005/hour) | $3.60 |
| One stopped instance, doing nothing | $11.60 |
That's about $139 a year for a machine nobody is using. Five forgotten
instances from an old migration or a finished load test is around $700 a year —
and on gp2, or in regions like eu-central-1 or sa-east-1, it's more.
The real problem is that a stopped instance looks harmless in the console. It's
greyed out. Nobody thinks of it as spending money.
How to find every stopped instance yourself
Stopped instances are per region, and the console only shows one region at a
time. This loops through every region enabled on your account:
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
aws ec2 describe-instances --region "$r" \
--filters Name=instance-state-name,Values=stopped \
--query 'Reservations[].Instances[].[InstanceId, Tags[?Key==`Name`]|[0].Value, StateTransitionReason]' \
--output text | sed "s/^/$r\t/"
doneThe last column, StateTransitionReason, includes the date the instance was
stopped — so you can see which ones have been sitting there for months.
For any instance on that list, see what's still attached to it:
# Volumes still billing
aws ec2 describe-volumes --region us-east-1 \
--filters Name=attachment.instance-id,Values=i-0123456789abcdef0 \
--query 'Volumes[].[VolumeId, VolumeType, Size]' --output table
# Elastic IPs in the region, and what they're attached to
aws ec2 describe-addresses --region us-east-1 \
--query 'Addresses[].[PublicIp, InstanceId]' --output tableMultiply each volume's size by your region's rate, add $3.60 a month per Elastic
IP, and you have that instance's idle cost.
How to fix it
Pick one of three, depending on whether you'll need the machine again.
You'll start it again soon. Leave it — but release any Elastic IP it doesn't
strictly need. That's $3.60 a month back, and the machine is untouched.
You need the data, not the machine. Create an image, then terminate:
aws ec2 create-image --instance-id i-0123456789abcdef0 --name "archive-before-terminate"An AMI is backed by snapshots, which only bill for the blocks actually used — usually
far less than the full provisioned volume.
You don't need it. Terminate it. Before you do, check each attached volume'sDeleteOnTermination setting. The root volume is usually deleted with the
instance, but additional data volumes are often kept — and they become
unattached volumes that keep billing with no instance at all. That's its own
problem: how to find unattached EBS volumes.
Checking once isn't the hard part
Running the loop above takes five minutes. Remembering to run it every month,
across every region, after every migration and load test — that's what doesn't
happen.
Devopsly does it on every sync. Connect your AWS account read-only and it flags
every stopped instance in every region you have enabled, alongside your other
findings — unattached volumes, public S3 buckets, servers with no backup.
It uses 15 read-only IAM actions and never touches your infrastructure or opens
your files.