◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-06-21Advanced

Track Gemini API Costs in Production with usageMetadata — A Per-Request Logging Pattern That Reconciles With Your Bill

A production pattern for capturing Gemini API's usageMetadata per request to attribute spend by endpoint, user, and model — hardened for the 3.5 Flash GA era where the default model can shift under you. Covers pricing keyed on resp.model_version and a nightly audit that flags model drift and unknown models before the invoice does.

gemini-api286cost-management8usage-metadataproduction140python108billing5

✦ Premium Article

About six months into using the Gemini API in a side project, I opened a Google Cloud bill that was twice what I had budgeted for. Drilling into the console only got me as far as the daily aggregate — there was no way to see which endpoint, which user, or which model had eaten the budget. The real culprit, I realized, was that I had never bothered to record usageMetadata from each response.

usageMetadata is an unassuming field that ships with every Gemini API response. Logging it per request gives you near-perfect attribution of every dollar after the fact. This article shares the exact pattern I now run in production, including how to reconcile your aggregated logs against the actual Google Cloud invoice.

Know every field inside usageMetadata

The first thing that throws people off is how many fields usageMetadata actually has. Print a response from the Python SDK (google-genai) and you'll see this:

from google import genai
 
client = genai.Client(api_key="YOUR_API_KEY")
resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Explain Gemini API cost tracking in 2 sentences.",
)
 
print(resp.usage_metadata)
# UsageMetadata(
#   prompt_token_count=15,
#   candidates_token_count=84,
#   cached_content_token_count=0,
#   thoughts_token_count=0,
#   tool_use_prompt_token_count=0,
#   total_token_count=99
# )

Here's what each field actually represents:

  • prompt_token_count — Total input tokens, including system instructions and chat history
  • cached_content_token_count — Tokens served from Context Caching (typically priced at ~25% of regular input)
  • candidates_token_count — Output tokens generated by the model (sum across all candidates if candidate_count > 1)
  • thoughts_token_count — Thinking tokens emitted by Gemini 2.5 series; billed at the same rate as output tokens
  • tool_use_prompt_token_count — Input tokens consumed internally by Function Calling or Code Execution
  • total_token_count — Sum of the above

The crucial point: you cannot compute the bill from total_token_count alone. The same 1,000 tokens cost wildly different amounts depending on whether they were input, output, cached, or thinking — so you have to log each component separately.

Append every call to a JSONL file

The pattern I run in production is intentionally boring: a thin wrapper around generate_content that appends usageMetadata to a JSONL file before returning. The "save now, aggregate later" mindset is what saves the most time at month-end.

import json
import time
from pathlib import Path
from google import genai
 
LOG_PATH = Path("logs/gemini_usage.jsonl")
LOG_PATH.parent.mkdir(exist_ok=True)
client = genai.Client(api_key="YOUR_API_KEY")
 
def call_gemini_logged(model: str, prompt: str, *, user_id: str, endpoint: str):
    """Wrapper that always writes usageMetadata to JSONL before returning."""
    started = time.time()
    resp = client.models.generate_content(model=model, contents=prompt)
    um = resp.usage_metadata
 
    record = {
        "ts": time.time(),
        "elapsed_ms": int((time.time() - started) * 1000),
        "user_id": user_id,
        "endpoint": endpoint,
        "model": model,
        "prompt": um.prompt_token_count,
        "cached": um.cached_content_token_count or 0,
        "candidates": um.candidates_token_count or 0,
        "thoughts": um.thoughts_token_count or 0,
        "tool_use": um.tool_use_prompt_token_count or 0,
        "total": um.total_token_count,
    }
    with LOG_PATH.open("a", encoding="utf-8") as f:
        f.write(json.dumps(record) + "\n")
 
    return resp
 
# Example
resp = call_gemini_logged(
    model="gemini-2.5-flash",
    prompt="Summarise today's weather in one line",
    user_id="u_001",
    endpoint="/api/weather/summary",
)
print(resp.text)

JSONL is the right choice for two reasons. First, append writes are nearly atomic, so multiple processes can write without corrupting each other. Second, loading the file later into SQLite or DuckDB is a one-liner with read_json_auto().

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Re-key cost attribution on resp.model_version (the model that actually served the request) so totals stay aligned with the bill even when the default model changes
✦A nightly audit that raises on price-table-missing models instead of silently falling back to Pro pricing
✦Track the share of responses served by a model other than the one requested, catching default-model swaps the day they happen
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◈ API / SDK2026-10-16
The Day /models Returns 200 but generateContent Returns 403 — Decide Your Triage Order Before It Happens
A triage order for the case where listing models succeeds with 200 but generation fails with 403 permission_denied. Includes a small diagnostic script that checks key, project, and billing in turn, why you should not retry a 403, and how to assemble what support needs.
◈ API / SDK2026-07-15
Sample what you already accepted — an audit budget that catches silent quality drift
A confidence gate only ever looks at output the model hesitated on. Silent drift sinks into the batch that sailed through. Working from a fixed 30-minutes-a-day review budget, this walks through deriving detection time from the binomial, reallocating the same budget across risk strata, and catching slow decay with a cumulative monitor.
◈ API / SDK2026-07-04
When Gemini API Leaks Japanese Into Your English Output Once in a While — Field Notes on Measuring the Contamination Rate and Tightening It in Stages
You told Gemini to answer in English, and 3 out of 100 runs slip a Japanese sentence into the tail. Here is why you cannot stop that 'once in a while', and a production pattern that measures the contamination rate as an SLO and tightens it with graded recovery, with working code.