GCP FinOps CUD cost optimization billing

GCP CUD Purchased but Billing Still Shows On-Demand Rates

Diagnosing a non-applying GCP CUD: verifying machine family, region, commitment type (compute vs memory-optimised), checking billing export, and fixing the issue.

·
You purchased a 1-year or 3-year Committed Use Discount expecting up to 57% savings on Compute Engine instances, but the bill for the next billing cycle has not changed by a single cent. Instances are still charged at on-demand rates, while the commitment shows as active in the console. For fleets of tens or hundreds of vCPUs, this means thousands of dollars per month overpaid for resources that should be covered by the discount.

This runbook covers diagnosing and fixing a situation where a purchased GCP CUD is not being applied to running instances. For a detailed discussion of CUD strategies, Spot VMs, and FinOps practices, see our article GCP cost optimization - Committed Use Discounts, Spot VMs, and FinOps strategies. If you need help selecting and optimising commitments - book a consultation.

Symptom

The billing report shows on-demand rates despite an active commitment. CUD credits do not appear in the billing export:

# Lista aktywnych commitmentów w projekcie
gcloud compute commitments list \
  --project=my-project \
  --format="table(name, region, status, plan, resources[].type, resources[].amount, startTimestamp, endTimestamp)"

# Sprawdź status konkretnego commitmentu
gcloud compute commitments describe my-cud-commitment \
  --region=us-central1 \
  --project=my-project \
  --format="yaml(name, status, plan, resources, type, startTimestamp, endTimestamp, category)"

# Zweryfikuj running instances i ich machine type
gcloud compute instances list \
  --project=my-project \
  --format="table(name, zone, machineType.basename(), status)" \
  --filter="status=RUNNING"

BigQuery billing export query to verify CUD credit presence:

-- Sprawdź czy CUD credits pojawiają się w billing export
SELECT
  invoice.month,
  sku.description,
  service.description,
  SUM(cost) AS total_cost,
  SUM((SELECT SUM(c.amount) FROM UNNEST(credits) c WHERE c.type = 'COMMITTED_USE_DISCOUNT')) AS cud_credits,
  SUM((SELECT SUM(c.amount) FROM UNNEST(credits) c WHERE c.type = 'COMMITTED_USE_DISCOUNT_DOLLAR_BASE')) AS cud_dollar_credits
FROM `my-billing-project.billing_export.gcp_billing_export_v1_XXXXXX`
WHERE
  service.description = 'Compute Engine'
  AND usage_start_time >= TIMESTAMP('2026-07-01')
  AND project.id = 'my-project'
GROUP BY 1, 2, 3
HAVING total_cost > 0
ORDER BY total_cost DESC
LIMIT 20;

If the cud_credits and cud_dollar_credits columns return NULL or 0 for SKUs covered by the commitment - the CUD is not being applied.

Root Cause

GCP CUD is automatically applied to matching resources, but all criteria must be met simultaneously. The most common reasons for non-application:

  • Machine family mismatch - a CUD purchased for the N2 family does not cover E2, N1, or N2D instances. A GENERAL_PURPOSE commitment type covers only the specific machine family indicated at purchase. A common mistake: purchasing a CUD for N2 while running workloads on E2 (because they are cheaper per-vCPU on-demand).

  • Region mismatch - a CUD is bound to a specific GCP region. A commitment purchased in us-central1 does not cover instances running in europe-west1. There is no cross-region CUD mechanism - each region requires a separate commitment.

  • Commitment type mismatch - a COMPUTE_OPTIMIZED CUD (C2/C2D family) does not cover general-purpose workloads (N2/E2). Similarly, a MEMORY_OPTIMIZED CUD (M1/M2) covers only memory-optimised instances.

  • CUD not yet active - after purchase, a commitment passes through a CREATING state before becoming ACTIVE. This can take up to 24 hours. Billing credits only begin accruing from the moment of activation.

  • Sole-tenant nodes - instances running on sole-tenant nodes are not covered by standard CUDs. They require a separate sole-tenant commitment type.

  • Commitment exhausted - if the total vCPU/RAM of running instances exceeds the purchased commitment, the surplus is billed at on-demand rates. The CUD covers resources up to the purchased quantity limit.

Resolution

A) Verify commitment alignment with running instances:

# Szczegóły zakupionego CUD - machine type family i zasoby
gcloud compute commitments describe my-cud-commitment \
  --region=us-central1 \
  --project=my-project \
  --format="json(name, status, resources, type, category)"

# Wynik powinien pokazać np.:
# "resources": [
#   { "type": "VCPU", "amount": "32" },
#   { "type": "MEMORY", "amount": "131072" }   <- MB
# ]
# "type": "GENERAL_PURPOSE_N2"
# "category": "MACHINE"

# Porównaj z running instances - filtruj po machine family
gcloud compute instances list \
  --project=my-project \
  --filter="status=RUNNING AND machineType~'n2-'" \
  --format="table(name, zone, machineType.basename())" 

# Policz łączne vCPU instancji N2 w regionie us-central1
gcloud compute instances list \
  --project=my-project \
  --filter="status=RUNNING AND zone~'us-central1' AND machineType~'n2-'" \
  --format="value(machineType.basename())" | \
  awk -F'-' '{sum += $NF} END {print "Total vCPU (N2, us-central1):", sum}'

# Porównaj z commitmentem:
# Jeśli commitment = 32 vCPU, a running instances mają 48 vCPU
# -> 32 vCPU pokryte CUD, 16 vCPU rozliczane on-demand

B) Migrate instances to the matching machine family:

If the commitment is for N2 but instances are running on E2 - change the machine type:

# Zatrzymaj instancję przed zmianą machine type
gcloud compute instances stop my-instance \
  --zone=us-central1-a \
  --project=my-project

# Zmień machine type z E2 na N2 (zachowując podobną konfigurację)
# e2-standard-4 (4 vCPU, 16 GB) -> n2-standard-4 (4 vCPU, 16 GB)
gcloud compute instances set-machine-type my-instance \
  --machine-type=n2-standard-4 \
  --zone=us-central1-a \
  --project=my-project

# Uruchom instancję ponownie
gcloud compute instances start my-instance \
  --zone=us-central1-a \
  --project=my-project

# Dla MIG (Managed Instance Group) - zaktualizuj instance template
gcloud compute instance-templates create my-template-n2-v2 \
  --machine-type=n2-standard-4 \
  --image-family=debian-12 \
  --image-project=debian-cloud \
  --project=my-project

gcloud compute instance-groups managed set-instance-template my-mig \
  --template=my-template-n2-v2 \
  --zone=us-central1-a \
  --project=my-project

# Rolling update MIG do nowego template
gcloud compute instance-groups managed rolling-action start-update my-mig \
  --version=template=my-template-n2-v2 \
  --zone=us-central1-a \
  --max-surge=1 \
  --max-unavailable=0 \
  --project=my-project

C) Purchase an additional CUD in the correct region/family:

# Sprawdź dostępne typy CUD dla regionu
gcloud compute commitments create-help

# Zakup CUD dla E2 w europe-west1 (jeśli tam działają instancje)
gcloud compute commitments create cud-e2-europe-west1 \
  --region=europe-west1 \
  --project=my-project \
  --plan=twelve-month \
  --resources=vcpu=16,memory=65536 \
  --type=GENERAL_PURPOSE_E2

# Zakup CUD typu compute-optimised (C2) jeśli workload wymaga C2
gcloud compute commitments create cud-c2-us-central1 \
  --region=us-central1 \
  --project=my-project \
  --plan=thirty-six-month \
  --resources=vcpu=64,memory=262144 \
  --type=COMPUTE_OPTIMIZED_C2D

Note: Before purchasing an additional CUD, make sure the existing commitment is insufficient. Check utilisation via the recommendations in Billing - Commitments.

D) Flex CUD for multi-region scenarios:

If workloads change regions (e.g. DR failover, seasonal migration) - consider an approach with multiple shorter commitments:

# GCP nie oferuje oficjalnie "Flex CUD" - ale można stosować strategię:
# 1. Mniejsze commitmenty per-region zamiast jednego dużego
# 2. Monitorowanie utilization i dokupywanie w miarę potrzeb

# Sprawdź rekomendacje CUD od Google (Billing -> Commitments -> Recommendations)
gcloud billing budgets list --billing-account=XXXXXX-XXXXXX-XXXXXX

# Alternatywnie - BigQuery query na billing export do analizy coverage
SELECT
  location.region,
  sku.description,
  SUM(usage.amount_in_pricing_units) AS total_usage,
  SUM(cost) AS total_cost,
  SUM((SELECT SUM(c.amount) FROM UNNEST(credits) c WHERE c.type = 'COMMITTED_USE_DISCOUNT')) AS cud_applied
FROM `my-billing-project.billing_export.gcp_billing_export_v1_XXXXXX`
WHERE
  service.description = 'Compute Engine'
  AND usage_start_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
  AND sku.description LIKE '%Instance Core%'
GROUP BY 1, 2
ORDER BY total_cost DESC;

Validation

Once changes are deployed, verify that CUD credits begin appearing:

-- BigQuery: potwierdź naliczanie CUD credits w bieżącym dniu
SELECT
  usage_start_time,
  sku.description,
  cost AS gross_cost,
  (SELECT SUM(c.amount) FROM UNNEST(credits) c WHERE c.type = 'COMMITTED_USE_DISCOUNT') AS cud_credit,
  cost + IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c WHERE c.type = 'COMMITTED_USE_DISCOUNT'), 0) AS net_cost
FROM `my-billing-project.billing_export.gcp_billing_export_v1_XXXXXX`
WHERE
  service.description = 'Compute Engine'
  AND project.id = 'my-project'
  AND usage_start_time >= TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), DAY)
  AND sku.description LIKE '%N2%Instance Core%'
ORDER BY usage_start_time DESC
LIMIT 20;
-- Expected: cud_credit < 0 (ujemna wartość = naliczony rabat)
# Potwierdź utilization commitment
gcloud compute commitments list \
  --project=my-project \
  --region=us-central1 \
  --format="table(name, status, resources[].type, resources[].amount)"

# Sprawdź w Billing API commitment coverage
gcloud billing accounts describe XXXXXX-XXXXXX-XXXXXX \
  --format="json"

# Zweryfikuj brak instancji poza pokryciem CUD (on-demand spillover)
gcloud compute instances list \
  --project=my-project \
  --filter="status=RUNNING AND zone~'us-central1'" \
  --format="table(name, machineType.basename())" | \
  grep -v "n2-" | grep -v "NAME"
# Expected: pusta lista (wszystkie instancje w regionie to N2 pokryte CUD)

Full financial validation - compare the current month’s cost against the previous one:

-- Porównanie kosztów month-over-month z widocznością CUD
SELECT
  FORMAT_TIMESTAMP('%Y-%m', usage_start_time) AS month,
  SUM(cost) AS gross_cost,
  SUM((SELECT SUM(c.amount) FROM UNNEST(credits) c WHERE c.type = 'COMMITTED_USE_DISCOUNT')) AS cud_savings,
  SUM(cost) + SUM(IFNULL((SELECT SUM(c.amount) FROM UNNEST(credits) c WHERE c.type = 'COMMITTED_USE_DISCOUNT'), 0)) AS net_cost
FROM `my-billing-project.billing_export.gcp_billing_export_v1_XXXXXX`
WHERE
  service.description = 'Compute Engine'
  AND project.id = 'my-project'
  AND usage_start_time >= TIMESTAMP('2026-06-01')
GROUP BY 1
ORDER BY 1;
-- Expected: net_cost w bieżącym miesiącu niższy o ~30-57% vs on-demand baseline
An unused CUD represents a double cost: you pay for the commitment (the obligation is irrevocable for 1 or 3 years), and simultaneously for on-demand instances that should have been covered by it. For 32 vCPU N2 in us-central1, the difference between on-demand and a 3-year CUD is approximately $450/month. Over a year of misconfigured setup that is over $5,400 of wasted budget. Regularly monitor commitment utilisation and react immediately when coverage drops below 80%.

 

Jerzy Kopaczewski

CUD not reducing your GCP bill?

Book a free 30-minute call. We will analyse your commitments, identify coverage gaps, and select the optimal CUD strategy for your infrastructure.