A recommender once flagged one of my Compute Engine VMs: n1-standard-2, 7.5 GB of RAM, using about 2.5. The suggested fix was a custom-2-2560 machine type. One click, 12.59 dollars a month saved. On its own, that is a rounding error. The reason FinOps exists is that a real GCP estate has thousands of that same decision hiding in it, and nobody owns finding them.
Cost optimization on GCP is not a cleanup you run once before a budget review. It is an operating model: make spend visible, attack it where the money actually is, and put guardrails in place so it does not creep back. This is the playbook I work from, in the order I apply it, with the trade-offs that the marketing pages leave out.
First principle: you cannot optimize what you cannot see
Every optimization below depends on one thing being true first: you can answer "who spent this, on what, and why" without guessing. Two mechanisms make that possible on GCP.
Labels and the resource hierarchy. Labels are key/value pairs you attach to resources (team:payments, env:prod, cost-center:cc-4412). They flow into billing, so a bucket, a VM and a BigQuery job can all be traced to the team that owns them. Enforce them from the start, because retro-labelling a live estate is painful. The resource hierarchy (organization, folders, projects) gives you the same allocation for free when it is designed with cost in mind: one project per team-environment is a blunt but effective boundary.
The BigQuery billing export. The billing console is fine for a glance. Real analysis lives in the detailed billing export into BigQuery, which gives you per-SKU, per-label, per-day granularity. Once it is on, questions like "what is my top egress source this month" become a query instead of a support ticket:
SELECT
service.description AS service,
sku.description AS sku,
ROUND(SUM(cost), 2) AS cost
FROM `billing.gcp_billing_export_v1_XXXXXX`
WHERE _PARTITIONTIME >= TIMESTAMP('2026-01-01')
GROUP BY service, sku
ORDER BY cost DESC
LIMIT 20;
The FinOps Hub sits on top of this and turns it into ranked recommendations (idle VMs, oversized instances, unattached disks) with a dollar figure attached and a one-click apply. It also gives you a FinOps score and peer benchmarking, which matters more than it sounds: it turns "we should optimize" into "we are at the 40th percentile and here is the gap".
Visibility is not the optimization. It is the thing that makes every other section possible.
The biggest steady-state lever: commitments
For anything that runs 24/7, on-demand pricing is the most expensive way to pay. GCP has three discount mechanisms, and the order you reach for them matters.
Sustained Use Discounts (SUDs) are automatic. Run an eligible VM for a large share of the month and GCP applies up to 30 percent off with no action required. You get these for free; the only mistake is assuming they are the whole story.
Resource-based Committed Use Discounts (CUDs) are the deep discount. You commit to a specific amount of vCPU, memory, GPU or local SSD in a region for one or three years. In return, at the time of writing, up to 70 percent off memory-optimized families and 55 percent off the others, and on the OS side up to 79 percent for SUSE and 63 percent for SLES for SAP. This is the tool for predictable, steady-state usage where you can say "I will use X of Y in region Z" and mean it.
Compute flexible (spend-based) CUDs trade some of that discount for flexibility: 28 percent for a one-year commitment, 46 percent for three years, applied to a minimum hourly spend across Compute Engine, GKE and Cloud Run regardless of machine family or region. Commit 100 dollars an hour for three years and you pay an effective 54 for that slice; in an hour where your eligible spend is 200 GCE, 100 GKE and 100 Cloud Run, the commitment covers 50, 25 and 25 of it proportionally and you pay on-demand for the rest.
The decision rule I use: resource-based CUDs for the stable core you can forecast precisely, flexible CUDs for the part that moves between projects and services but that you know will exist. If you hold both, GCP applies the resource-based commitment first, then flexible on the remainder, which is exactly what you want. The trap is over-committing: a three-year resource commitment on a workload you kill in six months is a liability, not a saving. Commit to the floor of your usage, not the average.
Right-size and switch off compute
Commitments discount what you run. This section is about running less.
Right-size against real usage, not the spec sheet. The recommender watches actual CPU and memory and proposes smaller machine types or custom shapes. Custom machine types are underrated: instead of an e2-standard-8 (8 vCPU, 32 GB) you can define an e2-custom-8-28672 (8 vCPU, 28 GB) and stop paying for memory you never touch. Picking the right family compounds this. A dev box needing 4 vCPU and 16 GB is roughly 150 dollars a month on N1 and about 104 on E2, a 30 percent cut for the same shape, and newer generations widen the gap (N4 delivers up to 18 percent better price-performance than N2 and up to 70 percent better than N1).
Switch off what nobody uses at night. Production needs to stay up. Dev, staging and test do not, and they are often a third of the fleet sitting idle every night and weekend. Three ways to automate it: MIG scaling schedules, the VM run-time limit, or, if you manage infrastructure as code, a scheduled terraform destroy on Friday evening and terraform apply on Monday morning that takes non-prod to near zero over the weekend.
resource "google_compute_instance" "dev" {
name = "dev-box"
machine_type = "e2-medium"
zone = "us-central1-a"
# Friday: terraform destroy -target=google_compute_instance.dev
# Monday: terraform apply
}
Spot VMs for anything that can be interrupted. Spot is spare capacity at up to 91 percent off, with a 30-second preemption notice. That notice is the whole story: it is perfect for batch jobs, CI runners, stateless workers and ML training that checkpoint, and wrong for a stateful database. Manage preemption with checkpointing, a Managed Instance Group that replaces a preempted node automatically, and termination notifications so the app can shut down cleanly. Because a Spot VM can vanish in 30 seconds, infrastructure as code stops being nice-to-have and becomes the thing that makes Spot viable at all: the replacement has to come up identically without a human. A common pattern is a blended fleet, for example 70 percent on-demand and 30 percent Spot, tuned to how much interruption the workload tolerates.
Disable simultaneous multithreading where licences are per-vCPU. By default two vCPUs share a physical core. For compute-bound work, or where a software licence is billed per vCPU, disabling SMT can halve the vCPU count that the licence sees and give more predictable performance. It is a narrow lever, but on an expensive per-core licence it pays for itself.
Kubernetes: pay for pods, not nodes
GKE costs spiral for one reason: you pay for nodes, but value is delivered by pods, and the gap between the two is waste.
Right-size the pods first. Set requests and limits that reflect reality. Over-requesting is the single most common GKE cost bug, because the scheduler reserves what you asked for whether the pod uses it or not.
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
The Vertical Pod Autoscaler can propose or apply these from observed usage. Tighter requests mean more pods fit per node, which means fewer nodes.
Scale with demand, not with fear. The Horizontal Pod Autoscaler adds and removes pod replicas on a metric. A 50 percent CPU target is a safe default, but production with predictable load can usually push it to 60 or 70 percent, which packs nodes harder before the cluster is forced to add one.
spec:
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
Underneath, the Cluster Autoscaler resizes the node pool. Its optimize-utilization profile scales down more aggressively than the default balanced one: slightly longer cold starts, meaningfully lower bill. Node Auto Provisioning goes further and creates right-shaped node pools on demand.
Bin-pack instead of spreading. By default Kubernetes spreads pods for availability, which leaves nodes half-empty. Node auto-provisioning plus the autoscaler, guided by affinity rules, packs pods onto fewer, fuller nodes. The honest trade-off is availability: bin-packing and anti-affinity pull in opposite directions, so you tune per workload rather than globally.
Let the discounts stack, and consider Autopilot. SUDs, CUDs and Spot all apply to GKE nodes. For a large share of teams the bigger win is GKE Autopilot, which bills for the resources your pods actually request instead of for whole nodes, so unused node capacity stops being your problem. You give up some node-level control; you get bin-packing and node management handled for you.
Serverless: cap the blast radius
Cloud Run and Cloud Functions scale to zero, which feels free until a traffic surge or a retry storm scales them to a very expensive number.
Set a maximum instance count. This is the single most important serverless cost control, a cap on how far autoscaling can run:
gcloud run services update SERVICE --max-instances 20
gcloud functions deploy FUNCTION_NAME --max-instances 20
Set it from data, not vibes: for Cloud Run, read billable container instance time, take your average concurrent instance count and add roughly 30 percent of headroom. Too low and requests queue behind the cap; too high and a runaway keeps its ability to run away.
Fix CPU stranding. An instance can be pinned at low throughput while its CPU sits idle, usually because concurrency is set too low or a slow I/O call blocks the request. Let Cloud Run manage concurrency rather than hard-capping it, and push slow work (sending an email, calling a third party) onto Pub/Sub or Cloud Tasks so the request returns and the instance is freed to serve the next one. Fewer instances for the same traffic is a direct line to a smaller bill.
Storage: match the class to the access pattern
Object storage is where cost quietly accretes, because data is easy to write and nobody ever deletes it.
Use the right storage class. GCS has four, and the price gap between them is large. Standard is for hot data, no retrieval fee. Nearline is about 30 percent cheaper for data touched roughly monthly. Coldline is around 60 percent cheaper for quarterly access. Archive is up to 90 percent cheaper for data you must keep and hope never to read. The catch is retrieval and minimum-duration fees on the colder tiers, so the class has to match how often you actually read, not how old the data is.
Automate the tiering with lifecycle rules or Autoclass. You do not migrate objects by hand. A lifecycle policy does it on a schedule:
{
"rule": [
{"action": {"type": "SetStorageClass", "storageClass": "NEARLINE"},
"condition": {"age": 30}},
{"action": {"type": "SetStorageClass", "storageClass": "COLDLINE"},
"condition": {"age": 90}},
{"action": {"type": "Delete"},
"condition": {"age": 365}}
]
}
When access patterns are unpredictable, Autoclass moves objects between tiers automatically and promotes them back to Standard on access. The impact is not theoretical: a financial-services team put security logs on Autoclass for a 10 percent cut, then realised the logs were untouched after two weeks and set a rule to move them to Coldline at that mark, taking the bucket down by over 80 percent.
Two adjacent wins. Storage inventory reports surface duplicate and stale data (one media estate was holding the same assets four or five times; a checksum-based dedup cut GCS by 70 percent). And Cloud CDN in front of public, frequently-read objects serves them from edge caches, which cuts the egress fee that is often the real cost, not the storage itself.
BigQuery and databases: the analytics line item
BigQuery is the line item that surprises people, because a single careless query can scan terabytes.
Partition, cluster, and cap the scan. Partition tables by date and cluster by the columns you filter on, so a query reads a slice instead of the whole table. Then make a full-table scan impossible by accident:
-- Fail the query instead of paying for a 10 TB scan
SET @@maximum_bytes_billed = 100000000000; -- 100 GB ceiling
Pick the right pricing model. On-demand bills per byte scanned and suits spiky, exploratory usage. BigQuery editions with a committed baseline of slots suit steady, heavy pipelines where the reserved capacity is cheaper than paying per-query. The choice is a workload-shape decision, and getting it wrong in either direction costs real money: reserved slots sitting idle, or on-demand scans on a pipeline that never stops.
For Cloud SQL and AlloyDB, the compute rules still apply. Right-size the instance against real utilisation, stop non-production databases out of hours, and cover the steady core with Committed Use Discounts (25 to 52 percent on Cloud SQL). A managed database does not exempt you from paying for CPU you do not use.
Logging: the invisible line item
Cloud Logging is the cost almost nobody looks at until it is the third-largest line on the bill. Debug logs from a chatty service, ingested and retained by default, add up fast.
The three levers: exclusion filters to stop ingesting logs you will never query (health-check 200s, verbose debug in prod), log routing to send what you must keep to a cheaper sink instead of the default bucket, and retention tuned per log type rather than left at the default. You are not deleting signal; you are refusing to pay premium ingestion and retention for noise.
Make it stick: governance and automation
Every saving above decays. An engineer spins up a n1-standard-16 for a test and forgets it; a new service ships without labels. Optimization that is not enforced is optimization that reverts.
Budgets and alerts are the floor: a budget per project with alerts at 50, 90 and 100 percent means a runaway is a notification on day two, not a surprise on the invoice.
Policy as code is the ceiling: define the rules (no untagged resources, no public buckets, no VM above a size without approval) and let a pipeline enforce them instead of a human reviewing tickets. The same idea drives remediation. Rather than click each FinOps Hub recommendation, export them via the Recommender API into a pipeline: Cloud Scheduler triggers a parser on Cloud Run, which reads the recommendations, updates the Terraform manifests, and opens a pull request for a human to review before Cloud Build applies it. Cost optimization becomes a reviewed code change with an audit trail, not a console click nobody remembers making.
Unit economics is what turns all of this into a business conversation. "Cloud cost went up 12 percent" is an alarm. "Cost per active user dropped from 4 cents to 3 cents while traffic doubled" is a result. Tie spend to a business metric (cost per order, per tenant, per inference) and optimization stops being a cost-cutting exercise and becomes a measure of how efficiently the platform turns money into product.
Where to start
If you are staring at a GCP bill and do not know which thread to pull, this is the order that pays back fastest:
- Turn on the BigQuery billing export and enforce labels. You cannot manage what you cannot attribute.
- Kill the obvious waste the FinOps Hub already found: idle VMs, unattached disks, oversized instances.
- Schedule non-production to switch off nights and weekends.
- Move interruptible workloads to Spot, behind a MIG and infrastructure as code.
- Commit the steady-state core: SUDs are automatic, then layer CUDs on the floor of your usage.
- Set lifecycle rules on storage and a maximum-bytes ceiling on BigQuery.
- Wire budgets, alerts and policy-as-code so none of it drifts back.
None of this is a heroic one-off. The estate that stays cheap is the one where visibility, discounts and guardrails run continuously, and where the person who provisions the resource is the same person who sees its cost. That feedback loop is the entire job.
