Count Output Tokens
What Output Tokens Are
Output tokens are the tokens Claude generates in a response. They are reported in usage.output_tokens:
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": "Explain Python generators in three sentences."}],
)
print(message.usage.output_tokens) # e.g. 210
Output tokens deserve close attention for three reasons:
- They cost more. For current models, output tokens are priced five times higher than input tokens. For example, Claude Opus 5.5 costs $4 per million input tokens and $20 per million output tokens (Lesson 30).
- They determine latency. Tokens are generated one after another, so longer outputs take longer to complete.
- They are limited by
max_tokens. If the output reaches the limit, the response is truncated (Lesson 13).
Output Tokens Include Thinking
On current models, output_tokens includes all generated tokens: the visible answer and any thinking the model did before or during the answer (Lessons 13 and 16).
output_tokens = thinking tokens + visible answer tokens (+ tool call tokens, if any)
Thinking text is omitted from responses by default, but it is still generated and billed. That is why a three-sentence answer can report a couple of hundred output tokens: most of them may be thinking.
| What you see | What output_tokens counts |
|---|---|
| A 3-sentence answer (~60 tokens of text) | ~60 tokens of text plus all thinking tokens |
| A tool call | The tool call's tokens plus any thinking |
| An empty response (stopped during thinking) | All the thinking tokens generated before the stop |
The last row is important. A response truncated during thinking shows no text but still reports, and bills, its output tokens.
Visible Text vs. Total Output
Sometimes you want to know how much of the output was the visible answer, for example to see how much a task relies on thinking, or to compare prompts. The API does not report this split directly in the basic usage fields, but you can estimate the visible part by counting the answer text's tokens with count_tokens() (Lesson 46):
def estimate_text_tokens(client, model: str, text: str) -> int:
"""Approximate the tokens in a piece of text by counting it as a user message."""
with_text = client.messages.count_tokens(
model=model, messages=[{"role": "user", "content": text}]
).input_tokens
baseline = client.messages.count_tokens(
model=model, messages=[{"role": "user", "content": "x"}]
).input_tokens
return max(with_text - baseline + 1, 0)
How it works: counting a message adds a few formatting tokens on top of the text itself. Subtracting the count of a one-character message removes most of that overhead, and + 1 adds back the single character. The result is an approximation, which is good enough to see whether output is mostly answer or mostly thinking.
How Effort Affects Output Tokens
The effort setting (Lesson 13) guides how much the model thinks. Lower effort usually means fewer thinking tokens, and therefore fewer output tokens, lower cost, and lower latency, for the same visible answer:
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
output_config={"effort": "low"},
messages=[{"role": "user", "content": "Is 17 a prime number? Answer yes or no."}],
)
Use lower effort for simple, high-volume tasks such as classification, extraction, and short factual answers. Keep higher effort for complex reasoning. As Lesson 38 noted, effort is not a reliable way to change the visible length of answers. Use prompt instructions for that.
Output Tokens in Streaming
When streaming (Lessons 51–60), output tokens accumulate as text arrives. The final usage is available on the complete message at the end of the stream:
with client.messages.stream(
model="claude-opus-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": "Explain list comprehensions."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message()
print(f"\nOutput tokens: {final.usage.output_tokens}")
Do not count streamed text chunks to measure usage. Use the final message's usage.
Measuring Output to Set max_tokens
How do you pick a good max_tokens value for a feature? Measure real output sizes, then set the limit with headroom:
| Statistic | Why it matters |
|---|---|
| Average output tokens | Predicts typical cost per request |
| 95th percentile (p95) | 95% of responses are this size or smaller, so it is a good basis for the limit |
| Maximum | Shows the extremes |
| Truncation rate | The share of responses with stop_reason == "max_tokens", which should be close to 0 |
A practical rule: set max_tokens to roughly 1.5–2 × the p95 of observed output tokens. Since thinking is included in output tokens, this automatically leaves room for thinking as well as the answer.
Practical Example: An Output Token Profiler
This profiler runs a set of representative prompts for one feature, records output tokens and stop reasons, computes statistics, and recommends a max_tokens value.
"""output_profiler.py: measure output tokens for a feature and recommend max_tokens."""
import math
import statistics
from anthropic import Anthropic
client = Anthropic()
MODEL = "claude-opus-5-5"
SYSTEM = "You are a programming tutor. Answer in under 120 words with one short code example."
PROMPTS = [
"What is a Python dictionary?",
"Explain list slicing.",
"What does the 'with' statement do?",
"How do I read a file line by line?",
"What is a lambda function?",
"Explain *args and **kwargs.",
"What is the difference between a list and a tuple?",
"How do I handle exceptions?",
]
def profile(max_tokens: int = 4096, effort: str = "medium") -> list[int]:
outputs, truncated = [], 0
for prompt in PROMPTS:
reply = client.messages.create(
model=MODEL,
max_tokens=max_tokens,
system=SYSTEM,
output_config={"effort": effort},
messages=[{"role": "user", "content": prompt}],
)
outputs.append(reply.usage.output_tokens)
truncated += reply.stop_reason == "max_tokens"
p95 = statistics.quantiles(outputs, n=20)[-1]
recommended = math.ceil(p95 * 1.75 / 256) * 256
print(f"effort={effort}")
print(f" requests: {len(outputs)}")
print(f" average output: {statistics.mean(outputs):.0f} tokens")
print(f" p95 output: {p95:.0f} tokens")
print(f" max output: {max(outputs)} tokens")
print(f" truncation rate: {truncated / len(outputs):.0%}")
print(f" suggested max_tokens: {recommended}\n")
return outputs
if __name__ == "__main__":
profile(effort="medium")
profile(effort="low")
Example output (numbers vary):
effort=medium
requests: 8
average output: 268 tokens
p95 output: 402 tokens
max output: 415 tokens
truncation rate: 0%
suggested max_tokens: 768
effort=low
requests: 8
average output: 171 tokens
p95 output: 238 tokens
max output: 244 tokens
truncation rate: 0%
suggested max_tokens: 512
How It Works
PROMPTSare representative of real requests for one feature, here short tutoring answers. Profile each feature separately, because output sizes differ greatly between features.- A generous
max_tokens(4096) during profiling avoids truncation, so the measurements show the true output size. statistics.quantiles(outputs, n=20)divides the data into 20 equal parts. The last cut point ([-1]) is the 95th percentile.math.ceil(p95 * 1.75 / 256) * 256applies a 1.75× safety margin, then rounds up to a multiple of 256 to give a tidy limit.truncated += reply.stop_reason == "max_tokens"counts truncations, becauseTrueadds 1 andFalseadds 0.- Comparing effort levels shows how much of the output is thinking. Here, low effort used about a third fewer output tokens for the same kind of answer, which is a direct cost saving.
With only eight prompts, these statistics are rough. Use more, and more varied, prompts before relying on the numbers, and re-profile when you change the model, prompt, or effort.
Common Mistakes
Estimating Output Cost from the Visible Text
What happens: costs are underestimated, sometimes by a large factor, because thinking tokens are invisible but billed.
Fix: always use usage.output_tokens.
Setting max_tokens from Answer Length Alone
What happens: limits that fit the visible answer are too small once thinking is included, so responses are truncated or empty.
Fix: base limits on measured output_tokens, which include thinking, with headroom.
Profiling with a Low max_tokens
What happens: measured outputs are capped by the limit itself, so the statistics underestimate the true sizes.
Fix: profile with a generous limit.
Measuring One Prompt
What happens: a single response is not representative, and limits fail on other inputs.
Fix: profile a varied set of realistic prompts, and look at p95 and maximum values, not just averages.
Counting Streamed Chunks
What happens: chunk counts do not equal token counts, and they exclude thinking.
Fix: read usage from the final message.
Best Practices
- Log
output_tokensfor every request, together with the stop reason. - Remember that output includes thinking, which is billed but usually not shown.
- Profile each feature with realistic prompts, and set
max_tokensfrom p95 with headroom. - Use lower effort for simple tasks to reduce output tokens, cost, and latency.
- Track the truncation rate and keep it near zero.
- Re-profile after changing models, prompts, or effort settings.
Exercise
- Run
output_profiler.pywith your own feature's prompts. Whatmax_tokensvalue does it suggest? - Use
estimate_text_tokens()on three replies and compare the estimate withusage.output_tokens. Roughly what share of each output was visible text? - Add a
"high"effort run to the profiler, and compare the average output tokens across all three effort levels. - Modify
profile()to also return the estimated cost of all requests, using Claude Opus 5.5's prices ($4 input / $20 output per million tokens).
def profile(max_tokens: int = 4096, effort: str = "medium") -> tuple[list[int], float]:
outputs, truncated, cost = [], 0, 0.0
for prompt in PROMPTS:
reply = client.messages.create(
model=MODEL, max_tokens=max_tokens, system=SYSTEM,
output_config={"effort": effort},
messages=[{"role": "user", "content": prompt}],
)
outputs.append(reply.usage.output_tokens)
truncated += reply.stop_reason == "max_tokens"
cost += (reply.usage.input_tokens * 4 + reply.usage.output_tokens * 20) / 1_000_000
# ... statistics and printing as before ...
print(f" estimated cost: ${cost:.4f}")
return outputs, cost
Comparing the cost across effort levels shows the financial effect of thinking. For simple tasks, low effort is often significantly cheaper, with no loss in answer quality.