Skip to main content
idle · aws

EC2 instances stopped 30+ days that still bill for volumes and Elastic IPs

resource types
1
rule IDs covered
1
severity
medium

What does ZopNight detect here?

ZopNight targets EC2 instances that have sat in the `stopped` state for at least 30 days while their attached EBS volumes or associated Elastic IP keep charging. Compute billing ended at the stop, so ZopNight reports only that residual storage and address cost as the saving, and stays silent when no attachment can be priced.

Signal and threshold

How ZopNight evaluates EC2 instances stopped 30+ days that still bill for volumes and Elastic IPs.
Field Value
Rule IDsRC-001
Categoryidle
Severitymedium
MetricCPUUtilization
Threshold30 days stopped
Evaluation window30d
SourceZopNight
Permissions usedec2:DescribeInstances · ec2:DescribeVolumes · ec2:DescribeAddresses · cloudwatch:GetMetricData

What a stopped EC2 instance keeps charging you for

AWS stops charging for instance usage once an instance leaves the running state, and a stopped instance incurs no usage or data transfer fees. The disks do not get the same treatment. AWS documents that attached EBS root and data volumes persist through a stop and continue to incur storage charges, and that an Elastic IP left associated with a stopped instance is billed as well. VPC pricing puts every public IPv4 address, in use or idle, at $0.005 per hour, which comes to about $3.65 over a 730-hour month.

So an instance that was switched off “for now” last quarter is not free. It is a storage bill with nobody using the storage. This rule exists to surface that bill once the stop has clearly become permanent.

Listing long-stopped instances with the AWS CLI

Start with the stopped instances in a Region. StateTransitionReason usually carries the time of the stop, although AWS notes it can be an empty string:

Terminal window
aws ec2 describe-instances \
--filters Name=instance-state-name,Values=stopped \
--query 'Reservations[].Instances[].[InstanceId,StateTransitionReason]' \
--output table

For each instance stopped longer than a month, list what is still attached and whether it would survive termination:

Terminal window
aws ec2 describe-volumes \
--filters Name=attachment.instance-id,Values=i-0123456789abcdef0 \
--query 'Volumes[].[VolumeId,Size,VolumeType,Attachments[0].DeleteOnTermination]'
aws ec2 describe-addresses \
--filters Name=instance-id,Values=i-0123456789abcdef0 \
--query 'Addresses[].[PublicIp,AllocationId]'

Three conditions a stopped instance must meet before it is flagged

  1. The instance status ZopNight last saw is stopped.
  2. The stop is proven to be at least 30 days old, either from ZopNight’s own record of when the instance changed to stopped or from the stop time AWS reports for the instance.
  3. At least one sibling resource with a real price is linked to it: an EBS volume attached to the instance, or an Elastic IP associated with it.

When a stopped instance is not flagged

A CPUUtilization datapoint measures CPU time spent running the instance, so if CloudWatch holds any CPU activity for it inside the last 30 days, the “stopped” snapshot is treated as stale and the rule raises no finding. Someone probably started the machine after ZopNight last checked it.

With no recorded stop time, the 30-day window is unproven and the rule waits until ZopNight’s own record covers 30 days. An Elastic IP attached to a standalone network interface rather than the instance is not linked to it, and an address that is not associated at all belongs to Unassociated Elastic IP instead. When no volume or address resolves to a price, there is no finding; the rule never reports a $0 saving and never substitutes the compute rate of a box that is not running.

Pricing the saving from volumes and addresses left behind

Terminal window
saving = sum of attached EBS volume costs + sum of associated Elastic IP costs
cost after fix = 0

The description names only the components that actually contribute, so an instance with no Elastic IP gets no step about releasing one.

Retiring a long-stopped instance safely

  1. Check that no DNS record, target group, scheduler or runbook still expects the instance.
  2. Create an AMI or snapshot if the disk contents may be needed again.
  3. Terminate the instance.
  4. Delete the volumes that terminate left behind; data volumes attached after launch default to being preserved, per the termination behaviour table.
  5. Release the Elastic IP so the hourly address charge stops.

See it fire on your bill.

Connect an account read-only. The first findings land in minutes.

472 rule families across 353 resource types on 22 platforms. Every threshold, metric, and IAM action is documented on these pages before you grant anything.

472 rule families documented
353 resource types covered
read-only default access level
Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console· Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console·