Count Tokens Before Sending

Su Suriya Ravichandran Updated 10 Nov 2026
9 min read ·Lesson 46 of 200

Why Count Before Sending?

usage tells you how many tokens a request used after it has run and been billed. Many decisions need that number before sending:

  • Will this request fit within the model's context window?
  • Is this document too large for the feature's budget?
  • How much will this request cost?
  • Should this request go to a larger or a cheaper model?
  • How much conversation history can I include?

The token counting endpoint answers these questions. It counts the input tokens of a request without generating a response:

from anthropic import Anthropic

client = Anthropic()

count = client.messages.count_tokens(
    model="claude-opus-5-5",
    system="You are a helpful assistant.",
    messages=[{"role": "user", "content": "Explain Python decorators."}],
)

print(count.input_tokens)   # e.g. 21

It corresponds to the endpoint POST /v1/messages/count_tokens.


Key Facts About Token Counting

According to the token counting documentation:

FactWhat it means for you
It accepts the same inputs as message creationSystem prompts, messages, tools, images, and PDFs can all be counted
It is free to useCounting does not add to your API bill
It has its own rate limitsRequests per minute depend on your usage tier, and are separate from Messages API limits, so counting does not use up your message capacity
The result is an estimateActual input usage can differ by a small amount
It does not use prompt cachingYou can include cache_control markers, but caching only happens during real message creation
All active models support itAlways count with the model you will actually use, because tokenizers differ between model generations (Lesson 44)

What It Does Not Count

count_tokens() counts input only. Output size cannot be known in advance; it is bounded by max_tokens. For a cost estimate, combine the counted input with an expected output size, such as the p95 from Lesson 45.


Reusing a Request Dictionary

When you build requests as dictionaries (Lesson 22), you can count the same request before sending it. count_tokens() accepts the input parameters but not generation settings such as max_tokens, so select the relevant keys:

COUNTABLE_KEYS = {"model", "system", "messages", "tools", "tool_choice", "thinking"}


def count_request(client, request: dict) -> int:
    params = {key: value for key, value in request.items() if key in COUNTABLE_KEYS}
    return client.messages.count_tokens(**params).input_tokens
request = {
    "model": "claude-opus-5-5",
    "max_tokens": 2048,
    "system": "You are a helpful assistant.",
    "messages": [{"role": "user", "content": "Summarize this report..."}],
}

tokens = count_request(client, request)     # Count first
if tokens < 100_000:
    reply = client.messages.create(**request)   # Then send

Why a whitelist (COUNTABLE_KEYS) rather than removing known extra keys: the counting function stays correct even when you add new generation settings to your requests later. Check the API reference if you need to count requests that use other input features.


Use Case 1: Guarding Against Oversized Inputs

Reject or adjust requests that would be too large before they fail, or before they cost more than a feature should:

MAX_INPUT_TOKENS = 50_000   # Feature budget, well below the context window

tokens = count_request(client, request)
if tokens > MAX_INPUT_TOKENS:
    raise ValueError(f"Document too large: {tokens:,} tokens (limit {MAX_INPUT_TOKENS:,}).")

A clear error before sending is better than a slow, expensive request, or a failure halfway through a workflow.


Use Case 2: Estimating Cost

INPUT_PRICE, OUTPUT_PRICE = 4.00, 20.00   # Claude Opus 5.5, USD per million tokens
EXPECTED_OUTPUT = 600                     # e.g. the p95 from profiling (Lesson 45)

input_tokens = count_request(client, request)
estimate = (input_tokens * INPUT_PRICE + EXPECTED_OUTPUT * OUTPUT_PRICE) / 1_000_000
print(f"Estimated cost: ${estimate:.4f}")

This is useful for showing users a cost before a large job, for enforcing per-user budgets (Lesson 179), and for choosing between models.


Use Case 3: Routing by Size

Route requests to the model that fits their size and complexity (Lesson 12):

tokens = count_request(client, request)
request["model"] = "claude-opus-5-5" if tokens > 20_000 else "claude-sonnet-5-5"

Counting with one model and sending with another is a reasonable approximation for routing between models of the same generation. For exact budgets, count with the final model.


Use Case 4: Fitting History into a Budget

Instead of trimming conversation history by a fixed number of messages (Lesson 24), trim it to a token budget, so long messages and short messages are treated fairly. This is shown in the practical example below.


Practical Example: A Pre-Flight Checker

This module checks a request before it is sent. It trims conversation history to fit a token budget, rejects requests that are still too large, estimates the cost, and only then sends the request.

"""preflight.py: count, trim, and estimate a request before sending it."""

from dataclasses import dataclass

from anthropic import Anthropic

client = Anthropic()
MODEL = "claude-opus-5-5"
INPUT_PRICE, OUTPUT_PRICE = 4.00, 20.00
COUNTABLE_KEYS = {"model", "system", "messages", "tools", "tool_choice", "thinking"}


@dataclass
class Preflight:
    input_tokens: int
    removed_messages: int
    estimated_cost: float


def count_request(request: dict) -> int:
    params = {key: value for key, value in request.items() if key in COUNTABLE_KEYS}
    return client.messages.count_tokens(**params).input_tokens


def fit_to_budget(request: dict, budget: int) -> tuple[dict, int, int]:
    """Remove the oldest exchanges until the request fits the budget."""
    request = {**request, "messages": list(request["messages"])}
    original_length = len(request["messages"])
    tokens = count_request(request)

    while tokens > budget and len(request["messages"]) > 1:
        request["messages"] = request["messages"][2:]          # Drop the oldest user+assistant pair
        while request["messages"] and request["messages"][0]["role"] != "user":
            request["messages"] = request["messages"][1:]
        tokens = count_request(request)

    removed = original_length - len(request["messages"])
    return request, tokens, removed


def preflight_and_send(request: dict, budget: int, expected_output: int = 600):
    fitted, tokens, removed = fit_to_budget(request, budget)
    if tokens > budget:
        raise ValueError(f"Request still too large after trimming: {tokens:,} > {budget:,} tokens.")

    estimate = (tokens * INPUT_PRICE + expected_output * OUTPUT_PRICE) / 1_000_000
    report = Preflight(input_tokens=tokens, removed_messages=removed, estimated_cost=estimate)
    print(f"[preflight] {report}")

    return client.messages.create(**fitted), report


if __name__ == "__main__":
    long_history = []
    for i in range(1, 9):
        long_history.append({"role": "user", "content": f"Question {i}: " + "Tell me about Python. " * 60})
        long_history.append({"role": "assistant", "content": f"Answer {i}: " + "Python is versatile. " * 60})
    long_history.append({"role": "user", "content": "Summarize what we discussed in one sentence."})

    request = {
        "model": MODEL,
        "max_tokens": 2048,
        "system": "You are a concise programming tutor.",
        "messages": long_history,
    }

    print(f"Original request: {count_request(request):,} tokens")
    reply, report = preflight_and_send(request, budget=2_000)
    print("".join(block.text for block in reply.content if block.type == "text"))

Example output (exact numbers vary by model):

Original request: 4,830 tokens
[preflight] Preflight(input_tokens=1726, removed_messages=10, estimated_cost=0.018904)
We discussed several aspects of Python and how versatile it is.

How It Works

  1. count_request() keeps only the countable keys and calls count_tokens(). The same request dictionary is used for counting and for sending, so the two can never drift apart.
  2. fit_to_budget() works on a copy of the request and its message list, so the caller's history is never modified.
  3. The trimming loop removes the oldest user and assistant pair, then removes any leading non-user messages, so the conversation still starts with a user message (Lesson 24). After each removal, it counts again.
  4. removed is calculated once at the end, from the lengths before and after trimming. That is simpler and always correct, however many messages each iteration removed.
  5. The loop stops when the request fits, or when only one message is left. If even the last message alone is too large, preflight_and_send() raises a clear error instead of sending a request that exceeds the budget.
  6. The cost estimate combines the counted input with an expected output size. In this example, the full history (about 4,800 tokens) was trimmed to fit the 2,000-token budget by removing 10 messages (five exchanges).

Note: each loop iteration makes one counting request. Counting is free but rate-limited. For very long histories, remove several exchanges per step, or use a binary search over how many messages to keep (see the exercise).


Common Mistakes

Passing Generation Settings to count_tokens()

What happens: unexpected-argument errors, because parameters such as max_tokens are not accepted.

Fix: pass only input parameters, for example with a whitelist like COUNTABLE_KEYS.

Counting with a Different Model Generation

What happens: counts are noticeably lower or higher than the real usage, because tokenizers differ.

Fix: count with the model you will send to, or at least with a model of the same generation.

Treating Counts as Exact

What happens: a request counted at exactly the limit sometimes exceeds it.

Fix: keep a safety margin below hard limits.

Counting Inside Tight Loops Without Considering Rate Limits

What happens: trimming loops that count after every small change can hit the token counting rate limit.

Fix: remove larger chunks per step, or use a binary search over how many messages to keep.

Expecting Counting to Warm the Cache

What happens: developers count a request expecting the next real request to be cached.

Fix: caching happens only when messages are actually created.


Best Practices

  • Count before sending whenever input size is unpredictable: documents, long histories, user uploads.
  • Count with the model you will use.
  • Reuse request dictionaries with a whitelist of countable keys.
  • Keep safety margins below budgets and context limits.
  • Combine counted input with profiled output for cost estimates.
  • Trim to token budgets rather than message counts for long conversations.
  • Remember that counting is free but rate-limited.

Exercise

  1. Count the same document with Claude Opus 5.5 and Claude Haiku 5.5. Are the counts the same?
  2. Add an expected_output value per feature, based on a profile from Lesson 45, and show the estimated cost to the user before sending.
  3. Change fit_to_budget() to keep the first user message (which may contain important context) and trim from the second exchange onward.
  4. Rewrite the trimming as a binary search over the number of most recent exchanges to keep, so a history of 100 exchanges needs about 7 counting requests instead of up to 100.
Show answers for steps 3 and 4

Step 3: keep the first message separately and trim only the rest:

first, rest = request["messages"][0], request["messages"][1:]
while tokens > budget and len(rest) > 1:
    rest = rest[2:]
    while rest and rest[0]["role"] != "assistant":
        rest = rest[1:]
    request["messages"] = [first] + rest
    tokens = count_request(request)

The remaining list after first (a user message) must start with an assistant message to keep the roles alternating.

Step 4: search for the largest number of recent messages that fits:

def fit_with_binary_search(request: dict, budget: int) -> dict:
    messages = request["messages"]
    low, high = 1, len(messages)        # Number of most recent messages to keep
    best = None
    while low <= high:
        keep = (low + high) // 2
        candidate_messages = messages[-keep:]
        while candidate_messages and candidate_messages[0]["role"] != "user":
            candidate_messages = candidate_messages[1:]
        candidate = {**request, "messages": candidate_messages}
        if candidate_messages and count_request(candidate) <= budget:
            best, low = candidate, keep + 1     # Fits: try keeping more
        else:
            high = keep - 1                      # Too large: keep fewer
    if best is None:
        raise ValueError("Even the most recent message exceeds the budget.")
    return best

Each iteration halves the search range, so the number of counting requests grows only logarithmically with the length of the history.

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 Tokens Before Sending and get answers drawn from it.

Signed-in readers only.