Back to blog
AWSBest PracticesCloud SecurityCost OptimizationStorage

Unattached EBS Volumes: Stop Paying for Storage You Are Not Using

Learn why unattached EBS volumes waste money and risk data exposure, plus CLI fixes, Terraform settings, and policy-as-code to prevent them.

TL;DR

This check flags EBS volumes sitting in the available state, meaning they are detached from any instance but still billed every hour. Snapshot anything worth keeping, then delete the volume to stop paying for storage you are not using.

Detached EBS volumes are one of the most common sources of silent AWS waste. Every volume you create keeps billing whether or not it is attached to a running instance. When an EC2 instance is terminated, its root volume usually goes away, but additional data volumes and volumes created manually often linger in the available state. Nobody notices until the storage line on the bill creeps up month after month.

The Unattached EBS Volume check (ebs_unused) in the ebs_checks module looks across your AWS account for volumes that have no attachment and reports them so you can decide whether to keep, snapshot, or delete them.


What this check detects

EBS volumes have a lifecycle state. The ones you care about here are:

  • in-use — attached to an instance and doing its job.
  • available — fully provisioned, fully billed, attached to nothing.

This check queries the EC2 API for every volume where State equals available. A volume in that state is reserved capacity on AWS hardware that you pay for at the standard per-GB-month rate for its type (gp3, gp2, io2, st1, and so on). Provisioned IOPS volumes are worse, since you pay for the IOPS too.

Note: An available volume is not the same as a deleted one. The data is still intact and the volume can be reattached at any time. That is exactly why these volumes accumulate, people leave them around "just in case" and forget about them.


Why it matters

The headline problem is cost, but there are a few angles worth understanding.

Direct waste on your bill

A single 500 GB gp3 volume runs about $40 per month doing absolutely nothing. Multiply that across dozens of forgotten volumes from old test instances, failed deployments, and decommissioned services, and you have a meaningful recurring charge for empty storage. Provisioned IOPS volumes can be several times more expensive.

Hidden data exposure

Detached volumes often hold real data: old database files, application logs, snapshots of customer information. An unencrypted volume that nobody is tracking is a governance gap. If it gets reattached to the wrong instance, or restored later by someone without context, you may be exposing data you assumed was gone.

Warning: Deleting a volume is permanent. Before you remove anything, confirm whether the data has compliance or retention requirements. "We do not need this" is a decision someone should sign off on, not an assumption.

Operational noise

Hundreds of orphaned volumes clutter the console, slow down inventory exports, and make it harder to spot the volumes that actually matter. Clean accounts are easier to audit and reason about.


How to fix it

The safe pattern is the same every time: identify, decide, snapshot if needed, then delete.

Step 1: List the unattached volumes

aws ec2 describe-volumes \
  --filters Name=status,Values=available \
  --query 'Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,AZ:AvailabilityZone,Created:CreateTime,Encrypted:Encrypted}' \
  --output table

This gives you a clean table of every available volume with its size, type, and creation date. Old creation dates are a strong signal that a volume is genuinely abandoned.

Step 2: Check the tags before you touch anything

aws ec2 describe-volumes \
  --volume-ids vol-0abc123def4567890 \
  --query 'Volumes[0].Tags'

Tags often tell you which team, environment, or project owned the volume. If there is an owner, ask before deleting. If there are no tags at all, that is itself a sign of a forgotten resource.

Step 3: Snapshot if the data might matter

A snapshot is much cheaper than the live volume (it is incremental and stored in S3), so when in doubt, snapshot first and delete the volume.

aws ec2 create-snapshot \
  --volume-id vol-0abc123def4567890 \
  --description "Backup before deleting unattached volume - $(date +%F)" \
  --tag-specifications 'ResourceType=snapshot,Tags=[{Key=Reason,Value=cleanup-archive}]'

Wait for the snapshot to reach completed before deleting the source:

aws ec2 wait snapshot-completed --snapshot-ids snap-0fedcba9876543210

Step 4: Delete the volume

Danger: This permanently destroys the volume and its data. There is no recovery once it runs, unless you took a snapshot first. Double-check the volume ID and confirm it is truly unused.

aws ec2 delete-volume --volume-id vol-0abc123def4567890

Doing it in bulk (carefully)

If you have audited a list and are confident, you can iterate. Keep a snapshot step in the loop unless you are certain the data is disposable.

for vol in $(aws ec2 describe-volumes \
  --filters Name=status,Values=available \
  --query 'Volumes[*].VolumeId' --output text); do
    echo "Snapshotting and deleting $vol"
    snap=$(aws ec2 create-snapshot --volume-id "$vol" \
      --description "cleanup $vol" --query SnapshotId --output text)
    aws ec2 wait snapshot-completed --snapshot-ids "$snap"
    aws ec2 delete-volume --volume-id "$vol"
done

Tip: Add a tag filter like Name=tag:Environment,Values=dev to the describe call so you only ever sweep non-production accounts in bulk. Run production cleanups by hand.


How to prevent it from happening again

Cleaning up once is fine. The goal is to stop new orphans from piling up.

Delete volumes on instance termination

The most common source of orphans is data volumes that outlive their instance. Set DeleteOnTermination to true for volumes that do not need to survive the instance. In Terraform:

resource "aws_instance" "app" {
  ami           = "ami-0abc123"
  instance_type = "t3.medium"

  root_block_device {
    delete_on_termination = true
  }

  ebs_block_device {
    device_name           = "/dev/sdf"
    volume_size           = 100
    volume_type           = "gp3"
    delete_on_termination = true
  }
}

Automate detection with a scheduled sweep

A small Lambda on an EventBridge schedule can find volumes that have been available for more than N days and either tag them for review or alert a channel. The age check matters, you do not want to flag a volume that was detached two minutes ago during a maintenance window.

Gate it in CI/CD with policy-as-code

Catch missing termination settings before they reach production. An OPA/Conftest rule against a Terraform plan:

# policy/ebs.rego
package main

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_instance"
  block := resource.change.after.ebs_block_device[_]
  block.delete_on_termination == false
  msg := sprintf("EBS block device on %s must set delete_on_termination", [resource.address])
}

Tip: Pair this with a continuous check in Lensix so you have both a preventive gate (CI) and a detective control (ongoing scanning). The gate stops the obvious mistakes, the scan catches the volumes created outside of IaC.


Best practices

  • Tag everything. An Owner and Environment tag on every volume turns a mystery resource into a quick decision.
  • Encrypt by default. Enable EBS encryption by default at the account level so even forgotten volumes are not a plaintext data risk.
    aws ec2 enable-ebs-encryption-by-default
  • Prefer snapshots for archival. If you want to keep data "just in case," snapshot it and delete the live volume. Snapshots cost a fraction of an idle volume.
  • Set a retention window. Agree on a rule like "available for 30 days gets snapshotted and deleted" and automate it. Predictable cleanup beats one-off purges.
  • Review snapshot sprawl too. Cleaning volumes can quietly create a pile of snapshots. Apply a lifecycle policy to those with Amazon Data Lifecycle Manager so the savings stick.

Unattached EBS volumes are easy money left on the table and an avoidable data risk. A quick audit plus a couple of preventive controls keeps your account lean and your bill honest.