Claude Code Usage Stats Trap: Why One API Response Gets Counted Multiple Times

In Claude Code transcripts, one API response is split across multiple lines each carrying the full usage object, so naive summation overestimates usage — deduplicate by message.id.
Claude Code stores transcripts in JSONL format; when an API response contains multiple content blocks, it writes one assistant record per block, each carrying the complete usage object. Naively summing these records significantly overestimates token consumption — issue #87303 measured a 1.92x overestimate on cache_creation. Anthropic confirms this is documented design behavior, and consumers should count once per message.id. After deduplication, input and cache tokens are fairly accurate, but output_tokens is still a placeholder value; real output must come from the result message or structured output. Dollar amounts are only client-side estimates and should follow the Usage and Cost API. Additionally, branch copying, compaction rewriting, restored-session accumulation, and high-load ID reuse are four other sources of double counting.
If you directly sum up token usage from Claude Code session transcripts in a script, the number you get is likely far higher than the actual consumption. The root cause: a single API response may be split across multiple lines in the transcript file, and each line carries the complete usage object.
One Response, Multiple Lines
Claude Code stores sessions in JSONL format at ~/.claude/projects/<project>/<session-id>.jsonl, with each line corresponding to a message, tool call, or metadata entry. When an API response contains multiple content blocks (thinking, text, tool_use), Claude Code writes one assistant record for each content block. These records each have their own independent uuid, but share the same message.id and an identical usage object.
The real-world case in GitHub issue #87303 is compelling: the session contained 521 usage records but corresponded to only 323 distinct message IDs. Naively summing cache_creation_input_tokens yields 1,075,420, versus 560,751 after deduplicating by ID — an overestimate of 1.92x; the cache read field was overestimated by 1.59x. Anthropic closed the issue the same day, marking it as "documented design behavior."
This pattern isn't new. Issue #6805 from August 2025 described the same problem: in stream-json output, a message containing thinking, text, and tool_use was split into three events, each carrying the complete usage data. A reporter at the time noted that one message ID's cache_read_input_tokens appeared 8 times in a single session file.
Anthropic's Rule: Count Once Per message.id
When closing #87303, the Anthropic maintainer explicitly stated that this recording shape is intentional and documented, and that consumers should count once per message.id. The Agent SDK cost tracking guide gives the same advice: when Claude uses tools in parallel, multiple messages share the same ID and identical usage data, so you need to track already-counted IDs and skip duplicates.
Even after deduplication, however, there are two things to watch out for.
Output Tokens Are Placeholder Values
Per the docs, deduplicated per-step values are accurate for input and cache tokens, but output_tokens is just the placeholder value from message_start before the response was generated. The real output volume is found in the SDK's final result message or in the structured output of claude -p --output-format json. The maintainer also confirmed when closing #87303 that transcript files and stream-json output belong to the same message stream, so this rule applies to on-disk records as well.
The message_start event is the first event of an Anthropic Messages API streaming response; it's emitted before the model actually generates any tokens, and the output_tokens in its usage is only an initial estimate made at request time (usually very small). As streaming generation progresses, message_delta events carry continuously updated usage, with the final value appearing before message_stop. Claude Code's transcript records the snapshot at the start of the response, so output_tokens remains at the placeholder value. The real output volume must be read from the SDK's final result message (result type) or the usage field returned by claude -p --output-format json, which aggregates the actual output tokens of all responses in that invocation.
Dollar Amounts Are Only Client-Side Estimates
All dollar figures computed by Claude Code are local estimates based on a built-in price table. To know your actual bill, query the Usage and Cost API or the Console usage page. Transcript summation is good for observing token flow, but always verify against official billing data before citing any dollar amount.

Anthropic's Usage and Cost API provides usage and cost queries by organization, workspace, and API key dimension, with data coming directly from the billing system — it's the official billing basis. The Console usage page is the visualized version. The key differences from Claude Code's local estimates are: official data includes calls from all clients (not just Claude Code), reflects actual billing rules (such as volume discounts and different unit prices for cache writes/reads), and updates promptly. Local estimates are based on Claude Code's built-in price table, which may lag behind official price changes and doesn't distinguish enterprise contract pricing. So transcript summation is good for observing token flow and relative proportions, but any external citation or budget decision should be verified against official billing.
Verify Your Own Records with a Script
The most direct way to verify is to write a script that performs both naive summation and ID-deduplicated summation on the same transcript, then compares the difference. The core logic: iterate over all lines where type === "assistant" and that have message.usage, naively accumulating each field; for deduplication, key on message.id (falling back to uuid when there's no ID), keeping the record with stop_reason, or otherwise the latest one.
If you're more comfortable with the shell, you can quickly do single-field comparisons with jq. The first command is naive summation; the second uses group_by(.message.id) to take the first of each group and then sum. Note that jq -s stops entirely on an unparseable line, whereas a Node script skips invalid lines; group_by also lumps all records without an ID into the same group.
There's another shared limitation: the session docs explicitly state that the recording format is a Claude Code internal implementation and may change between versions, so scripts that parse files directly can break at any time. For automation, claude -p --output-format json is the safer choice.
Four Other Sources of Double Counting
Split responses are the most common case, but there are more traps across files and runtimes.
Branching copies history. New sessions created by /branch and --fork-session copy the full conversation up to the branch point. If you sum file by file within a project folder, the copied portion gets counted once per branch. The correct approach is to globally deduplicate by message.id across all files.
Compaction may rewrite history. Issue #66947 reported that each compaction or resume seems to rewrite prior records into the same transcript, using the same uuid and timestamp, with one message appearing five times in a single file. Anthropic hasn't confirmed this, but deduplicating by ID absorbs the impact regardless.
Restored session totals already include the past. On normal exit, Claude Code saves the session total into the transcript and reads it back on subsequent resumes, so each resume invocation's result already includes prior spending. The docs put it plainly: "summing results double-counts the restored spend" — accumulating results from multiple invocations double-counts the restored portion; you should read the latest result as the session total.
IDs may repeat under high load. The still-open issue #64880 reported that under high server load, six independent sequential responses returned the same message.id. The reporter later re-checked during a normal load window and found no case of the same ID appearing under multiple requestIds. The split check in the script is designed for exactly this: if the output is greater than zero, that file needs deduplication keyed on the combination of message.id plus requestId.
/branch and --fork-session are Claude Code's session branching mechanisms: the former creates a new branch on top of the current session, the latter derives a new session from an existing one. Both copy the full conversation history up to the branch point into a new file; the new session has its own session ID but inherits the same message.ids. If you sum file by file within a project folder, the history before the branch point gets counted once per branch — the more branches, the more duplication. The correct approach is to globally deduplicate by message.id across all transcript files, rather than summing per file or per session.
Cost Tracking Tools Have Fallen into the Same Trap
This kind of bug doesn't only appear in user scripts. Hermes IDE version 2.2.9 fixed a related issue: cost and token totals kept growing as a session stayed open, because each session's usage was being double-counted; inflated totals from earlier versions get corrected on first launch.
The release notes don't explicitly attribute it to duplicate transcript lines, so we shouldn't over-infer. But the shape of the problem is worth remembering: any tool that continuously reads a file being appended to must remember what it has already counted, or the numbers will keep climbing even while the session is idle.
Practical Recommendations
Treat transcript summation as a diagnostic tool, not a billing basis. Deduplicated input and cache numbers are relatively trustworthy; output tokens need to come from the result message; and dollar amounts should follow the Usage and Cost API. For cross-branch and cross-resume scenarios, always deduplicate globally by ID, and add requestId as a second dimension when high-load anomalies occur.
Additional Background
The structure of JSONL and transcripts. Claude Code's session records use JSONL (JSON Lines) format, where each line is an independent JSON object corresponding to a message, a tool call, or a metadata entry. Lines of type assistant contain message.id (corresponding to the API response ID), uuid (the record's unique identifier), and message.usage (token usage). Lines of type user carry tool results and user input. Lines of type system record meta-events like compaction and resume. Understanding this line-by-line structure is a prerequisite for correct summation: each line is parsed independently, but multiple lines with the same message.id share the same usage data.
Related articles

Chain of Thought Isn't Just for AI: How Humans Can Use CoT to Improve Thinking Quality
Chain of Thought isn't just a prompt trick for large models. Starting from a Hacker News discussion, this article analyzes why CoT works for human thinking too, breaks down actionable ways to transfer it, and highlights the key differences between human CoT and machine CoT.
Cloudflare Teams Up with OpenAI: Closi…
Cloudflare Teams Up with OpenAI: Closing the Loop from Vulnerability Discovery to Defense with AI
Cloudflare uses OpenAI Daybreak to investigate security vulnerabilities and generate reviewable remediation plans, while developers use Codex to boost coding efficiency and quality.

Orca Multi-Agent Orchestration in Practice: A Guide to Parallel Collaboration with Claude and Codex
Orca is Stable AI's free open-source agent workbench supporting 34 agents like Claude Code and Codex in parallel. Learn its Git worktree isolation, multi-model orchestration, and mobile management.