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;
countTokenstells 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
- Pick the exact model id you are going to generate with. Tokenization is per-model. Counting against
gemini-2.5-flashand generating with a different family is a silent mismatch. - POST to the
:countTokensmethod on that model, with a body that is the samecontentsarray 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."}]} ] }' - Read
totalTokensfrom the response and compare it against the model’s documented input limit before issuing the generation call. - 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 * poMultiply 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.
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
cachedContentTokenCountand 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 sawThe 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.
countTokens, not to decide whether the request fits.