Measure Token Usage

Su Suriya Ravichandran Updated 10 Nov 2026
5 min read ·Lesson 185 of 200

You Cannot Control What You Do Not Measure

Every response reports how many tokens it used, in the usage field. Those numbers are the raw material for everything about cost and performance: the bill, the latency, the rate-limit headroom, and the size of the context. An application that records them can answer questions that otherwise have no answer. Which feature uses the most tokens? Did a prompt change make answers longer? Is one user far heavier than the rest? Are there outliers, and what caused them?

This lesson covers how to capture usage consistently, how to group it by feature, and how to summarize it with statistics that describe typical behavior and rare spikes. The emphasis is on measurement before optimization. A team that knows its usage distribution can decide where to act, and it can tell whether an action worked.


Capturing Usage Consistently

Usage should be captured at a single point, the place where the client is called, and recorded with the feature that asked for it. A record of this kind has four fields: the feature, the input tokens, the output tokens, and the time. The feature label is the important addition, because the same model call can serve many purposes, and the totals are useful only when they are split by purpose.

from dataclasses import dataclass


@dataclass(frozen=True)
class Usage:
    feature: str
    input_tokens: int
    output_tokens: int


def record_usage(feature: str, response) -> Usage:
    return Usage(feature, response.usage.input_tokens, response.usage.output_tokens)

The usage object's fields are the counts reported by the API for that call (Lesson 44). Cache reads and writes, covered in Lesson 125, are reported separately, and a complete record includes them when caching is in use, so the cost computed from the record matches the bill.


Why Averages Are Not Enough

The average output length of a feature is the first number most people compute, and it hides the most important information. A feature whose answers are usually 140 tokens but occasionally reach 900 has an average that looks moderate, while its costs and latency are dominated by the rare long answers. Percentiles show the shape of the distribution: the median shows the typical case, and the 95th percentile shows the tail that matters for budgets and timeouts.

Outliers deserve their own attention. A long answer may be legitimate, or it may signal a loop, a prompt that invites rambling, or an input that was not expected. Measuring the distribution makes those cases visible, so they can be investigated rather than averaged away.


Summarizing a Distribution

The function below computes the number of calls, the mean output, the median and 95th percentile of output tokens, and the outliers, defined as answers more than three times the median. The percentile uses the nearest-rank method, which picks an actual observed value, so each figure corresponds to a real call:

"""usage_summary.py: summarize token usage by feature, with percentiles and outliers."""

import math
from collections import defaultdict
from dataclasses import dataclass


@dataclass(frozen=True)
class Usage:
    feature: str
    input_tokens: int
    output_tokens: int


def percentile(values: list[int], p: float) -> int:
    ordered = sorted(values)
    rank = math.ceil(p / 100 * len(ordered))
    return ordered[rank - 1]


def summarize(records: list[Usage]) -> list[str]:
    grouped: dict[str, list[int]] = defaultdict(list)
    for record in records:
        grouped[record.feature].append(record.output_tokens)
    lines = []
    for feature, outputs in grouped.items():
        median = percentile(outputs, 50)
        p95 = percentile(outputs, 95)
        mean = sum(outputs) / len(outputs)
        outliers = [o for o in outputs if o > 3 * median]
        lines.append(f"{feature}: n={len(outputs)} mean_output={mean:.1f} "
                     f"p50={median} p95={p95} outliers={outliers}")
    return lines


if __name__ == "__main__":
    summarize_outputs = [120, 150, 130, 900, 140, 135, 160, 128, 142, 137]
    classify_outputs = [3, 3, 4, 3, 3]
    records = [Usage("summarize", 500, o) for o in summarize_outputs]
    records += [Usage("classify", 85, o) for o in classify_outputs]
    for line in summarize(records):
        print(line)

Output:

summarize: n=10 mean_output=214.2 p50=137 p95=900 outliers=[900]
classify: n=5 mean_output=3.2 p50=3 p95=4 outliers=[]

How It Works

  1. Records are grouped by feature. The grouping is what makes the figures meaningful, because summarization and classification have very different typical sizes.
  2. The median is the middle observed value. For ten summaries, the rank is five, which picks 137, the fifth value in sorted order.
  3. The 95th percentile is the tail. With ten values, the rank is ten, which picks the largest value, 900. The mean of 214.2 hides this, and the percentile shows it directly.
  4. The outlier rule flags values far above the median. The 900-token answer is more than three times 137, so it is listed, while the typical answers are not.
  5. The classify feature has no outliers. Its answers are short and consistent, which the output confirms with an empty list.

The thresholds, such as three times the median, are choices for a team to make. They should be set from what counts as unusual for the feature, and revisited when the feature changes.


Watching Usage Over Time

A single summary describes one period. Usage changes as prompts change, as traffic grows, and as users behave differently, so the summary should be computed for each day or week and compared with the previous period. A change in the median points to a change in typical behavior, such as a new prompt that makes answers longer. A change only in the 95th percentile points to a shift in the tail, such as an input type that produces long outputs. Each kind of change suggests a different investigation.


Where the Numbers Go

Usage records are operational data, and they belong in the same logging and monitoring system as the other signals (Lessons 147 and 190), with the same care about personal data. They describe calls, not content, so they can be retained longer than the content itself. The summaries feed the cost tracking in Lesson 186, and the alerts in Lesson 190.


Summary

Every response reports its token usage, and recording it with the feature that requested it makes the numbers useful. Averages hide the tail that drives cost and latency, so percentiles and outliers describe the distribution honestly. Nearest-rank percentiles report values that actually occurred. Summaries computed over time show whether typical behavior or the tail is changing, which points to different causes. The next lesson turns these measurements into money, by tracking what the usage costs.

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 Measure Token Usage and get answers drawn from it.

Signed-in readers only.