GCP unattached disks: delete without paying twice
Published · OhChimp Engineering

In us-central1, an unattached Google Cloud Persistent Disk bills its full capacity rate every month it exists: $0.17 per GiB-month for pd-ssd, $0.10 for pd-balanced and $0.04 for pd-standard, so a forgotten 500 GiB pd-ssd disk costs $85.00 a month to hold data nobody mounts. Deleting it is one command. The safety snapshot most runbooks take first is billed too, and with default settings it can keep 49% of that $85.00 on the bill.
Why a detached disk costs exactly what an attached one does
Persistent Disk bills provisioned capacity, and the attachment has no part in the price. Google's Persistent Disk pricing page says you are billed for the entire disk space "regardless of how you use it, until you relinquish it." Detaching a disk changes where it is mounted and leaves the meter running.
Here are the list prices in us-central1 as of September 2026, converted from Google's hourly rates at 730 hours a month.
| Disk type, us-central1 | Zonal, per GiB-month | Regional, per GiB-month | 500 GiB zonal, per month |
|---|---|---|---|
| pd-standard | $0.04 | $0.08 | $20.00 |
| pd-balanced | $0.10 | $0.20 | $50.00 |
| pd-ssd | $0.17 | $0.34 | $85.00 |
Regional disks keep a synchronous copy in a second zone and cost exactly twice the zonal rate, so an orphaned regional pd-ssd disk of 500 GiB is $170.00 a month. In us-central1, pd-extreme lists at $0.125 per GiB-month plus $0.065 per provisioned IOPS-month, and both are charged on what you provision rather than on what a workload uses. The pricing page lists the first 30 GiB-month of pd-standard as free, which is why small HDD orphans can show up as zero.
Orphans pile up in three predictable ways. A VM gets deleted while a data disk created separately survives it. A migration builds a replacement disk and never deletes the original. A GKE PersistentVolume with a Retain reclaim policy outlives its cluster, and the disk it points at stays in the project with a pvc- name nobody recognizes.
The safety snapshot can cost more than the disk it replaces
Every sane deletion runbook says snapshot first, because Compute Engine has no recycle bin for disks and a deleted disk does not come back. Three details of snapshot billing decide whether that snapshot costs you pocket change or most of the saving.
The first is location. Without --storage-location, Google stores the snapshot in the multi-region closest to the source disk, so a us-central1 disk's snapshot lands in the us multi-region at $0.083 per GiB-month instead of the regional $0.05, and pays a network charge to move the data out of the region. Google's own snapshot settings page recommends a regional location unless you need the extra replication.
The second is the snapshot type. An archive snapshot stores at $0.019 per GiB-month regionally, but it bills a 90-day minimum and charges $0.019 per GiB to restore. A standard snapshot bills a one-hour minimum.
The third is what gets measured. Snapshots bill on the compressed bytes actually used on the disk, never the provisioned size, so the figures below are ceilings for a disk filled to capacity. A half-empty disk snapshots for roughly half, and compressible data for less.
Here is what a retained snapshot of a full 500 GiB disk costs in us-central1, as a share of the disk bill it replaces.
| Source disk, 500 GiB in us-central1 | Disk per month | Default us multi-region |
Regional standard | Regional archive |
|---|---|---|---|---|
| pd-ssd | $85.00 | $41.50 (49%) | $25.00 (29%) | $9.50 (11%) |
| pd-balanced | $50.00 | $41.50 (83%) | $25.00 (50%) | $9.50 (19%) |
| pd-standard | $20.00 | $41.50 (208%) | $25.00 (125%) | $9.50 (48%) |
On pd-standard, a standard snapshot's per-GiB rate is higher than the disk's, so a full snapshot kept indefinitely costs more than leaving the disk alone. The regional snapshot comes out cheaper only when the compressed used data is under 80% of the disk's provisioned size, and under 48% when the snapshot sits in the default multi-region.
Archive wins once you plan to keep the snapshot long enough to outrun its minimum. Three months at $0.019 is $0.057 per GiB, which a standard snapshot at $0.05 per GiB-month overtakes after about 35 days. Add one restore at $0.019 per GiB and the break-even moves to about 46 days. For a "just in case" snapshot you expect never to touch, archive is the one to keep. For a snapshot you might restore next week, standard is cheaper.
Restores carry a trap of their own. The gcloud compute disks create reference says "The default disk type is pd-standard," so a restore from snapshot with no --type brings a pd-ssd disk back as an HDD. The data returns and the performance class does not, which you tend to discover under production load.
Find your unattached disks by hand
An unattached disk has an empty users list. The -users:* filter matches disks where that key is undefined, and lastDetachTimestamp tells you how long each one has been sitting there.
gcloud compute disks list \
--filter="-users:*" \
--format="table(name,zone.basename(),region.basename(),type.basename(),sizeGb,lastAttachTimestamp,lastDetachTimestamp)"
A disk with no lastAttachTimestamp was never attached at all. Regional disks fill the region column and leave zone empty.
To price the list rather than eyeball it, pull JSON and multiply each disk by its class. sizeGb comes back as a string, and regional disks bill double.
gcloud compute disks list --filter="-users:*" --format=json | jq -r '
{"pd-standard":0.04,"pd-balanced":0.10,"pd-ssd":0.17,"pd-extreme":0.125} as $rate
| .[]
| (.type | split("/") | last) as $t
| (if .region then 2 else 1 end) as $copies
| [ .name,
((.zone // .region) | split("/") | last),
$t,
.sizeGb,
(.lastDetachTimestamp // "never attached"),
(($rate[$t] // 0) * $copies * (.sizeGb | tonumber) * 100 | round / 100)
] | @tsv' | sort -t$'\t' -k6 -rn
The last column is the monthly list price at us-central1 rates, largest first. It reads zero for Hyperdisk types, which price capacity and performance separately, so check those by hand. It also leaves out pd-extreme's IOPS charge.
That filter runs in one project. Cloud Asset Inventory finds disks across a whole organization, which tells you which projects to open:
gcloud asset search-all-resources \
--scope=organizations/ORG_ID \
--asset-types=compute.googleapis.com/Disk \
--format="table(name.basename(),location,project)"
Google's idle disk recommender flags the same disks with its own rule: a disk detached for at least 15 days, or created at least 15 days ago and never attached. It runs per zone.
for ZONE in $(gcloud compute zones list --format="value(name)"); do
gcloud recommender recommendations list \
--project=PROJECT_ID \
--location="$ZONE" \
--recommender=google.compute.disk.IdleResourceRecommender \
--format="table(name.basename(),description)" 2>/dev/null
done
The -users:* filter misses one expensive case. A disk attached to a stopped VM still lists that VM under users, and Google's stop and start documentation confirms a stopped instance keeps billing for "any resources that remain attached." List the stopped VMs and their disks separately:
gcloud compute instances list \
--filter="status=TERMINATED" \
--format="table(name,zone.basename(),lastStopTimestamp,disks[].source.basename().list())"
A VM stopped for months with a large pd-ssd disk on it is an orphan with an alibi.
Delete a disk without losing the way back
Before deleting anything, read the disk's description and labels. Disks provisioned by the GKE Persistent Disk CSI driver record the PersistentVolumeClaim they were created for in the description, and a claim that still exists in a live cluster means someone expects that data to come back.
gcloud compute disks describe DISK_NAME --zone=us-central1-a \
--format="yaml(description,labels,lastDetachTimestamp,sizeGb,type)"
Then take the snapshot you actually want to pay for. Pin the location to the disk's own region so it bills at the regional rate and never leaves the region:
gcloud compute snapshots create DISK_NAME-predelete \
--source-disk=DISK_NAME \
--source-disk-zone=us-central1-a \
--storage-location=us-central1 \
--snapshot-type=STANDARD
For a snapshot you expect to keep past about 35 days and hope never to restore, use --snapshot-type=ARCHIVE instead. Snapshot names share one namespace across the project, so a name that includes the disk and zone keeps two data-disk orphans in different zones from colliding.
Once the snapshot reports READY, delete the disk:
gcloud compute snapshots describe DISK_NAME-predelete --format="value(status)"
gcloud compute disks delete DISK_NAME --zone=us-central1-a --quiet
Rolling back
Recreate the disk from the snapshot, and name the original type every time:
gcloud compute disks create DISK_NAME \
--zone=us-central1-a \
--type=pd-ssd \
--source-snapshot=DISK_NAME-predelete
For a regional disk, swap --zone for --region and add --replica-zones with both zones, or the restore comes back zonal with one copy of your data. Once nobody has asked for the data within your retention window, delete the snapshot as well. Until you do, the snapshot line is the part of the saving you have not collected.
gcloud compute snapshots delete DISK_NAME-predelete --quiet
To stop producing new orphans, check auto-delete on the disks of the VMs you keep. Boot disks default to auto-delete. Data disks depend on how they were attached, so check the flag rather than assume it, because a disk with auto-delete off outlives its instance.
gcloud compute instances describe VM_NAME --zone=us-central1-a \
--format="table(disks[].deviceName,disks[].autoDelete)"
gcloud compute instances set-disk-auto-delete VM_NAME \
--zone=us-central1-a \
--device-name=DEVICE_NAME \
--auto-delete
A quick decision tree
graph TD
A[Disk with no users] --> B{GKE PVC or labeled owner still active?}
B -- Yes --> C[Ask the owner, leave it]
B -- No --> D{Could anyone need this data again?}
D -- No --> E[Delete the disk, no snapshot]
D -- Yes --> F{Keep the backup longer than about 35 days?}
F -- Yes --> G[Archive snapshot pinned to the disk's region]
F -- No --> H[Standard snapshot pinned to the disk's region]
G --> I[Delete the disk]
H --> I
I --> J[Restore with --type set to the original class]
Related reading
- GCP pd-ssd vs pd-balanced: cut disk costs 40% covers the disks that are attached and doing work, where the saving is a cheaper class rather than a deletion.
- Azure VM stopped vs deallocated: how to deallocate is the Azure version of the stopped-VM case, where the compute meter has a second off switch and the managed disks keep billing through both.
- EBS gp2 to gp3: cut storage costs about 20% is the AWS block storage line, priced per GiB the same way.
- MongoDB Atlas backups: continuous bills 6.6x explains why deleting incremental snapshots frees less space than their count suggests.
Common questions
Does an unattached persistent disk cost money in Google Cloud?
Yes. Persistent Disk bills its full provisioned capacity until the disk is deleted, whether it is attached or not. In us-central1 that is $0.04 per GiB-month for pd-standard, $0.10 for pd-balanced and $0.17 for pd-ssd, doubled for regional disks.
How do I list unattached disks with gcloud?
Run gcloud compute disks list --filter="-users:*". The filter matches disks with no users entry, meaning no VM holds them. Add lastDetachTimestamp to the output format to see how long each one has been detached.
Do disks on a stopped VM still bill?
Yes. A stopped VM stops its compute charge, but attached disks and other resources keep billing. Those disks list the VM under users, so the unattached filter never finds them. List VMs with status=TERMINATED to catch them.
Where does a snapshot go if I do not set a storage location?
To the multi-region nearest the source disk, so a us-central1 disk's snapshot is stored in us at $0.083 per GiB-month instead of the regional $0.05 per GiB-month. Pass --storage-location=us-central1, or change the project's snapshot settings, to keep it regional.
Is a snapshot cheaper than keeping the disk?
For pd-ssd and pd-balanced, yes, even at the multi-region rate. For pd-standard in us-central1, a standard snapshot's $0.05 per GiB-month is above the disk's $0.04 per GiB-month, so it is cheaper only when the compressed data on the disk is under 80% of its size. An archive snapshot at $0.019 per GiB-month is cheaper than every disk class if you keep it past its 90-day minimum.
Can I recover a deleted persistent disk?
No. Compute Engine has no recycle bin for disks, so the snapshot you take before deleting is the only way back. When you restore, pass --type explicitly, because gcloud compute disks create defaults to pd-standard.
How OhChimp approaches this
OhChimp lists every zonal disk with no users across your project's zones and prices each one at its own class rate, so a 500 GiB pd-ssd orphan sorts above a terabyte of pd-standard rather than hiding behind it. Regional disks are not in the plan yet, so check those by hand with the list command above. The plan carries a snapshot command pinned to the disk's region, a restore command that names the original type, and the command that deletes the snapshot once you no longer need it, with the snapshot's cost stated next to the saving. Nothing is deleted until you click apply, and the saving stays flagged as not implemented until your real Google Cloud bill drops and holds.
The Google Cloud integration page covers connecting a project.