
Gemini API Cost Control: Token Pricing, Quotas & Budget Alerts on Vertex AI | 2026
Gemini API cost on Vertex AI is controlled with four levers: measure tokens with count_tokens and response usage_metadata, route each task to the cheapest sufficient model (flash-lite/flash/pro), cap max_output_tokens and cache repeated prompts, and enforce hard guardrails with Terraform budget alerts plus consumer quota overrides. Move offline work to batch prediction for roughly 50% off.
By Mateusz Chmielewski · Aug 19, 2026 · 14 min read
What Is Gemini API Cost Control on Vertex AI?
Gemini API cost control on Vertex AI is the discipline of metering, shaping, and capping what you pay for generative inference. Gemini is billed per 1M tokens, with separate rates for input and output and different price points per model (flash-lite, flash, pro) and modality (text, image, audio); gemini-2.5-pro additionally tiers up for prompts above 200K tokens. Because the meter runs on tokens — not requests — cost control means measuring token usage per call, choosing the cheapest model that does the job, bounding generation length, caching repeated input, and wrapping everything in billing budget alerts and consumer quota overrides so a bug or retry storm cannot silently multiply spend.
Think of it like managing a mobile phone plan with per-MB roaming rates. The data meter (tokens) runs whether you watch it or not, downloading 4K video (gemini-2.5-pro) costs far more than text (flash-lite) for the same message, and the only things that save you from a horror bill are an itemized usage log (usage_metadata), picking wifi over roaming (model routing), and a hard carrier-side cap that cuts you off at a limit (consumer quota override) — because the app-store receipt (budget alert email) always arrives after the money is spent.
| Concept | Explanation | When to use |
|---|---|---|
| Token-Based Billing | Charges are per 1M input and output tokens, priced differently per model and modality. | Always — every cost decision starts from knowing input vs output token volume per feature. |
| usage_metadata | Per-response token counters (prompt_token_count, candidates_token_count) returned by the API. | Log it on every call with a feature label; it is the raw material for per-feature cost attribution. |
| Model Routing | Sending each request to the cheapest model that meets the quality bar for that task. | Classification and extraction go to flash-lite, default chat/summary to flash, hard reasoning to pro. |
| Budget Alert | Cloud Billing budget with threshold rules (50/90/100%) that fires notifications to email, channels, or Pub/Sub. | On every project from day one — wire it to Pub/Sub or chat, not just an inbox nobody reads. |
| Consumer Quota Override | A self-imposed cap that lowers an API quota (e.g. requests per minute) below the Google-set limit. | As a hard guardrail on dev/staging projects and any workload where a retry loop could burn budget. |
| Batch Prediction | Asynchronous bulk inference from JSONL in Cloud Storage, priced at roughly half the online rate. | Nightly scoring, backfills, evaluations — anything that does not need a sub-second response. |
Why Does Gemini Spend Get Out of Control?
Teams treat the Gemini API like a fixed-price API and discover the real pricing model on the invoice: output tokens cost roughly 4-8x more than input tokens, so one chatty system prompt with an uncapped max_output_tokens can multiply spend overnight; a 429 retry loop quietly replays the same paid request hundreds of times; grounding fees accumulate outside token metrics entirely; and the default budget alert lands as an email three days after the damage is done. Without per-feature token logging you cannot even answer which of your five features spent the money.
This guide builds the full cost-control loop: measure tokens before and after every call, route tasks to the cheapest sufficient model, cap output length, cache repeated system prompts, then enforce it with a Terraform-managed budget that publishes to Pub/Sub and a consumer quota override that physically caps request rate. If you are still wiring up the API itself, start with [Gemini API on Vertex AI: Setup, IAM & Quotas](/tutorial/gemini-api-vertex-ai-setup-iam-quotas-python); if your workload uses grounding, note that [RAG on Vertex AI with Vector Search and grounding](/tutorial/rag-vertex-ai-vector-search-embeddings-grounding-terraform) adds per-query grounding fees on top of token costs.
| Feature | thisService | altA | altB |
|---|---|---|---|
| Cost visibility | Per-call usage_metadata logged per feature, queried in BigQuery | Monthly invoice only (no attribution) | Third-party LLM proxy/gateway metering |
| Hard spend guardrail | Consumer quota override caps requests/min natively | Budget alert email after money is spent | Proxy rate limits (extra infra to run) |
| Offline workload pricing | Batch prediction ~50% discount, native | Full online price | Varies; some providers discount batch too |
| Repeated prompt cost | Context caching bills repeated input at reduced rate | Full input price on every call | Provider-dependent |
| Alerting automation | Budget -> Pub/Sub -> Cloud Run/Function actions via Terraform | Manual email triage | Webhook integrations |
Prerequisites
- GCP project with billing enabled and the Vertex AI API (aiplatform.googleapis.com) enabled
- gcloud CLI v450.0+ and Python 3.10+ installed and authenticated
- IAM roles: roles/aiplatform.user on the project, plus roles/billing.admin (or roles/billing.costsManager) on the billing account for budget creation
- Python package: google-genai>=1.0 (pip install google-genai)
- Terraform v1.5+ with the google provider v5.x configured
- Your billing account ID (gcloud billing accounts list)
Step-by-Step Guide
Step 1: Measure Tokens Before You Optimize
Use count_tokens to price a prompt before sending it, and log response.usage_metadata (prompt_token_count, candidates_token_count) on every call with a feature label — this is the metering layer every other control depends on. You cannot attribute or shrink spend you do not measure. count_tokens tells you what a request will cost before you pay for it; usage_metadata tells you what you actually paid, per feature, so the invoice stops being a mystery.
from google import genai
client = genai.Client(vertexai=True, project="PROJECT_ID", location="europe-west1")
MODEL = "gemini-2.5-flash"
prompt = "Summarize this support ticket in one sentence: <ticket>...</ticket>"
# Pre-flight: what will the INPUT cost?
count = client.models.count_tokens(model=MODEL, contents=prompt)
print(f"Input tokens (pre-flight): {count.total_tokens}")
response = client.models.generate_content(model=MODEL, contents=prompt)
# Post-call: what did we actually burn?
u = response.usage_metadata
print(f"prompt={u.prompt_token_count} candidates={u.candidates_token_count} "
f"total={u.total_token_count}")
Step 2: Route Each Task to the Cheapest Sufficient Model
Build a thin routing wrapper that sends cheap tasks (classification, extraction, routing) to gemini-2.5-flash-lite, default work to gemini-2.5-flash, and reserves gemini-2.5-pro for requests that genuinely need deep reasoning. Input pricing differs by ~12x between flash-lite (~$0.10/1M in) and pro (~$1.25/1M in), and output by ~25x (~$0.40 vs ~$10 per 1M, approximate as of mid-2026). Running everything on pro is the most common way teams overpay for Gemini by an order of magnitude.
from google import genai
client = genai.Client(vertexai=True, project="PROJECT_ID", location="europe-west1")
MODEL_MAP = {
"classify": "gemini-2.5-flash-lite", # cheap, high volume
"default": "gemini-2.5-flash", # balanced workhorse
"reason": "gemini-2.5-pro", # hard problems only
}
def generate(task: str, prompt: str) -> str:
model = MODEL_MAP[task]
resp = client.models.generate_content(model=model, contents=prompt)
u = resp.usage_metadata
print(f"[{task}] model={model} in={u.prompt_token_count} "
f"out={u.candidates_token_count}")
return resp.text
generate("classify", "Intent of: 'where is my invoice?'")
generate("default", "Summarize the attached quarterly report.")
generate("reason", "Prove whether this distributed lock is safe under partitions.")
Step 3: Cap Output Tokens and Cache Repeated Prompts
Set max_output_tokens on every request so generation length is bounded, and use context caching for large repeated system prompts or reference documents so repeated input tokens are billed at a reduced rate instead of full price every call. Output tokens are the expensive half of the bill (~4-8x input rates). A runaway generation or an agent loop without an output cap is the classic five-figure surprise. Context caching directly shrinks the input side when the same 50K-token document prefixes thousands of calls.
from google import genai
from google.genai import types
client = genai.Client(vertexai=True, project="PROJECT_ID", location="europe-west1")
# 1) Hard cap on generation length — always.
cfg = types.GenerateContentConfig(max_output_tokens=512, temperature=0.2)
resp = client.models.generate_content(
model="gemini-2.5-flash",
contents="Summarize the attached incident timeline.",
config=cfg,
)
# 2) Cache a large repeated prompt (e.g. a policy manual).
cache = client.caches.create(
model="gemini-2.5-flash",
config=types.CreateCachedContentConfig(
contents=[types.Content(role="user", parts=[
types.Part.from_text(text=open("policy_manual.txt").read())
])],
ttl="3600s",
),
)
resp2 = client.models.generate_content(
model="gemini-2.5-flash",
contents="Does this expense claim comply with section 4.2?",
config=types.GenerateContentConfig(cached_content=cache.name),
)
Step 4: Deploy Budget Alerts with Terraform
Create a Cloud Billing budget scoped to the project with threshold rules at 50%, 90%, and 100%, routed to a monitoring notification channel (email) and a Pub/Sub topic for automation — all in Terraform so every project gets it by default. The console-created budget that emails one founder is how bills get noticed on the credit card statement. Terraform makes the budget a non-optional part of project provisioning, and Pub/Sub delivery enables automated reactions (Slack alert, disable API key, throttle the caller).
resource "google_pubsub_topic" "budget_alerts" {
name = "billing-budget-alerts"
project = var.project_id
}
resource "google_monitoring_notification_channel" "finops_email" {
display_name = "FinOps on-call email"
type = "email"
labels = { email_address = "[email protected]" }
}
resource "google_billing_budget" "vertex_ai_monthly" {
billing_account = var.billing_account_id
display_name = "vertex-ai-monthly-${var.project_id}"
budget_filter {
projects = ["projects/${data.google_project.current.number}"]
}
amount {
specified_amount {
currency_code = "USD"
units = "200"
}
}
threshold_rules { threshold_percent = 0.5 }
threshold_rules { threshold_percent = 0.9 }
threshold_rules { threshold_percent = 1.0 }
all_updates_rule {
monitoring_notification_channels = [
google_monitoring_notification_channel.finops_email.id,
]
pubsub_topic = google_pubsub_topic.budget_alerts.id
}
}
data "google_project" "current" {}
Step 5: Add a Consumer Quota Override as a Hard Cap
Lower the Vertex AI generate-content request quota below the Google-set limit with a consumer override, so a retry storm or leaked key physically cannot exceed your chosen request rate. Budget alerts are reactive — they tell you money was spent. A consumer quota override is preventive: requests past your cap are rejected with 429 before they cost anything. It is the only control that bounds worst-case spend instead of reporting it.
# Cap generate-content requests at 60 per minute for this project
gcloud alpha service-quotas consumer-overrides create \
--service=aiplatform.googleapis.com \
--metric=aiplatform.googleapis.com/generate_content_requests \
--unit=1/{min}/{project} \
--force --value=60 \
--project=$PROJECT_ID
# Verify the override
gcloud alpha service-quotas consumer-overrides list \
--service=aiplatform.googleapis.com \
--metric=aiplatform.googleapis.com/generate_content_requests \
--unit=1/{min}/{project} --project=$PROJECT_ID
Step 6: Build a Per-Feature Cost Dashboard in BigQuery
Export the structured token logs from step 1 to BigQuery (log sink or direct insert) and query cost per feature by joining token counts against the per-1M price table. Aggregate invoice numbers cannot answer 'which feature burned the budget'. Per-feature attribution is what lets you route, cap, or kill the expensive 5% of traffic instead of blanket-throttling everyone.
-- Cost per feature, current month (prices approximate, per 1M tokens)
WITH prices AS (
SELECT 'gemini-2.5-flash-lite' AS model, 0.10 AS in_price, 0.40 AS out_price
UNION ALL SELECT 'gemini-2.5-flash', 0.30, 2.50
UNION ALL SELECT 'gemini-2.5-pro', 1.25, 10.00
)
SELECT
l.feature,
l.model,
COUNT(*) AS requests,
SUM(l.input_tokens) AS input_tokens,
SUM(l.output_tokens) AS output_tokens,
ROUND(SUM(l.input_tokens) / 1e6 * p.in_price +
SUM(l.output_tokens) / 1e6 * p.out_price, 2) AS est_cost_usd
FROM `PROJECT_ID.gemini_usage.token_logs` l
JOIN prices p USING (model)
WHERE l.ts >= TIMESTAMP_TRUNC(CURRENT_TIMESTAMP(), MONTH)
GROUP BY feature, model
ORDER BY est_cost_usd DESC;
Step 7: Move Offline Workloads to Batch Prediction
Convert non-interactive jobs (nightly scoring, evaluations, backfills) from online generate_content calls to batch prediction running from JSONL in Cloud Storage, at roughly half the per-token price. Batch prediction on Vertex AI is priced at approximately a 50% discount versus online inference (as of mid-2026 — verify current pricing). Any request that does not need a live response is money left on the table at online rates.
from google import genai
from google.genai import types
client = genai.Client(vertexai=True, project="PROJECT_ID", location="europe-west1")
# Input: one JSON request per line in gs://PROJECT_ID-batch/input/tickets.jsonl
job = client.batches.create(
model="gemini-2.5-flash",
src="gs://PROJECT_ID-batch/input/tickets.jsonl",
config=types.CreateBatchJobConfig(
dest="gs://PROJECT_ID-batch/output/",
),
)
print("Batch job:", job.name, "state:", job.state)
Verification & Health Check
Best Practices
- Always Cap max_output_tokens
- Log usage_metadata on Every Call
- Budget Alerts Go to Pub/Sub, Not Just Email
- Retry with Backoff and a Budget, Not Blindly
Common Mistakes
- {"errorCode":"BUDGET_BLOWUP_UNCAPPED_OUTPUT","symptoms":"Invoice shows output-token charges 5-10x input-token charges; some responses are thousands of tokens of repetition.","rootCause":"max_output_tokens never set, so degenerate generations (repetition loops, runaway agents) bill at the high output rate with no ceiling.","fixCommand":"python -c \"from google.genai import types; print(types.GenerateContentConfig(max_output_tokens=512))\"\n","code":"config = types.GenerateContentConfig(max_output_tokens=512)\nresp = client.models.generate_content(model=MODEL, contents=prompt, config=config)\nif resp.candidates[0].finish_reason.name == \"MAX_TOKENS\":\n log.warning(\"truncated response; consider raising cap for this feature\")\n","language":"python","filename":"fix-output-cap.py","prevention":"Make max_output_tokens a required parameter of your internal generate wrapper so no call site can omit it."}
- {"errorCode":"RETRY_STORM_SPEND","symptoms":"429 RESOURCE_EXHAUSTED errors in logs alongside a matching spike in billed requests; daily spend 3-5x normal.","rootCause":"An unbounded retry loop replays paid requests against an exhausted quota; each retry is billed as a new request even though none succeed.","fixCommand":"gcloud alpha service-quotas consumer-overrides create \\\n --service=aiplatform.googleapis.com \\\n --metric=aiplatform.googleapis.com/generate_content_requests \\\n --unit=1/{min}/{project} --force --value=60 --project=$PROJECT_ID\n","prevention":"Bound retries (max 5, exponential backoff with jitter), alert on 429 rate, and keep a consumer quota override as the physical backstop."}
- {"errorCode":"GROUNDING_FEES_INVISIBLE","symptoms":"Invoice exceeds the token-based cost estimate by a consistent margin; usage dashboards show nothing anomalous.","rootCause":"Grounding with Google Search bills per grounded query after the ~1,500/day free tier — a per-query fee that does not appear in usage_metadata token counts.","fixCommand":"gcloud logging read 'jsonPayload.grounding_used=true' --project=$PROJECT_ID \\\n --freshness=7d --format=\"value(timestamp)\" | wc -l\n","prevention":"Log a grounding_used flag whenever you enable the Google Search tool, and include the per-1K grounding fee in your cost model for grounded features."}
- {"errorCode":"BUDGET_ALERT_NEVER_READ","symptoms":"Budget threshold emails sit unread in a shared inbox; the 100% alert is discovered on the credit card statement.","rootCause":"Budget created with email-only notification and no owner; alerts have no automated or on-call path.","fixCommand":"terraform apply -target=google_billing_budget.vertex_ai_monthly\n","code":"all_updates_rule {\n monitoring_notification_channels = [google_monitoring_notification_channel.finops_email.id]\n pubsub_topic = google_pubsub_topic.budget_alerts.id\n}\n","language":"hcl","filename":"fix-budget-channels.tf","prevention":"Provision budgets only via Terraform with both a monitored notification channel and a Pub/Sub topic; subscribe something that pages a human."}
- {"errorCode":"WRONG_METRIC_QUOTA_OVERRIDE","symptoms":"Consumer override command succeeds (or no-ops) but request rate is never capped; 429s never fire at the expected value.","rootCause":"Metric/unit strings typed from memory don't match the actual aiplatform quota metric — per-model and per-region quotas have distinct metric names.","fixCommand":"gcloud alpha service-quotas list --service=aiplatform.googleapis.com \\\n --project=$PROJECT_ID --filter=\"metric:generate_content\"\n","prevention":"Copy the metric and unit verbatim from the console Quotas page, and verify with consumer-overrides list after creating."}
Cost Analysis
| Feature | metric | cost1k | cost10k | cost100k | cost1m |
|---|---|---|---|---|---|
| gemini-2.5-flash-lite ($0.10 in / $0.40 out per 1M) | $0.17 | $1.70 | $17.00 | $170.00 | |
| gemini-2.5-flash ($0.30 in / $2.50 out per 1M) | $0.90 | $9.00 | $90.00 | $900.00 | |
| gemini-2.5-pro, <=200K prompt ($1.25 in / $10 out per 1M) | $3.63 | $36.25 | $362.50 | $3,625.00 | |
| gemini-2.5-flash via batch prediction (~50% off) | $0.45 | $4.50 | $45.00 | $450.00 | |
| Routed mix (70% flash-lite / 25% flash / 5% pro) | $0.53 | $5.25 | $52.50 | $525.00 |