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

Analogy

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:

  1. Identify candidates. Use gcloud list commands, Active Assist recommendations, or billing exports to find resources that appear unused.
  2. Verify ownership. Check resource labels for team and environment information. A stopped VM may be intentionally paused for a seasonal workload.
  3. 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.
  4. Delete or downsize. Remove the resource entirely or rightsize it if it is still needed but over-provisioned.
  5. Record savings. Note what was removed and its estimated monthly cost. This builds the case for continued cleanup investment.
  6. Schedule the next review. Set a recurring calendar reminder or automate candidate detection so waste does not accumulate again.
Mental model

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 typeWhy it gets left behindHow to detect it fastestSafest actionLikely cost impact
Unattached persistent disksVM deleted without --delete-disks=allgcloud compute disks list --filter="users.len()=0"Snapshot first, then deleteHigh: SSD disks bill per GB every month
Reserved static IPsLoad balancer or VM deleted without releasing IPgcloud compute addresses list --filter="status=RESERVED"Release the addressLow per IP, adds up at scale
Stopped VMsDeveloper stopped instead of deletinggcloud compute instances list --filter="status=TERMINATED"Snapshot disks, then delete VM with --delete-disks=allMedium: disks and IPs still bill
Old snapshotsCreated as backups, never cleaned upgcloud compute snapshots list --sort-by=creationTimestampDelete snapshots older than your retention windowLow individually, high in aggregate
Unused Cloud SQL instancesDev/test database never decommissionedgcloud sql instances list plus connection log checkExport data, then delete instanceHigh: bills for vCPU and memory continuously
Orphaned load balancer componentsService deleted but forwarding rules left behindgcloud compute forwarding-rules listDelete forwarding rule and associated backend servicesMedium: 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 InventoryActive AssistBigQuery billing export
Best forFull inventory of every resource across projects and foldersActionable recommendations with estimated savingsCost attribution and trend analysis over time
Where it is weakDoes not tell you whether a resource is idle, only that it existsOnly covers supported resource types and may miss edge casesRequires the detailed export to be enabled and is not real-time
When a beginner should use itWhen you want a complete list of what exists in your projectWhen you want GCP to tell you what to fix, with estimated savingsWhen you want to identify your most expensive resources by actual cost
When an ops team should use itFor cross-project audits and compliance reportingFor weekly triage of idle resource recommendations across projectsFor building dashboards and tracking savings over time
Where to start

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.

  1. Check the resource’s labels for env, team, and owner information
  2. Verify when it was last used. Check lastDetachTimestamp for disks, lastStartTimestamp for VMs, or connection logs for databases
  3. Confirm with the owning team that the resource is no longer needed
  4. If the resource holds data (disk, database, bucket), create a snapshot or export first
  5. If the resource has a static IP, decide whether the IP needs to be preserved for DNS or firewall rules
  6. Delete the resource and note its name, type, and estimated monthly cost
  7. Set a reminder to delete the safety-net snapshot after 30 days if no one has asked for the data
Quick rule of thumb

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.

Before you delete

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=ZONE

Delete the unattached disk after confirming the snapshot exists:

gcloud compute disks delete DISK_NAME --zone=ZONE

Reserved 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.

Why this catches people off guard

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=REGION

Release an unused global static IP:

gcloud compute addresses delete IP_NAME --global

Stopped 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.

Stopped does not mean free

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=all

Old 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.

The irony of cleanup snapshots

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_NAME

Unused 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.

Before you delete

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=desc

Delete an unused Cloud SQL instance after exporting data:

gcloud sql instances delete INSTANCE_NAME

Orphaned 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.

Mental model

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=true are 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.

Never auto-delete unlabeled resources

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

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.

Last verified: 27 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.