GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-05-01Intermediate

Why count_tokens Lies: 5 Reasons Your Gemini API Bill Is Higher Than You Estimated — A Reconciliation Playbook

count_tokens said 1,200 tokens. Cloud Console billed you for 4,800. I made the same mistake building my first indie app on Gemini. This guide walks through the five hidden contributors — thinking, tools, multimodal, history, caching — and how to reconcile them with reproducible code.

gemini-api277count-tokensbilling4cost-management8troubleshooting82

"count_tokens() returned 1,200 tokens, so I built my monthly cost model around that. Two months later, the Cloud Console showed nearly four times the usage I budgeted for." That was me, in my second month of shipping a Gemini-powered indie app. I only noticed when the free-tier overage email landed.

The thing is: count_tokens() is a useful tool, but the number it returns rarely matches the billed token count in usage_metadata.total_token_count. About five distinct factors inflate the real charge, and most cost-modeling spreadsheets I see online quietly ignore four of them. In this article, I'll walk you through each contributor with reproducible code and finish with a template that gets your forecasts within striking distance of the actual bill.

Step zero: observe the gap precisely

Before chasing causes, put the two numbers side by side in a single script. Comparing the count_tokens() estimate with usage_metadata from the same prompt makes every later step easier — no more "I think it's higher" hand-waving.

# tools/compare_token_count.py
# Goal: compare the count_tokens estimate with the actual usage_metadata in one place
import os
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
MODEL = "gemini-2.5-flash"
 
def compare(prompt: str, system_instruction: str | None = None, tools: list | None = None):
    config = types.GenerateContentConfig(
        system_instruction=system_instruction,
        tools=tools,
    )
 
    # 1. Pre-flight estimate (free tokenizer call, no billing)
    estimated = client.models.count_tokens(model=MODEL, contents=prompt).total_tokens
 
    # 2. Real request
    res = client.models.generate_content(model=MODEL, contents=prompt, config=config)
    usage = res.usage_metadata
 
    print(f"prompt        : {prompt[:40]}...")
    print(f"count_tokens  : {estimated}")
    print(f"usage prompt  : {usage.prompt_token_count}")
    print(f"usage thoughts: {getattr(usage, 'thoughts_token_count', 0)}")
    print(f"usage output  : {usage.candidates_token_count}")
    print(f"usage TOTAL   : {usage.total_token_count}")
    print(f"diff (est→real): {usage.total_token_count - estimated:+d}")
 
compare("Briefly explain how Gemini API token billing works.")

Run this against your real workload before going any further. The bigger the gap, the more likely one of the five contributors below is in play.

Cause 1: thinking models bill "thought tokens" as a separate line item

The first thing to internalize: Gemini 2.5 thinking models (gemini-2.5-pro / gemini-2.5-flash) emit a separate thoughts_token_count that is billed independently. count_tokens() only measures input tokens, so this category is invisible to your forecast by default.

In practice, on long reasoning tasks I've seen thought tokens come in at 2–3x the visible output. Pro charges them at the output rate, so missing this contributor alone can multiply your bill by 2–3x.

# Practical knobs to keep thought tokens predictable
config = types.GenerateContentConfig(
    thinking_config=types.ThinkingConfig(
        thinking_budget=512,     # Hard cap
        include_thoughts=False,  # Don't return summaries
    ),
)

In chat-style apps I cap thinking_budget at 512 to keep monthly variance compressed. For coding and analysis workloads that's too aggressive — quality drops noticeably — so I keep separate presets per use case rather than one global value. The full sizing approach is in the Gemini 2.5 thinking budget optimization guide.

The sneaky part is that thought tokens scale with task complexity, not with output length. A 50-word answer to a hard reasoning question can carry 2,000 thought tokens behind it. If you build cost forecasts purely from observed output lengths, you'll consistently underestimate thinking-model bills. Whenever I onboard a team to Gemini 2.5, the first dashboard I have them build is "thoughts vs output ratio per route" — that single chart usually surfaces an overlooked endpoint within the first day.

Cause 2: tool definitions are an "invisible prompt" that's billed

This one catches a lot of people. When you pass tools=[my_function], that function's schema is sent as part of the prompt and counted toward prompt_token_count. count_tokens() only inspects contents, so tool tokens fall outside its view.

# Demonstration: tool definitions inflate billed prompt tokens
def get_weather(location: str) -> str:
    """Return the current weather for the given city."""
    return "sunny"
 
# Compare with-tools vs no-tools
for has_tools in [False, True]:
    cfg = types.GenerateContentConfig(tools=[get_weather] if has_tools else None)
    r = client.models.generate_content(
        model=MODEL,
        contents="Should I head out today?",
        config=cfg,
    )
    label = "with tools" if has_tools else "no tools"
    print(f"{label}: prompt_tokens={r.usage_metadata.prompt_token_count}")

In my own measurements, each function adds roughly 50–120 tokens of overhead per request, mostly driven by the length of the schema's description fields. Keep tool definitions short and lean — every adjective you trim is a few thousand tokens of monthly savings on a high-traffic endpoint. For more on the system-instruction / tool-definition split, see seven reasons Gemini API ignores your system instruction.

Cause 3: images, video, and audio are billed by tile, not by character

Multimodal inputs are where forecasts go furthest off the rails. Text scales roughly with byte length, but images, video, and audio each follow their own formula.

These are the rough numbers I keep on a sticky note:

  • Images: ~258 tokens flat for anything up to 768×768. Above that the image is split into 768px tiles and you pay 258 tokens per tile.
  • Video: about 263 tokens per second on Gemini 2.5 Flash, varying with resolution.
  • Audio: about 32 tokens per second.

count_tokens(contents=[image_part]) does support multimodal parts, but URI-based files require uploading through the File API first. Long videos and high-resolution images can each add thousands of tokens, so always pass the actual multimodal parts when you call count_tokens().

# Multimodal pre-flight estimate
import pathlib
from google.genai import types
 
img = types.Part.from_bytes(
    data=pathlib.Path("./screenshot.png").read_bytes(),
    mime_type="image/png",
)
prompt_parts = [img, "List three likely error causes from this screen."]
 
est = client.models.count_tokens(model=MODEL, contents=prompt_parts).total_tokens
print(f"multimodal estimated: {est}")

The full multimodal cost-tuning playbook is in optimizing multimodal input cost on Gemini API.

Cause 4: system instruction and chat history are resent on every turn

This is the one I see overlooked the most. system_instruction and any chat history you maintain are resent in full on every request, so you pay for them every turn.

A "200-token system instruction plus 200-turn conversation" chat looks innocent until you realize the 200th turn ships every prior message back to the API. To get a realistic estimate, you have to call count_tokens() with the same contents list (history + new message) you'll actually send.

# Realistic estimate that includes chat history
history = [
    types.Content(role="user", parts=[types.Part.from_text("first question")]),
    types.Content(role="model", parts=[types.Part.from_text("first answer")]),
    # ... longer histories mean larger billed prompts every turn
]
new_user_msg = types.Content(role="user", parts=[types.Part.from_text("follow-up question")])
contents = history + [new_user_msg]
 
est = client.models.count_tokens(model=MODEL, contents=contents).total_tokens
print(f"with history: {est}")

In production, naively appending every turn breaks down quickly on cost. My default pattern is rolling summarization: keep the last N turns verbatim, replace earlier turns with a model-generated summary. The savings are visible in the first week and the implementation is approachable for solo developers.

A useful sanity check: write down the maximum conversation length you actually want to support, multiply that by your average per-turn token count, and see what count_tokens() returns for the resulting payload. If the worst-case number is higher than your monthly budget tolerates, decide on a truncation strategy before launch — not after the first user has a 300-turn conversation that costs more than their subscription. I learned this the hard way: my first chat-style indie app had a single power user whose 280-turn conversation accounted for nearly 18% of that month's API bill.

Cause 5: Context Caching introduces a separate billed line at a discounted rate

The final landmine is Context Caching. When you use cached_content, usage_metadata gains a cached_content_token_count field, and those tokens bill at roughly 25% of the normal input rate (it varies by model).

That means your forecast is wrong in both directions if you treat tokens uniformly. If you forget about the cached portion, you over-estimate; if you assume cache pricing for everything, you under-estimate. A correct forecast separates the two pools.

# Always re-check pricing on the official page; values shown are illustrative
PRICE_INPUT = 0.30 / 1_000_000        # e.g. $0.30 / 1M tokens
PRICE_CACHED_INPUT = 0.075 / 1_000_000 # e.g. $0.075 / 1M tokens
PRICE_OUTPUT = 2.50 / 1_000_000        # e.g. $2.50 / 1M tokens
PRICE_THOUGHTS = 2.50 / 1_000_000      # thinking models: same as output
 
def estimate_cost(usage) -> float:
    cached = getattr(usage, "cached_content_token_count", 0) or 0
    fresh_input = usage.prompt_token_count - cached
    thoughts = getattr(usage, "thoughts_token_count", 0) or 0
    output = usage.candidates_token_count
    return (
        fresh_input * PRICE_INPUT
        + cached * PRICE_CACHED_INPUT
        + output * PRICE_OUTPUT
        + thoughts * PRICE_THOUGHTS
    )

The full caching design is covered in Context Caching cost optimization on Gemini API.

A "production-shaped estimate" template

Pulling all five causes together, this is the template I now stand up at the start of every Gemini-based project before I commit to a pricing plan. Even for small indie apps, doing this once before you ship saves a lot of late-night Console anxiety.

# tools/realistic_estimate.py
# Goal: count_tokens with the full payload, then correct for the 5 hidden contributors
from dataclasses import dataclass
 
@dataclass
class CostEstimate:
    prompt_tokens: int
    expected_output: int
    expected_thoughts: int
    cached_tokens: int
 
    def total_cost(self) -> float:
        fresh = self.prompt_tokens - self.cached_tokens
        return (
            fresh * PRICE_INPUT
            + self.cached_tokens * PRICE_CACHED_INPUT
            + self.expected_output * PRICE_OUTPUT
            + self.expected_thoughts * PRICE_THOUGHTS
        )
 
# Usage
prompt_tokens = client.models.count_tokens(
    model=MODEL,
    contents=full_contents_with_history_and_tools_and_media,
).total_tokens
 
estimate = CostEstimate(
    prompt_tokens=prompt_tokens,
    expected_output=400,    # 95th percentile from logs
    expected_thoughts=600,  # measured value if using a thinking model
    cached_tokens=0,         # only set when cache is used
)
print(f"per request: ${estimate.total_cost():.6f}")

Start with educated guesses for the "expected" fields. Once you have a week of production logs, replace them with the 95th percentile observed in real traffic — at that point, you'll be able to predict the monthly bill within a few percent.

Putting it together: a worked example

Here's a worked example from one of my own apps — a multimodal note-taking app where a user uploads a photo of a whiteboard along with a short request like "summarize this into action items."

A naive count_tokens() on the text part returned 22 tokens. The actual prompt_token_count came back at 1,068 — nearly 50x the estimate. Walking through the contributors:

  • The whiteboard photo was 1536x1152, which split into four 768px tiles, adding 4 × 258 = 1,032 tokens.
  • The system instruction (about 80 tokens) is invisible to count_tokens() when passed via config.system_instruction.
  • A small tools=[create_action_item] definition added another 90 tokens of schema overhead.

Add 22 + 1,032 + 80 + 90 and you land at roughly 1,224 — still 156 tokens short of the real number, which I attribute to internal framing tokens added by the API. That gap is small enough to model with a flat 5–10% buffer, and the structure of the estimate now matches reality. Without doing this exercise, my pricing page would have been mis-calibrated by an order of magnitude.

The lesson I took away: the moment your app uses any combination of multimodal input, system instructions, tools, or thinking, count_tokens() becomes a starting point — not the answer. Build a thin wrapper that runs the full configuration through the API, and base your forecasts on what comes out of usage_metadata over your first week of real traffic.

Three misconceptions I keep hearing

The first is "if I set a budget alert based on count_tokens() results, I'm safe." That alert misses every contributor above and is anchored to a number that's much smaller than the real bill — you'll blow past it midway through the month. Anchor budget alerts on accumulated usage_metadata.total_token_count instead.

The second is "I'm on the free tier, so I don't need to think about cost yet." The free tier rate-limits you with 429 errors, but the moment you upgrade, all five contributors above turn into real money simultaneously. The single most common support thread I see is "I migrated to paid and my budget exploded the same week."

The third is "if I disable thinking, thought tokens stop being billed." On gemini-2.5-flash, you have to set thinking_config.thinking_budget=0 explicitly. Without that flag, the model still spends a small budget internally and you still get billed. Make this knob explicit in code, not implicit.

Where to go from here

The fastest first step is to log usage_metadata from every production request and store it for at least a week. Once you have that data, the ratio between count_tokens() predictions and actual total_token_count will stabilize, and you can start applying correction factors per route or per use case. For deeper instrumentation patterns — including dashboards and per-user cost tracking — pair this article with tracking Gemini API cost via usage_metadata.

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 →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-06-21
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.
API / SDK2026-05-04
Gemini API FAILED_PRECONDITION Error: Case-by-Case Diagnosis and Fixes
FAILED_PRECONDITION in Gemini API means 'the current system state doesn't allow this operation.' Learn to diagnose and fix all common causes: billing setup, API enablement, context cache expiry, and model access restrictions.
API / SDK2026-04-01
Gemini API Pricing & Billing [2026]: From Free Tier to Token Costs Explained
A clear breakdown of Gemini API pricing in 2026 — free tier limits, token-based billing, model cost comparisons, usage estimation, and spend cap setup to keep your costs under control.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →