Omni FinOps

How to find and delete unattached EBS volumes

Unattached EBS volumes and Google Cloud persistent disks bill every month for nothing. How to list them, check them, snapshot them and delete them.

Mateo VargasUpdated · 5 min read
Contents
  1. What an unattached volume costs
  2. Why they pile up
  3. Find them on AWS
  4. Check before you delete
  5. Snapshot, then delete
  6. Google Cloud: persistent disks with no users
  7. Keep them from coming back

In short

  • A volume in the available state is attached to nothing, and still bills for every GiB.
  • They’re usually left behind when an instance was terminated without deleting its volumes.
  • Snapshot first if you’re unsure: a snapshot usually costs less than the volume and lets you restore it.

An EBS volume in the available state is attached to no instance. Nothing reads it and nothing writes to it, but AWS bills every provisioned GiB, every month, for as long as it exists. Google Cloud does the same with persistent disks that no VM uses.

They’re easy to miss because they never show up as a spike. This guide shows how to list them on AWS and Google Cloud, check that nobody needs them, keep a cheap copy, and delete them, then how to stop new ones from piling up.

What an unattached volume costs

You pay for the size you provisioned, not the data on it. In us-east-1, as of October 2026 (EBS pricing):

What Price A 500 GiB volume, per month
gp3 volume $0.08 per GB-month $40.00
gp2 volume $0.10 per GB-month $50.00
Standard snapshot $0.05 per GB-month $25.00 at most
Archived snapshot $0.0125 per GB-month $6.25

A snapshot stores only the blocks that were written, so it usually costs less than the figure above. That gap is why snapshot-then-delete is the safe default: you keep the data for a fraction of the volume’s price.

On Google Cloud, in us-central1 as of October 2026 (disk pricing), a pd-standard disk costs about $0.04 per GiB-month, pd-balanced about $0.10 and pd-ssd about $0.17. A 500 GiB pd-balanced disk nobody uses costs about $50 a month.

Why they pile up

  • Terminating an instance keeps its extra volumes. Root volumes are deleted with the instance by default. Volumes you attach after launch keep DeleteOnTermination off, so they survive the instance and turn available.
  • Manual detaches. A volume detached during a migration, a resize or a debugging session, then forgotten.
  • One-off restores. A volume created from a snapshot to recover a file, never cleaned up.
  • Infrastructure code. A stack that destroyed the instance but not a volume it created separately.

Find them on AWS

List every available volume, region by region:

for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  echo "== $r"
  aws ec2 describe-volumes --region "$r" --filters Name=status,Values=available \
    --query 'Volumes[].[VolumeId,Size,VolumeType,CreateTime,Tags[?Key==`Name`]|[0].Value]' \
    --output table
done

The API doesn’t say when a volume was detached. If the detach is recent enough to be in CloudTrail’s event history, this shows who did it and when:

aws cloudtrail lookup-events --region us-east-1 \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue=vol-07c2d81a5e \
  --query 'Events[].[EventTime,EventName,Username]' --output table

Its tags and the snapshot it came from often name the owner or the project:

aws ec2 describe-volumes --region us-east-1 --volume-ids vol-07c2d81a5e \
  --query 'Volumes[0].[SnapshotId,CreateTime,Encrypted,Tags]' --output json

Check before you delete

An available volume is attached to nothing, including stopped instances: a stopped instance keeps its volumes attached and in-use. So the questions are about intent, not state.

  • Is someone about to reattach it? A blue/green switch, a disaster recovery copy or a database restore in progress. Ask the owner that the tags or CloudTrail point to.
  • Is it the only copy of something? Then the snapshot below is not optional.
  • Is it encrypted with a customer managed KMS key? The snapshot keeps that encryption, so keep the key as long as you keep the snapshot.

If nobody claims it within a week, it is waste.

Snapshot, then delete

Take a final snapshot, wait for it to complete, then delete the volume. The && between the steps stops the chain if any of them fails, so the volume is only deleted once its copy exists:

VOL=vol-07c2d81a5e
SNAP=$(aws ec2 create-snapshot --region us-east-1 --volume-id "$VOL" \
  --description "Final copy of $VOL before deletion" \
  --tag-specifications "ResourceType=snapshot,Tags=[{Key=deleted-volume,Value=$VOL}]" \
  --query SnapshotId --output text) &&
aws ec2 wait snapshot-completed --region us-east-1 --snapshot-ids "$SNAP" &&
aws ec2 delete-volume --region us-east-1 --volume-id "$VOL"

On a large volume the waiter can give up before the snapshot finishes, and then nothing is deleted. Run the wait and delete-volume lines again once the snapshot shows completed.

If you might need the data for months but probably won’t, move the snapshot to the archive tier. It costs $0.0125 per GB-month instead of $0.05, with a 90-day minimum and $0.03 per GB to restore (EBS pricing):

aws ec2 modify-snapshot-tier --region us-east-1 --snapshot-id "$SNAP" --storage-tier archive

To bring the volume back from a standard snapshot, create a new one from it in any Availability Zone of the region:

aws ec2 create-volume --region us-east-1 --snapshot-id "$SNAP" \
  --availability-zone us-east-1a --volume-type gp3

Decide now how long the safety copy lives, and delete it then. Otherwise it becomes the next guide: how to clean up old EBS snapshots.

Google Cloud: persistent disks with no users

A Compute Engine disk with an empty users list is attached to no VM. List them across your projects:

for p in $(gcloud projects list --format="value(projectId)"); do
  echo "== $p"
  gcloud compute disks list --project="$p" --filter="-users:*" \
    --format="table(name,zone.basename(),region.basename(),sizeGb,type.basename(),lastDetachTimestamp)"
done

Zonal disks show a zone and regional disks a region. lastDetachTimestamp tells you how long it has sat there.

Snapshot it, check the snapshot is ready, then delete the disk. As on AWS, && stops the chain if a step fails:

gcloud compute disks snapshot pd-build-cache --project=my-project --zone=us-central1-a \
  --snapshot-names=pd-build-cache-final &&
[ "$(gcloud compute snapshots describe pd-build-cache-final --project=my-project \
  --format='value(status)')" = READY ] &&
gcloud compute disks delete pd-build-cache --project=my-project --zone=us-central1-a --quiet

The disk is deleted only if the snapshot reports READY. gcloud compute snapshots create pd-build-cache-final --source-disk=pd-build-cache --source-disk-zone=us-central1-a does the same as the first command. A standard snapshot costs $0.05 per GB-month in us-central1 as of October 2026, and holds the data written to the disk, not its provisioned size. A full pd-standard disk can cost more to keep as a snapshot than as a disk, so for those, snapshot only what you’d really restore (disk pricing).

Keep them from coming back

  • Delete data volumes with their instance. Set DeleteOnTermination to true in launch templates and block device mappings for volumes that hold nothing worth keeping. On a running instance:
aws ec2 modify-instance-attribute --instance-id i-0a41f9c2e7b3 \
  --block-device-mappings '[{"DeviceName":"/dev/sdf","Ebs":{"DeleteOnTermination":true}}]'
  • Do the same on Google Cloud:
gcloud compute instances set-disk-auto-delete web-1 --zone=us-central1-a --disk=data-1 --auto-delete
  • Tag every volume with an owner in your infrastructure code, so the next available one has a name attached.
  • Automate it if you want AWS to act. Compute Optimizer automation rules can clean up unattached EBS volumes on a schedule once you turn them on.
  • Review on a schedule. A monthly pass with the commands above is enough for most accounts. Omni FinOps does it on every refresh, flagging each volume in the available state and each Google Cloud disk with no users, with its monthly cost and the snapshot-then-delete command.

If the volumes you find are attached but on gp2, the next saving is moving them to gp3.

Mateo Vargas writes Omni FinOps’ cost guides, from the same rules the product runs on every refresh. Prices are list prices in us-east-1 or us-central1 as of October 11, 2026; check your region’s pricing page.

More guides