Count Tokens Before Sending
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:
| Fact | What it means for you |
|---|---|
| It accepts the same inputs as message creation | System prompts, messages, tools, images, and PDFs can all be counted |
| It is free to use | Counting does not add to your API bill |
| It has its own rate limits | Requests 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 estimate | Actual input usage can differ by a small amount |
| It does not use prompt caching | You can include cache_control markers, but caching only happens during real message creation |
| All active models support it | Always 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
count_request()keeps only the countable keys and callscount_tokens(). The same request dictionary is used for counting and for sending, so the two can never drift apart.fit_to_budget()works on a copy of the request and its message list, so the caller's history is never modified.- 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.
removedis calculated once at the end, from the lengths before and after trimming. That is simpler and always correct, however many messages each iteration removed.- 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. - 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
- Count the same document with Claude Opus 5.5 and Claude Haiku 5.5. Are the counts the same?
- Add an
expected_outputvalue per feature, based on a profile from Lesson 45, and show the estimated cost to the user before sending. - Change
fit_to_budget()to keep the first user message (which may contain important context) and trim from the second exchange onward. - 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.
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.