Skip to main content
emnode / learn
Cost

Delete unattached EBS volumes

An "available" EBS volume costs the same as an attached one — find what's lingering and snapshot-then-delete before the bill compounds.

13 min·10 sections·AWS

Last reviewed

Unattached EBS volumes: the basics

What does it mean for an EBS volume to be "available"?

An EBS volume has a State attribute that AWS uses to describe its attachment lifecycle: creating, in-use, available, deleting, deleted, error. The state most people misread is available — it sounds like a positive thing, almost like "ready and waiting," but it actually means the volume exists, is fully provisioned, and is not attached to any instance. AWS bills it identically to one that is attached.

Pricing is per GB-month, prorated to the second, regardless of whether bytes are being read or written. A 100 GB gp3 volume sitting in available costs roughly $8/month; a 100 GB gp2 about $10/month; io2 with provisioned IOPS can run an order of magnitude higher. The volume keeps billing until someone explicitly deletes it — there is no idle timeout, no automatic cleanup, and no warning email.

Wastage checks (EC2-012 "Unattached EBS Volumes") and AWS Cost Optimization Hub's "Delete unused EBS storage volume" recommendation both surface these resources directly. Findings range from $1.50/month for a forgotten 15 GB gp2 to over $100/month for a 1 TB encrypted database disk that was detached for a migration and never cleaned up. The dollars look small per volume; the count and the duration are what compound.

In this lesson you'll learn how unattached EBS volumes accumulate, how to tell which ones are safe to delete and which hold valuable data, and how to apply the snapshot-then-delete pattern that gives you a recovery path at roughly a fifth of the cost. You'll see the AWS CLI queries to inventory volumes by state and age, the Cost Optimization Hub recommendation shape for a deletion action, and the AWS Config rule that catches new offenders before they sit for months.

Fun fact

The DeleteOnTermination default that wasn't

Root EBS volumes default to DeleteOnTermination=true — when you terminate the instance, the root disk goes with it. Additional (non-root) volumes default to the opposite: DeleteOnTermination=false. The reasoning is conservative — AWS would rather charge you forever than auto-delete a database disk — but it means every team that mounts a data volume and later terminates the instance leaves an orphan behind. Audits routinely turn up volumes older than the engineers who created them.

Cleaning up an EBS graveyard

Nina inherits the AWS account for a healthcare imaging product. The finance team flags that EBS spend has crept up 30% year-over-year while instance count is flat. She suspects orphans.

She runs a Cost Optimization Hub query and gets back 47 unattached volume recommendations across three regions. The biggest is DB,ODP.xvdc-clear (vol-0f2b770bb6c5c84bc), 1 TB io1, savings of $106.72/month. Several smaller ones — a FUJIAWSPDICOM02-PROD-Dicom-DDrive-enc at $3.42/month, a handful of forgotten 15 GB gp2 boot disks at $1.50/month each.

Before deleting anything, Nina takes snapshots of every volume older than 30 days. Snapshots are incremental and compressed — typically 10-25% of the live volume's cost — so the recovery insurance for the whole fleet is cheaper than two months of carrying the originals.

First, inventory every volume currently in the available state, sorted by size to surface the big-ticket targets.

$ aws ec2 describe-volumes --filters Name=status,Values=available --query 'Volumes[].{Id:VolumeId,Size:Size,Type:VolumeType,Created:CreateTime,Tags:Tags[?Key==`Name`]|[0].Value}' --output table
---------------------------------------------------------------------------------------------------
| DescribeVolumes |
+----------------------+-----------------------+------+--------+------------------------------+
| Created | Id | Size | Type | Tags |
+----------------------+-----------------------+------+--------+------------------------------+
| 2024-08-12T14:22:01Z| vol-0f2b770bb6c5c84bc | 1000 | io1 | DB,ODP.xvdc-clear |
| 2025-01-04T09:11:47Z| vol-000efb2936b1832ce | 150 | gp3 | FUJIAWSPDICOM02-DDrive-enc |
| 2025-11-30T17:03:18Z| vol-07c4a92e3f1b8b021 | 15 | gp2 | null |
| 2025-12-14T08:47:55Z| vol-04dd2581e6a7b59c2 | 30 | gp2 | test-instance-root |
+----------------------+-----------------------+------+--------+------------------------------+
# 47 volumes total, $312/mo bleed — the io1 alone is a third of it.

Sorted inventory. Volumes with null tags and dates over a year old are the obvious candidates.

Now pull the Cost Optimization Hub recommendation for the largest offender — it does the math and confirms the deletion is low-risk.

$ aws cost-optimization-hub get-recommendation --recommendation-id <id-for-vol-0f2b770bb6c5c84bc>
{
"recommendationId": "7a4f9c1e-...",
"actionType": "Delete",
"currentResourceType": "EbsVolume",
"resourceArn": "arn:aws:ec2:eu-west-1:123456789012:volume/vol-0f2b770bb6c5c84bc",
"estimatedMonthlySavings": 106.72,
"estimatedMonthlyCost": 106.72,
"estimatedSavingsPercentage": 100.0,
"implementationEffort": "VeryLow",
"restartNeeded": false,
"rollbackPossible": false,
"currentResourceDetails": { "ebsVolume": { "configuration": { "volumeType": "io1", "sizeInGigaBytes": 1000, "attachmentState": "detached" } } }
}
# rollbackPossible=false — snapshot before you act, or the data is gone for good.

Cost Optimization Hub confirms 100% savings (the volume contributes only cost, no value) but flags that deletion is irreversible.

Unattached volumes under the hooddeep dive

An EBS volume is a network-attached block device backed by replicated storage in the AZ where it was created. When you detach it from an instance — manually or as a side-effect of terminating that instance with DeleteOnTermination=false — the storage stays exactly where it was. No bytes are moved, no replicas torn down. AWS keeps billing at the same per-GB-month rate because the underlying capacity is still reserved on your behalf.

Volumes also retain metadata that hints at their origin. CreateTime tells you how long they've been around; the Attachments field is empty for unattached volumes but the volume's tags often still carry the name of the terminated instance, an AMI block-device-mapping reference, or a CloudFormation stack tag pointing at a stack that was deleted years ago. CloudWatch's VolumeReadOps and VolumeWriteOps metrics are usually zero for detached volumes — a clean signal that nothing is touching them — but check both within the last 14 days before deleting, since occasionally volumes get used by Lambda or one-off attach/detach scripts.

Snapshots are the safety net. A snapshot is an incremental, point-in-time copy stored in S3 (via the EBS snapshot service); only the changed blocks since the last snapshot are billed. Pricing is roughly $0.05/GB-month for snapshot data — so a 1 TB volume that snapshots to a 600 GB compressed footprint costs about $30/month to keep as insurance vs. ~$125/month to keep live. You can restore it to a new volume in any AZ in the same region with one API call.

# Find the last write timestamp for a volume — confirm it's truly idle.
aws cloudwatch get-metric-statistics \
  --namespace AWS/EBS \
  --metric-name VolumeWriteOps \
  --dimensions Name=VolumeId,Value=vol-0f2b770bb6c5c84bc \
  --start-time $(date -u -d '14 days ago' +%FT%TZ) \
  --end-time $(date -u +%FT%TZ) \
  --period 86400 \
  --statistics Sum

# Inspect the AMI block device mappings to see if any AMIs reference this volume's snapshots.
aws ec2 describe-images --owners self \
  --query 'Images[].BlockDeviceMappings[?Ebs.SnapshotId!=`null`].Ebs.SnapshotId' \
  --output text

What is the impact of leaving unattached volumes lying around?

The direct cost is straightforward and easy to underestimate. A single 15 GB gp2 boot disk left behind from a test instance costs about $1.50/month — call it $18/year. Multiply that across an organization with three regions, ten teams, and five years of accumulated cruft and the same trivial figure becomes tens of thousands of dollars annually. The Fuji healthcare example in our data set had a single 150 GB encrypted disk at $3.42/month sitting next to a 1 TB io1 at $106.72/month — a $110/month bleed from two volumes nobody was using.

The second-order impact is encryption and compliance drift. Volumes that were created before your account turned on default encryption stay unencrypted forever — they're not retroactively re-keyed. If those volumes end up restored or attached during an incident response, you've reintroduced an unencrypted disk into a fleet you thought was fully encrypted. The same applies to KMS key rotation: orphan volumes keep references to keys you may have wanted to schedule for deletion, blocking cleanup of old key material.

Snapshot sprawl is the third effect. Every orphan volume tends to have at least one orphan snapshot — and EBS snapshots also bill per GB-month. A common audit finding is a fleet with $300/month of unattached volumes carrying $200/month of stale snapshots behind them. The cleanup loop has to cover both: snapshot the volume for safety, then delete the volume, then later prune the snapshots that are older than your retention policy.

Operationally, large numbers of available volumes also slow down the EC2 console and clutter every describe-volumes call your tooling makes. Drift detection tools spend cycles reconciling resources nobody owns, and on-call engineers see them as noise during incident triage.

How do you clean up safely?

Cleaning up unattached EBS volumes is a four-step loop. Skip any step and you either delete data you needed or leave the next generation of orphans growing behind you.

1. Inventory with confidence

Pull every volume in State=available per region, joined with CreateTime, Tags, and the last VolumeWriteOps timestamp from CloudWatch. Filter the list to anything older than 30 days with no writes in the last 14 — that's your safe-to-act bucket. Cross-check against AMI block device mappings: any volume whose snapshots are referenced by an active AMI deserves a closer look before deletion.

2. Snapshot before you delete

Take a snapshot of every volume you intend to delete — tag the snapshot with the volume's original name, the deletion ticket ID, and a retention date. Snapshots cost roughly 10-25% of the live volume per month. Keep them for 30-90 days depending on your data classification policy, then prune. This is the cheapest insurance you'll ever buy against "the email we sent last quarter said it was safe to delete."

3. Delete in batches with a guardrail

Cost Optimization Hub exposes a Delete EbsVolume action with restartNeeded=false and rollbackPossible=false — meaning the deletion is non-disruptive to running infrastructure but cannot be undone. Use aws ec2 delete-volume in a loop, but add a guardrail: refuse to delete anything created in the last 7 days, anything with a tag matching do-not-delete, or anything still showing CloudWatch writes. Log each deletion with the volume ID, size, and pre-deletion snapshot ID.

4. Prevent recurrence

Enable the AWS Config rule ec2-volume-inuse-check to flag any new unattached volume as soon as it appears. In your launch templates and CloudFormation stacks, set DeleteOnTermination=true on non-root volumes wherever the data isn't stateful — most ephemeral disks should auto-delete with the instance. For stateful disks, tag them clearly with Lifecycle=Stateful and a clear owner so a future cleanup script knows to skip them. Pair the Config rule with an EventBridge rule that posts to Slack whenever a new orphan appears, so the bleed never restarts.

# Snapshot-then-delete loop with safety filters.
VOLUMES=$(aws ec2 describe-volumes \
  --filters Name=status,Values=available \
  --query 'Volumes[?CreateTime<=`'$(date -u -d '30 days ago' +%Y-%m-%d)'`].[VolumeId,Size,VolumeType]' \
  --output text)

while read -r VOL_ID SIZE TYPE; do
  # Skip anything tagged do-not-delete.
  if aws ec2 describe-tags --filters Name=resource-id,Values=$VOL_ID Name=key,Values=do-not-delete --query 'Tags[0]' --output text | grep -qv None; then
    echo "Skipping $VOL_ID (do-not-delete tag)"; continue
  fi

  SNAP_ID=$(aws ec2 create-snapshot \
    --volume-id $VOL_ID \
    --description "Pre-delete snapshot of $VOL_ID" \
    --tag-specifications "ResourceType=snapshot,Tags=[{Key=Lifecycle,Value=PreDelete},{Key=SourceVolume,Value=$VOL_ID}]" \
    --query SnapshotId --output text)

  echo "$VOL_ID ($SIZE GB $TYPE) snapshotted as $SNAP_ID — deleting volume."
  aws ec2 delete-volume --volume-id $VOL_ID
done <<< "$VOLUMES"

Quick quiz

Question 1 of 5

An EBS volume has been in the available state for 18 months, has no VolumeWriteOps in the last 14 days, no do-not-delete tag, and Cost Optimization Hub flags it as Delete EbsVolume with restartNeeded=false and rollbackPossible=false. What's the right next step?

You've completed Delete unattached EBS volumes. You now know why available is the most expensive word in the EBS state machine, how to inventory orphans honestly, and how the snapshot-then-delete loop gives you a recovery path at a fraction of the storage cost. Next time the bill creeps up while your instance count holds flat, you'll have a four-step loop — inventory, snapshot, delete, prevent — ready to run.

Back to the library