A stopped EC2 instance still pays full price for its attached EBS volumes even though you get zero compute in return. Audit stopped instances, snapshot anything worth keeping, then delete the orphaned volumes or terminate the instance entirely.
When you stop an EC2 instance, AWS halts the billing for compute hours almost immediately. That part is easy to remember. What catches a lot of teams off guard is the storage bill that keeps running quietly in the background. Every EBS volume attached to that stopped instance continues to bill at its full provisioned rate, hour after hour, whether the instance ever boots again or not.
This check flags exactly that situation: an instance in the stopped state that still has one or more EBS volumes attached. It is one of the most common sources of slow cloud cost creep, and it shows up in almost every AWS account that has been running for more than a year.
What this check detects
The ec2_stopped_with_volumes check scans your EC2 instances and reports any instance whose state is stopped while it still has EBS volumes in the attached state. The check correlates two pieces of data: the instance lifecycle state and its block device mappings.
A typical finding looks like an instance that someone stopped months ago "just in case we need it again" with a 200 GB gp3 root volume and a couple of secondary data volumes still riding along. The compute meter stopped. The storage meter did not.
Note: EBS pricing is based on provisioned capacity, not used capacity. A 500 GB volume that is 5% full still bills for the full 500 GB. Stopping the instance changes none of this.
Why it matters
The impact here is almost entirely financial, but that does not make it minor. The cost is invisible in the sense that nothing breaks and no alarm fires. It just accumulates.
The math adds up faster than you think
A single gp3 volume runs about $0.08 per GB-month in most regions. Consider a forgotten instance with a 100 GB root volume and a 500 GB data volume:
- 600 GB total at $0.08/GB-month = $48 per month
- That is $576 per year for an instance doing nothing
- Multiply across a fleet of 30 or 40 stopped instances and you are looking at real money
Provisioned IOPS volumes (io1/io2) are far worse, since you also pay for the provisioned IOPS regardless of whether anything is reading or writing.
It hides other problems too
A pile of stopped instances with attached volumes is usually a symptom of weak lifecycle hygiene. The same accounts tend to accumulate unattached volumes, old snapshots, and stale AMIs. Each forgotten resource is also a small expansion of your attack surface. An EBS volume left attached to a dormant instance may hold credentials, customer data, or application secrets that nobody is watching anymore.
Warning: Do not assume a stopped instance is safe to ignore from a security standpoint. The data on its volumes still exists, still needs to meet your encryption and retention requirements, and is still subject to compliance scope.
How to fix it
The right fix depends on whether you still need the instance, the data, or neither. Work through this in order.
Step 1: Identify the stopped instances and their volumes
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=stopped" \
--query "Reservations[].Instances[].{ID:InstanceId,Name:Tags[?Key=='Name']|[0].Value,Volumes:BlockDeviceMappings[].Ebs.VolumeId}" \
--output table
This gives you each stopped instance, its Name tag, and the volume IDs attached to it. Pull the volume sizes so you can estimate the savings:
aws ec2 describe-volumes \
--filters "Name=attachment.status,Values=attached" \
--query "Volumes[].{VolumeId:VolumeId,Size:Size,Type:VolumeType,State:State,Instance:Attachments[0].InstanceId}" \
--output table
Step 2: Decide what each instance actually needs
For every stopped instance, pick one of three paths:
- Still needed soon — leave it, but tag it with an owner and a review date so it does not become orphaned.
- Data worth keeping, compute not needed — snapshot the volumes, then terminate the instance and delete the volumes.
- Neither needed — terminate and clean up.
Step 3: Snapshot before you delete anything
A snapshot is cheaper than a live volume because it is incremental and compressed, and it sits in S3-backed storage. This is the safe way to retain data without paying for an idle volume.
aws ec2 create-snapshot \
--volume-id vol-0abc123def456789 \
--description "Pre-deletion backup of stopped instance i-0123456789abcdef0" \
--tag-specifications 'ResourceType=snapshot,Tags=[{Key=Purpose,Value=archive},{Key=SourceInstance,Value=i-0123456789abcdef0}]'
Wait for the snapshot to reach the completed state before going further:
aws ec2 describe-snapshots \
--snapshot-ids snap-0abc123def456789 \
--query "Snapshots[0].State" --output text
Danger: The next steps terminate instances and delete volumes. These actions are irreversible. Confirm your snapshot is in the completed state and verify the instance and volume IDs before running anything below.
Step 4: Terminate the instance
If the root volume has DeleteOnTermination set to true, terminating the instance removes it automatically. Check that flag first so you know what will happen:
aws ec2 describe-instances \
--instance-ids i-0123456789abcdef0 \
--query "Reservations[].Instances[].BlockDeviceMappings[].{Device:DeviceName,Volume:Ebs.VolumeId,DeleteOnTerm:Ebs.DeleteOnTermination}" \
--output table
aws ec2 terminate-instances --instance-ids i-0123456789abcdef0
Step 5: Delete any volumes left behind
Secondary data volumes usually have DeleteOnTermination set to false, so they survive termination and become unattached. Delete them once you have confirmed the snapshot:
aws ec2 delete-volume --volume-id vol-0abc123def456789
Tip: If you might restart the workload later but not soon, you do not have to keep the volume live. Snapshot it, delete the volume, and recreate the volume from the snapshot when you actually need it. You only pay snapshot storage rates in the meantime.
How to prevent it from happening again
Manual cleanup is a one-time win. Prevention keeps the problem from coming back.
Set DeleteOnTermination intentionally in IaC
Define this behavior at provisioning time so volumes are not left orphaned by surprise. In Terraform:
resource "aws_instance" "app" {
ami = "ami-0abc123def456789"
instance_type = "t3.medium"
root_block_device {
volume_size = 30
volume_type = "gp3"
delete_on_termination = true
encrypted = true
}
ebs_block_device {
device_name = "/dev/sdf"
volume_size = 100
volume_type = "gp3"
delete_on_termination = false # keep data volume by choice, not by accident
encrypted = true
}
tags = {
Owner = "platform-team"
Environment = "staging"
}
}
Require ownership and lifecycle tags
Most orphaned instances are orphaned because nobody knows who owns them. Enforce an Owner tag and a ReviewDate tag through a Service Control Policy or an AWS Config rule, and you remove the ambiguity that lets resources rot.
Schedule automated cleanup of long-stopped instances
An EventBridge scheduled rule plus a small Lambda can find instances that have been stopped past a threshold and notify the owner, or auto-snapshot-and-terminate in lower environments. A starting query for the Lambda:
# Find instances stopped for more than 30 days
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=stopped" \
--query "Reservations[].Instances[?StateTransitionReason!=''].[InstanceId,StateTransitionReason]" \
--output text
Note: The StateTransitionReason field includes the timestamp of when the instance was stopped, for example User initiated (2024-03-12 09:41:00 GMT). Parse that date to measure how long an instance has been idle.
Gate it in CI/CD with policy-as-code
Catch risky patterns before they ship. An OPA/Conftest policy can flag instances provisioned without an owner tag or with unencrypted volumes, both of which correlate with the kinds of resources that get abandoned later.
package terraform.ec2
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_instance"
not resource.change.after.tags.Owner
msg := sprintf("EC2 instance %s must have an Owner tag", [resource.address])
}
Best practices
- Snapshot, then delete. Treat snapshots as your archive tier. They are cheaper than live volumes and let you safely reclaim storage you are not using.
- Tag everything with an owner. Untagged resources are the ones that survive for years because nobody feels responsible for them.
- Review stopped instances on a schedule. A monthly sweep of instances stopped longer than 30 days catches drift before it becomes expensive.
- Encrypt EBS by default. Enable account-level default EBS encryption so any volume, even on a forgotten instance, meets your data-at-rest requirements.
- Watch the related findings. Stopped instances with volumes usually travel alongside unattached volumes and stale snapshots. Clean up the whole family, not just one finding.
- Prefer ephemeral patterns where you can. If a workload can be rebuilt from an AMI or IaC on demand, you rarely need to keep a stopped instance around at all.
The fix here is rarely complicated. The hard part is having the visibility to know which instances are stopped, how long they have been that way, and what they are quietly costing you each month. Get that loop running, and this entire class of waste stops accumulating.

