# Summing Claude Code's Transcript Usage? One API Response Can Appear Several Times

> Source: <https://dev.to/gabrielanhaia/summing-claude-codes-transcript-usage-one-api-response-can-appear-several-times-15g7>
> Published: 2026-10-19 11:02:10+00:00

An agent step that writes a sentence and then calls two tools is one API response. In the transcript [Claude Code](https://code.claude.com/docs/en/overview) keeps on disk, that one response can be three lines, and every line carries the full `usage` object of the response. Sum the lines and you have counted the same tokens three times.

The numbers are public. In a session posted to Claude Code's issue tracker in August 2026, a naive sum of `cache_creation_input_tokens` came to 1,075,420. Counting each `message.id` once gave 560,751, a 1.92x overcount, and cache reads were over by 1.59x ([issue #87303](https://github.com/anthropics/claude-code/issues/87303)). Anthropic closed the issue the same day as documented behaviour. So if your cost script sums lines, the bug is in your script.

A short Node script sums your own transcript both ways, so you can see the gap on your data.

Claude Code stores each session as JSONL at `~/.claude/projects/<project>/<session-id>.jsonl`, one JSON object per line for a message, a tool use or metadata ([sessions docs](https://code.claude.com/docs/en/sessions#where-transcripts-are-stored)).

When one API response contains several content blocks (a `thinking` block, some `text`, one or more `tool_use` calls), Claude Code writes one `assistant` entry per block. The #87303 report describes each entry as having its own `uuid` and a single content block, while sharing the response's `message.id` and a byte-identical `usage` object. Condensed from the issue's example:

```
assistant uuid=70abebf3 content=[text]
assistant uuid=2c277f40 content=[tool_use]
assistant uuid=99c159e5 content=[tool_use]

all three: message.id = msg_011Ce7zpWBJMrj6gSiJAuY7o
all three: usage = { input_tokens: 2,
  cache_creation_input_tokens: 24896,
  output_tokens: 244, ... }
```

In that session, 521 usage records covered 323 distinct message IDs, and every one of the reporter's 10 automated sessions had duplicates, between 13 and 86 each ([#87303](https://github.com/anthropics/claude-code/issues/87303)).

The pattern is older than that report. [Issue #6805](https://github.com/anthropics/claude-code/issues/6805), opened in August 2025 against Claude Code 1.0.96, described the same split in `stream-json` output: one message with thinking, text and tool_use blocks emitted as three events, each with the full usage. Its reporter said one message ID's `cache_read_input_tokens` value showed up 8 times in a session file. The stale bot closed it in February 2026 without a maintainer reply.

Closing #87303, a maintainer wrote that "this shape is intentional and documented" and that "consumers are expected to count each `message.id` once" ([#87303](https://github.com/anthropics/claude-code/issues/87303)). The Agent SDK's [cost tracking guide](https://code.claude.com/docs/en/agent-sdk/cost-tracking#track-per-step-usage) says the same: when Claude uses tools in parallel, "multiple messages share the same `id` with identical usage data. Track which IDs you've already counted and skip duplicates to avoid inflated totals."

The same guide adds two caveats worth keeping in your head.

First, output tokens. It says deduplicated per-step values "are accurate for input and cache tokens", but per-step `output_tokens` is a placeholder: the count the API had reported at `message_start`, before the response was generated. The real output count goes on the SDK's final result message ([cost tracking](https://code.claude.com/docs/en/agent-sdk/cost-tracking#read-output-tokens-from-the-result-message)). That passage is about the SDK's message stream, but closing #87303 the maintainer wrote that "the transcript file and `stream-json` output are that same message stream", so the placeholder applies on disk too ([#87303](https://github.com/anthropics/claude-code/issues/87303)). [ccstats](https://github.com/majiayu000/ccstats), a Rust usage CLI, still keeps the entry that has a `stop_reason`, because its [accounting notes](https://github.com/majiayu000/ccstats/blob/main/docs/algorithm/authoritative-token-accounting.md) say streamed entries for one `message.id` can each update usage. Trust your deduplicated input and cache numbers more than your output number.

Second, every dollar figure Claude Code computes is a client-side estimate from a bundled price table. For what you are actually billed, the guide points to the [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api) or the Console's usage page ([cost tracking](https://code.claude.com/docs/en/agent-sdk/cost-tracking#estimates-not-billing)). Use a transcript sum to see where tokens go, and check the Usage and Cost API before you quote a dollar figure.

This script reads one transcript, sums every usage line, then sums again with one entry per `message.id`. It follows the ccstats rule for which duplicate to keep: the one with `stop_reason`, otherwise the latest. Entries with no `message.id` fall back to their `uuid`, since the #87303 reporter notes those can't be deduplicated reliably. It also counts message IDs seen under more than one `requestId`, for a reason in the next section.

``` js
// usage-sum.mjs: node usage-sum.mjs <session.jsonl>
import { readFileSync } from "node:fs";

const F = ["input_tokens", "cache_creation_input_tokens",
  "cache_read_input_tokens", "output_tokens"];
const zero = () => Object.fromEntries(F.map((f) => [f, 0]));
const naive = zero();
const dedup = zero();
const kept = new Map(); // one entry per API response
const reqs = new Map(); // message.id -> requestIds seen
let records = 0;

const text = readFileSync(process.argv[2], "utf8");
for (const line of text.split("\n")) {
  let e;
  try { e = JSON.parse(line); } catch { continue; }
  const m = e?.message;
  if (e?.type !== "assistant" || !m?.usage) continue;
  records++;
  for (const f of F) naive[f] += m.usage[f] ?? 0;

  const key = m.id ?? e.uuid;
  const prev = kept.get(key);
  // prefer the entry with stop_reason, else the latest one
  if (!prev || m.stop_reason || !prev.message.stop_reason) {
    kept.set(key, e);
  }
  if (m.id && e.requestId) {
    if (!reqs.has(m.id)) reqs.set(m.id, new Set());
    reqs.get(m.id).add(e.requestId);
  }
}

for (const e of kept.values()) {
  for (const f of F) dedup[f] += e.message.usage[f] ?? 0;
}
const split = [...reqs.values()].filter((s) => s.size > 1);
console.log(`${records} usage lines, ${kept.size} responses`);
console.log(`ids under more than one requestId: ${split.length}`);
console.table(F.map((f) => ({ field: f, naive: naive[f],
  dedup: dedup[f] })));
```

Point it at your most recent session with any current [Node.js](https://nodejs.org):

```
node usage-sum.mjs "$(ls -t ~/.claude/projects/*/*.jsonl | head -1)"
```

I haven't run this script myself. Your gap depends on how often the agent thinks and calls tools in parallel in one step. If the two columns match, that session had no split responses. Read the `output_tokens` row as a duplicate check only: per Anthropic it is a placeholder even after dedup, and the real output count is on the result message or in `claude -p --output-format json`.

If you would rather stay in the shell, [jq](https://jqlang.org) can do one field. The first command is the naive sum, the second keeps one entry per ID:

```
F=$(ls -t ~/.claude/projects/*/*.jsonl | head -1)
jq -s 'map(select(.type == "assistant" and .message.usage))
  | map(.message.usage.cache_read_input_tokens // 0) | add' "$F"
jq -s 'map(select(.type == "assistant" and .message.usage))
  | group_by(.message.id) | map(.[0])
  | map(.message.usage.cache_read_input_tokens // 0) | add' "$F"
```

Taking the first entry of each group is fine for input and cache fields, which #87303 found byte-identical across duplicates. Two limits: `group_by` lumps every entry without an ID into one group, and `jq -s` stops at the first line it can't parse, where the Node script skips it.

Both scripts share one more limit. The sessions docs say the entry format "is internal to Claude Code and changes between versions, so scripts that parse these files directly can break on any release" ([sessions docs](https://code.claude.com/docs/en/sessions#where-transcripts-are-stored)). For scripted runs, `claude -p --output-format json` gives you usage and cost as structured output instead.

Split responses are the common case. Once you sum across files or across runs, there are more.

**Branches copy history.** `/branch` and `--fork-session` create a new session whose transcript holds a copy of the conversation up to that point ([sessions docs](https://code.claude.com/docs/en/sessions#branch-a-session)). Sum every file in a project folder and the copied part counts once per branch. Deduplicate by `message.id` across all files, not file by file. The ccstats notes make the same point about copies across project and subagent files.

**Compaction may rewrite history.** [Issue #66947](https://github.com/anthropics/claude-code/issues/66947) reported that each compaction or continuation appeared to re-write prior entries into the same transcript with the same `uuid` and timestamp; one message showed up five times in one file. Anthropic never confirmed it, and the bot closed it as not planned. Keying on IDs absorbs it either way.

**Resumed totals already include the past.** Claude Code saves a session's totals to its transcript on a normal exit and restores them when a later call resumes it, so each result of a resumed call "already includes the session's earlier spend" (for SDK and `claude -p` resumes, since 2.1.277). The guide's words: "summing results double-counts the restored spend" ([cost tracking](https://code.claude.com/docs/en/agent-sdk/cost-tracking#accumulate-costs-across-multiple-calls)). Read the latest result for the session total.

**One report says IDs repeated under load.** [Issue #64880](https://github.com/anthropics/claude-code/issues/64880), still open, reports six separate sequential responses that came back with the same `message.id` under server load. Its reporter later checked a normal-load window and found no `message.id` under more than one `requestId`. That is what the `split` line in the script checks. If it prints anything above zero, key on `message.id` plus `requestId` for that file.

Getting this wrong is easy, including for the tools you use to watch spend. I build [Hermes IDE](https://hermes-ide.com), so weigh this with that in mind. Its [2.2.9 release](https://github.com/hermes-hq/hermes-ide/releases/tag/v2.2.9), published October 12, 2026, fixed a bug from the same family: "Cost and token totals no longer grow while a session sits open: each session's usage is counted once. Totals inflated by earlier versions are corrected on the first launch."

The release note doesn't say the cause was duplicate transcript lines, and I won't claim it was. The shape of the bug is the lesson: anything that keeps reading a file Claude Code is still appending to has to remember what it already counted, or the number climbs while the session sits idle.

*Hermes 2.3.4: Claude Code's /context breakdown, per-session context gauges in the sidebar, and the status strip with the token total and estimated cost.*

Per session, Hermes shows the cost the agent reports, or an estimate from its transcript marked as estimated, with token totals ([README](https://github.com/hermes-hq/hermes-ide)). It is free to [download](https://hermes-ide.com/download), source-available under BSL 1.1, and runs on macOS, Windows and Linux. It is not affiliated with Anthropic. If you run the script above next to it and the numbers disagree, [open an issue](https://github.com/hermes-hq/hermes-ide/issues) with both outputs.

A surprise LLM bill often starts with a number nobody checked: a usage field read from the wrong place, or the same response counted twice. *AI That Answers* is the first book in my series, and it covers token cost from the first call onward, along with prompts and structured output, in TypeScript on Node. If you just wrote a dedup script for usage records, it is the book that explains what those fields cost you.

It is book 1 of *AI in TypeScript*, five books from your first LLM call to agents in production: [the whole series](https://xgabriel.com/ai-in-typescript). And if you want per-session spend next to your agents, [Hermes IDE](https://hermes-ide.com/download) is free.
