How to Find and Clean Up Unused GCP Resources Safely
Every GCP project accumulates resources that no one is using anymore. A VM spun up for a demo and never shut down. A persistent disk left behind when its VM was deleted. A static IP address reserved for a load balancer that was decommissioned months ago. These forgotten resources generate real charges every billing cycle. This guide walks you through finding them, verifying who owns them, protecting any data worth keeping, and removing the rest safely.
Simple explanation
Imagine renting a storage unit, putting a few boxes in it, and then forgetting about it. Every month the rental fee appears on your credit card, but you never visit. Cloud resources work the same way. A disk, a database, or an IP address is like a storage unit that keeps billing you whether you use it or not. Cleanup means walking through your units, checking what is inside, keeping what matters, and cancelling the rest.
“Unused resources” are cloud infrastructure components that are still provisioned and billing but are no longer doing useful work. They get left behind through normal development activity: someone deletes a VM but forgets the disk, a test environment stays running after the sprint ends, or a static IP address outlives the service it was attached to.
Cleaning up is not just about deleting things. The real workflow is: find candidates, verify ownership, protect data, remove or downsize, and record your savings. Skipping the verification step is how teams accidentally delete production data. Skipping the recording step is how cleanup efforts lose organisational support.
When to use this
- Your monthly bill is higher than expected and you want to find quick wins
- You have just finished a project, sprint, or proof of concept and need to tear down temporary resources
- Your team is doing a quarterly cost review as part of a FinOps practice
- You received a billing budget alert and need to reduce spend
- You are onboarding to a project and want to understand what is actually in use
How it works
Safe cleanup follows a consistent workflow regardless of the resource type:
- Identify candidates. Use
gcloudlist commands, Active Assist recommendations, or billing exports to find resources that appear unused. - Verify ownership. Check resource labels for team and environment information. A stopped VM may be intentionally paused for a seasonal workload.
- Protect data if needed. Create a snapshot or export before deleting any resource that holds data. Snapshots are far cheaper than the original disk and act as a safety net.
- Delete or downsize. Remove the resource entirely or rightsize it if it is still needed but over-provisioned.
- Record savings. Note what was removed and its estimated monthly cost. This builds the case for continued cleanup investment.
- Schedule the next review. Set a recurring calendar reminder or automate candidate detection so waste does not accumulate again.
Think of this workflow like closing down a market stall at the end of the day. You would not just sweep everything into a bin. First you check what is still sellable (identify), confirm nothing belongs to the stall next door (verify ownership), pack up perishables properly (protect data), throw away only what is truly rubbish (delete), count your takings (record savings), and set an alarm so you come back tomorrow (schedule the next review).
What to clean up first
Not all unused resources cost the same. This table ranks the most common types by how easy they are to find, how safely they can be removed, and their typical cost impact. Start at the top.
| Resource type | Why it gets left behind | How to detect it fastest | Safest action | Likely cost impact |
|---|---|---|---|---|
| Unattached persistent disks | VM deleted without --delete-disks=all | gcloud compute disks list --filter="users.len()=0" | Snapshot first, then delete | High: SSD disks bill per GB every month |
| Reserved static IPs | Load balancer or VM deleted without releasing IP | gcloud compute addresses list --filter="status=RESERVED" | Release the address | Low per IP, adds up at scale |
| Stopped VMs | Developer stopped instead of deleting | gcloud compute instances list --filter="status=TERMINATED" | Snapshot disks, then delete VM with --delete-disks=all | Medium: disks and IPs still bill |
| Old snapshots | Created as backups, never cleaned up | gcloud compute snapshots list --sort-by=creationTimestamp | Delete snapshots older than your retention window | Low individually, high in aggregate |
| Unused Cloud SQL instances | Dev/test database never decommissioned | gcloud sql instances list plus connection log check | Export data, then delete instance | High: bills for vCPU and memory continuously |
| Orphaned load balancer components | Service deleted but forwarding rules left behind | gcloud compute forwarding-rules list | Delete forwarding rule and associated backend services | Medium: each forwarding rule has a base charge |
Cloud Asset Inventory vs Active Assist vs billing export
GCP offers several tools for finding unused resources. Each has different strengths, and most teams benefit from using more than one. Use this table to decide which to start with.
| Cloud Asset Inventory | Active Assist | BigQuery billing export | |
|---|---|---|---|
| Best for | Full inventory of every resource across projects and folders | Actionable recommendations with estimated savings | Cost attribution and trend analysis over time |
| Where it is weak | Does not tell you whether a resource is idle, only that it exists | Only covers supported resource types and may miss edge cases | Requires the detailed export to be enabled and is not real-time |
| When a beginner should use it | When you want a complete list of what exists in your project | When you want GCP to tell you what to fix, with estimated savings | When you want to identify your most expensive resources by actual cost |
| When an ops team should use it | For cross-project audits and compliance reporting | For weekly triage of idle resource recommendations across projects | For building dashboards and tracking savings over time |
If you are new to GCP cleanup, start with Active Assist. It does the analysis for you and shows estimated monthly savings next to each recommendation. You can graduate to billing exports and Cloud Asset Inventory as your projects grow.
Safe cleanup checklist before deleting anything
Run through this checklist before deleting any resource. It takes two minutes and prevents the most common cleanup mistakes.
- Check the resource’s labels for
env,team, andownerinformation - Verify when it was last used. Check
lastDetachTimestampfor disks,lastStartTimestampfor VMs, or connection logs for databases - Confirm with the owning team that the resource is no longer needed
- If the resource holds data (disk, database, bucket), create a snapshot or export first
- If the resource has a static IP, decide whether the IP needs to be preserved for DNS or firewall rules
- Delete the resource and note its name, type, and estimated monthly cost
- Set a reminder to delete the safety-net snapshot after 30 days if no one has asked for the data
If you cannot answer “who owns this?” and “when was it last used?” within 60 seconds,
do not delete it yet. Flag it, label it with review-needed=true, and come
back to it next cycle. Patience costs less than data loss.
Unattached persistent disks
A persistent disk is the virtual hard drive attached to a VM. When you delete a VM without
specifying —delete-disks=all, the disk stays behind and continues to bill at
the full per-GB rate for its disk type. Unattached disks are one of the most common and most
costly forms of cloud waste because they are easy to create accidentally and invisible unless
you look for them.
For a deeper understanding of disk types and their pricing tiers, see Storage Cost Optimisation.
Deleted disks cannot be recovered. Always create a snapshot before deleting any disk you are not certain is disposable. Snapshots cost a fraction of the original disk and can be kept as a safety net for 30 days. See Snapshots Explained for details on how snapshots work and how to manage retention.
List all unattached persistent disks across your project:
gcloud compute disks list \
--filter="users.len()=0" \
--format="table(name,zone,sizeGb,type,lastDetachTimestamp)"Sort by detach time to find the oldest (most likely to be forgotten):
gcloud compute disks list \
--filter="users.len()=0" \
--sort-by=lastDetachTimestamp \
--format="table(name,zone,sizeGb,type,lastDetachTimestamp)"Create a snapshot before deleting:
gcloud compute snapshots create backup-DISK_NAME \
--source-disk=DISK_NAME \
--source-disk-zone=ZONEDelete the unattached disk after confirming the snapshot exists:
gcloud compute disks delete DISK_NAME --zone=ZONEReserved static IP addresses
A static external IP address is a fixed public IP you can assign to a VM or load balancer. When you delete the VM or load balancer without releasing the IP, it stays reserved in your project and bills at an hourly rate. Individual IPs are relatively cheap, but teams that provision infrastructure frequently can accumulate dozens of orphaned IPs.
For more on how GCP networking costs work, see GCP Pricing Models.
A static IP that is attached to a running VM is free. The billing only starts when the IP is reserved but not attached to anything. This means deleting a VM can actually increase your IP costs if you forget to release the address.
Find all static IPs in RESERVED state (not attached to anything):
gcloud compute addresses list \
--filter="status=RESERVED" \
--format="table(name,region,status,address,creationTimestamp)"Find global reserved IPs that are not in use:
gcloud compute addresses list \
--global \
--filter="status=RESERVED"Release an unused regional static IP:
gcloud compute addresses delete IP_NAME --region=REGIONRelease an unused global static IP:
gcloud compute addresses delete IP_NAME --globalStopped VMs and idle running VMs
A stopped (TERMINATED) VM does not bill for compute, but its attached persistent disks, any reserved static IP addresses, and certain licences continue to charge. A VM that has been stopped for months is almost certainly forgotten. An idle running VM (one with consistently low CPU and network usage) is even more wasteful because it bills for compute on top of everything else.
One of the most common misconceptions in GCP. Stopping a VM saves you the vCPU and memory
charges, but the disks attached to it keep billing at their full rate. If you have a stopped
VM with a 500 GB SSD disk, you are still paying for 500 GB of SSD storage every month. If
you are done with the VM, delete it with —delete-disks=all.
If a running VM is genuinely needed but over-provisioned, consider rightsizing it instead of deleting it. For VMs that only need to run at specific times, see Compute Engine Cost Optimisation for scheduling patterns.
Find all stopped VMs:
gcloud compute instances list \
--filter="status=TERMINATED" \
--format="table(name,zone,status,lastStartTimestamp)"Use Active Assist to find idle running VMs with low utilisation:
gcloud recommender recommendations list \
--project=PROJECT_ID \
--location=ZONE \
--recommender=google.compute.instance.IdleResourceRecommender \
--format="table(description,primaryImpact.costProjection.cost.units)"Delete a stopped VM and all its attached disks (after snapshotting):
gcloud compute instances delete INSTANCE_NAME \
--zone=ZONE \
--delete-disks=allOld disk snapshots
Disk snapshots are point-in-time copies of a persistent disk. They are cheaper than the original disk, but they are not free. Snapshots created as backups or safety nets during previous cleanup rounds can accumulate indefinitely if no one sets a retention policy. Over time, the aggregate cost of hundreds of old snapshots becomes significant.
For details on how snapshot storage and incremental snapshots work, see Snapshots Explained.
Every time you clean up a disk, you create a snapshot as a safety net. If you never clean up those safety-net snapshots, your cleanup process itself becomes a source of waste. Set a retention window (30 to 90 days is typical) and delete old snapshots as part of each review cycle.
List all snapshots sorted by age (oldest first):
gcloud compute snapshots list \
--sort-by=creationTimestamp \
--format="table(name,diskSizeGb,creationTimestamp,storageBytes)"Find snapshots older than a specific date (adjust the date to your retention window):
gcloud compute snapshots list \
--filter="creationTimestamp < '2026-01-01'" \
--format="table(name,diskSizeGb,creationTimestamp)"Delete a specific old snapshot after confirming it is no longer needed:
gcloud compute snapshots delete SNAPSHOT_NAMEUnused Cloud SQL instances
A Cloud SQL instance in RUNNABLE state bills continuously for its provisioned vCPUs, memory, and storage, even if nothing is connecting to it. Dev and test databases are the most common culprits because they are created for a specific task and forgotten when that task ends.
Deleting a Cloud SQL instance destroys all databases and data on it. Always export your
data before deletion if there is any chance it is still needed. Use gcloud sql export sql
or gcloud sql export csv to write the data to a Cloud Storage bucket.
List all Cloud SQL instances and their current state:
gcloud sql instances list \
--format="table(name,state,databaseVersion,settings.tier,settings.activationPolicy)"Check recent connection activity using Logs Explorer via the CLI:
gcloud logging read \
'resource.type="cloudsql_database"
AND resource.labels.database_id="PROJECT_ID:INSTANCE_NAME"' \
--limit=10 \
--order=descDelete an unused Cloud SQL instance after exporting data:
gcloud sql instances delete INSTANCE_NAMEOrphaned load balancer components
A GCP load balancer is made up of several components: forwarding rules, target proxies, URL maps, and backend services. When a service is decommissioned, these components are sometimes deleted out of order or only partially removed. The remaining forwarding rules continue to bill even when they point to backend services with no backends.
List all forwarding rules to find potential orphans:
gcloud compute forwarding-rules list \
--format="table(name,region,IPAddress,target,portRange)"List global forwarding rules (used by HTTPS load balancers):
gcloud compute forwarding-rules list \
--global \
--format="table(name,IPAddress,target)"Check whether a backend service has any backends attached:
gcloud compute backend-services describe BACKEND_SERVICE_NAME \
--global \
--format="value(backends)"If the output is empty, no backends are attached. The backend service is orphaned and can be safely deleted along with its forwarding rule.
Automating cleanup safely
Manual cleanup works for small teams, but as your environment grows, automating the detection and notification process saves significant time. The key principle is: automate detection and notification, but keep deletion as a deliberate human action until you have high confidence in your automation.
Think of cleanup automation like a smoke detector, not a sprinkler system. A smoke detector alerts you to a problem so you can investigate. A sprinkler system reacts automatically and drenches everything, including things that did not need drenching. Start with the smoke detector (detection and notification). Only graduate to sprinklers (automated deletion) once you have proven that your detection logic never triggers false positives.
A common pattern uses Cloud Scheduler to
trigger a Cloud Run function on a weekly or monthly schedule. The function runs the same
gcloud list commands shown on this page, filters for candidates, and sends a
summary to your team’s Slack channel or email list. No resources are deleted automatically.
If you want to progress to automated deletion, use an opt-in label approach:
- Only resources with a label like
auto-cleanup=trueare eligible for automated deletion - Resources without the label are flagged for manual review but never auto-deleted
- The automation logs every action and sends a summary notification after each run
- Include a dry-run mode so you can verify what would be deleted before enabling real deletion
If you manage your infrastructure with Terraform,
cleanup can be as simple as removing the resource from your configuration and running
terraform apply. Terraform handles the deletion order and dependency resolution for you.
It is tempting to write a rule that says “delete anything without a team label.”
The problem is that an unlabeled resource is not necessarily unused. It may simply have been
created before your labelling policy existed, or by a tool that does not apply labels.
Auto-deleting unlabeled resources will eventually destroy something important.
Common mistakes
Deleting before verifying ownership. A stopped VM or idle database may be intentionally in that state. It could be waiting for a deployment, paused for a seasonal workload, or owned by another team. Always check labels and contact the owner before deleting anything you did not create yourself.
Deleting disks without creating a snapshot first. Deleted disks cannot be recovered. A snapshot costs a fraction of the original disk and takes seconds to create. There is no good reason to skip this step.
Treating stopped VMs as free. Stopped VMs do not bill for compute, but their attached disks, reserved IPs, and licences continue to charge. If you are not going to start the VM again, delete it and its disks.
Leaving labels off resources at creation time. Identifying who owns an unlabeled resource that was created months ago is slow and often impossible. Enforce labels at creation time through organisational policy or your IaC templates.
Hard-coding automated deletion without an approval step. Automation that blindly deletes unlabeled or idle resources will eventually delete something important. Always require an opt-in label and a review notification before automated deletion.
Summary
- Unused resources are provisioned infrastructure that is still billing but no longer serving a purpose
- The safe cleanup workflow is: identify, verify ownership, protect data, delete, record savings, schedule the next review
- Start with unattached persistent disks and reserved static IPs because they are the easiest to find and the safest to remove
- Always create a snapshot before deleting any disk or database that might contain needed data
- Stopped VMs still cost money because their disks and IPs continue to bill
- Automate detection and notification first. Only automate deletion with opt-in labels and review workflows
- Enforce labels at creation time so that future cleanup reviews can quickly identify resource owners
Frequently asked questions
What counts as an unused resource in GCP?
An unused resource is anything provisioned in your project that is no longer serving production traffic or active development. Common examples include persistent disks left behind after a VM was deleted, static IP addresses reserved but not attached to anything, VMs that have been stopped for weeks, old disk snapshots from resources that no longer exist, and Cloud SQL instances with no recent connections. The key indicator is that the resource is generating charges without providing value.
Are stopped VMs still costing money?
Stopped VMs do not incur compute (vCPU/memory) charges, but their attached persistent disks, reserved static IPs, and any associated licences continue to bill. A stopped VM with a 200 GB SSD disk still costs the same for disk storage as a running one. If you no longer need the VM, snapshot the disk, delete the VM with --delete-disks=all, and release any associated static IP.
What is the safest way to delete an unattached disk?
First, check the lastDetachTimestamp to see when the disk was last used. Contact the team or person who created it if you can identify them from labels. Then create a snapshot of the disk, because snapshots are significantly cheaper than keeping the full disk. Once the snapshot exists, delete the disk. Keep the snapshot for at least 30 days before deleting it, in case someone needs the data.
How often should I review unused resources?
Most teams benefit from a monthly lightweight review and a more thorough quarterly audit. The monthly review can be as simple as running the gcloud list commands on this page and checking Active Assist recommendations. The quarterly audit should include verifying labels, reviewing billing exports for anomalies, and cleaning up old snapshots. Teams with high resource churn (such as those running frequent dev/test environments) should review more often.
Can I automate GCP cleanup safely?
Yes, but automation should be opt-in and review-first. The safest approach is to use Cloud Scheduler to trigger a Cloud Run function that identifies candidates and sends a notification (email, Slack, or PagerDuty) rather than deleting immediately. Only resources with an explicit opt-in label like auto-cleanup=true should be eligible for automated deletion. Never auto-delete unlabeled resources. They may simply be missing labels rather than truly unused.