Skip to main content
idle · aws

Stopped EC2 instances that still bill for storage

resource types
1
rule IDs covered
1
severity
medium

What does ZopNight detect here?

Stopping an EC2 instance ends the compute charge, but its attached EBS volumes and any associated Elastic IP keep billing every month. ZopNight flags instances stopped for 30 days or more where those attachments still cost money, and prices the exact residual rather than estimating it.

Signal and threshold

How ZopNight evaluates Stopped EC2 instances that still bill for storage.
Field Value
Rule IDsRC-001
Categoryidle
Severitymedium
MetricCPUUtilization
Threshold30 days stopped
Evaluation window30d
Sourceidle_ec2.go
Permissions usedec2:DescribeInstances · ec2:DescribeVolumes · ec2:DescribeAddresses · cloudwatch:GetMetricData

Why a stopped instance still costs money

Stopping an instance releases the CPU and memory, and the instance-hour charge ends the moment the state changes. Everything attached to it keeps billing. A 500 GB gp3 root volume costs the same whether the instance runs or has been off since last quarter, and since February 2024 AWS charges for every allocated public IPv4 address, so an Elastic IP left associated with a stopped instance bills around $3.60 a month for nothing.

That is the gap this rule closes. It does not tell you an instance looks under-used. It tells you an instance is already off, has been for a month, and is still generating a bill.

How do I find these myself?

List the stopped instances and read the transition reason, which carries the timestamp:

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

Then check what is still attached to any instance older than 30 days:

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

Every volume returned is billing at full rate. Repeat with describe-addresses for the Elastic IP.

What has to be true before it fires

Three gates, all of which must pass:

  1. The instance status is stopped.
  2. The stop is provably at least 30 days old, established from recorded state history, or from an orphanSince timestamp the discoverer parses out of the instance’s StateTransitionReason field.
  3. At least one attached EBS volume or associated Elastic IP resolves to a real price.

When it deliberately says nothing

The rule abstains more often than it fires, which is the point.

CPUUtilization is one of the series EC2 sends to CloudWatch only while an instance runs. So if any CPU datapoint exists inside the 30-day window, the “stopped” snapshot must be stale: someone started the instance after discovery ran. The rule returns nothing rather than reporting a saving on a machine that is currently serving traffic.

It also abstains when an instance was stopped 45 days ago but restarted 3 days ago: the most recent start is tracked separately, so a restart inside the window disqualifies it.

If neither an EBS volume nor an Elastic IP can be priced, the rule emits nothing at all. It never reports a $0 finding, and it never invents a compute rate for a box that is not running.

One limitation stated plainly: a batch host with an on/off cadence longer than the 30-day lookback shows no recent CPU and can still be flagged. Cadences longer than the window are out of scope.

What the saving actually is

The reported figure is the sum of the residual attachments, not a percentage of the instance’s old bill:

Terminal window
SavingsUSD = Σ attached EBS volumes + Σ associated Elastic IPs
OptimizedCostUSD = 0

Terminating the instance, deleting the volumes and releasing the address reclaims all of it.

How to fix it

  1. Confirm nothing depends on the instance: check DNS records, load balancer target groups and any automation that starts it on a schedule.
  2. Take an AMI if the disk contents still matter. This is the step people skip and regret.
  3. Terminate the instance.
  4. Delete the detached EBS volumes. Termination does not remove volumes whose DeleteOnTermination flag is false, which is common for data volumes.
  5. Release the Elastic IP. An allocated address that is not associated with a running instance keeps billing.

See it fire on your bill.

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

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

417 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·