Count Output Tokens

Su Suriya Ravichandran Updated 10 Nov 2026
8 min read ·Lesson 45 of 200

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:

  1. 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).
  2. They determine latency. Tokens are generated one after another, so longer outputs take longer to complete.
  3. 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 seeWhat output_tokens counts
A 3-sentence answer (~60 tokens of text)~60 tokens of text plus all thinking tokens
A tool callThe 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:

StatisticWhy it matters
Average output tokensPredicts typical cost per request
95th percentile (p95)95% of responses are this size or smaller, so it is a good basis for the limit
MaximumShows the extremes
Truncation rateThe 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

  1. PROMPTS are representative of real requests for one feature, here short tutoring answers. Profile each feature separately, because output sizes differ greatly between features.
  2. A generous max_tokens (4096) during profiling avoids truncation, so the measurements show the true output size.
  3. statistics.quantiles(outputs, n=20) divides the data into 20 equal parts. The last cut point ([-1]) is the 95th percentile.
  4. math.ceil(p95 * 1.75 / 256) * 256 applies a 1.75× safety margin, then rounds up to a multiple of 256 to give a tidy limit.
  5. truncated += reply.stop_reason == "max_tokens" counts truncations, because True adds 1 and False adds 0.
  6. 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_tokens for 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_tokens from 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

  1. Run output_profiler.py with your own feature's prompts. What max_tokens value does it suggest?
  2. Use estimate_text_tokens() on three replies and compare the estimate with usage.output_tokens. Roughly what share of each output was visible text?
  3. Add a "high" effort run to the profiler, and compare the average output tokens across all three effort levels.
  4. 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).
Show a possible solution for step 4
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.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Count Output Tokens and get answers drawn from it.

Signed-in readers only.