Skip to content

The countTokens Method in the Gemini API

9 min read · updated August 11, 2026

countTokens answers one question — how many input tokens is this request — before you spend anything generating from it. It is the only reliable way to size a Gemini prompt, because the tokenizer is server-side and the multimodal parts are not countable locally at all.

What countTokens is for

Three jobs, in rough order of how often they matter:

  • Fitting the context window. A request over the model’s input limit fails rather than truncating. Counting first lets you drop history or chunks deterministically instead of catching an error.
  • Estimating spend before committing. Input tokens are the half of the bill you know in advance. Output you can only bound in advance with maxOutputTokens.
  • Checking a cache threshold. Context caching has a documented minimum token count; countTokens tells you whether a candidate prefix clears it.

The method is documented in Google’s Gemini API reference for tokens. It is a separate RPC on the model resource, not a flag on generateContent.

Calling it

  1. Pick the exact model id you are going to generate with. Tokenization is per-model. Counting against gemini-2.5-flash and generating with a different family is a silent mismatch.
  2. POST to the :countTokens method on that model, with a body that is the same contents array you intend to send:
    curl "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:countTokens" \
      -H "x-goog-api-key: $GEMINI_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "contents": [
          {"role": "user",  "parts": [{"text": "Summarise the attached contract."}]},
          {"role": "model", "parts": [{"text": "Which clauses matter most to you?"}]},
          {"role": "user",  "parts": [{"text": "Termination and liability."}]}
        ]
      }'
  3. Read totalTokens from the response and compare it against the model’s documented input limit before issuing the generation call.
  4. Branch on the number. Over the limit, trim. Comfortably over the cache minimum and reused across requests, consider declaring an explicit cache. Otherwise, generate.

The same call in the Google Gen AI SDK for Python, which is the form most application code will actually take:

from google import genai

client = genai.Client()          # reads GEMINI_API_KEY from the environment

resp = client.models.count_tokens(
    model="gemini-2.5-flash",
    contents=history,            # the same object you pass to generate_content
)
print(resp.total_tokens)

The response shape

The response is a CountTokensResponse. The field you want is totalTokens, an integer covering everything you passed in:

{
  "totalTokens": 31,
  "promptTokensDetails": [
    { "modality": "TEXT", "tokenCount": 31 }
  ]
}

promptTokensDetails breaks the total down by modality, which is the field to read when a request mixes text with images, audio or a PDF and you want to know which part is expensive. The modality values mirror the ones used in usageMetadata on a generation response — TEXT, IMAGE, AUDIO, VIDEO,DOCUMENT.

countTokens returns input tokens only. There is no way to know the output count before generating, because it depends on what the model writes. Bound it with maxOutputTokens and treat that bound as the worst case when budgeting.

Counting a real request, not a string

The common mistake is counting the user message and forgetting the rest of what gets billed. A system instruction is input tokens. Tool declarations are input tokens, and a large set of function schemas is not small. Anything you attach — an inline image, a file URI, a PDF — is input tokens.

countTokens accepts the whole request, so give it the whole request:

{
  "systemInstruction": {
    "parts": [{"text": "You are a contracts analyst. Answer only from the document."}]
  },
  "contents": [
    {"role": "user", "parts": [
      {"text": "Summarise the termination clause."},
      {"fileData": {"mimeType": "application/pdf",
                    "fileUri": "https://generativelanguage.googleapis.com/v1beta/files/abc123"}}
    ]}
  ],
  "tools": [{
    "functionDeclarations": [{
      "name": "lookup_precedent",
      "description": "Find comparable clauses in the precedent library.",
      "parameters": {
        "type": "OBJECT",
        "properties": {"clause_type": {"type": "STRING"}},
        "required": ["clause_type"]
      }
    }]
  }]
}

Counted this way the number is directly comparable to the promptTokenCount you will see on the generation response. Count only the user text and the two numbers will disagree by however much you left out, which for a tool-heavy agent is frequently the majority of the prompt.

Turning the count into a cost estimate

The arithmetic, with the assumptions written down because the prices are not ours to assert:

Let  P = totalTokens from countTokens        (input tokens, known)
     O = maxOutputTokens you set             (output tokens, worst case)
     pi = input price per 1M tokens          (from Google's pricing page)
     po = output price per 1M tokens         (from Google's pricing page)

worst-case cost per request = (P / 1e6) * pi  +  (O / 1e6) * po

Worked shape, with a 40,000-token prompt and a 1,000-token cap:
     input  = 40000 / 1e6 = 0.040 * pi
     output =  1000 / 1e6 = 0.001 * po

Multiply through by your request volume and you have a ceiling. Two things move the real figure below it: most responses stop well short of maxOutputTokens, and cached input is billed at a reduced rate. Read the current per-million figures from Google’s Gemini API pricing page rather than from any secondary source, including this one.

Prices per model change, and reasoning models bill thinking tokens as output. If you call a model with a thinking budget, the worst case above is not the worst case — add the budget to O.

Why it will not match your bill exactly

After a generation call, usageMetadata on the response is authoritative. It carries promptTokenCount, candidatesTokenCount, totalTokenCount, and where relevant cachedContentTokenCount and thoughtsTokenCount. Expect small differences from your countTokens figure for three reasons:

  • You counted a slightly different body — an extra turn appended between counting and calling is the usual culprit in a chat loop.
  • Cached tokens are reported separately in cachedContentTokenCount and billed at the cached rate, so the billable input is lower than the raw prompt count.
  • Server-side tools that inject content — grounding with search, code execution — add tokens that did not exist when you counted.

Treat countTokens as the planning number and usageMetadata as the settlement number. If they diverge by more than a rounding error on a request with no cache and no server-side tools, you counted a different body than you sent.

Counting when you stream

With streamGenerateContent the accounting arrives at a different time, and code written against the non-streaming endpoint gets this wrong in a way that is invisible until you look at a bill.

Each chunk in the stream is a full GenerateContentResponse, but the usage figures are only meaningful on the last one. Intermediate chunks may carry no usageMetadata at all, or a partial figure that is not the total. If you record usage from the first chunk you see it, you will systematically under-report output tokens — the prompt count is known from the start and the candidate count is not.

resp = client.models.generate_content_stream(
    model="gemini-2.5-flash", contents=history,
)

usage = None
for chunk in resp:
    if chunk.text:
        emit(chunk.text)
    if chunk.usage_metadata:
        usage = chunk.usage_metadata     # keep overwriting; the last one wins

record_cost(usage)   # not the first one you saw

The consequence for a client that disconnects mid-stream is worth planning for: you never receive the final chunk, so you never learn the output count, but the tokens generated up to that point were generated. A cancelled stream is not a free request. If per-request cost attribution matters, either count the text you did receive as a floor or accept that abandoned streams are unattributed.

One further caution about countTokens itself: it is a real API call against your project’s request quota. Calling it before every generation doubles your request rate, which on a high-throughput service can put you into rate limiting on the counting call rather than the generating one. Count when the answer changes a decision — near a window limit, near a cache threshold, on user-supplied content of unknown size — and skip it for requests you know are small.

The obvious way to avoid the extra call is to estimate locally from character count divided by four. That is fine for a rough guardrail and unsafe as a limit check: the ratio is a rule of thumb for English prose and is badly wrong for code, JSON, non-Latin scripts and any request carrying media. Use it to decide whether to call countTokens, not to decide whether the request fits.