EC2 instances stopped 30+ days that still bill for volumes and Elastic IPs
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
| Field | Value |
|---|---|
| Rule IDs | RC-001 |
| Category | idle |
| Severity | medium |
| Metric | CPUUtilization |
| Threshold | 30 days stopped |
| Evaluation window | 30d |
| Source | ZopNight |
| Permissions used | ec2:DescribeInstances · ec2:DescribeVolumes · ec2:DescribeAddresses · cloudwatch:GetMetricData |
Where it applies
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:
aws ec2 describe-instances \ --filters Name=instance-state-name,Values=stopped \ --query 'Reservations[].Instances[].[InstanceId,StateTransitionReason]' \ --output tableFor each instance stopped longer than a month, list what is still attached and whether it would survive termination:
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
- The instance status ZopNight last saw is
stopped. - The stop is proven to be at least 30 days old, either from ZopNight’s own record of when the
instance changed to
stoppedor from the stop time AWS reports for the instance. - 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
saving = sum of attached EBS volume costs + sum of associated Elastic IP costscost after fix = 0The 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
- Check that no DNS record, target group, scheduler or runbook still expects the instance.
- Create an AMI or snapshot if the disk contents may be needed again.
- Terminate the instance.
- Delete the volumes that terminate left behind; data volumes attached after launch default to being preserved, per the termination behaviour table.
- Release the Elastic IP so the hourly address charge stops.